feat(otel): enrich resource with process, os, host, and container detectors

This commit is contained in:
Julien Neuhart
2026-06-02 19:16:01 +02:00
parent 387728f09a
commit bbebad1175
2 changed files with 64 additions and 40 deletions
+39 -28
View File
@@ -21,27 +21,47 @@ import (
)
// buildResource assembles the OpenTelemetry resource shared by the tracer,
// meter, and logger providers.
func buildResource(serviceName, serviceVersion string) (*resource.Resource, error) {
hostname, err := os.Hostname()
if err != nil {
return nil, fmt.Errorf("get hostname: %w", err)
}
// meter, and logger providers. Detection is best-effort: a detector or merge
// failure is logged and the build proceeds with whatever was gathered, so a
// flaky environment never prevents telemetry from starting.
func buildResource(ctx context.Context, logger *slog.Logger, serviceName, serviceVersion string) *resource.Resource {
base := resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName(serviceName),
semconv.ServiceVersion(serviceVersion),
)
res, err := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName(serviceName),
semconv.ServiceVersion(serviceVersion),
semconv.HostName(hostname),
),
// Granular detectors only. The WithProcess() bundle is deliberately omitted
// because it adds process.command_args/process.command_line, which can carry
// proxy credentials and host-resolver rules passed on the command line.
detected, err := resource.New(ctx,
resource.WithFromEnv(),
resource.WithTelemetrySDK(),
resource.WithHost(),
resource.WithHostID(),
resource.WithOS(),
resource.WithContainer(),
resource.WithProcessPID(),
resource.WithProcessExecutableName(),
resource.WithProcessExecutablePath(),
resource.WithProcessRuntimeName(),
resource.WithProcessRuntimeVersion(),
resource.WithProcessRuntimeDescription(),
)
if err != nil {
return nil, fmt.Errorf("merge resource: %w", err)
logger.WarnContext(ctx, fmt.Sprintf("partially detect OpenTelemetry resource: %s", err))
}
if detected == nil {
return base
}
return res, nil
merged, err := resource.Merge(detected, base)
if err != nil {
logger.WarnContext(ctx, fmt.Sprintf("merge OpenTelemetry resource: %s", err))
return base
}
return merged
}
// InitTracerProvider initializes the OpenTelemetry tracer provider.
@@ -50,10 +70,7 @@ func InitTracerProvider(logger *slog.Logger, serviceName, serviceVersion string)
ctx := context.Background()
res, err := buildResource(serviceName, serviceVersion)
if err != nil {
return nil, fmt.Errorf("build resource: %w", err)
}
res := buildResource(ctx, logger, serviceName, serviceVersion)
traceOpts := []trace.TracerProviderOption{
trace.WithResource(res),
@@ -85,10 +102,7 @@ func InitMeterProvider(logger *slog.Logger, serviceName, serviceVersion string)
ctx := context.Background()
res, err := buildResource(serviceName, serviceVersion)
if err != nil {
return nil, fmt.Errorf("build resource: %w", err)
}
res := buildResource(ctx, logger, serviceName, serviceVersion)
metricOpts := []metric.Option{
metric.WithResource(res),
@@ -127,10 +141,7 @@ func InitLoggerProvider(logger *slog.Logger, serviceName, serviceVersion string)
ctx := context.Background()
res, err := buildResource(serviceName, serviceVersion)
if err != nil {
return nil, nil, fmt.Errorf("build resource: %w", err)
}
res := buildResource(ctx, logger, serviceName, serviceVersion)
logOpts := []log.LoggerProviderOption{
log.WithResource(res),
+25 -12
View File
@@ -15,24 +15,37 @@ import (
)
func TestBuildResource(t *testing.T) {
res, err := buildResource("gotenberg", "v8.0.0")
if err != nil {
t.Fatalf("build resource: %v", err)
}
res := buildResource(context.Background(), slog.New(slog.DiscardHandler), "gotenberg", "v8.0.0")
got := map[string]string{}
got := map[string]struct{}{}
values := map[string]string{}
for _, kv := range res.Attributes() {
got[string(kv.Key)] = kv.Value.AsString()
got[string(kv.Key)] = struct{}{}
values[string(kv.Key)] = kv.Value.AsString()
}
if got[string(semconv.ServiceNameKey)] != "gotenberg" {
t.Errorf("service.name = %q, want %q", got[string(semconv.ServiceNameKey)], "gotenberg")
if values[string(semconv.ServiceNameKey)] != "gotenberg" {
t.Errorf("service.name = %q, want %q", values[string(semconv.ServiceNameKey)], "gotenberg")
}
if got[string(semconv.ServiceVersionKey)] != "v8.0.0" {
t.Errorf("service.version = %q, want %q", got[string(semconv.ServiceVersionKey)], "v8.0.0")
if values[string(semconv.ServiceVersionKey)] != "v8.0.0" {
t.Errorf("service.version = %q, want %q", values[string(semconv.ServiceVersionKey)], "v8.0.0")
}
if _, ok := got[string(semconv.HostNameKey)]; !ok {
t.Error("expected host.name attribute to be present")
for _, key := range []string{
string(semconv.HostNameKey),
string(semconv.OSTypeKey),
string(semconv.ProcessRuntimeNameKey),
} {
if _, ok := got[key]; !ok {
t.Errorf("expected resource attribute %q to be present", key)
}
}
// The command-line bundle must never be detected (it can leak credentials).
for _, key := range []string{"process.command_args", "process.command_line"} {
if _, ok := got[key]; ok {
t.Errorf("did not expect sensitive resource attribute %q", key)
}
}
}