Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,18 @@ ARG TARGETOS TARGETARCH
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -ldflags="-s -w" -o /out/proxy-sampler ./cmd/app

FROM debian:bookworm-slim
# The nonroot account matches the numeric USER below. Without it the runtime UID
# resolves to no account at all, which breaks every os/user lookup in the
# process -- including the OpenTelemetry process.owner resource detector.
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates tzdata \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --system --gid 65532 nonroot \
&& useradd --system --uid 65532 --gid 65532 \
--home-dir /home/nonroot --create-home --shell /usr/sbin/nologin nonroot
COPY --from=go-builder /out/proxy-sampler /usr/local/bin/proxy-sampler
ENV HTTP_ADDR=:8080
EXPOSE 8080
# Numeric so Kubernetes runAsNonRoot can verify it without resolving the name.
USER 65532:65532
ENTRYPOINT ["/usr/local/bin/proxy-sampler"]
11 changes: 9 additions & 2 deletions internal/telemetry/otel.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ var (
newLogExporter = func(ctx context.Context) (sdklog.Exporter, error) {
return otlploghttp.New(ctx)
}
newResource = buildResource
)

// Init installs OTLP/HTTP trace, metric, and log providers. With no common
Expand All @@ -58,9 +59,15 @@ func Init(ctx context.Context, getenv func(string) string, logger *slog.Logger)
logger = slog.Default()
}

res, err := buildResource(ctx)
// resource.New reports every detector failure but still returns the
// attributes the remaining detectors produced, so a failure here degrades
// the resource rather than invalidating it. Note that not all of these
// errors wrap resource.ErrPartialResource: the process owner detector
// returns user.Current's error verbatim, which is how a container UID
// without an /etc/passwd entry surfaces.
res, err := newResource(ctx)
if err != nil {
return nil, fmt.Errorf("build telemetry resource: %w", err)
logger.Warn("incomplete telemetry resource", "error", err)
}
metricExporter, err := newMetricExporter(ctx)
if err != nil {
Expand Down
53 changes: 53 additions & 0 deletions internal/telemetry/otel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
sdklog "go.opentelemetry.io/otel/sdk/log"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace"
)
Expand Down Expand Up @@ -123,6 +124,51 @@ func TestInitCleansMetricExporterWhenTraceSetupFails(t *testing.T) {
}
}

func TestInitKeepsPartialResourceWhenDetectionFails(t *testing.T) {
// resource.New reports detector failures but still returns everything the
// remaining detectors produced. The process owner detector returns
// user.Current's error verbatim rather than wrapping ErrPartialResource, so
// a container UID with no /etc/passwd entry yields a plain error here. This
// is the wording the CGO_ENABLED=0 build emits; cgo builds instead report
// "user: unknown userid 65532".
detectionErr := errors.New("error detecting resource: user: Current requires cgo or $USER set in environment")
partial := resource.NewSchemaless(attribute.String("service.name", "proxy-sampler"))
restoreResource := replaceResourceFactory(t, func(context.Context) (*resource.Resource, error) {
return partial, detectionErr
})
defer restoreResource()
restore := replaceExporterFactories(t,
func(context.Context) (sdkmetric.Exporter, error) { return &fakeMetricExporter{}, nil },
func(context.Context) (sdktrace.SpanExporter, error) { return &fakeTraceExporter{}, nil },
func(context.Context) (sdklog.Exporter, error) { return &fakeLogExporter{}, nil },
)
defer restore()

originalTracer := otel.GetTracerProvider()
originalMeter := otel.GetMeterProvider()
originalLogger := global.GetLoggerProvider()
t.Cleanup(func() {
otel.SetTracerProvider(originalTracer)
otel.SetMeterProvider(originalMeter)
global.SetLoggerProvider(originalLogger)
})

var logs bytes.Buffer
shutdown, err := Init(context.Background(), endpointGetenv, slog.New(slog.NewJSONHandler(&logs, nil)))
if err != nil {
t.Fatalf("Init with an incomplete resource: %v", err)
}
if otel.GetTracerProvider() == originalTracer || otel.GetMeterProvider() == originalMeter {
t.Fatal("incomplete resource detection suppressed the trace and metric providers")
}
if !strings.Contains(logs.String(), "Current requires cgo") {
t.Fatalf("incomplete resource detection was not reported: %s", logs.String())
}
if err := shutdown(context.Background()); err != nil {
t.Fatalf("shutdown: %v", err)
}
}

func TestBuildResourceForcesProxySamplerServiceName(t *testing.T) {
t.Setenv("OTEL_SERVICE_NAME", "wrong-service")
res, err := buildResource(context.Background())
Expand All @@ -144,6 +190,13 @@ func endpointGetenv(name string) string {

type exporterFactoryRestore func()

func replaceResourceFactory(t *testing.T, factory func(context.Context) (*resource.Resource, error)) exporterFactoryRestore {
t.Helper()
original := newResource
newResource = factory
return func() { newResource = original }
}

func replaceExporterFactories(
t *testing.T,
metricFactory func(context.Context) (sdkmetric.Exporter, error),
Expand Down