Skip to content
Open
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
76 changes: 55 additions & 21 deletions cmd/tesseract/gcp/otel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down Expand Up @@ -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) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible for an InstanceID to be set without any of these resources being set? This looks a bit brittle at first sight and seems to be CloudRun specific, so maybe it calls for a comment and could be extracted in its own function?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, no: the first of these (semconv.ServiceInstanceIDKey) is the key which carries the service instance ID: https://pkg.go.dev/go.opentelemetry.io/otel/semconv/v1.20.0#ServiceInstanceIDKey

So this condition is only true if the detectors/env didn't come up with any sort of local instance identifier (i.e an explicit instance ID, a Cloud Run style instance ID, or a host ID key).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the first of these (semconv.ServiceInstanceIDKey) is the key which carries the service instance ID

Is that the case for CloudRun? My understanding chatting with Gemini is that it's in FaaSInstanceKey (but I might be wrong).

Also if OpenTelemetry changed how this works, this code might now work as intended anymore. Otel could for instance remove one of these keys, or introduce a new one.

I'm not sure we can write better code, but maybe we could add comments to explain that:

  • ServiceInstanceIDKey is the default field, which can be set with an environment variable?
  • FaaSInstanceKey is for CloudRun
  • HostIDKey is for GCE
  • GKE and more are not supported

(That's the conclusion that I came to chatting with Gemini about this.)

Ideally, there would be a way to fetch these keys from Otel / to check whether the Instance ID has been set... but I'm not sure whether we can do that.

Approving since the priority is to unblock CloudRun, and this PR does that. We can polish later.

instanceID, err := hostname()
if err != nil {
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.
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
}
106 changes: 106 additions & 0 deletions cmd/tesseract/gcp/otel_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
Loading