feat: collect service override source#4500
feat: collect service override source#4500gh-worker-dd-mergequeue-cf854d[bot] merged 11 commits intomainfrom
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files
🚀 New features to boost your workflow:
|
|
✅ Tests 🎉 All green!❄️ No new flaky tests detected 🎯 Code Coverage (details) 🔗 Commit SHA: b9ab5b0 | Docs | Datadog PR Page | Was this helpful? React with 👍/👎 or give us feedback! |
b2c209c to
4512e9e
Compare
5eb2d95 to
202b067
Compare
BenchmarksBenchmark execution time: 2026-03-12 18:11:15 Comparing candidate commit b9ab5b0 in PR branch Found 0 performance improvements and 5 performance regressions! Performance is the same for 152 metrics, 7 unstable metrics.
|
9004f07 to
3a4506d
Compare
contrib/database/sql/option.go
Outdated
| } | ||
| cfg.dbmPropagationMode = tracer.DBMPropagationMode(mode) | ||
| cfg.serviceName = defaultServiceName(driverName, rc) | ||
| cfg.serviceSource = "database/sql" |
There was a problem hiding this comment.
For each contrib, we already have something called componentName that is pulled from our instrumentation package. For this contrib, it's set in the sql.go file, but also accessible at instrumentation.PackageDatabaseSQL. Instead of creating a new string, can we reuse that variable?
There was a problem hiding this comment.
This also goes for the other contribs + future contribs changes
| assert.Len(spans, 1) | ||
| span := spans[0] | ||
| assert.Equal("global-service", span.Tag(ext.ServiceName)) | ||
| assert.Nil(span.Tag("_dd.svc_src"), "_dd.svc_src should not be set when service matches global service") |
There was a problem hiding this comment.
Instead of using _dd.svc_src, let's import ext (see one of my comments below)
internal/tracer.go
Outdated
| // KeyServiceSource is the internal span meta key for tracking the origin of a | ||
| // service name override. It is intercepted by the tracer and stored in an | ||
| // internal field rather than written directly to span meta. | ||
| const KeyServiceSource = "_dd.svc_src" |
There was a problem hiding this comment.
Can we move this to tag.go (under ddtrace/ext)? We have other span tags living there, and then we don't have to reflect this in span.go or hardcode the string from integrations, as I mentioned above (just import ext).
Of course, that's a public package, so if we want to keep this internal then maybe that's not the best idea. I'm open to discussing this! I just want to avoid hardcoding the strings in tests/from the contribs as we currently are doing.
contrib/gin-gonic/gin/option.go
Outdated
| } | ||
|
|
||
| func newConfig(serviceName string) *config { | ||
| serviceSource := "opt.middleware" |
There was a problem hiding this comment.
I wonder if it's worth having a file of potential serviceSource values stored somewhere in instrumentation or in ddtrace/ext for the commonly used values (ie opt.middleware, manual, opt.mapping). Up to you to decide if this is worthwhile (to avoid magic strings).
rarguelloF
left a comment
There was a problem hiding this comment.
Nice! This is a cool feature :) left some nit comments
instrumentation/instrumentation.go
Outdated
|
|
||
| // ServiceSourceMiddleware is the service source value used when the service | ||
| // name is set via a middleware parameter (e.g. gin's Middleware(serviceName)). | ||
| ServiceSourceMiddleware = "opt.middleware" |
There was a problem hiding this comment.
hmm I'm not sure this would be a generic enough pattern to be considered here.
Basically because of legacy/historical reasons, we have a number of integrations where the main function that "installs" the integration requires the service name as a mandatory argument. In the case of gin, Middleware is the main entrypoint of the integration and sadly it requires the mandatory argument so we will see many service overrides here as a consequence.
Ideally we will deprecate these functions at some point and follow the same pattern as the rest of the integrations (the service name being optional through the opt.WithService option).
My suggestion here:
- something like
entrypointArgumentto signal its just an integration where the main entrypoint has a mandatoryserviceargument (maybe this will be confusing among some folks? not sure). - just an specific
gin.Middlewareorhttp.WrapHandlerfunctions that are doing this (I don't think there are that many in the repo).
WDYT?
There was a problem hiding this comment.
sounds good, I'll keep the prefix opt. as it's used to distinguish what is a user option, in our backend and is not user facing. For example here the customer even if forced to use the API could leave it empty string
We'll have to discuss plan to deprecate
instrumentation/instrumentation.go
Outdated
|
|
||
| // ServiceSourceDriverName is the service source value used when the service | ||
| // name is derived from the driver name (e.g. database/sql Register driverName). | ||
| ServiceSourceDriverName = "opt.drivername" |
There was a problem hiding this comment.
I don't think this is generic enough to have it in the shared instrumentation package (I think database/sql is the only place where we do something like this). WDYT about sql.inferredFromDriver?
mtoffl01
left a comment
There was a problem hiding this comment.
Mainly nits.
Also:
- I'm a little concerned that the new ServiceNameWithSource API, which modifies both service name and service source, does not have a clear enough purpose, given we have a separate API for setting service name alone. Is it necessary to bundle them together, or can we separate APIs for service name and service source?
- In the screenshot found in the PR description, the svc src tag values are in different formats (some are camel case while some are not, some have dot notation while others include a slash). I think it's important to standardize the format, both in terms of characters/casing as well as maybe defining prefixes, similar to how Datadog Metrics have standard prefixes
|
|
||
| // serviceSourceGinMiddleware is the service source value used when the service | ||
| // name is set via gin's Middleware(serviceName) parameter. | ||
| const serviceSourceGinMiddleware = "opt.gin_middleware" |
There was a problem hiding this comment.
This seems inconsistent with serviceSourceSQLDriver.
This one is set when a user has explicitly passed in a serviceName parameter to Middleware, but serviceSourceSQLDriver is set in the default case.
There was a problem hiding this comment.
yes the behaviors are actually different between the integrations
- on gin if the string is empty, the default services become the tracer global service
- on database/sql if the string is empty, the service still is built from it and results in
.db
See the following tests
One of my planned follow-up is to deprecate these APIs that pushes/forces users to have service overrides that are then confusing in the product. Replacing them with the options WithService for those who want to keep the behaviour
There was a problem hiding this comment.
As far as I understand this new property the source is the place where the name comes from.
For SQL opt.sql_driver is used when the name is set by the package. It makes sense here as well because the middleware is the origin of the name and when no name is given we fallback to another source (default)
| service: s.service, | ||
| pprofCtx: s.pprofCtxActive, | ||
| service: s.service, | ||
| serviceSource: s.serviceSource, |
There was a problem hiding this comment.
I'm concerned that this could be misleading.
Let's say we "inherit" context from a service we don't own -- like some third party client made the request and we're serving it. This client, let's say, had instrumentation configured using the WithService parameter.
On our server side, we'll see that as the service source without having any insight into it. Could be confusing.
There was a problem hiding this comment.
yes, but it would map the service on the spans that were set with WithService. The service source targets service overrides (service set on spans) which is different from the base service source project that is set on process level tags
There was a problem hiding this comment.
inheritedData serves as a default value for a span created as child of another span.
- If the span has no
WithServicethis means that we're still in the same service as the parent meaning that we can re-use the same value - If the span has
WithServiceit will change the service andserviceSourcevalues to what we want
| // or when no source was determined. | ||
| // +checklocks:s.mu | ||
| func (s *Span) enrichServiceSource() { | ||
| if s.serviceSource == "" || s.service == globalconfig.ServiceName() { |
There was a problem hiding this comment.
(For internal conversations)
@genesor see globalconfig.ServiceName() -- here is another example where global access to service name is blocked by config migration.
globalconfig was created to share tracer config data across packages; it works as the "source of truth" here only because globalconfig.SetServiceName is called after the tracer.config has evaluated its own service name.
Since we are within the tracer package here, we don't really have a need for it, other than it being a convenient API. It's probably safe to use in this case but it's not ideal.
Alternatives (other than completing service name migration) include...
- get access to tracer.config's service name via GetGlobalTracer() and calling it's
configfield? - expand TracerConf() to share service name....
|
@mtoffl01 thanks for the review, regarding your questions:
|
genesor
left a comment
There was a problem hiding this comment.
nit about naming to have something a tad clearer otherwise this looks fine to me.
instrumentation/instrumentation.go
Outdated
| const ( | ||
| // ServiceSourceWithService is the service source value used when the service | ||
| // name is explicitly set via a WithService option. | ||
| ServiceSourceWithService = "opt.WithService" |
There was a problem hiding this comment.
nit: the name would be clearer if extended with `Option
| ServiceSourceWithService = "opt.WithService" | |
| ServiceSourceWithServiceOption = "opt.WithService" |
contrib/database/sql/option.go
Outdated
| } | ||
| cfg.dbmPropagationMode = tracer.DBMPropagationMode(mode) | ||
| cfg.serviceName = defaultServiceName(driverName, rc) | ||
| cfg.serviceSource = serviceSourceSQLDriver |
There was a problem hiding this comment.
Would be nice to have the defaultServiceName func handling both name and source.
If it causes to much change having an helper func defaultSource could work.
|
|
||
| // serviceSourceGinMiddleware is the service source value used when the service | ||
| // name is set via gin's Middleware(serviceName) parameter. | ||
| const serviceSourceGinMiddleware = "opt.gin_middleware" |
There was a problem hiding this comment.
As far as I understand this new property the source is the place where the name comes from.
For SQL opt.sql_driver is used when the name is set by the package. It makes sense here as well because the middleware is the origin of the name and when no name is given we fallback to another source (default)
| service: s.service, | ||
| pprofCtx: s.pprofCtxActive, | ||
| service: s.service, | ||
| serviceSource: s.serviceSource, |
There was a problem hiding this comment.
inheritedData serves as a default value for a span created as child of another span.
- If the span has no
WithServicethis means that we're still in the same service as the parent meaning that we can re-use the same value - If the span has
WithServiceit will change the service andserviceSourcevalues to what we want
This PR is a first of many that enriches service source to service overrides. In particalar the PR contains:
instrumentation.ServiceNameWithSourcemethod to be used by all integrationsSee other similar PRs in dd-trace-java PR1 - integration services, PR2- client stats, PR3 - config cases, PR4 - manual source
Reviewer's Checklist
make lintlocally.make testlocally.make generatelocally.make fix-moduleslocally.Unsure? Have a question? Request a review!