From 1f1399825f49316ef658d3e7a01429931c2d19cf Mon Sep 17 00:00:00 2001 From: Jared Taylor Date: Sat, 31 Jan 2026 11:56:01 -0500 Subject: [PATCH 01/10] TracerProvider ForceFlush() joins errors together and continues iteration --- CHANGELOG.md | 2 ++ sdk/trace/provider.go | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebe62c6988a..75d7f332f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Improve performance of concurrent synchronous gauge measurements in `go.opentelemetry.io/otel/sdk/metric`. (#7478) - Improve performance of concurrent exponential histogram measurements in `go.opentelemetry.io/otel/sdk/metric`. (#7702) - Improve the concurrent performance of `FixedSizeReservoir` in `go.opentelemetry.io/otel/sdk/metric/exemplar`. (#7447) +- TracerProvider `ForceFlush()` Joins errors together and continues iteration through SpanProcessors as opposed + to returning the first encountered error without attempting exports on subsequent SpanProcessors. ### Fixed diff --git a/sdk/trace/provider.go b/sdk/trace/provider.go index d2cf4ebd3e7..4ba4acf130e 100644 --- a/sdk/trace/provider.go +++ b/sdk/trace/provider.go @@ -5,6 +5,7 @@ package trace // import "go.opentelemetry.io/otel/sdk/trace" import ( "context" + "errors" "fmt" "sync" "sync/atomic" @@ -262,6 +263,7 @@ func (p *TracerProvider) ForceFlush(ctx context.Context) error { return nil } + var err error for _, sps := range spss { select { case <-ctx.Done(): @@ -269,11 +271,9 @@ func (p *TracerProvider) ForceFlush(ctx context.Context) error { default: } - if err := sps.sp.ForceFlush(ctx); err != nil { - return err - } + err = errors.Join(err, sps.sp.ForceFlush(ctx)) } - return nil + return err } // Shutdown shuts down TracerProvider. All registered span processors are shut down From 1a37d99e11151f68d814eaffbf05575b4b6be882 Mon Sep 17 00:00:00 2001 From: Jared Taylor Date: Sat, 31 Jan 2026 12:19:14 -0500 Subject: [PATCH 02/10] Add PR ID to CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75d7f332f9e..daa1ef44134 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Improve performance of concurrent exponential histogram measurements in `go.opentelemetry.io/otel/sdk/metric`. (#7702) - Improve the concurrent performance of `FixedSizeReservoir` in `go.opentelemetry.io/otel/sdk/metric/exemplar`. (#7447) - TracerProvider `ForceFlush()` Joins errors together and continues iteration through SpanProcessors as opposed - to returning the first encountered error without attempting exports on subsequent SpanProcessors. + to returning the first encountered error without attempting exports on subsequent SpanProcessors. (#7856) ### Fixed From 81f2e6223eb65183edbc4e063528a0e913bfb97c Mon Sep 17 00:00:00 2001 From: Jared Taylor Date: Mon, 9 Feb 2026 13:22:50 -0500 Subject: [PATCH 03/10] Add test for new ForceFlush logic --- sdk/trace/provider_test.go | 50 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/sdk/trace/provider_test.go b/sdk/trace/provider_test.go index 9b024b02af4..79f1d47a211 100644 --- a/sdk/trace/provider_test.go +++ b/sdk/trace/provider_test.go @@ -28,6 +28,7 @@ type basicSpanProcessor struct { flushed bool closed bool injectShutdownError error + injectExportError error } func (t *basicSpanProcessor) Shutdown(context.Context) error { @@ -38,6 +39,10 @@ func (t *basicSpanProcessor) Shutdown(context.Context) error { func (*basicSpanProcessor) OnStart(context.Context, ReadWriteSpan) {} func (*basicSpanProcessor) OnEnd(ReadOnlySpan) {} func (t *basicSpanProcessor) ForceFlush(context.Context) error { + if t.injectExportError != nil { + t.flushed = false + return t.injectExportError + } t.flushed = true return nil } @@ -227,6 +232,51 @@ func TestRegisterAfterShutdownWithProcessors(t *testing.T) { assert.Empty(t, stp.getSpanProcessors()) } +func TestTracerProviderForceFlush(t *testing.T) { + t.Run("AfterShutdown", func(t *testing.T) { + stp := NewTracerProvider() + sp1 := &basicSpanProcessor{} + stp.RegisterSpanProcessor(sp1) + ctx := t.Context() + + require.NoError(t, stp.ForceFlush(ctx)) + require.True(t, sp1.flushed, "SpanProcessor ForceFlush not called") + + sp1.flushed = false + require.NoError(t, stp.Shutdown(ctx)) + + require.NoError(t, stp.ForceFlush(ctx)) + assert.False(t, sp1.flushed, "SpanProcessor ForceFlush called after Shutdown") + }) + + t.Run("Multi", func(t *testing.T) { + stp := NewTracerProvider() + sp1 := &basicSpanProcessor{} + sp2 := &basicSpanProcessor{} + stp.RegisterSpanProcessor(sp1) + stp.RegisterSpanProcessor(sp2) + ctx := t.Context() + + require.NoError(t, stp.ForceFlush(ctx)) + require.True(t, sp1.flushed, "SpanProcessor ForceFlush not called") + require.True(t, sp2.flushed, "SpanProcessor ForceFlush not called") + }) + + t.Run("MultiWithSPError", func(t *testing.T) { + stp := NewTracerProvider() + spErr := errors.New("basic span processor export failure") + sp1 := &basicSpanProcessor{injectExportError: spErr} + sp2 := &basicSpanProcessor{} + stp.RegisterSpanProcessor(sp1) + stp.RegisterSpanProcessor(sp2) + ctx := t.Context() + + assert.ErrorIs(t, stp.ForceFlush(ctx), sp1.injectExportError, "span processor error not returned") + require.False(t, sp1.flushed, "SpanProcessor wrongly considered flushed") + require.True(t, sp2.flushed, "SpanProcessor ForceFlush not called") + }) +} + func TestTracerProviderSamplerConfigFromEnv(t *testing.T) { type testCase struct { sampler string From 67347565e77234a9f1bc59e1d3a66d2f83be98f0 Mon Sep 17 00:00:00 2001 From: Jared Taylor Date: Mon, 9 Feb 2026 16:05:40 -0500 Subject: [PATCH 04/10] Move this PR changelog entry to Unreleased --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e725282bec2..197db8806b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### Changed + +- TracerProvider `ForceFlush()` Joins errors together and continues iteration through SpanProcessors as opposed + to returning the first encountered error without attempting exports on subsequent SpanProcessors. (#7856) + From 5ca871f452032c8dc6f304c5c2ee2b99b868c149 Mon Sep 17 00:00:00 2001 From: Jared Taylor Date: Mon, 9 Feb 2026 17:08:47 -0500 Subject: [PATCH 05/10] Add timed-out context test for full coverage --- sdk/trace/provider_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/sdk/trace/provider_test.go b/sdk/trace/provider_test.go index 79f1d47a211..c9364ec994d 100644 --- a/sdk/trace/provider_test.go +++ b/sdk/trace/provider_test.go @@ -9,6 +9,7 @@ import ( "fmt" "math/rand/v2" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -275,6 +276,14 @@ func TestTracerProviderForceFlush(t *testing.T) { require.False(t, sp1.flushed, "SpanProcessor wrongly considered flushed") require.True(t, sp2.flushed, "SpanProcessor ForceFlush not called") }) + + t.Run("WithTimeout", func(t *testing.T) { + stp := NewTracerProvider() + sp1 := &basicSpanProcessor{} + stp.RegisterSpanProcessor(sp1) + ctx, _ := context.WithTimeout(t.Context(), time.Nanosecond) + assert.ErrorIs(t, stp.ForceFlush(ctx), context.DeadlineExceeded) + }) } func TestTracerProviderSamplerConfigFromEnv(t *testing.T) { From f861c7e2032152eb24de0305ba6b83be45bf5701 Mon Sep 17 00:00:00 2001 From: Jared Taylor Date: Mon, 9 Feb 2026 17:34:32 -0500 Subject: [PATCH 06/10] change timeout test to use cancel instead. actually used precommit this time. --- sdk/trace/provider_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sdk/trace/provider_test.go b/sdk/trace/provider_test.go index c9364ec994d..47e161bbadd 100644 --- a/sdk/trace/provider_test.go +++ b/sdk/trace/provider_test.go @@ -9,7 +9,6 @@ import ( "fmt" "math/rand/v2" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -234,7 +233,7 @@ func TestRegisterAfterShutdownWithProcessors(t *testing.T) { } func TestTracerProviderForceFlush(t *testing.T) { - t.Run("AfterShutdown", func(t *testing.T) { + t.Run("AfterShutdown", func(t *testing.T) { stp := NewTracerProvider() sp1 := &basicSpanProcessor{} stp.RegisterSpanProcessor(sp1) @@ -250,7 +249,7 @@ func TestTracerProviderForceFlush(t *testing.T) { assert.False(t, sp1.flushed, "SpanProcessor ForceFlush called after Shutdown") }) - t.Run("Multi", func(t *testing.T) { + t.Run("Multi", func(t *testing.T) { stp := NewTracerProvider() sp1 := &basicSpanProcessor{} sp2 := &basicSpanProcessor{} @@ -263,7 +262,7 @@ func TestTracerProviderForceFlush(t *testing.T) { require.True(t, sp2.flushed, "SpanProcessor ForceFlush not called") }) - t.Run("MultiWithSPError", func(t *testing.T) { + t.Run("MultiWithSPError", func(t *testing.T) { stp := NewTracerProvider() spErr := errors.New("basic span processor export failure") sp1 := &basicSpanProcessor{injectExportError: spErr} @@ -277,12 +276,13 @@ func TestTracerProviderForceFlush(t *testing.T) { require.True(t, sp2.flushed, "SpanProcessor ForceFlush not called") }) - t.Run("WithTimeout", func(t *testing.T) { + t.Run("WithCancel", func(t *testing.T) { stp := NewTracerProvider() sp1 := &basicSpanProcessor{} stp.RegisterSpanProcessor(sp1) - ctx, _ := context.WithTimeout(t.Context(), time.Nanosecond) - assert.ErrorIs(t, stp.ForceFlush(ctx), context.DeadlineExceeded) + ctx, cancel := context.WithCancel(t.Context()) + cancel() + assert.ErrorIs(t, stp.ForceFlush(ctx), context.Canceled) }) } From 72c3af3609f774b379aa7c67e3065da5711e7d3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Mon, 23 Feb 2026 19:25:19 +0100 Subject: [PATCH 07/10] Update CHANGELOG.md --- CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a38d091562..4ce2862d956 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,6 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] -### Changed - -- TracerProvider `ForceFlush()` Joins errors together and continues iteration through SpanProcessors as opposed - to returning the first encountered error without attempting exports on subsequent SpanProcessors. (#7856) This release is the last to support [Go 1.24]. The next release will require at least [Go 1.25]. @@ -19,6 +15,11 @@ The next release will require at least [Go 1.25]. - Support testing of [Go 1.26]. (#7902) +### Changed + +- TracerProvider `ForceFlush()` Joins errors together and continues iteration through SpanProcessors as opposed + to returning the first encountered error without attempting exports on subsequent SpanProcessors. (#7856) + From d11692adab4be2014b68cd1a50ce7cfa957dccd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Mon, 23 Feb 2026 19:26:26 +0100 Subject: [PATCH 08/10] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ce2862d956..002c944c370 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ The next release will require at least [Go 1.25]. ### Changed -- TracerProvider `ForceFlush()` Joins errors together and continues iteration through SpanProcessors as opposed +- `TracerProvider.ForceFlush` in `go.opentelemetry.io/otel/sdk/trace` joins errors together and continues iteration through SpanProcessors as opposed to returning the first encountered error without attempting exports on subsequent SpanProcessors. (#7856) From 3b845fdfe5e5fcb5c3cffff178374e5e6b7df48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Mon, 23 Feb 2026 19:27:01 +0100 Subject: [PATCH 09/10] Fix formatting in CHANGELOG.md Removed extra line breaks in CHANGELOG for clarity. --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 002c944c370..7d9e8c2865e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,8 +17,7 @@ The next release will require at least [Go 1.25]. ### Changed -- `TracerProvider.ForceFlush` in `go.opentelemetry.io/otel/sdk/trace` joins errors together and continues iteration through SpanProcessors as opposed - to returning the first encountered error without attempting exports on subsequent SpanProcessors. (#7856) +- `TracerProvider.ForceFlush` in `go.opentelemetry.io/otel/sdk/trace` joins errors together and continues iteration through SpanProcessors as opposed to returning the first encountered error without attempting exports on subsequent SpanProcessors. (#7856) From a0700bbef917d5d75ddfbfe5778ad0d5dca97736 Mon Sep 17 00:00:00 2001 From: sawamurataxman Date: Mon, 23 Feb 2026 16:41:58 -0500 Subject: [PATCH 10/10] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Robert PajÄ…k --- sdk/trace/provider_test.go | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/sdk/trace/provider_test.go b/sdk/trace/provider_test.go index 47e161bbadd..77d3e87a025 100644 --- a/sdk/trace/provider_test.go +++ b/sdk/trace/provider_test.go @@ -39,12 +39,8 @@ func (t *basicSpanProcessor) Shutdown(context.Context) error { func (*basicSpanProcessor) OnStart(context.Context, ReadWriteSpan) {} func (*basicSpanProcessor) OnEnd(ReadOnlySpan) {} func (t *basicSpanProcessor) ForceFlush(context.Context) error { - if t.injectExportError != nil { - t.flushed = false - return t.injectExportError - } t.flushed = true - return nil + return t.injectExportError } type shutdownSpanProcessor struct { @@ -262,17 +258,16 @@ func TestTracerProviderForceFlush(t *testing.T) { require.True(t, sp2.flushed, "SpanProcessor ForceFlush not called") }) - t.Run("MultiWithSPError", func(t *testing.T) { + t.Run("Error", func(t *testing.T) { stp := NewTracerProvider() - spErr := errors.New("basic span processor export failure") - sp1 := &basicSpanProcessor{injectExportError: spErr} + sp1 := &basicSpanProcessor{injectExportError: assert.AnError} sp2 := &basicSpanProcessor{} stp.RegisterSpanProcessor(sp1) stp.RegisterSpanProcessor(sp2) ctx := t.Context() - assert.ErrorIs(t, stp.ForceFlush(ctx), sp1.injectExportError, "span processor error not returned") - require.False(t, sp1.flushed, "SpanProcessor wrongly considered flushed") + assert.ErrorIs(t, stp.ForceFlush(ctx), assert.AnError, "span processor error not returned") + require.True(t, sp1.flushed, "SpanProcessor ForceFlush not called") require.True(t, sp2.flushed, "SpanProcessor ForceFlush not called") })