From 4a5454519f9017fae91d4ce00aa71e2edb89ad84 Mon Sep 17 00:00:00 2001 From: Al Cutter Date: Mon, 9 Nov 2026 17:11:24 +0000 Subject: [PATCH 1/2] Make OTel instanceID work with Cloud Run too --- cmd/tesseract/gcp/otel.go | 76 ++++++++++++++++------- cmd/tesseract/gcp/otel_test.go | 106 +++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 21 deletions(-) create mode 100644 cmd/tesseract/gcp/otel_test.go diff --git a/cmd/tesseract/gcp/otel.go b/cmd/tesseract/gcp/otel.go index 5dd9c9bb0..97eb57b78 100644 --- a/cmd/tesseract/gcp/otel.go +++ b/cmd/tesseract/gcp/otel.go @@ -57,27 +57,7 @@ func initOTel(ctx context.Context, traceFraction float64, origin string, project } } - instanceID, err := os.Hostname() - if err != nil { - slog.ErrorContext(ctx, "os.Hostname() failed, setting OTel service instance ID to UUID", slog.Any("error", err)) - instanceID = uuid.NewString() - } - attrs := []attribute.KeyValue{ - semconv.ServiceNamespaceKey.String("tesseract"), - semconv.ServiceNameKey.String(origin), - semconv.ServiceInstanceIDKey.String(instanceID), - } - if projectID != "" { - // Selects the destination project for the Cloud Telemetry API. - attrs = append(attrs, attribute.String("gcp.project_id", projectID)) - } - resources, err := resource.New(ctx, - resource.WithTelemetrySDK(), - // Add your own custom attributes to identify your application - resource.WithAttributes(attrs...), - resource.WithFromEnv(), // unpacks OTEL_RESOURCE_ATTRIBUTES - resource.WithDetectors(gcp.NewDetector()), - ) + resources, err := detectResources(ctx, origin, projectID, os.Hostname, gcp.NewDetector()) if err != nil { fatal(ctx, "Failed to detect resources", slog.Any("error", err)) } @@ -139,3 +119,57 @@ func initOTel(ctx context.Context, traceFraction float64, origin string, project } return shutdown } + +// detectResources builds the OTel Resource describing this service instance. +// +// It only sets service.instance.id when the detectors (e.g. gcp.NewDetector()) +// and environment variables do not already supply faas.instance (Cloud Run), +// host.id (GCE/GKE), or service.instance.id. When falling back to hostname, +// a random UUID is used if hostname resolution fails or returns "localhost" +// (as on Cloud Run gen2 containers). +func detectResources(ctx context.Context, origin, projectID string, hostname func() (string, error), detectors ...resource.Detector) (*resource.Resource, error) { + attrs := []attribute.KeyValue{ + semconv.ServiceNamespaceKey.String("tesseract"), + semconv.ServiceNameKey.String(origin), + } + if projectID != "" { + // Selects the destination project for the Cloud Telemetry API. + attrs = append(attrs, attribute.String("gcp.project_id", projectID)) + } + resources, err := resource.New(ctx, + resource.WithTelemetrySDK(), + resource.WithAttributes(attrs...), + resource.WithFromEnv(), // unpacks OTEL_RESOURCE_ATTRIBUTES + resource.WithDetectors(detectors...), + ) + if err != nil { + return nil, err + } + // Set an instance ID if the detectors and env extractor above didn't come up with one. + if allAttrsEmpty(resources.Set(), semconv.ServiceInstanceIDKey, semconv.FaaSInstanceKey, semconv.HostIDKey) { + instanceID, err := hostname() + if err != nil { + slog.ErrorContext(ctx, "os.Hostname() failed, setting OTel service instance ID to UUID", slog.Any("error", err)) + instanceID = uuid.NewString() + } else if instanceID == "" || instanceID == "localhost" { + // localhost isn't very useful, so use a random UUID as a last resort. + instanceID = uuid.NewString() + } + resources, err = resource.Merge(resources, resource.NewSchemaless(semconv.ServiceInstanceIDKey.String(instanceID))) + if err != nil { + return nil, err + } + } + return resources, nil +} + +// allAttrsEmpty returns true if the provided attribute set does not contain a non-empty value +// associated with any of the provided keys. +func allAttrsEmpty(s *attribute.Set, keys ...attribute.Key) bool { + for _, k := range keys { + if v, ok := s.Value(k); ok && v.AsString() != "" { + return false + } + } + return true +} diff --git a/cmd/tesseract/gcp/otel_test.go b/cmd/tesseract/gcp/otel_test.go new file mode 100644 index 000000000..051da5f6b --- /dev/null +++ b/cmd/tesseract/gcp/otel_test.go @@ -0,0 +1,106 @@ +// Copyright 2026 The Tessera authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "errors" + "testing" + + "github.com/google/uuid" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" +) + +type fakeDetector struct { + attrs []attribute.KeyValue +} + +func (f fakeDetector) Detect(context.Context) (*resource.Resource, error) { + return resource.NewSchemaless(f.attrs...), nil +} + +func TestDetectResources(t *testing.T) { + for _, test := range []struct { + desc string + detectorAttrs []attribute.KeyValue + hostname func() (string, error) + wantNoServiceInstance bool + wantServiceInstance string + wantUUIDInstance bool + }{ + { + desc: "Cloud Run supplies faas.instance, service.instance.id not set", + detectorAttrs: []attribute.KeyValue{ + semconv.FaaSInstanceKey.String("00bf4bf02da24966c43a39d0c6d3bd884d3b81c4"), + }, + hostname: func() (string, error) { return "localhost", nil }, + wantNoServiceInstance: true, + }, + { + desc: "GCE supplies host.id, service.instance.id not set", + detectorAttrs: []attribute.KeyValue{ + semconv.HostIDKey.String("1234567890123456789"), + }, + hostname: func() (string, error) { return "gce-vm-1", nil }, + wantNoServiceInstance: true, + }, + { + desc: "fallback to non-localhost hostname when detector supplies no instance ID", + hostname: func() (string, error) { return "workstation-01", nil }, + wantServiceInstance: "workstation-01", + }, + { + desc: "fallback to UUID when hostname is localhost", + hostname: func() (string, error) { return "localhost", nil }, + wantUUIDInstance: true, + }, + { + desc: "fallback to UUID when os.Hostname fails", + hostname: func() (string, error) { return "", errors.New("hostname error") }, + wantUUIDInstance: true, + }, + } { + t.Run(test.desc, func(t *testing.T) { + t.Setenv("OTEL_RESOURCE_ATTRIBUTES", "") + res, err := detectResources(t.Context(), "example.com/log", "my-project", test.hostname, fakeDetector{attrs: test.detectorAttrs}) + if err != nil { + t.Fatalf("detectResources() unexpected error: %v", err) + } + + s := res.Set() + val, hasServiceInstance := s.Value(semconv.ServiceInstanceIDKey) + if test.wantNoServiceInstance { + if hasServiceInstance { + t.Errorf("service.instance.id = %q, want unset", val.AsString()) + } + return + } + + if !hasServiceInstance { + t.Fatalf("service.instance.id not set on resource: %v", res) + } + got := val.AsString() + if test.wantUUIDInstance { + if _, err := uuid.Parse(got); err != nil { + t.Errorf("service.instance.id = %q, want valid UUID: %v", got, err) + } + } else if got != test.wantServiceInstance { + t.Errorf("service.instance.id = %q, want %q", got, test.wantServiceInstance) + } + }) + } +} From 13b947663efcdc33aad646594938a516d060a7e9 Mon Sep 17 00:00:00 2001 From: Al Cutter Date: Mon, 9 Nov 2026 17:41:43 +0000 Subject: [PATCH 2/2] Fix log message --- cmd/tesseract/gcp/otel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/tesseract/gcp/otel.go b/cmd/tesseract/gcp/otel.go index 97eb57b78..0602a6d78 100644 --- a/cmd/tesseract/gcp/otel.go +++ b/cmd/tesseract/gcp/otel.go @@ -149,7 +149,7 @@ func detectResources(ctx context.Context, origin, projectID string, hostname fun if allAttrsEmpty(resources.Set(), semconv.ServiceInstanceIDKey, semconv.FaaSInstanceKey, semconv.HostIDKey) { instanceID, err := hostname() if err != nil { - slog.ErrorContext(ctx, "os.Hostname() failed, setting OTel service instance ID to UUID", slog.Any("error", err)) + slog.ErrorContext(ctx, "hostname() failed, setting OTel service instance ID to UUID", slog.Any("error", err)) instanceID = uuid.NewString() } else if instanceID == "" || instanceID == "localhost" { // localhost isn't very useful, so use a random UUID as a last resort.