feat(#6458): export eval measurement scores via OTLP - #6459
Conversation
Wire MeasureAndExport to emit gen_ai.evaluation.result span events on the same TraceID when OTEL_EXPORTER_OTLP_* is set, matching ADR 0087 / 0050. Local JSONL stays source of truth; remote export is fail-open. Signed-off-by: Adam Scerra <ascerra@redhat.com> Co-authored-by: Cursor <cursoragent@cursor.com>
PR Summary by QodoExport eval measurement scores as OTLP GenAI evaluation span events
AI Description
Diagram
High-Level Assessment
Files changed (11)
|
Site previewPreview: https://ac85078b-site.fullsend-ai.workers.dev Commit: |
|
🤖 Review · Commit: |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Code Review by Qodo
1.
|
Bound post-hoc export retries/budget, share fullsend resource identity, batch scores, skip empty span IDs, keep Ok status for all labels, omit score.value on skip, and sync docs that still said OTLP was planned. Signed-off-by: Adam Scerra <ascerra@redhat.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Review squad follow-upAddressed verified findings from the review pass:
Deferred (documented / intentional): separate remote-export ledger for OTLP retry after local success — remote remains best-effort once; Assessments stay a MLflow-side consumer, not core. |
|
🤖 Finished Review · ✅ Success · Started 7:52 PM UTC · Completed 8:07 PM UTC Commit: |
ReviewFindingsMedium
Low
Next steps:
Previous runReviewFindingsMedium
Low
Next steps:
Previous run (2)ReviewFindingsLow
Next steps:
Previous run (3)ReviewFindingsLow
Previous run (4)ReviewFindingsLow
Previous run (5)ReviewFindingsLow
Previous run (6)ReviewFindingsLow
Previous run (7)ReviewFindingsMedium
Low
Previous run (8)ReviewFindingsLow
Previous run (9)ReviewFindingsLow
Info
Previous run (10)ReviewFindingsLow
Previous run (11)ReviewFindingsLow
Previous run (12)ReviewFindingsMedium
Low
|
waynesun09
left a comment
There was a problem hiding this comment.
Review (MEDIUM+)
Four inline comments on unique issues. Fail-open OTLP path looks solid (empty IDs skipped, export bounded, batch processor). The correctness gap to fix is resource identity.
Already posted (skipped): ADR 0087 Decision rewrite on docs/ADRs/0087-eval-measurements-online-trace-scoring.md — existing thread already covers it. Preferred fix remains a 0050-style annotation rather than editing the Decision.
Align score resource service.version with CLI Version(), no-op OTLP when inbound TRACEPARENT is unsampled, apply shared span limits and truncate evaluation explanations, cite GenAI semconv and clarify vendor UI mapping, and restore ADR 0087 Decision with an Implemented annotation. Signed-off-by: Adam Scerra <ascerra@redhat.com> Co-authored-by: Cursor <cursoragent@cursor.com>
|
🤖 Finished Review · ✅ Success · Started 11:33 AM UTC · Completed 11:53 AM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Cost: $6.68 |
waynesun09
left a comment
There was a problem hiding this comment.
Additional review pass (8 findings, all verified against PR head 911b9bf and cross-checked against existing threads for duplicates).
Scope TRACEPARENT suppression per TraceID via W3C propagator, export already-persisted scores on mid-loop persist failure, clear transient export latch on success, hermetic OTEL in Measure tests, and refresh the GenAI semconv citation. Signed-off-by: Adam Scerra <ascerra@redhat.com> Co-authored-by: Cursor <cursoragent@cursor.com>
|
🤖 Finished Review · ✅ Success · Started 5:17 PM UTC · Completed 5:34 PM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $7.35 |
…o-delay backoff With resetRetryDelay=0, select between ctx.Done and time.After(0) is racy; check ctx.Err at the start of each poll so unit tests and -race CI reliably see context cancelled. Signed-off-by: Adam Scerra <ascerra@redhat.com> Co-authored-by: Cursor <cursoragent@cursor.com>
|
🤖 Finished Review · ✅ Success · Started 5:40 PM UTC · Completed 5:55 PM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $5.55 |
# Conflicts: # pkg/behaviourtest/drivers/install/ensure.go
Signed-off-by: Adam Scerra <ascerra@redhat.com>
|
🤖 Finished Review · ✅ Success · Started 5:35 PM UTC · Completed 5:52 PM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $8.72 |
Signed-off-by: Adam Scerra <ascerra@redhat.com>
|
🤖 Finished Review · ✅ Success · Started 5:54 PM UTC · Completed 6:14 PM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $7.68 |
|
🤖 Review · Commit: |
Signed-off-by: Adam Scerra <ascerra@redhat.com>
|
🤖 Finished Review · ✅ Success · Started 6:23 PM UTC · Completed 6:42 PM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $8.02 |
| // eval-measurements.jsonl. Idempotent per ledger. | ||
| func MeasureFile(telemetryPath, registryPath, outDir string) ([]EvaluationResult, error) { | ||
| r, _, err := MeasureAndExport(context.Background(), telemetryPath, registryPath, outDir) | ||
| r, _, err := MeasureAndExport(context.Background(), telemetryPath, registryPath, outDir, "") |
There was a problem hiding this comment.
[low] api-shape-consistency
MeasureFile passes empty serviceVersion to MeasureAndExport. Inside BuildResource, empty string defaults to unknown. Callers using MeasureFile will emit OTLP score spans with service.version=unknown if OTLP is configured.
Suggested fix: Add a serviceVersion parameter to MeasureFile or document the unknown default prominently in the godoc.
| sdktrace.WithResource(telemetry.BuildResource(serviceVersion)), | ||
| sdktrace.WithSampler(sdktrace.AlwaysSample()), | ||
| sdktrace.WithRawSpanLimits(telemetry.SpanLimits()), | ||
| sdktrace.WithSpanProcessor(sdktrace.NewBatchSpanProcessor(capExp, sdktrace.WithMaxQueueSize(len(exportable)))), |
There was a problem hiding this comment.
[low] edge-case
BatchSpanProcessor queue size is set to exactly len(exportable). A test validates 2049 spans work, but the exact-size match leaves no margin for future SDK internal changes.
| if len(errs) == 0 { | ||
| return nil | ||
| } | ||
| // Transport failure: nothing is known to have landed — do not claim N/M |
There was a problem hiding this comment.
[low] error-handling
When BatchSpanProcessor exports in multiple batches and an early batch succeeds but a later batch fails, the error message says otlp export failed for all N scores, which is conservative by design but could mislead debugging in partial-success scenarios.
| return out | ||
| } | ||
|
|
||
| func clearOTLPEnv(t *testing.T) { |
There was a problem hiding this comment.
[low] naming-convention
clearOTLPEnv is duplicated identically in export_otlp_test.go and cli/evalmeasure_test.go. The CLI copy cross-references the pattern. Reasonable Go testing pattern given the separate packages.
| // but some traces were still recovered. Callers should warn; MeasureAndExport | ||
| // still scores those traces and returns a nil error. | ||
| Incomplete string | ||
| // RemoteExportWarning is set when portable OTLP score export failed |
There was a problem hiding this comment.
[low] struct-placement
RemoteExportWarning is added to ParseStats but is set by the OTLP export path, not parsing. The struct already serves as the MeasureAndExport return carrier.
Summary
eval-measurements.jsonl, newly scored rows also emitgen_ai.evaluation.resultspan events on the same TraceID whenOTEL_EXPORTER_OTLP_*is set (same path as ADR 0050 agent traces).run-telemetry.jsonl.MLFLOW_*/ Assessments) in core — MLflow Assessments UI can be a separate consumer of the OTLP event.Closes #6458
Test plan
go test ./internal/evalmeasure/ ./internal/telemetry/84d470ba…)tr-84d470ba2451ffeccfe09022d9b2aebdeval-measureposts scores when OTEL is setMade with Cursor