mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
99bdd23986
Adds a yaegi-safe inline telemetry helper that fires a single
fire-and-forget ping at plugin load. Helps track adoption and version
spread. No persistent identifiers are collected.
Implementation notes:
- inline (no external dep) so Traefik plugin loader does not need to
resolve a new vendored module
- stdlib-only, no generics, no range-over-int — verified to load under
yaegi 0.16.x (full plugin import + CreateConfig/New symbol lookup OK)
- avoids `switch{case A,B,C:}` blocks where some yaegi releases
mis-evaluate comma-separated case lists
- sync.Once guards against amplified pings on Traefik dynamic config
reloads (which re-instantiate the middleware)
Opt out via any of:
DO_NOT_TRACK=1
OSS_TELEMETRY_DISABLED=1
TRAEFIKOIDC_DISABLE_TELEMETRY=1
168 lines
4.6 KiB
Go
168 lines
4.6 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// resetTelemetryState restores package-level mutable state so tests do not
|
|
// contaminate one another. The cleanup waits for any in-flight ping goroutine
|
|
// to finish before restoring telemetryEndpoint — without that drain step the
|
|
// goroutine and the cleanup would race on the var.
|
|
func resetTelemetryState(t *testing.T) {
|
|
t.Helper()
|
|
telemetryOnce = sync.Once{}
|
|
prev := telemetryEndpoint
|
|
t.Cleanup(func() {
|
|
telemetryInflight.Wait()
|
|
telemetryEndpoint = prev
|
|
telemetryOnce = sync.Once{}
|
|
})
|
|
}
|
|
|
|
func newTelemetryServer(t *testing.T, status int) (hits *int32, lastBody func() string) {
|
|
t.Helper()
|
|
var counter int32
|
|
var mu sync.Mutex
|
|
var body string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
atomic.AddInt32(&counter, 1)
|
|
b, _ := io.ReadAll(r.Body)
|
|
_ = r.Body.Close()
|
|
mu.Lock()
|
|
body = string(b)
|
|
mu.Unlock()
|
|
w.WriteHeader(status)
|
|
}))
|
|
telemetryEndpoint = srv.URL
|
|
t.Cleanup(srv.Close)
|
|
return &counter, func() string {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
return body
|
|
}
|
|
}
|
|
|
|
func TestValidTelemetryVersion(t *testing.T) {
|
|
good := []string{"1.2.3", "1.4.0-beta1", "2.0", "v1.0.0", "1.0.0+meta", "dev"}
|
|
for _, v := range good {
|
|
if !validTelemetryVersion(v) {
|
|
t.Errorf("validTelemetryVersion(%q) = false, want true", v)
|
|
}
|
|
}
|
|
bad := []string{"", "has space", "semi;colon", strings.Repeat("1", 33)}
|
|
for _, v := range bad {
|
|
if validTelemetryVersion(v) {
|
|
t.Errorf("validTelemetryVersion(%q) = true, want false", v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTelemetryDisabledByEnv(t *testing.T) {
|
|
for _, k := range []string{"DO_NOT_TRACK", "OSS_TELEMETRY_DISABLED", "TRAEFIKOIDC_DISABLE_TELEMETRY"} {
|
|
t.Run(k, func(t *testing.T) {
|
|
t.Setenv(k, "1")
|
|
if !telemetryDisabledByEnv() {
|
|
t.Fatalf("%s=1 should disable", k)
|
|
}
|
|
})
|
|
}
|
|
t.Run("falsy_values_do_not_disable", func(t *testing.T) {
|
|
t.Setenv("DO_NOT_TRACK", "0")
|
|
t.Setenv("OSS_TELEMETRY_DISABLED", "false")
|
|
t.Setenv("TRAEFIKOIDC_DISABLE_TELEMETRY", "no")
|
|
if telemetryDisabledByEnv() {
|
|
t.Fatal("falsy env values should not disable")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSendTelemetry_FiresOnceAcrossManyCalls(t *testing.T) {
|
|
resetTelemetryState(t)
|
|
hits, lastBody := newTelemetryServer(t, http.StatusNoContent)
|
|
|
|
for i := 0; i < 50; i++ {
|
|
sendTelemetry("1.2.3")
|
|
}
|
|
telemetryInflight.Wait()
|
|
|
|
if got := atomic.LoadInt32(hits); got != 1 {
|
|
t.Fatalf("expected exactly 1 hit, got %d", got)
|
|
}
|
|
|
|
var payload struct {
|
|
Project string `json:"project"`
|
|
Version string `json:"version"`
|
|
Ts int64 `json:"ts"`
|
|
}
|
|
if err := json.Unmarshal([]byte(lastBody()), &payload); err != nil {
|
|
t.Fatalf("server received non-JSON body: %q (err: %v)", lastBody(), err)
|
|
}
|
|
if payload.Project != "traefikoidc" || payload.Version != "1.2.3" || payload.Ts <= 0 {
|
|
t.Fatalf("unexpected payload: %+v", payload)
|
|
}
|
|
}
|
|
|
|
func TestSendTelemetry_RespectsDisableEnv(t *testing.T) {
|
|
resetTelemetryState(t)
|
|
hits, _ := newTelemetryServer(t, http.StatusNoContent)
|
|
t.Setenv("DO_NOT_TRACK", "1")
|
|
|
|
sendTelemetry("1.2.3")
|
|
telemetryInflight.Wait()
|
|
|
|
if got := atomic.LoadInt32(hits); got != 0 {
|
|
t.Fatalf("DO_NOT_TRACK should suppress; got %d hits", got)
|
|
}
|
|
}
|
|
|
|
func TestSendTelemetry_DropsInvalidVersion(t *testing.T) {
|
|
resetTelemetryState(t)
|
|
hits, _ := newTelemetryServer(t, http.StatusNoContent)
|
|
|
|
sendTelemetry("has space")
|
|
telemetryInflight.Wait()
|
|
|
|
if got := atomic.LoadInt32(hits); got != 0 {
|
|
t.Fatalf("invalid version should suppress; got %d hits", got)
|
|
}
|
|
}
|
|
|
|
func TestSendTelemetry_DoesNotBlock(t *testing.T) {
|
|
resetTelemetryState(t)
|
|
// Hanging server proves the caller is never blocked. The 2s context
|
|
// timeout in doTelemetryPost ensures the goroutine eventually exits;
|
|
// resetTelemetryState's cleanup waits for that drain before restoring
|
|
// telemetryEndpoint so there is no race with this test's mutation.
|
|
hung := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
|
|
time.Sleep(5 * time.Second)
|
|
}))
|
|
t.Cleanup(hung.Close)
|
|
telemetryEndpoint = hung.URL
|
|
|
|
start := time.Now()
|
|
sendTelemetry("1.2.3")
|
|
if elapsed := time.Since(start); elapsed > 50*time.Millisecond {
|
|
t.Fatalf("sendTelemetry blocked for %v, expected near-instant return", elapsed)
|
|
}
|
|
}
|
|
|
|
func TestSendTelemetry_SurvivesServerError(t *testing.T) {
|
|
resetTelemetryState(t)
|
|
hits, _ := newTelemetryServer(t, http.StatusInternalServerError)
|
|
|
|
sendTelemetry("1.2.3")
|
|
telemetryInflight.Wait()
|
|
|
|
if got := atomic.LoadInt32(hits); got != 1 {
|
|
t.Fatalf("request should still reach server even on 500; got %d hits", got)
|
|
}
|
|
}
|