Repository: open-telemetry/opentelemetry-go-contrib (go)

Issue:
Request for new component: Baggage Span Processor
The Honeycomb distro currently provides a Span processor that takes items from the baggage and adds those items as attributes onto spans. We'd like to donate this code upstream if there's an interest from the community, the code is pretty minimal: https://github.com/honeycombio/honeycomb-opentelemetry-go/blob/main/baggage_span_processor.go

Pull request description:
processors/baggage: add baggage span processor
Fixes #5397

Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index bb6bf7d72..7d956abcb 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -541,6 +541,15 @@ updates:
     schedule:
       interval: weekly
       day: sunday
+  - package-ecosystem: gomod
+    directory: /processors/baggage/baggagetrace
+    labels:
+      - dependencies
+      - go
+      - Skip Changelog
+    schedule:
+      interval: weekly
+      day: sunday
   - package-ecosystem: gomod
     directory: /propagators/autoprop
     labels:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f8c17b4a..02cdb36e1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 - Add an experimental `OTEL_METRICS_PRODUCERS` environment variable to `go.opentelemetry.io/contrib/autoexport` to be set metrics producers. (#5281)
   - `prometheus` and `none` are supported values. You can specify multiple producers separated by a comma.
   - Add `WithFallbackMetricProducer` option that adds a fallback if the `OTEL_METRICS_PRODUCERS` is not set or empty.
+- The `go.opentelemetry.io/contrib/processors/baggage/baggagetrace` module. This module provides a Baggage Span Processor. (#5404)
 
 ## [1.26.0/0.51.0/0.20.0/0.6.0/0.1.0] - 2024-04-24
 
diff --git a/CODEOWNERS b/CODEOWNERS
index 01ae4b69a..197c0971b 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -46,6 +46,8 @@ instrumentation/net/http/httptrace/otelhttptrace/                       @open-te
 instrumentation/net/http/otelhttp/                                      @open-telemetry/go-approvers @Aneurysm9 @dmathieu
 instrumentation/runtime/                                                @open-telemetry/go-approvers @MadVikingGod
 
+processors/baggage/baggagetrace                                         @open-telemetry/go-approvers @codeboten @MikeGoldsmith
+
 propagators/autoprop/                                                   @open-telemetry/go-approvers @MrAlias
 propagators/aws/                                                        @open-telemetry/go-approvers @Aneurysm9
 propagators/b3/                                                         @open-telemetry/go-approvers @pellared
diff --git a/README.md b/README.md
index 6ce390cd5..913696a40 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ Collection of 3rd-party packages for [OpenTelemetry-Go](https://github.com/open-
 - [Exporters](./exporters/): Packages providing OpenTelemetry exporters for 3rd-party export formats.
 - [Samplers](./samplers/): Packages providing additional implementations of OpenTelemetry samplers.
 - [Bridges](./bridges/): Packages providing adapters for 3rd-party instrumentation frameworks.
+- [Processors](./processors/): Packages providing additional implementations of OpenTelemetry processors.
 
 ## Project Status
 
diff --git a/processors/baggage/baggagetrace/doc.go b/processors/baggage/baggagetrace/doc.go
new file mode 100644
index 000000000..c2af10501
--- /dev/null
+++ b/processors/baggage/baggagetrace/doc.go
@@ -0,0 +1,5 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+// Package baggagetrace implements a baggage span processor.
+package baggagetrace // import "go.opentelemetry.io/contrib/processors/baggage/baggagetrace"
diff --git a/processors/baggage/baggagetrace/go.mod b/processors/baggage/baggagetrace/go.mod
new file mode 100644
index 000000000..92f72785e
--- /dev/null
+++ b/processors/baggage/baggagetrace/go.mod
@@ -0,0 +1,20 @@
+module go.opentelemetry.io/contrib/processors/baggage/baggagetrace
+
+go 1.21
+
+require (
+	github.com/stretchr/testify v1.9.0
+	go.opentelemetry.io/otel v1.25.0
+	go.opentelemetry.io/otel/sdk v1.25.0
+)
+
+require (
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/go-logr/logr v1.4.1 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	go.opentelemetry.io/otel/metric v1.25.0 // indirect
+	go.opentelemetry.io/otel/trace v1.25.0 // indirect
+	golang.org/x/sys v0.18.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/processors/baggage/baggagetrace/go.sum b/processors/baggage/baggagetrace/go.sum
new file mode 100644
index 000000000..c94c7b187
--- /dev/null
+++ b/processors/baggage/baggagetrace/go.sum
@@ -0,0 +1,27 @@
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k=
+go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg=
+go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA=
+go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s=
+go.opentelemetry.io/otel/sdk v1.25.0 h1:PDryEJPC8YJZQSyLY5eqLeafHtG+X7FWnf3aXMtxbqo=
+go.opentelemetry.io/otel/sdk v1.25.0/go.mod h1:oFgzCM2zdsxKzz6zwpTZYLLQsFwc+K0daArPdIhuxkw=
+go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM=
+go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/processors/baggage/baggagetrace/processor.go b/processors/baggage/baggagetrace/processor.go
new file mode 100644
index 000000000..7207bb597
--- /dev/null
+++ b/processors/baggage/baggagetrace/processor.go
@@ -0,0 +1,42 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package baggagetrace // import "go.opentelemetry.io/contrib/processors/baggage/baggagetrace"
+
+import (
+	"context"
+
+	"go.opentelemetry.io/otel/attribute"
+	otelbaggage "go.opentelemetry.io/otel/baggage"
+	"go.opentelemetry.io/otel/sdk/trace"
+)
+
+// SpanProcessor is a processing pipeline for spans in the trace signal.
+type SpanProcessor struct{}
+
+var _ trace.SpanProcessor = (*SpanProcessor)(nil)
+
+// New returns a new SpanProcessor.
+//
+// The Baggage span processor duplicates onto a span the attributes found
+// in Baggage in the parent context at the moment the span is started.
+func New() trace.SpanProcessor {
+	return &SpanProcessor{}
+}
+
+// OnStart is called when a span is started and adds span attributes for baggage contents.
+func (processor SpanProcessor) OnStart(ctx context.Context, span trace.ReadWriteSpan) {
+	for _, entry := range otelbaggage.FromContext(ctx).Members() {
+		span.SetAttributes(attribute.String(entry.Key(), entry.Value()))
+	}
+}
+
+// OnEnd is called when span is finished and is a no-op for this processor.
+func (processor SpanProcessor) OnEnd(s trace.ReadOnlySpan) {}
+
+// Shutdown is called when the SDK shuts down and is a no-op for this processor.
+func (processor SpanProcessor) Shutdown(context.Context) error { return nil }
+
+// ForceFlush exports all ended spans to the configured Exporter that have not yet
+// been exported and is a no-op for this processor.
+func (processor SpanProcessor) ForceFlush(context.Context) error { return nil }
diff --git a/versions.yaml b/versions.yaml
index e3163f568..2569e6d74 100644
--- a/versions.yaml
+++ b/versions.yaml
@@ -77,6 +77,10 @@ module-sets:
     version: v0.1.0
     modules:
       - go.opentelemetry.io/contrib/bridges/otelslog
+  experimental-processors:
+    version: v0.0.1
+    modules:
+      - go.opentelemetry.io/contrib/processors/baggage/baggagetrace
 excluded-modules:
   - go.opentelemetry.io/contrib/instrgen
   - go.opentelemetry.io/contrib/instrgen/driver


Test patch:
diff --git a/processors/baggage/baggagetrace/processor_test.go b/processors/baggage/baggagetrace/processor_test.go
new file mode 100644
index 000000000..3b50bcf3e
--- /dev/null
+++ b/processors/baggage/baggagetrace/processor_test.go
@@ -0,0 +1,62 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package baggagetrace
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"go.opentelemetry.io/otel/attribute"
+	otelbaggage "go.opentelemetry.io/otel/baggage"
+	"go.opentelemetry.io/otel/sdk/trace"
+)
+
+var _ trace.SpanExporter = &testExporter{}
+
+type testExporter struct {
+	spans []trace.ReadOnlySpan
+}
+
+func (e *testExporter) Start(ctx context.Context) error    { return nil }
+func (e *testExporter) Shutdown(ctx context.Context) error { return nil }
+
+func (e *testExporter) ExportSpans(ctx context.Context, ss []trace.ReadOnlySpan) error {
+	e.spans = append(e.spans, ss...)
+	return nil
+}
+
+func NewTestExporter() *testExporter {
+	return &testExporter{}
+}
+
+func TestSpanProcessorAppendsBaggageAttributes(t *testing.T) {
+	suitcase, err := otelbaggage.New()
+	require.NoError(t, err)
+	packingCube, err := otelbaggage.NewMemberRaw("baggage.test", "baggage value")
+	require.NoError(t, err)
+	suitcase, err = suitcase.SetMember(packingCube)
+	require.NoError(t, err)
+	ctx := otelbaggage.ContextWithBaggage(context.Background(), suitcase)
+
+	// create trace provider with baggage processor and test exporter
+	exporter := NewTestExporter()
+	tp := trace.NewTracerProvider(
+		trace.WithSpanProcessor(New()),
+		trace.WithSpanProcessor(trace.NewSimpleSpanProcessor(exporter)),
+	)
+
+	// create tracer and start/end span
+	tracer := tp.Tracer("test")
+	_, span := tracer.Start(ctx, "test")
+	span.End()
+
+	assert.Len(t, exporter.spans, 1)
+	assert.Len(t, exporter.spans[0].Attributes(), 1)
+
+	want := []attribute.KeyValue{attribute.String("baggage.test", "baggage value")}
+	assert.Equal(t, want, exporter.spans[0].Attributes())
+}