SaFi Bank Space : Fire OpenTelemetry

Fire OpenTelemetry is a flutter package that wraps Open Telemetry to make it easier. It depends on https://github.com/tatashidayat/opentelemetry-dart, null-safety version of https://pub.dev/packages/opentelemetry.

How does It work?

The span will be started or created if a function inside a class that uses FireOtelAgentMixin is invoked. It will record the function as Span. Then FireOpenTelemetry will add it to OpenTelemetry-Dart Collector. Once it's the export time, the collector will export it as to our Tempo via CloudFlare and Tyk. We can add an authentication method like an API Key on Tyk Level.

How to set it up?

Fire OpenTelemetry requires you to set up its global tracer provider. Here's the way:

FireOtel.isEnabled = Features.otelEnabled.value;
Features.otelEnabled.listen((value) => FireOtel.isEnabled = value);
final remoteExporterClient = CustomHeaderClient();
final remoteExporter = FireOtel.getCollectorExporter(
  host: Uri.parse(AppConfigs.otelUrl.value),
  client: remoteExporterClient,
);
AppConfigs.otelUrl.listen((value) => remoteExporter.uri = Uri.parse(value));
final remoteProcessor = FireOtel.getBatchSpanProcessor(remoteExporter);
final deviceInfo =
    await app_injector.Injector.resolve<DeviceInfoUtil>().getDeviceInfo();
final appInfo = await PackageInfo.fromPlatform();

FireOtel.setGlobalProvider(
  processors: [
    remoteProcessor,
  ],
  deviceAttribute: FireOtelDeviceAttribute(
    id: deviceInfo.deviceId,
    modelIdentifier: deviceInfo.model,
    modelName: '${deviceInfo.manufacturer} ${deviceInfo.model}',
    manufacturer: deviceInfo.manufacturer,
  ),
  serviceAttribute: FireOtelServiceAttribute(
    name: appInfo.appName,
    version: '${appInfo.version}+${appInfo.buildNumber}',
  ),
);

In the first line, we are telling FireOtel (an abstract class to configure FireOpentelemetry) whether we want to enable it or not. And the following line we are listening to Feature Flag in case we want to change it during runtime.

Next, we need to create a Collector Exporter which requires a CustomHeaderClient (an HttpClient that enables us to modify the request before sending it to our OpenTelemetry Receiver). We can create it by invoking FireOtel.getCollectorExporter. In the following line, we can change our receiver URL during runtime by listening to AppConfig. Once we have an exporter then we can create a SpanProcessor. We can get the BatchSpanProcessor by invoking FireOtel.getBatchSpanProcessor and put our exporter on the param.

Last, we need to set Global Tracer Provider by invoking FireOtel.setGlobalProvider and give our SpanProcessor, DeviceAttribute, and ServiceAttribute. For DeviceAttribute we can construct it by getting device info from DeviceInfoUtil. For ServiceAttribute, we can use the PackageInfo package to get our app details. And once all is provided, then we have successfully set up FireOpentelemetry.

How to record a class?

To record a class, we need to add FireOtelAgentMixinV2 mixin to our class. It requires us to override a getter called otelClassName and wraps our functions with:

  • record: to record an asynchronous function

  • recordStream: to record a function that returns Stream. It will stop the span until the stream is closed. If you don’t want to wait for it then use recordSync instead.

  • recordSync: to record a synchronous function or function that returns Stream.

Here’s an example:

Before

After

class MyUsecase {
  Future<void> asyncFunction() async {}

  void syncFunction() {}

  Stream<int> streamFunction() {
    return Stream.value(0);
  }

  Stream<int> streamFunctionAsSync() {
    return Stream.value(0);
  }
}
import 'package:fire_opentelemetry/fire_opentelemetry.dart';

class MyUsecase with FireOtelAgentMixinV2 {
  @override
  String get otelClassName => 'MyUsecase';

  Future<void> asyncFunction() => record(
        functionName: 'asyncFunction',
        fn: () async {},
      );

  void syncFunction() => recordSync(
        functionName: 'syncFunction',
        fn: () {},
      );

  Stream<int> streamFunction() => recordStream(
        functionName: 'streamFunction',
        fn: () {
          return Stream.value(0);
        },
      );

  Stream<int> streamFunctionAsSync() => recordSync(
        functionName: 'streamFunctionAsSync',
        fn: () {
          return Stream.value(0);
        },
      );
}

We should hardcode our class name in the otelClassName getter since it will be changed if we use runtimeType.toString() on an obfuscated build.

How test our wrapped functions or class?

No worries, we made it easy for your test class. You only need to invoke FireOtel.mockTracerAgent() on your test setUp function.

group('MyUsecase', () {
  setUp(() {
    FireOtel.mockTracerAgent();
    // other setup things before each test
  });
});

Is there an easier way to wrap up our class?

Yup, we feel you. We have FireOpenTelemetryGenerator that will generate the wrapped class for us. To use it you only need to add an annotation on top of your class declaration and add the part file. Here’s an example:

import 'package:fire_opentelemetry/fire_opentelemetry.dart';

part 'my_usecase.g.dart';

@FireSpanClass()
class MyUsecase {
  /// You can copy this factory from `my_usecase.g.dart`
  /// the factory is commented on the generated file
  factory MyUsecase() => _$MyUsecaseSpan();

  /// You need to create a private constructor called `_` (underscore)
  MyUsecase._();
  
  Future<void> asyncFunction() async {}

  void syncFunction() {}
  
  Stream<int> streamFunction() {
    return Stream.value(0);
  }
  
  /// This function will not be recorded or traced
  @FireSpanFunction(
    skip: true,
  )
  void skippedFunction() {}
  
  /// This function kind was changed to `remote`
  /// from `internal` (default)
  @FireSpanFunction(
    kind: FireSpanKind.remote,
  )
  Future<void> aRemoteCall() {}
  
  /// This stream function is recored as a Sync function
  /// Use `recordSync` instead of `recordStream`
  @FireSpanFunction(
    useRecordStream: false,
  )
  Stream<int> streamFunctionAsSync() {
    return Stream.value(0);
  }
}

Once you annotated and add the generated file name as a part of the file, then you can trigger the build generator.

flutter pub run build_runner build --delete-conflicting-outputs;

Don’t forget to add fire_opentelemetry_generator as your dev dependency.

Attachments: