-
Notifications
You must be signed in to change notification settings - Fork 38
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
base: master
Are you sure you want to change the base?
Conversation
lib/src/sdk/logs/log_record.dart
Outdated
@override | ||
Int64? get timeStamp => _timeStamp != null ? Int64(_timeStamp!.microsecondsSinceEpoch) * 1000 : _timeProvider.now; | ||
|
||
@override | ||
Int64? get observedTimestamp => | ||
_observedTimestamp != null ? Int64(_observedTimestamp!.microsecondsSinceEpoch) * 1000 : _timeProvider.now; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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();
}
There was a problem hiding this comment.
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.
Future<void> forceFlush() async { | ||
await Future.forEach(processors, (e) => e.forceFlush()); | ||
} | ||
|
||
Future<void> shutdown() async { | ||
await Future.forEach(processors, (e) => e.shutdown()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are these async?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this class necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really, but from opentelemetry-js it is there https://github.com/open-telemetry/opentelemetry-js/blob/15ba2d7da3bd6663ee6008e189f004bab93ff6fe/experimental/packages/sdk-logs/src/export/NoopLogRecordProcessor.ts#L21 so I put it for parity.
Let me know if I should remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove it
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated to ReadWriteLogRecord
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( |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated
Future<void> forceFlush() async { | ||
await Future.forEach(processors, (e) => e.forceFlush()); | ||
} | ||
|
||
Future<void> shutdown() async { | ||
await Future.forEach(processors, (e) => e.shutdown()); | ||
} |
There was a problem hiding this comment.
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
After changed the return type to |
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed, changed to class
lib/src/sdk/logs/logger.dart
Outdated
body: body, | ||
timeProvider: timeProvider, | ||
); | ||
onLogEmit?.call(log); |
There was a problem hiding this comment.
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
lib/src/sdk/common/limits.dart
Outdated
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); |
There was a problem hiding this comment.
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);
}
There was a problem hiding this comment.
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
lib/src/sdk/logs/log_record.dart
Outdated
@override | ||
Int64? get timeStamp => _timeStamp != null ? Int64(_timeStamp!.microsecondsSinceEpoch) * 1000 : _timeProvider.now; | ||
|
||
@override | ||
Int64? get observedTimestamp => | ||
_observedTimestamp != null ? Int64(_observedTimestamp!.microsecondsSinceEpoch) * 1000 : _timeProvider.now; |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
Requesting for review on #207 (comment) |
hi @blakeroberts-wk . Can I get some feedback on #207 (comment) if I got this right? |
acd8b32
to
d4a919f
Compare
d4a919f
to
741ed89
Compare
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; |
There was a problem hiding this comment.
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
final int _attributeCountLimit; | ||
final int _attributeValueLengthLimit; |
There was a problem hiding this comment.
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?
final sdk.InstrumentationScope instrumentationScope; | ||
final sdk.Resource _resource; | ||
final sdk.LogRecordLimits logRecordLimits; | ||
final sdk.TimeProvider timeProvider; | ||
final List<sdk.LogRecordProcessor> processors; |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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);
}
Which problem is this PR solving?
How Has This Been Tested?
Checklist: