-
Notifications
You must be signed in to change notification settings - Fork 2
feat(otel): add proprietary span converter #117
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
Merged
+399
−0
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
2a43fbe
feat(otel): normalize native and user-wired paths
pradystar a648ab6
feat(otel): add canonical attribute normalization
pradystar f142048
fix(otel): correct structured GenAI attribute serialization
pradystar e5396a3
address attribute normalization review findings
pradystar 5b777ed
ix(otel): extract schema-valid messages
pradystar 80e0d5f
feat(otel): add proprietary span converter
pradystar cbc5c4d
fix(otel): reject proprietary trace envelopes
pradystar adf3cd5
remove unsupported exception-based status mapping
pradystar de9726d
Merge remote-tracking branch 'origin/main' into feat/user-wired-otel-…
pradystar bc51095
defensive cast to prevent ValueError
pradystar 5c2e392
validate exporter configuration
pradystar 6ba442c
Merge branch 'feat/user-wired-otel-paths' into feat/canonical-otel-at…
pradystar 13a79fc
Merge branch 'feat/canonical-otel-attribute-normalization' into feat/…
pradystar 75398eb
Merge branch 'main' into feat/canonical-otel-attribute-normalization
pradystar e7a9f07
Merge branch 'feat/canonical-otel-attribute-normalization' into feat/…
pradystar 4a38146
Merge remote-tracking branch 'origin/main' into feat/proprietary-span…
pradystar File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| """Convert completed Galileo spans to OpenTelemetry spans.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import time | ||
| from datetime import UTC, datetime | ||
|
|
||
| from opentelemetry.sdk.resources import Resource | ||
| from opentelemetry.sdk.trace import ReadableSpan | ||
| from opentelemetry.sdk.util.instrumentation import InstrumentationScope | ||
| from opentelemetry.trace import SpanContext, SpanKind, Status, StatusCode | ||
|
|
||
| from galileo_core.schemas.logging.step import BaseStep, StepType | ||
| from splunk_ao.converter.attribute_mapping import build_span_attributes | ||
| from splunk_ao.utils.headers_data import get_package_version | ||
|
|
||
| _EPOCH = datetime(1970, 1, 1, tzinfo=UTC) | ||
| _INSTRUMENTATION_SCOPE = InstrumentationScope(name="splunk_ao", version=get_package_version()) | ||
|
|
||
| _NAME_PARTS: dict[StepType, tuple[str, str]] = { | ||
| StepType.llm: ("chat", "model"), | ||
| StepType.tool: ("execute_tool", "name"), | ||
| StepType.retriever: ("retrieval", "name"), | ||
| StepType.workflow: ("invoke_workflow", "name"), | ||
| StepType.agent: ("invoke_agent", "name"), | ||
| StepType.control: ("", "name"), | ||
| } | ||
|
|
||
| _KIND_BY_STEP_TYPE = { | ||
| step_type: SpanKind.CLIENT if step_type is StepType.llm else SpanKind.INTERNAL for step_type in _NAME_PARTS | ||
| } | ||
|
|
||
|
|
||
| def _step_type(span: BaseStep) -> StepType: | ||
| raw_type = getattr(span, "type", None) | ||
| try: | ||
| step_type = StepType(raw_type) | ||
| except (TypeError, ValueError) as exc: | ||
| raise TypeError(f"Unsupported step type: {raw_type}") from exc | ||
|
|
||
| if step_type is StepType.trace: | ||
| raise TypeError("LoggedTrace is a trace envelope and cannot be converted") | ||
| if step_type not in _NAME_PARTS: | ||
| raise TypeError(f"Unsupported step type: {step_type.value}") | ||
| return step_type | ||
|
|
||
|
|
||
| def _span_name(span: BaseStep, step_type: StepType) -> str: | ||
| prefix, field_name = _NAME_PARTS[step_type] | ||
| detail = getattr(span, field_name, None) | ||
| return " ".join(part for part in (prefix, str(detail).strip() if detail is not None else "") if part) | ||
|
|
||
|
|
||
| def _to_unix_ns(value: datetime) -> int: | ||
| if value.tzinfo is None: | ||
| value = value.replace(tzinfo=UTC) | ||
| delta = value.astimezone(UTC) - _EPOCH | ||
| return ((delta.days * 86_400 + delta.seconds) * 1_000_000_000) + delta.microseconds * 1_000 | ||
|
|
||
|
|
||
| def _span_status(span: BaseStep) -> Status: | ||
| status_code = span.status_code | ||
| if status_code is None or status_code < 400: | ||
| return Status(StatusCode.UNSET) | ||
| return Status(StatusCode.ERROR) | ||
|
|
||
|
|
||
| def _end_time_ns(span: BaseStep, start_time_ns: int, end_time_ns: int | None) -> int: | ||
| duration_ns = span.metrics.duration_ns | ||
| if duration_ns is not None: | ||
| return start_time_ns + duration_ns | ||
| if end_time_ns is not None: | ||
| return end_time_ns | ||
| return time.time_ns() | ||
|
|
||
|
|
||
| class SpanConverter: | ||
| """Convert one completed Galileo span into an OTel readable span.""" | ||
|
|
||
| def convert_span( | ||
| self, | ||
| span: BaseStep, | ||
| span_context: SpanContext, | ||
| parent_span_context: SpanContext | None, | ||
| session_id: str | None, | ||
| resource: Resource, | ||
| end_time_ns: int | None = None, | ||
| ) -> ReadableSpan: | ||
| """Build a readable span using the supplied identity and resource.""" | ||
| step_type = _step_type(span) | ||
| start_time_ns = _to_unix_ns(span.created_at) | ||
|
|
||
| return ReadableSpan( | ||
| name=_span_name(span, step_type), | ||
| context=span_context, | ||
| parent=parent_span_context, | ||
| resource=resource, | ||
| attributes=build_span_attributes(span, session_id), | ||
| events=(), | ||
| links=(), | ||
| kind=_KIND_BY_STEP_TYPE[step_type], | ||
| instrumentation_scope=_INSTRUMENTATION_SCOPE, | ||
| status=_span_status(span), | ||
| start_time=start_time_ns, | ||
| end_time=_end_time_ns(span, start_time_ns, end_time_ns), | ||
| ) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
🟡 minor (question): The PR description lists
traceamong the supported conversion types, but_step_typeexplicitly rejectsStepType.trace(and it was removed from_NAME_PARTS), withtest_trace_envelope_is_not_convertibleasserting the rejection. Meanwhileattribute_mapping.build_span_attributesstill handlesStepType.tracevia_set_generic_content. The two modules now disagree on whether a trace is in scope. Assuming the converter's rejection is the intended contract (traces are envelopes, not spans), the PR description should be updated to droptracefrom the supported list to avoid confusion. Can you confirm the intent?🤖 Generated by the Astra agent
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.
Yes, this is intentional.
LoggedTraceis an internal lifecycle and correlation envelope, not application work, so converting it would create an artificial parent otel span. Only its contained LLM, tool, retriever, workflow, agent, and control spans are converted.build_span_attributes()is a lower-level shared attribute builder its trace handling does not make trace envelopes eligible forSpanConverter.