-
Notifications
You must be signed in to change notification settings - Fork 836
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
Sync SDK - Async resource detection #1484
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1484 +/- ##
==========================================
- Coverage 93.74% 93.28% -0.46%
==========================================
Files 152 138 -14
Lines 4635 3858 -777
Branches 931 776 -155
==========================================
- Hits 4345 3599 -746
+ Misses 290 259 -31
|
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.
Generally it looks very good.
I have one big concern with regards getFinishedSpans
. The following piece of code even strengthen that
await new Promise(resolve => setTimeout(resolve));
const spans = memoryExporter.getFinishedSpans();
And because getFinishedSpans
is a public api so people might be confused why it doesn't contain the finished spans after this change. Based on that I would make getFinishedSpans
to be a Promise.
This way you would be able to call await memoryExporter.getFinishedSpans()
which will look more intuitive.
Otherwise this might produce many unwanted bugs and confusion why something doesn't work and why I'm not getting finished spans if they are already finished. WDYT ?
The problem is not that const span1 = tracer.startSpan('1');
const span2 = tracer.startSpan('2');
span1.end();
await exporter.getFinishedSpans(); // does this wait for 1 or 2 spans? Also, the exporter doesn't even know the span exists until the span processor tells it, and that only happens after the span ends. So there would be no way for the exporter to even know if any spans are started, let alone how many to wait for. |
@dyladan I was thinking of keeping it as a promise so it can resolve with next tick so in the mentioned example
As currently the same piece of code would produce exactly the same effect (you would get |
And method |
or we should then convert method |
edit: fixed, but I still don't like the global SIGTERM handling. IMO that should be handled by the end user app |
Could we just have a |
}); | ||
|
||
it('should export spans on graceful shutdown from two span processor', () => { | ||
it('should export spans on graceful shutdown from two span processors', done => { |
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.
@obecny this test fails in browser because the unload event handler is actually never called. It wasn't called previously, but it was never caught because the assertions are never run. I changed the test to have a done
callback and wait for finish, and it never finishes.
I expect that |
@obecny @mwear The longer this PR is open and the more I work on it, the less I like this solution. My primary issue with it is the export pipeline becoming asynchronous. For an example of why this is a problem, take a lambda function. In lambda, you want to force flush on every invocation because you don't know if your function will be called again in the future and you don't want to lose the traces. Currently, it is possible to do something like this: async function handler(event) {
const span = tracer.startSpan("invoke");
// some work
span.end()
await exporter.forceFlush();
return someObj;
} If we land this PR, the forceFlush will not export the span because the resource promise will not yet be resolved and the span will not yet have been sent to the span processor and exporter. |
That's why I originally deferred the span resource on end not on start. This way the span was already in processor and then you could simply wait for it until it ends. I was thinking of moving this logic to span processor and then defer the spans that needs to wait for the resource. |
That still has the same problem of making the export pipeline async. I am working on another approach right now that makes the |
@dyladan what next with this ? |
After talking to the maintainers group and the spec, they want us to hold off. None of the other sigs have async/remote resource detection and if the user needs that they're expected to do it themselves. They want to specify something here after GA |
What is the current workaround for sync startup, then? So far I just set |
Well i've wrote this workaround that work for me: import { detectResources, Resource } from '@opentelemetry/resources'
import { gcpDetector } from '@opentelemetry/resource-detector-gcp'
export class CustomResource extends Resource {
public attributes: ResourceAttributes = {}
addAttributes (attributes: ResourceAttributes) {
this.attributes = Object.assign(this.attributes, attributes)
return this
}
}
const run = () => {
detectResources({
detectors: [ gcpDetector ],
logger
}).then((detectedResources) => {
resource.addAttributes(detectedResources.attributes)
}).catch(err => {
logger?.error(`Error while detecting ressources`, err)
})
const provider = new NodeTracerProvider({
resource
})
api.trace.setGlobalTracerProvider(provider)
} However this is non-compliant in regard of the spec, sadly i think that would be the easiest way to solve this problem :/ |
Could resources be "pre-detected", maybe put an environment variable that specifies exactly what is being detected there in a way it can be loaded synchronously? I'm not running this in a variety of environments, so I don't need auto-detection. |
Definitely. You can always pass a resource to the constructor of the |
FWIW I have been using this pattern for async loading, and it's been working for me: Assume my service is started by calling const opentelemetry = require("@opentelemetry/sdk-node");
const process = require("process");
const sdk = new opentelemetry.NodeSDK({
// configure exporters, etc
})
// start the sdk and wait for any installed resource detection to run.
sdk.start().then(() => {
// require your original application startup file here.
require('./server');
});
function shutdown(){
sdk.shutdown()
.then(
() => console.log("SDK shut down successfully"),
(err) => console.log("Error shutting down SDK", err),
)
.finally(() => process.exit(0))
};
process.on('beforeExit', shutdown);
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown); This allows for async sdk loading, without the risk of pre-loading any of my application modules or making a hash out of my original application startup in Obviously, if async resources are going away, a sync approach is easier. But the approach above feels clean, and requires no changes to what we have today. Am I missing something important? Every other approach I have tried has been a pretzel, but this two-phase pattern feels elegant, or at least easy. |
The problem with the two-phase approach is that the scripts I am running with instrumentation are not instrumentation-aware and use |
Thank for the clarification @dobesv, I had forgotten about that pattern. |
Hi @dyladan - just curious if this PR may get resurrected? The issue of resource detection being async has come up in the lambda support because the SDK may not be ready on the first due to the detectors being async, it would be nice if there's a way to allow the Resource to not block initialization of the SDK |
No chance. It actually isn't implementable this way in the current spec because the spec requires |
There have been quite a few conversations around making resources appendable, which would in my opinion be a much better solution to this problem, but every time someone tries to make the spec change there are people who fight it. I have tried several times and at this point it feels to me like it's just not going to happen. |
* chore: release main * chore: release main
* chore: release main * chore: release main
* chore: release main * chore: release main
This makes it so that the resource supplied to the providers may be a
Resource
or aPromise<Resource>
which allows async detection to be applied without waiting for it to finish before starting the SDK.Summary of changes:
Tracer and Meter Providers
config.resource
may now be aResource
or aPromise<Resource>
this.resource
is nowPromise<Resource>
Tracer and Meter
config.resource
may now be aResource
or aPromise<Resource>
this.resource
is nowPromise<Resource>
Span
this.resource
property is nowPromise<Resource>
onStart
/onEnd
Metric base class
Promise<Resource>
instead ofResource
this.resource
is nowPromise<Resource>
getMetricRecord
*Metric classes
Promise<Resource>
instead ofResource
Tests
Many of the tests assumed that the export would happen synchronously. For instance, it was not uncommon to see the following in tests:
Now that the span start/end may wait before calling the processor, it is necessary to yield to the event loop before getting finished spans: