feat(contrib/otel): add OpenTelemetry tracing submodule#79
Merged
Conversation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements Task 3 of the contrib-otel-tracing change: NewStepInterceptor emits exactly one OpenTelemetry span per Step lifetime (covering all retry attempts), with workflow.step.name + workflow.step.status attributes and codes.Error / RecordError on failure (context.Canceled included). Skipped/Canceled-by-Condition steps bypass the chain and produce no span. Replaces deps_test.go: helpers_test.go now imports the SDK + tracetest packages directly, anchoring them to the test graph. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ests Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements NewAttemptInterceptor: one OTel span per attempt, default name "<step> (attempt N)", canonical attrs workflow.step.name and workflow.step.attempt (int64), error path records via RecordError + SetStatus(codes.Error). User-supplied attributes are appended but the canonical pair always wins (last-write-wins). WithAttemptAttributes godoc in options.go updated to document this precedence symmetric with WithStepAttributes. Tests (package otel_test, reusing helpers from step_test.go): - TestAttemptInterceptor_OneSpanPerAttempt - TestAttemptInterceptor_DefaultName - TestAttemptInterceptor_FailingAttemptRecorded - TestAttemptInterceptor_ChildOfCallerSpan - TestAttemptInterceptor_CustomNamer - TestAttemptInterceptor_CustomAttributes (regression: user cannot override workflow.step.attempt)
- TestBothLayers_AttemptIsChildOfStep: attempt span shares TraceID with step and has step span as parent. - TestBothLayers_RetryAttemptCount: one step span + N attempt spans across retries, all in the same trace. - TestProviderResolutionAtFactoryTime: locks in that NewStepInterceptor and NewAttemptInterceptor snapshot the global TracerProvider at construction time, not on every interception. - Example: runnable godoc that wires both interceptors with a stdouttrace exporter on a 2-step pipeline. // Output: omitted because span IDs and timestamps are non-deterministic. - Add stdouttrace as a test-only dependency; runtime audit confirms no production deps on sdk/stdouttrace.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move openspec/changes/contrib-otel-tracing/ to archive/2026-05-15-* and copy the delta spec into openspec/specs/contrib-otel/spec.md so the new capability is part of the main spec set.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
contrib/otel(github.com/Azure/go-workflow/contrib/otel, packageflowotel) that integrates OpenTelemetry traces via the existingStepInterceptor/AttemptInterceptorextension points — no new core hooks needed.flowotel.NewStepInterceptor(opts...)(one span per step lifetime, across retries) andflowotel.NewAttemptInterceptor(opts...)(one span per attempt). Used independently or together (attempt span becomes child of step span when both registered).WithTracerProvider,WithTracerName,WithStepSpanNamer,WithAttemptSpanNamer,WithStepAttributes,WithAttemptAttributes. Canonical attributes (workflow.step.name,workflow.step.status,workflow.step.attempt) always win over user-supplied keys.go.opentelemetry.io/otel,…/otel/trace); SDK /tracetest/stdouttraceare test-only. Corego.mod/go.sumis byte-identical — OTel dependency does NOT enter core's transitive graph.Why a separate submodule?
If
contrib/otelwere a subpackage of core, every core user would inherit the OpenTelemetry dependency tree. Releasing it as a separate Go module (versioned independently ascontrib/otel/v0.x.y) keeps that cost opt-in, mirroring howgin-contrib,otelhttp, etc. are structured.What's in the change
contrib/otel/options.go—Option+ sixWith*constructors +(*config).resolveTracer()(resolves provider once at factory call time).contrib/otel/step.go+attempt.go— the two factories. ~70 LOC each, mirror each other's structure.contrib/otel/consts.go— shared attribute keys / status values.contrib/otel/doc.go,contrib/otel/README.md, rootREADME.mdupdated.openspec/changes/contrib-otel-tracing/(proposal + design + tasks + spec deltas under capabilitycontrib-otel).Test plan
go test ./...from repo root passes (core unchanged).cd contrib/otel && go test -race -count=1 ./...passes (18 tests + Example).go vet ./...andgofmt -l .clean insidecontrib/otel/.cd contrib/otel && go list -deps -test=false ./... | grep -E 'otel/sdk|stdouttrace|tracetest'produces no output (SDK / exporter / tracetest stay test-only).go.mod/go.sumbyte-identical to before (git diff fca6b68c..HEAD -- go.mod go.sumempty).openspec validate contrib-otel-tracing --strictreports valid.Follow-ups (intentionally not in this PR; documented in
contrib/otel/README.md)go test ./...(and-race) insidecontrib/otel/in addition to the root-module job.contrib/otel/v0.1.0, drop or pin thereplace github.com/Azure/go-workflow => ../..directive incontrib/otel/go.modsogo get …/contrib/otel@v0.1.0resolves cleanly.