Skip to content

feat: expose OTel semantic convention version in trace metadata#166

Open
AaronProbha18 wants to merge 4 commits into
agentevals-dev:mainfrom
AaronProbha18:feat/otel-genai-attr-aliases
Open

feat: expose OTel semantic convention version in trace metadata#166
AaronProbha18 wants to merge 4 commits into
agentevals-dev:mainfrom
AaronProbha18:feat/otel-genai-attr-aliases

Conversation

@AaronProbha18

Copy link
Copy Markdown

Description

  • Capture and propagate OTel schema_url to expose semconv_version in conversion metadata.
  • Resolve GenAI attribute aliases at read time via the extractor (extract_*_from_attrs) without rewriting attributes during ingestion.
  • Introduce a flat attribute resolver for canonical-first alias lookup (currently for gen_ai.provider.namegen_ai.system).
  • Keep semantic convention version informational only; no schema-version-based branching or GenAI detection changes.
  • Add test coverage for alias resolution, semconv_version extraction, metadata propagation, and API serialization.

Related Issues

Closes #162

Notes

  • Scope intentionally limited to the extractor and conversion metadata; no run/storage model changes or migrations.
  • otel.schema_url is treated as generic OTel metadata and not as a GenAI detection signal.
  • Fix: add explicit UTF-8 encoding when opening files in loaders and runner.

@krisztianfekete krisztianfekete left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR, it's going in the correct direction as per our discussion on the issue. Added some comments!

Comment thread src/agentevals/loader/auto.py Outdated
"""
try:
with open(path) as f:
with open(path, encoding="utf-8") as f:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not really part of this exercise, and since it's incomplete, I'd rather have this done properly in a follow-up PR.

Can you pull this one out and open another PR with the changes covering all relevant paths?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense!
I've reverted the changes in this PR and raised a new PR that covers all the relevant UTF-8 encoding updates here: #167

Comment on lines +68 to +72
# Matches a semver-like segment (e.g. "1.37.0") anywhere in a schema URL such
# as "https://opentelemetry.io/schemas/1.37.0". Deliberately permissive since
# schema_url values are free-form OTel-defined strings, not something we
# control the shape of.
_SEMCONV_VERSION_RE = re.compile(r"(\d+\.\d+\.\d+)")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incorrect. Can you please follow the definition described here: https://opentelemetry.io/docs/specs/otel/schemas/#schema-url

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd also want a test for this.

if match:
return match.group(1)

return "unknown"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since TraceConversionMetadata.semconv_version is typed, this can never be null, but "unknown". Let's return None and serialise to null.

scope = scope_span.get("scope") or scope_span.get("instrumentationLibrary") or {}
scope_name = scope.get("name", "")
scope_version = scope.get("version", "")
schema_url = scope_span.get("schemaUrl", "")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OTLP defines schemaUrl at both ScopeSpans and ResourceSpans. This reads only the scope level, so a trace setting it only at the resource level reports "unknown". resource_span is already in scope from the loop above. Scope stays first as the more specific signal.

Suggested change
schema_url = scope_span.get("schemaUrl", "")
schema_url = scope_span.get("schemaUrl", "") or resource_span.get("schemaUrl", "")

scope = scope_span.get("scope", {})
scope_name = scope.get("name", "")
scope_version = scope.get("version", "")
schema_url = scope_span.get("schemaUrl", "")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same as the loader path so both ingest routes should behave the same. See my other comment in otlp.py.

model: str | None = None
response_model: str | None = None
provider: str | None = None
semconv_version: str | None = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the schema spec, the version number is scoped to a Schema Family (the URL prefix before the version). So basically two URLs with the same version but different hosts are different schemas, and "OpenTelemetry schema version numbers match OpenTelemetry Semantic Conventions version numbers" is only true for the OTel official family. So a bare "1.40.0" from a custom family (e.g. https://randomcompany.internal/schemas/1.40.0) would be recorded as if it were OTel semconv 1.40.0.

Since this field is informational and never drives logic, I wouldn't hard-gate on the host (mirrors exist, and the full otel.schema_url attribute is already preserved for anyone who needs the family). But please make this clear and add note in the field description / the resolve_semconv_version docstring that this is the version segment of the emitting scope's schema URL, comparable only within a schema family and equal to the OTel semconv version for the OTel family.

We could even call it schema_version, since "semconv" asserts the OTel family already.

metadata["response_model"] = ext["response_model"]
if ext["provider"]:
metadata["provider"] = ext["provider"]
metadata["semconv_version"] = ext["semconv_version"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, ext comes from llm_spans[0] only, so a trace mixing instrumentors at different versions will only record the first. It's fine, but please add a comment calling this out.

Comment thread tests/test_runner.py

def test_extract_trace_metadata_semconv_version_from_valid_schema_url(self):
trace = _make_tool_trace(["tool_a"])
trace.all_spans[1].tags["otel.schema_url"] = "https://opentelemetry.io/schemas/1.39.0"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (also on line 271): this tags all_spans[1] by position and relies on that being the span the extractor reads. If we change _make_tool_trace ordering, the test passes for the wrong reason or breaks in a weird way. Let's select the LLM span by the same predicate the extractor uses and tag that.

Comment thread tests/test_api.py
assert traces[0]["traceId"] == "dd547580319ab0312cee07f1def50dad"
assert len(traces[0]["invocations"]) == 1

@patch("agentevals.api.routes.extract_trace_metadata")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mocking extract_trace_metadata like this verifies the wiring and camelCase serialization, which is good, but can we please also cover OTLP with scopeSpans.schemaUrl to /api/convert to semconvVersion, so we have something e2e as well?

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.

Version-aware OTel GenAI semconv normalization

2 participants