Skip to content

feat: opentelemetry sdk LoggerProvider #207

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 39 commits into
base: master
Choose a base branch
from

Conversation

yuzurihaaa
Copy link
Contributor

Which problem is this PR solving?

  1. To add LoggerProvider in SDK.
  2. This is from Log SDK PR but broken down to be smaller. This PR only for LoggerProvider and related parameters to ease review.

How Has This Been Tested?

  1. Unit test

Checklist:

  • Unit tests have been added
  • Documentation has been updated

Comment on lines 108 to 113
@override
Int64? get timeStamp => _timeStamp != null ? Int64(_timeStamp!.microsecondsSinceEpoch) * 1000 : _timeProvider.now;

@override
Int64? get observedTimestamp =>
_observedTimestamp != null ? Int64(_observedTimestamp!.microsecondsSinceEpoch) * 1000 : _timeProvider.now;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to avoid using the int64 type until exporting OTLP format?

Copy link
Contributor Author

@yuzurihaaa yuzurihaaa Jan 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you suggesting to use int (Dart primitive type) and convert to Int64 later on or use DateTime here?

Tbh, I quite agree with you for not using Int64 since it is only being used when converting to OTLP format.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think DateTime makes sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need some feedback for using DateTime. Currently I am injecting DateTimeTimeProvider, but with DateTime, should I change LogRecord constructor to be

LogRecord({
...,
    DateTime? timeStamp,
    DateTime? observedTimestamp,
}): 
_timeStamp = timeStamp ?? DateTime.now(),
_observedTimestamp = observedTimestamp ?? DateTime.now(),

or keep it null and use DateTime? get timeStamp => _timeStamp ?? DateTime.fromMicrosecondsSinceEpoch((_timeProvider.now ~/ 1000).toInt()); in the getter?

I also would like you to verify if the logic here is correct.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What we want is:

  • use timestamps from performance.now() when developing for the web
  • use timestamps from Stopwatch (or similar) when developing for the VM
  • do not accept or return Int64 types leaving this as an internal implementation detail of an otlp exporter

To support both platforms, the shared type we want to use might be double. And we should align this type with what is returned by a time provider. Of course changing the return type of the time provider interface method is a breaking change. So we'd need to mark the method as deprecated stating that the return type will change in the next minor version.

Copy link
Contributor Author

@yuzurihaaa yuzurihaaa Feb 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that should be the in this PR 🤔

that is more like for different PR where it changes together with Span as currently Span is using Int64 for start and end.

Maybe we can create a separate issue for that change?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could. But if we can get around exposing the Int64 type on public APIs now, we avoid piling additional breaking changes onto the breaking changes of removing Int64 from Span APIs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me try to sort this one out too then

can I know if this is the expected change?

abstract class TimeProvider {
  // The smallest increment that DateTime can report is in microseconds, while
  // OpenTelemetry expects time in nanoseconds.
  @Deprecated('This constant will be removed in 0.19.0 without replacement.')
  static const int nanosecondsPerMicrosecond = 1000;

  // window.performance API reports time in fractional milliseconds, while
  // OpenTelemetry expects time in nanoseconds.
  @Deprecated('This constant will be removed in 0.19.0 without replacement.')
  static const int nanosecondsPerMillisecond = 1000000;

