-
Notifications
You must be signed in to change notification settings - Fork 845
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
feat: allow Resource to be created with some attributes resolving asynchronously #2933
Conversation
Codecov Report
@@ Coverage Diff @@
## main #2933 +/- ##
==========================================
- Coverage 92.64% 91.32% -1.33%
==========================================
Files 187 68 -119
Lines 6169 1878 -4291
Branches 1303 399 -904
==========================================
- Hits 5715 1715 -4000
+ Misses 454 163 -291
|
I really like this proposal - to propagate the async task down to resource attribute consumers. The promise means that every time we need to access this resource attributes, the caller will have to For example, in aspecto's distro we read the resource attributes in a span processor and use the values to apply filtering rules on span attributes. This is done in the Another possible issue is that we cannot have any guarantee on how long it will take for the promise to resolve. Until it resolves, spans might be piling up in the exporter Really interesting approach, looking forward to discussing this more. |
fd6d202
to
3ae3618
Compare
According to spec resource is immutable. I think delaying the read until first use in e.g. exporter sounds to me like it's still spec conform. |
3ae3618
to
2f60366
Compare
I wonder if top level await + esm preload would be able to achieve this as well? @aabmass / @Flarna , do you know? EDIT Played around with it a bit. For ESM, adding an loader module would work and is able to asyncronously wait for stuff to resolve. The thing is, it's only for ESM. For CJS, there's no TLA, so we still need a solution for today. |
That's quite cool. Is the limitation of only a single loader module removed yet? |
I think top Level await can be used but as far as I know it works only in ECMAScript module mode not for commonJs. I requires node.js 14.8 as far as I know. Not sure about browsers. The limitation to have only single loader is gone on node master (see here), backport to 18.x has not yet happened as some other breaking changes in loaders are planned and target is to combine all breaking changes in a single 18.x release. |
01ba5d5
to
944e392
Compare
46ecbba
to
f3c9913
Compare
f3c9913
to
eb6446f
Compare
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 this is ready for reviews. I am planning to add more tests in a follow up PR to keep the size manageable
// Avoid scheduling a promise to make the behavior more predictable and easier to test | ||
if (pendingResources.length === 0) { |
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.
Without this check, I had to make extensive changes to the tests because the fire-and-forget nature of L179
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 guess now span processors are expected to handle these async promises? We already have some users which have implemented custom span processors. I guess this is considered breaking for them? What does the spec say about breaking changes for sdk interface implementers?
@@ -130,9 +130,9 @@ export class NodeSDK { | |||
/** | |||
* Once the SDK has been configured, call this method to construct SDK components and register them with the OpenTelemetry API. | |||
*/ | |||
public async start(): Promise<void> { | |||
public start(): void { |
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.
🎉
|
||
// SpanAttributes from resource overwrite attributes from other resource. | ||
const mergedAttributes = Object.assign( | ||
{}, | ||
this.attributes, | ||
other.attributes | ||
); | ||
return new Resource(mergedAttributes); | ||
|
||
let mergedAsyncAttributesPromise: Promise<ResourceAttributes> | undefined; |
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.
Worth checking if async promises are resolved and adding them to the sync attributes? Probably not very likely since resources will generally all be constructed almost at the same time.
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 thinking to leave it like since we already have a few special cases already. Not a strong opinion tho
let mergedAsyncAttributesPromise: Promise<ResourceAttributes> | undefined; | ||
if (this.asyncAttributesPromise && other.asyncAttributesPromise) { | ||
mergedAsyncAttributesPromise = Promise.all([ | ||
this.asyncAttributesPromise.catch(() => ({})), |
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.
no error handling of any kind?
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 these extra catch handlers needed at all? There is already one add in constructor.
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 catch handler in the constructor is not part of the this.asyncAttributesPromise (there is a branch in the promise tree). The idea is that waitForAsyncAttributes()
will reject if the promise rejected so a user can handle it still.
this.asyncAttributesPromise
(adds the async attributes to attributes)
/ | \
/ | \
.catch handler | .waitForAsyncAttributes()
for logging |
|
.catch() here to to avoid
Promise.all() from rejecting
Open to suggestions on how to simplify this. I would use Promise.allSettled()
but it's not available
*/ | ||
export interface Detector { | ||
detect(config?: ResourceDetectionConfig): Promise<Resource>; | ||
detect(config?: ResourceDetectionConfig): Promise<Resource> | Resource; |
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.
@open-telemetry/javascript-maintainers Can this be considered a breaking change? Users shouldn't be calling resource detectors directly but if they are I can easily see some code like detect().then(something)
breaking (TypeError: detect(...).then is not a function).
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 this is a breaking 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.
Any ideas on alternatives?
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.
* @returns true if no async attributes promise was provided or if the promise has resolved | ||
* and been merged together with the sync attributes. | ||
*/ | ||
asyncAttributesHaveResolved(): boolean { |
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.
maybe use a getter here?
@@ -25,6 +26,10 @@ import { defaultServiceName } from './platform'; | |||
*/ | |||
export class Resource { | |||
static readonly EMPTY = new Resource({}); | |||
private _attributes: ResourceAttributes; | |||
private asyncAttributesPromise: Promise<ResourceAttributes> | undefined; |
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.
private asyncAttributesPromise: Promise<ResourceAttributes> | undefined; | |
private _asyncAttributesPromise: Promise<ResourceAttributes> | undefined; |
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.
Happy to do this, but curious why since we have private
. Is it for people using vanilla JS?
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.
It is a convention in many TS projects including this one.
- When looking at the generated JS code it is clear what is private
- When accessing with
obj["propname"]
syntax it is possible to access private properties without TS complaining
Maybe other reasons but those are the ones on top of my mind
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.
personally I don't care about using _ or not. But in this project it's used (hopefully) everywhere and I think we should stay consistent.
let mergedAsyncAttributesPromise: Promise<ResourceAttributes> | undefined; | ||
if (this.asyncAttributesPromise && other.asyncAttributesPromise) { | ||
mergedAsyncAttributesPromise = Promise.all([ | ||
this.asyncAttributesPromise.catch(() => ({})), |
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 these extra catch handlers needed at all? There is already one add in constructor.
const resourceOrPromise = d.detect(internalConfig); | ||
let resource: Resource; | ||
if (resourceOrPromise instanceof Promise) { | ||
diag.info('Resource detector %s should return a Resource directly instead of a promise.', d.constructor.name); |
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 might result in quite some log spam. The detectors API allows to return a Promise<Resource>
and it's done at a lot places so logging here as info seems to be not correct to me.
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.
Ack on the spam. Would we like to move all existing resource detectors to return the non promise form? I.e. deprecate the Promise<Resource>
variant?
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.
Ideally yes, but it needs to be considered carefully. We probably can't remove the async detectors in many cases and would be breaking. #3055 for details
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 all detectors could be moved to sync return a Resource
object which includes the optional async part.
But I think we should define a deprecation cycle for such changes which should include docs, deprecation logs (maybe to be enabled via some OTEL_LOG_DEPRECATION env var but that's a separate discussion).
As of now returning Promise<Resource>
is ok and most/all detectors do this so we should not yet log like this.
|
||
it('should return true for asyncAttributesHaveResolved() if no promise provided', () => { | ||
assert.ok(new Resource({'foo': 'bar'}).asyncAttributesHaveResolved()); | ||
assert.ok(Resource.empty().asyncAttributesHaveResolved()); |
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 is already on line 107, maybe remove one of them
const detector: Detector = { | ||
async detect() { | ||
return new Resource( | ||
{ 'sync': 'fromsync', 'async': 'fromasync' }, |
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.
it doesn't matter for the test but the 'async': 'fromasync'
entry is actually sync here.,
@@ -166,6 +168,17 @@ export abstract class BatchSpanProcessorBase<T extends BufferConfig> implements | |||
} | |||
} | |||
); | |||
|
|||
const pendingResources = spans.map(span => span.resource) |
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 it really needed to iterate all spans on every flush? Actually all spans should refer to the same Resource
instance therefore checking the first span should be fine. This would reduce the number of promises created and avoid the need of a Promise.all
below.
Once this resource of this span is resolved it should be not needed anymore to check again.
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 remember there was some proposal to allow Resource to change for browser sessions, not sure if that is still a concern. Otherwise I'm happy to just check the first one. Any thoughts @dyladan ?
if (pendingResources.length === 0) { | ||
doExport(); | ||
} else { | ||
Promise.all(pendingResources.map(resource => resource.waitForAsyncAttributes())) |
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 resource detection is slower then _exportTimeoutMillis
we may end up in exporting spans even the scheduled export has already timed out.
Usually parallel exports are not done by BatchSpanProcessor.
if (span.resource.asyncAttributesHaveResolved()) { | ||
doExport(); | ||
} else { | ||
span.resource.waitForAsyncAttributes() |
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.
SimpleSpanProcessor
and async exporting is not the best match as the main intention of SimpleSpanProcessor is to export immediately. But I have no idea how to solve that...
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 do you mean by async exporting here? It's already fire and forget on the export, since on_end()
can't block
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 simple span processor is meant to be as simple as possible and as fast as possible. The fact that everything happens synchronously means that in an ephemeral environment like lambda export is fully synchronous until the exporter itself makes it async (or doesn't in case of e.g. log exporter)
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.
To my understanding the main usecase for SimpleSpanProcessor
is local development and using a console exporter (or similar) as the overhead in production to do a http request per span would be crazy.
This usecase is somewhat "broken" as you may not see your span on console after span.end()
.
Unfortunately I have no good solution for this... One could say for local development it's also possible to us a non async resource setup and problem is solved.
I would consider this also as a breaking change so this would be semver major. |
This PR actually introduces another problem from the specification:
|
I actually realized this isn't a problem. If it was, then the batch span processor would be not compliant. |
This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 14 days. |
This PR was closed because it has been stale for 14 days with no activity. |
Which problem is this PR solving?
Fixes #2912
Short description of the changes
@opentelemetry/sdk-node
Promise<ResourceAttributes>
to the Resource constructor which can asynchronously resolve some attributes. When this promise resolves, the attributes will be merged with the "synchronous" attributes.detectResources()
in favor of a new methoddetectResourcesSync()
which defers promises into the resource to be resolved laterDetector.detect()
signature to allow it to be synchronous and updated documentation that the synchronous variant should be used@opentelemetry/sdk-node
, updatedNodeSDK.start()
and its dependencies to now be synchronous (doesn't return a promise). This is a breaking change, but that package is experimental.BatchSpanProcessorBase
andSimpleSpanProcessor
to await the resource promise before calling exporters, if the resource has asynchronous attributes AND they have not already resolved. I.e. if there is no promise in the Resource (the case for existing detectors and code), the behavior is unchanged.There may be some test breakages in contrib because of the nature of asynchronous tests on fire-and-forget
span.end()
. Users shouldn't see any behavior change.Type of change
Please delete options that are not relevant.
How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
Resource.test.ts
detect-resources.test.ts
Checklist: