Compare commits

...
2 Commits
Author SHA1 Message Date
releaser 92db06698c chore(release): prepare for v0.6.1 (#185)
otelsetup / test (push) Skipped
otelsetup / vulnerabilities (push) Skipped
Unbound Release / Check Preconditions (push) Successful in 25s
Unbound Release / Create Tag (push) Skipped
Unbound Release / Generate Changelog and Handle PR (push) Successful in 26s
Unbound Release / Create Release (push) Successful in 28s
Release / release (push) Successful in 1m13s
pre-commit / pre-commit (push) Successful in 3m24s
## [0.6.1] - 2026-09-19

### 🐛 Bug Fixes

- Close idle OTLP connections before the collector does (#184)

<!-- generated by git-cliff -->

---

**Note:** Please use **Squash Merge** when merging this PR.

Reviewed-on: https://gitea.unbound.se/shiny/otelsetup/pulls/185
Co-authored-by: Unbound Releaser <releaser@unbound.se>
2026-09-19 13:58:49 +00:00
argoyle 8b3d3110fe fix: close idle OTLP connections before the collector does (#184)
Unbound Release / Check Preconditions (push) Successful in 27s
Unbound Release / Create Tag (push) Skipped
otelsetup / test (push) Skipped
otelsetup / vulnerabilities (push) Skipped
Unbound Release / Generate Changelog and Handle PR (push) Successful in 39s
Unbound Release / Create Release (push) Successful in 29s
pre-commit / pre-commit (push) Successful in 3m27s
Release / release (push) Successful in 1m14s
2026-09-19 13:51:36 +00:00
5 changed files with 63 additions and 3 deletions

No files matched your search

+1 -1
View File
@@ -1,3 +1,3 @@
{
"version": "v0.6.0"
"version": "v0.6.1"
}
+6
View File
@@ -2,6 +2,12 @@
All notable changes to this project will be documented in this file.
## [0.6.1] - 2026-09-19
### 🐛 Bug Fixes
- Close idle OTLP connections before the collector does (#184)
## [0.6.0] - 2026-09-17
### 🚀 Features
+1
View File
@@ -42,3 +42,4 @@ http.Handle("/", otelsetup.Handler(myHandler))
- `OTEL_RESOURCE_ATTRIBUTES` - Auto-set if not provided (service.name, service.version, service.environment)
- Standard OTLP environment variables for endpoint configuration
- The trace and metric exporters use our own HTTP client (idle connections closed after 30s, before the collector's 1m idle timeout), so `OTEL_EXPORTER_OTLP_*TIMEOUT` and the `OTEL_EXPORTER_OTLP_*CERTIFICATE` / `*CLIENT_*` TLS variables are ignored
+33 -2
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"net"
"net/http"
"os"
"time"
@@ -65,7 +66,7 @@ func SetupOTelSDK(ctx context.Context, enabled bool, serviceName, buildVersion,
)
otel.SetTextMapPropagator(prop)
traceExporter, err := otlptracehttp.New(ctx)
traceExporter, err := otlptracehttp.New(ctx, otlptracehttp.WithHTTPClient(otlpHTTPClient()))
if err != nil {
return handleErr(err)
}
@@ -88,7 +89,7 @@ func SetupOTelSDK(ctx context.Context, enabled bool, serviceName, buildVersion,
global.SetLoggerProvider(logProvider)
shutdownFuncs = append(shutdownFuncs, logProvider.Shutdown)
exp, err := otlpmetrichttp.New(ctx)
exp, err := otlpmetrichttp.New(ctx, otlpmetrichttp.WithHTTPClient(otlpHTTPClient()))
if err != nil {
return handleErr(err)
}
@@ -99,6 +100,36 @@ func SetupOTelSDK(ctx context.Context, enabled bool, serviceName, buildVersion,
return shutdown, err
}
// otlpHTTPClient is the OTLP exporters' default client, except that it drops idle
// connections after 30s. The collector (Alloy's OTLP receiver, idle_timeout 1m) closes
// connections idle for a minute, and metrics are pushed every 60s. With the exporters'
// default of 90s, a push could reuse a connection the collector was closing. The push
// then failed (EOF, connection reset) and was dropped: net/http doesn't retry a POST and
// the exporters don't retry transport errors. Traces hit the same race after a quiet
// spell. Closing first means such a push dials fresh.
//
// The transport mirrors the exporters' own (otlpmetrichttp/otlptracehttp v1.46.0
// ourTransport). A custom client makes the exporters ignore OTEL_EXPORTER_OTLP_*TIMEOUT
// and the OTEL_EXPORTER_OTLP_*CERTIFICATE/CLIENT_* TLS variables; nothing sets them.
func otlpHTTPClient() *http.Client {
return &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
// The exporters' default.
Timeout: 10 * time.Second,
}
}
func Handler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header))
+22
View File
@@ -0,0 +1,22 @@
package otelsetup
import (
"net/http"
"testing"
"time"
)
func TestOTLPHTTPClient_ClosesIdleConnectionsBeforeTheCollector(t *testing.T) {
c := otlpHTTPClient()
tr, ok := c.Transport.(*http.Transport)
if !ok {
t.Fatalf("transport is %T, want *http.Transport", c.Transport)
}
// Alloy's OTLP receiver closes connections idle for 1m; the client must close first.
if tr.IdleConnTimeout <= 0 || tr.IdleConnTimeout >= time.Minute {
t.Errorf("IdleConnTimeout = %v, want in (0, 1m)", tr.IdleConnTimeout)
}
if c.Timeout != 10*time.Second {
t.Errorf("Timeout = %v, want 10s (the exporters' default)", c.Timeout)
}
}