  /// The current time, in nanoseconds since Unix Epoch.
+  @Deprecated('This constant will be removed in future without replacement.')
  Int64 get now;

+  double get nowNanoseconds;
}
/// DateTimeTimeProvider retrieves timestamps using DateTime.
class DateTimeTimeProvider implements TimeProvider {
  @override
  Int64 get now => Int64(DateTime.now().microsecondsSinceEpoch) * 1000;

+  @override
+  double get nowNanoseconds => DateTime.now().microsecondsSinceEpoch * 1.0;
}
class WebTimeProvider implements TimeProvider {
  /// The current time, in nanoseconds since Unix Epoch.
  ///
  /// Note that this time may be inaccurate if the executing system is suspended
  /// for sleep.  See https://github.com/open-telemetry/opentelemetry-js/issues/852
  /// for more information.
  @override
  Int64 get now => fromDOMHighResTimeStamp(window.performance.now());

+  @override
+  double get nowNanoseconds => window.performance.now();
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @blakeroberts-wk , I pushed the changes with this new idea.

Comment on lines 71 to 70
Future<void> forceFlush() async {
await Future.forEach(processors, (e) => e.forceFlush());
}

Future<void> shutdown() async {
await Future.forEach(processors, (e) => e.shutdown());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these async?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LogRecordProcessor is implemented with

Future<void> forceFlush();

Future<void> shutdown();

it would only make sense to use async here to ensure all the processors are done with their respective processes.

Also, with async it provides the ability for graceful shutdown for time-consuming operation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure if we want to return a future here. For example, the browser's sendBeacon() doesn't return a promise. Even so, I am sure we don't need the async or await keyword. Those can be safely removed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed async await keyword.

For example, the browser's sendBeacon() doesn't return a promise

This is actually something that I am unaware of and not really sure how to handle for graceful shutdown if there are async process going on, but the function only return void as the processes are still on oing. I've changed to FutureOr for better readability.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The navigator.sendBeacon() method asynchronously sends an HTTP POST request containing a small amount of data to a web server.

It's intended to be used for sending analytics data to a web server, and avoids some of the problems with legacy techniques for sending analytics, such as the use of XMLHttpRequest.

Note: For use cases that need the ability to send requests with methods other than POST, or to change any request properties, or that need access to the server response, instead use the fetch() method with keepalive set to true.

https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon

I think it makes sense to mimic the send beacon API such that the API is synchronous but internally manages some async processes. For example, an exporter will internally need to watch responses and potentially retry requests. But it can return synchronously as soon as the request is queued (with "keepalive").

And if callers of the synchronous API must execute the code asynchronously, they can wrap the synchronous function however they'd like (micro task or event loop) such as Future(() => mySyncFunc()).

To speak to graceful shutdown specifically: most likely, shutdown will be called via a listener registered to an app shutdown event. In this case, returning a future doesn't make sense. This would imply that the caller (the event listener) needs to await the future to accomplish graceful shutdown. And this await would be similarly superfluous to the async/await you removed from within these functions: we just make the function synchronous resulting in the removal of the async/await that would otherwise be necessary for the listener. The synchronous shutdown should be implemented similar to sendBeacon: force flush pending data using xhr with keepalive and without waiting for the response. But this is an implementation detail that can be solely contained within an exporter's implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. In that case, I'll change it to void and will keep this in mind for shutdown process that requires for await


import 'package:opentelemetry/src/experimental_sdk.dart' as sdk;

class NoopLogRecordProcessor implements sdk.LogRecordProcessor {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this class necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove it

Copy link
Contributor Author

@yuzurihaaa yuzurihaaa Mar 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated (removed)


/// https://opentelemetry.io/docs/specs/otel/logs/sdk/#logrecordprocessor
abstract class LogRecordProcessor {
void onEmit(sdk.ReadableLogRecord logRecord);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be a read write record that's only made a read record after all processors have been given the record?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ReadableLogRecord is the base abstract class. The processor can infer the type to be ReadWriteLogRecord. As long as the class is implemented as ReadableLogRecord then it is a valid LogRecord.

The sub class to this is the ReadWriteLogRecord which has setter for some parameters. For customisation, as long as the LogRecord adhere to ReadableLogRecord then it is good to use.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The processor can infer the type to be ReadWriteLogRecord.

Type inferencing does not apply here. The processor implementation can type check/cast, but this isn't inferring the type. And since a processor is allowed to mutate the record on emit, we should write the contract this way (define the API to use a mutable record). Otherwise, implementations must type check/cast to perform mutations

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to ReadWriteLogRecord

@yuzurihaaa
Copy link
Contributor Author

Hi @blakeroberts-wk , I saw that you've resolve some of the comments, but there are still some that are not resolved but I didn't see any feedback / response.

}
return _loggers.putIfAbsent(
key,
() => sdk.Logger(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we switch to a factory with package private scope, can this constructor be made private?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we switch to a factory with package private scope

I am sorry but I don't understand what do you mean here.

Currently the constructor is

Logger({
    required this.instrumentationScope,
    required this.logRecordLimits,
    this.onLogEmit,
    this.resource,
    this.timeProvider,
  });

but if I changed it to Logger._(... and factory Logger.create(... then I will still pass the same arguments so I don't see the benefit of it. Maybe I don't fully understand the statement here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annotating the constructor with @protected should be sufficient

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

Comment on lines 71 to 70
Future<void> forceFlush() async {
await Future.forEach(processors, (e) => e.forceFlush());
}

Future<void> shutdown() async {
await Future.forEach(processors, (e) => e.shutdown());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure if we want to return a future here. For example, the browser's sendBeacon() doesn't return a promise. Even so, I am sure we don't need the async or await keyword. Those can be safely removed

@yuzurihaaa
Copy link
Contributor Author

yuzurihaaa commented Feb 12, 2025

Currently failed on dart v2 but not on v3
> warning - lib/src/sdk/logs/logger_provider.dart:78:5 - Assignment to a variable of type 'void'. Try removing the assignment or changing the type of the variable. - void_checks
warning - lib/src/sdk/logs/logger_provider.dart:82:5 - Assignment to a variable of type 'void'. Try removing the assignment or changing the type of the variable. - void_checks

I'll look into this once review process is done.

After changed the return type to void this is no an issue.

@yuzurihaaa
Copy link
Contributor Author

Hi @blakeroberts-wk , been a while. Can I know if there are any issues that can be resolved. There are also issues that I need some help :)

// Licensed under the Apache License, Version 2.0. Please see https://github.com/Workiva/opentelemetry-dart/blob/master/LICENSE for more information

// https://opentelemetry.io/docs/specs/otel/logs/sdk/#logrecord-limits
abstract class LogRecordLimits {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the intended purpose of this abstract class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed, changed to class

body: body,
timeProvider: timeProvider,
);
onLogEmit?.call(log);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, I'm not sure it makes sense to follow either of those implementations. I imagine the designs above are due to legacy reasons or oversight. We should support multiple processors. Those other implementations accomplish this by introducing a MultiLogRecordProcessor. As far as I can tell, this is superfluous complexity. A logger provider should have a list of processors given to a logger during instantiation. This implementation follows the same lines as other providers (meter and tracer).

https://opentelemetry.io/docs/specs/otel/logs/sdk/#logrecordprocessor

Comment on lines 98 to 107
final listString = attr.value as List<String>;
final truncatedValues = listString
.map((e) =>
applyAttributeLengthLimit(e, limits.attributeValueLengthLimit))
.toList();

final equal = const ListEquality().equals(listString, truncatedValues);
if (equal) return attr;

return api.Attribute.fromStringList(attr.key, truncatedValues);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not avoid allocations because of map().toList(). To avoid allocations when attribute values aren't truncated, you'd need to iterate the list and instantiate and copy to the new array only once a value needing to be truncated is encountered:

    final list = value;
    List<String>? truncated;
    for (int i = 0; i < list.length; i++) {
      final s = list[i];
      if (s.length > limit) {
        truncated ??= List<String>.from(list, growable: false);
        truncated[i] = s.substring(0, limit);
      }
    }
    if (truncated != null) {
      return api.Attribute.fromStringList(attr.key, truncated);
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not avoid allocations because of map().toList()

Learning something new :)

Updated

Comment on lines 108 to 113
@override
Int64? get timeStamp => _timeStamp != null ? Int64(_timeStamp!.microsecondsSinceEpoch) * 1000 : _timeProvider.now;

@override
Int64? get observedTimestamp =>
_observedTimestamp != null ? Int64(_observedTimestamp!.microsecondsSinceEpoch) * 1000 : _timeProvider.now;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could. But if we can get around exposing the Int64 type on public APIs now, we avoid piling additional breaking changes onto the breaking changes of removing Int64 from Span APIs


import 'package:opentelemetry/src/experimental_sdk.dart' as sdk;

class NoopLogRecordProcessor implements sdk.LogRecordProcessor {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove it


/// https://opentelemetry.io/docs/specs/otel/logs/sdk/#logrecordprocessor
abstract class LogRecordProcessor {
void onEmit(sdk.ReadableLogRecord logRecord);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The processor can infer the type to be ReadWriteLogRecord.

Type inferencing does not apply here. The processor implementation can type check/cast, but this isn't inferring the type. And since a processor is allowed to mutate the record on emit, we should write the contract this way (define the API to use a mutable record). Otherwise, implementations must type check/cast to perform mutations

}
return _loggers.putIfAbsent(
key,
() => sdk.Logger(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annotating the constructor with @protected should be sufficient

@yuzurihaaa
Copy link
Contributor Author

Requesting for review on #207 (comment)

@yuzurihaaa
Copy link
Contributor Author

hi @blakeroberts-wk . Can I get some feedback on #207 (comment) if I got this right?

@yuzurihaaa yuzurihaaa force-pushed the feat/logger-provider branch from acd8b32 to d4a919f Compare March 18, 2025 05:06
@yuzurihaaa yuzurihaaa force-pushed the feat/logger-provider branch from d4a919f to 741ed89 Compare March 18, 2025 05:12
@yuzurihaaa
Copy link
Contributor Author

hi @blakeroberts-wk , can I know if there's any update? Or @evanweible-wf can help on the reviewing process?

final DateTime? _timeStamp;
final DateTime? _observedTimestamp;

bool _isReadonly = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove this flag? The tracing API is organized similarly and does not need it

Comment on lines +5 to +6
final int _attributeCountLimit;
final int _attributeValueLengthLimit;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these are final, can the getters be removed?

Comment on lines +12 to +16
final sdk.InstrumentationScope instrumentationScope;
final sdk.Resource _resource;
final sdk.LogRecordLimits logRecordLimits;
final sdk.TimeProvider timeProvider;
final List<sdk.LogRecordProcessor> processors;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can all of these fields be private?

@@ -51,4 +51,7 @@ class WebTimeProvider implements TimeProvider {
/// for more information.
@override
Int64 get now => fromDOMHighResTimeStamp(window.performance.now());

@override
double get nowNanoseconds => window.performance.now();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns milliseconds, not nanoseconds

Int64 get now;

double get nowNanoseconds;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if we want to change this API, we should do something like Duration get nowDuration;.

We'd want to introduce new API and deprecate the current prior to 0.19.0 release. Then in 0.19.0, the return type of now is updated. And in 0.20.0, nowDuration is removed.

abstract class TimeProvider {
  /// The current time in nanoseconds since Unix Epoch.
  ///
  /// **Warning:** Return type will change to `Duration` in 0.19.0.
  /// Use [nowDuration] instead.
  @Deprecated(
    'Return type will change to `Duration` in 0.19.0. '
    'Use `nowDuration` for now, and migrate back to `now` after 0.19.0.'
  )
  Int64 get now;

  /// The current time as a [Duration] since Unix Epoch.
  ///
  /// **Warning:** Temporary API, will be removed in 0.20.0.
  /// Use this for intermediate migration of `now` prior to 0.19.0.
  Duration get nowDuration;
}

Then in 0.19.0, the deprecation of now will be removed when its type is updated and nowDuration will get a deprecation message.

The implementation of nowDuration would be something like the following:

class WebTimeProvider implements TimeProvider {
  static final Duration timeOrigin = Duration(
    milliseconds: (window.performance.timeOrigin ??
            window.performance.timing.navigationStart)
        .round(),
  );

  @override
  Int64 get now => fromDOMHighResTimeStamp(window.performance.now());

  @override
  Duration get nowDuration =>
      WebTimeProvider.timeOrigin +
      Duration(microseconds: (window.performance.now() * 1000).round());
}
class DateTimeTimeProvider implements TimeProvider {
  @override
  Int64 get now => Int64(DateTime.now().microsecondsSinceEpoch) * 1000;

  @override
  Duration get nowDuration =>
      Duration(microseconds: DateTime.now().microsecondsSinceEpoch);
}

@evanweible-wf evanweible-wf removed their request for review April 25, 2025 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants