Skip to content

feat(Otel4s metrics): allow attaching custom attributes to recorded metrics - #2998

Open
remimomprive wants to merge 3 commits into
softwaremill:masterfrom
remimomprive:claude/sttp-otel-custom-attributes-5jepzd
Open

remimomprive wants to merge 3 commits into
softwaremill:masterfrom
remimomprive:claude/sttp-otel-custom-attributes-5jepzd

Conversation

@remimomprive

@remimomprive remimomprive commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Otel4sMetricsBackend records the standard OTel HTTP client metrics with the attributes defined by the semantic conventions. There was no way to attach an application-specific attribute (e.g. a business dimension such as a flow name) to the recorded measurements.

Add an Otel4sMetricsBackend.AttributesKey request attribute, holding the Attributes to add to all metrics recorded for that request: http.client.request.duration, http.client.request.body.size, http.client.response.body.size and http.client.active_requests.

basicRequest
  .get(uri"https://example.com/orders/42")
  .attribute(Otel4sMetricsBackend.AttributesKey, Attributes(Attribute("flow", "checkout")))

Specifying the attributes per request, rather than as a function in Otel4sMetricsConfig, keeps the call site that knows the labels decoupled from the (often shared) code that builds the backend. Otel4sMetricsConfig is unchanged, and no attributes are added unless a request carries them.

A feature like this is already implemented in go: open-telemetry/opentelemetry-go-contrib#5129

Before submitting pull request:

  • Check if the project compiles by running sbt compile
  • Verify docs compilation by running sbt compileDocs
  • Check if tests pass by running sbt test
  • Format code by running sbt scalafmt

`Otel4sMetricsBackend` records the standard OTel HTTP client metrics with
the attributes defined by the semantic conventions. There was no way to
attach an application-specific attribute (e.g. a business dimension such
as a flow name) to the recorded measurements.

Add an `Otel4sMetricsBackend.AttributesKey` request attribute, holding the
`Attributes` to add to all metrics recorded for that request:
`http.client.request.duration`, `http.client.request.body.size`,
`http.client.response.body.size` and `http.client.active_requests`.

    basicRequest
      .get(uri"https://example.com/orders/42")
      .attribute(Otel4sMetricsBackend.AttributesKey, Attributes(Attribute("flow", "checkout")))

Specifying the attributes per request, rather than as a function in
`Otel4sMetricsConfig`, keeps the call site that knows the labels decoupled
from the (often shared) code that builds the backend. `Otel4sMetricsConfig`
is unchanged, and no attributes are added unless a request carries them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011h5n8NqBUjPZYHiWkfgoQz
@remimomprive

Copy link
Copy Markdown
Contributor Author

@adamw What do you think about this one?

@adamw

adamw commented Sep 16, 2026

Copy link
Copy Markdown
Member

Automated review by Claude Code.

Verified locally: module compiles, otel4sMetricsBackend3/test passes (10 tests), compileDocs is clean. No correctness bugs found. Two comments on the chosen semantics/API:

1. Custom attributes can override semantic convention ones

In Otel4sMetricsBackend.scala, customAttributes(request) is added last, both in activeRequestAttributes and in fullAttributes. otel4s Attributes.Builder keys by attribute name only, ignoring the type. So a request carrying Attribute("http.response.status_code", "200") (a String) replaces the backend's Long-typed attribute. Same applies to error.type, http.request.method, server.address.

The effect: http.client.request.duration no longer follows the semantic conventions the backend advertises, and anything reading those attributes as numbers breaks. The existing semantic test doesn't catch this, as it sets no custom attributes.

Suggestion: add the custom attributes first, so the semantic convention ones always win. The docs sentence in docs/backends/wrappers/opentelemetry.md would need updating too.

2. A fixed public AttributeKey instead of a config field

The public AttributesKey is a different extension mechanism than the two siblings use: Otel4sMetricsConfig already has urlTemplate: GenericRequest[_, _] => Option[String], whose docs show exactly this "read it from a request attribute" pattern, and the Java OTel OpenTelemetryMetricsBackend uses config.requestAttributes / config.responseAttributes.

A config field, e.g. extraAttributes: GenericRequest[_, _] => Attributes = _ => Attributes.empty, gives the same decoupling at the call site (the call site stores its own typed attribute, the config reads it), plus the option to derive attributes from the request. It's also easier to change later than a public val.

@remimomprive

Copy link
Copy Markdown
Contributor Author

Thanks for the review!

I will handle the point 1.

For the point 2, my goal was to provide a way to specify the custom attributes per request.
If we add it to the config, the value will be shared across all the endpoints.

…ion ones

`Attributes.Builder` keys by attribute name only, ignoring the type, so a
request carrying e.g. `Attribute("http.response.status_code", "200")` (a
String) replaced the backend's Long-typed attribute. The same applied to
`error.type`, `http.request.method` and `server.address`. The recorded
metrics then no longer followed the semantic conventions the backend
advertises, and consumers reading those attributes as numbers would break.

Add the custom attributes first in both `activeRequestAttributes` and
`fullAttributes`, so the semantic convention attributes added afterwards
always take precedence.

The existing semantic test does not catch this, as it sets no custom
attributes; add a test which sets colliding attributes of a different type
and asserts the recorded ones are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011h5n8NqBUjPZYHiWkfgoQz
@remimomprive
remimomprive force-pushed the claude/sttp-otel-custom-attributes-5jepzd branch from 8b1e9e1 to 9a8d499 Compare September 16, 2026 19:14
…fig field

The public `Otel4sMetricsBackend.AttributesKey` was a different extension
mechanism than the one used by the siblings: `Otel4sMetricsConfig` already
has `urlTemplate`, whose docs show the same "read it from a request
attribute" pattern, and `OpenTelemetryMetricsConfig` (the Java OTel
backend) has `requestAttributes` / `responseAttributes`.

Replace it with `extraAttributes: GenericRequest[_, _] => Attributes`,
defaulting to no attributes. This gives the same decoupling at the call
site - which stores its own typed request attribute, read by the configured
function - and additionally allows the attributes to be derived from the
request. A config field is also easier to evolve than a fixed public key.

The attributes are still added to the builder first, so that the semantic
convention ones always take precedence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011h5n8NqBUjPZYHiWkfgoQz
@remimomprive

Copy link
Copy Markdown
Contributor Author

I think I have something relevant for the point 2 in the last commit

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants