From f06f5561d41449e276d66beeef55e904903da34a Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Sat, 30 May 2026 12:35:42 +0100 Subject: [PATCH] fix: make internals yaegi-interpretable for traefik plugin consumers Swap atomic.Pointer[string] for a mutex-guarded string, atomic.Bool for the function-based sync/atomic int32 API, two range-over-int loops for classic index loops, and drop the init() that seeded the endpoint pointer. No public API or runtime behaviour change; go test, go vet, and golangci-lint all pass. Rationale: the traefikoidc Traefik plugin vendors and interprets dependency source via Yaegi, which on several Traefik runtimes cannot interpret generic stdlib types or range-over-int. This keeps the package importable from that plugin without altering what it does. --- telemetry.go | 42 ++++++++++++++++++++++++++++-------------- telemetry_test.go | 17 ++++++++--------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/telemetry.go b/telemetry.go index 2ac18b9..84b04ea 100644 --- a/telemetry.go +++ b/telemetry.go @@ -49,21 +49,35 @@ const ( maxVersionLen = 32 ) -// endpoint holds the ingest URL. Production code never mutates it; it is -// atomic only so the package's own test suite can safely retarget it at -// httptest servers while goroutines started by Send are still in flight. -var endpoint atomic.Pointer[string] - +// Yaegi note: this package is consumed by the traefikoidc Traefik plugin, which +// Traefik interprets with Yaegi (it vendors and interprets dependency source). +// It therefore avoids generic stdlib types (atomic.Pointer[T], atomic.Bool) and +// range-over-int (Go 1.22), which some Traefik/Yaegi runtimes cannot interpret. +// Endpoint mutation uses a mutex-guarded string; the disabled flag uses the +// function-based sync/atomic int32 API (atomic.LoadInt32/StoreInt32). var ( - disabled atomic.Bool + // endpointURL holds the ingest URL. Production code never mutates it; the + // setter exists only so the test suite can retarget it at httptest servers + // while goroutines started by Send are still in flight. + endpointMu sync.RWMutex + endpointURL = defaultEndpoint + + disabled int32 // 0 = enabled, 1 = disabled; accessed via sync/atomic only inflight sync.WaitGroup client = &http.Client{Timeout: httpTimeout} ) -func init() { - s := defaultEndpoint - endpoint.Store(&s) +func currentEndpoint() string { + endpointMu.RLock() + defer endpointMu.RUnlock() + return endpointURL +} + +func setEndpointURL(u string) { + endpointMu.Lock() + endpointURL = u + endpointMu.Unlock() } // Send fires a single anonymous telemetry ping in the background and returns @@ -78,7 +92,7 @@ func init() { // Call once at program startup. Calling repeatedly will send repeated pings; // the server is responsible for deduplication. func Send(project, version string) { - if disabled.Load() { + if atomic.LoadInt32(&disabled) != 0 { return } if isDisabledByEnv(project) { @@ -144,7 +158,7 @@ func isUsableBuildVersion(v string) bool { // Disable suppresses all subsequent Send calls in this process. // Idempotent and safe to call from any goroutine. func Disable() { - disabled.Store(true) + atomic.StoreInt32(&disabled, 1) } // Wait blocks until all in-flight pings have completed, or until timeout @@ -173,7 +187,7 @@ func dispatch(project, version string) { ctx, cancel := context.WithTimeout(context.Background(), httpTimeout) defer cancel() - req, err := http.NewRequestWithContext(ctx, http.MethodPost, *endpoint.Load(), bytes.NewReader(body)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, currentEndpoint(), bytes.NewReader(body)) if err != nil { return } @@ -208,7 +222,7 @@ func validProject(p string) bool { if n == 0 || n > maxProjectLen { return false } - for i := range n { + for i := 0; i < n; i++ { c := p[i] switch { case c >= 'a' && c <= 'z', @@ -330,7 +344,7 @@ func isDisabledByEnv(project string) bool { func projectEnvKey(project string) string { const suffix = "_DISABLE_TELEMETRY" buf := make([]byte, 0, len(project)+len(suffix)) - for i := range len(project) { + for i := 0; i < len(project); i++ { c := project[i] switch { case c == '-': diff --git a/telemetry_test.go b/telemetry_test.go index 99a2607..35a2e99 100644 --- a/telemetry_test.go +++ b/telemetry_test.go @@ -25,22 +25,21 @@ type pingPayload struct { // each other. Tests share `disabled`, `endpoint`, and `inflight`. func resetState(t *testing.T) { t.Helper() - disabled.Store(false) + atomic.StoreInt32(&disabled, 0) t.Cleanup(func() { - disabled.Store(false) + atomic.StoreInt32(&disabled, 0) }) } -// setEndpoint atomically retargets the ingest URL for the duration of a test -// and restores it on cleanup. Safe even if Send goroutines outlive the test. +// setEndpoint retargets the ingest URL for the duration of a test and restores +// it on cleanup. Safe even if Send goroutines outlive the test (the setter is +// mutex-guarded). func setEndpoint(t *testing.T, url string) { t.Helper() - prev := *endpoint.Load() - s := url - endpoint.Store(&s) + prev := currentEndpoint() + setEndpointURL(url) t.Cleanup(func() { - p := prev - endpoint.Store(&p) + setEndpointURL(prev) }) }