Connect CDEvents from your CI/CD pipelines to OpenTelemetry traces, so every trace knows which deployment, commit and pipeline produced it.
It is 3 AM, latency is up, and the trace in Jaeger tells you what is slow but not which deployment changed it. Deployment facts live in Jenkins, GitHub Actions, Tekton or Argo CD; runtime facts live in your observability backend. Connecting the two is manual work, done under pressure, every time.
- Receives CDEvents over HTTP from any CI/CD tool that speaks the
CDEvents standard (
POST /api/v1/events). - Remembers the current deployment of every service and environment (in memory for development, Redis for production).
- Enriches traces. A small OpenTelemetry span processor (
pkg/otelbridge) asks the bridge for a service's deployment and stampsdeployment.version,deployment.commit,deployment.environment,deployment.pipeline_idand friends onto every span that service emits. - Reconstructs event chains from CDEvents links and chain ids, so an
incident can be walked back to the pipeline run and the merged commit
(
GET /api/v1/chain/{eventId}).
CI/CD tools Bridge Your services
───────────── ────── ─────────────
Jenkins / GitHub Actions / POST ┌─────────────────────────┐ GET ┌──────────────────────┐
Tekton / Argo CD / ... ───────► │ /api/v1/events │ ◄─────── │ app + OTel SDK │
service.deployed │ parse, validate, store│ │ + otelbridge │
pipelinerun.finished │ │ ───────► │ span processor │
change.merged │ /api/v1/deployments/{s} │ latest └──────────┬───────────┘
incident.detected │ /api/v1/chain/{eventId} │ deploy │ spans with
│ /metrics │ │ deployment.* attrs
└────────────┬────────────┘ ▼
│ memory | Redis Jaeger / Tempo / any OTLP
Prerequisites: Docker with Compose v2, make, and (optionally) jq.
make demo # builds and starts redis, jaeger, the bridge and a sample app
make demo-events # replays change.merged → pipelinerun.finished → service.deployed → incident.detectedThen look at the result:
# what is running as sample-app right now
curl -s localhost:8080/api/v1/deployments/sample-app | jq
# walk the incident back to the commit
curl -s localhost:8080/api/v1/chain/evt-incident-001 | jq
# make some traffic, then open Jaeger and pick service "sample-app"
curl -s localhost:8081/hello
open http://localhost:16686Every sample-app span in Jaeger carries deployment.version=v1.0.0,
deployment.commit=abc123def456, deployment.environment=production and the
pipeline and repository that produced it. make demo-clean stops everything.
./scripts/demo.sh does all of the above in one go, including a few sample
requests, and prints the chain at the end.
If a port is already taken on your machine (usually by a container from
another project; docker ps shows which), override it, for example
SAMPLE_APP_HOST_PORT=18081 JAEGER_UI_HOST_PORT=26686 make demo. See
deployments/docker-compose/.env.example
for the full list.
import (
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"github.com/iamrajiv/cdevents-otel-bridge/pkg/otelbridge"
)
client := otelbridge.NewClient("http://cdevents-otel-bridge:8080",
otelbridge.WithEnvironment("production"))
resolver := otelbridge.NewCachedResolver(client)
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res), // must carry service.name
sdktrace.WithSpanProcessor(otelbridge.NewSpanProcessor(resolver)),
)The processor reads service.name from the tracer provider's resource, asks
the bridge for that service's current deployment and adds these attributes:
| Attribute | Source |
|---|---|
deployment.id |
subject.id of the deployment event |
deployment.version |
subject.content.service.version or parsed from artifactId |
deployment.commit |
commitSha |
deployment.environment |
subject.content.environment.id |
deployment.pipeline_id |
pipelineId |
deployment.repository |
repository |
deployment.branch |
branch |
deployment.deployed_at |
event timestamp (RFC 3339) |
deployment.deployed_by |
deployer |
deployment.event_id |
context.id of the deployment event |
Lookups are cached (30 s, 10 s for "not found") and bounded by a 500 ms timeout, so an unreachable bridge never fails or noticeably slows a request. See examples/sample-app for a complete service.
| Method | Path | Description |
|---|---|---|
POST |
/api/v1/events |
Ingest a CDEvent (201 Created) |
GET |
/api/v1/events/{eventId} |
Fetch a stored event |
GET |
/api/v1/deployments |
List deployments (environment, since, limit) |
GET |
/api/v1/deployments/{service} |
Current deployment of a service (environment optional; latest across environments when omitted) |
GET |
/api/v1/chain/{eventId} |
Event chain and root cause for an event |
GET |
/api/v1/health |
Health with storage check (503 when storage is down) |
GET |
/metrics |
Prometheus metrics |
Full request and response shapes are in docs/api.md.
Settings come from built-in defaults, an optional YAML file (-config), and
BRIDGE_* environment variables, in that order of precedence.
| Variable | Default | Purpose |
|---|---|---|
BRIDGE_PORT |
8080 |
Listen port |
BRIDGE_STORAGE_TYPE |
memory |
memory or redis |
BRIDGE_REDIS_URL |
redis://[:password@]host:port[/db] |
|
BRIDGE_REDIS_TTL |
86400 |
Seconds to keep records in Redis (0 = forever) |
BRIDGE_OTEL_ENABLED |
false |
Export the bridge's own request traces |
BRIDGE_OTEL_ENDPOINT |
localhost:4317 |
OTLP gRPC endpoint |
BRIDGE_LOG_LEVEL |
info |
debug, info, warn, error |
BRIDGE_LOG_FORMAT |
json |
json or text |
Everything else is in docs/configuration.md.
- Binary:
make buildproducesbin/bridge; run it with-config configs/config.yaml. - Docker:
make docker-build, ordocker build -f deployments/docker/Dockerfile .Images are published toghcr.io/iamrajiv/cdevents-otel-bridgeon tagged releases. - Docker Compose:
deployments/docker-compose/docker-compose.ymlruns the full demo stack. - Kubernetes:
kubectl apply -k deployments/kubernetes(namespace, ConfigMap, Deployment with probes and a read-only root filesystem, Service).
Ready-made examples for Jenkins, GitHub Actions and Tekton live in examples/ci-integrations, and docs/ci-integration.md explains the event shape the bridge expects. The smallest useful event is:
curl -X POST http://localhost:8080/api/v1/events -H 'Content-Type: application/json' -d '{
"context": {"version": "0.4.1", "id": "evt-1", "source": "https://ci.example.com/run/1",
"type": "dev.cdevents.service.deployed.0.1.1", "timestamp": "2026-01-15T10:30:00Z"},
"subject": {"id": "my-app", "type": "service",
"content": {"environment": {"id": "production"}, "artifactId": "my-app:v1.2.3"}},
"customData": {"commitSha": "abc123", "repository": "github.com/org/my-app", "pipelineId": "build-42"}
}'make check # gofmt, go vet, golangci-lint, unit tests (-race), integration tests
make test # unit tests only
make test-integration # HTTP-level tests in test/integration
make run # run the bridge with configs/config.dev.yamlGo 1.25 or newer is required. make lint needs
golangci-lint v2 built with a Go version at least
as new as your toolchain.
Every source and configuration file starts with a single multi-line comment block that explains what the file is for; there are no other comments in the code. CONTRIBUTING.md has the full list of conventions.
cmd/bridge/ entry point, wiring, graceful shutdown
internal/api/ chi router, handlers, middleware
internal/cdevents/ CDEvent parsing, validation, link normalization, model conversion
internal/storage/ Storage interface, memory and Redis backends
internal/correlator/ chain reconstruction over stored events and links
internal/metrics/ Prometheus metrics
internal/otel/ the bridge's own tracer provider and storage-backed resolver
internal/config/ YAML + environment configuration
pkg/models/ Deployment, Link, EventChain, Incident, PipelineRun
pkg/otelbridge/ public span processor, HTTP client and cache for applications
examples/ sample app, mock deployer, CI pipeline snippets
deployments/ Dockerfile, Docker Compose stack, Kubernetes manifests
docs/ architecture, API, configuration, getting started, CI integration
See CONTRIBUTING.md. This project follows the Contributor Covenant.
Apache License 2.0. See LICENSE.
- CDEvents, a CD Foundation project
- OpenTelemetry
- chi and go-redis