Skip to content

Conversation

@bramwelt
Copy link
Contributor

@bramwelt bramwelt commented Jan 8, 2026

This pull request adds OpenTelemetry (OTel) tracing support to the lfx-v2-access-check service, enabling distributed tracing and improved observability. The changes include initializing the OTel SDK with configuration from environment variables, instrumenting the HTTP server, and introducing utility code for OTel setup and configuration. Unit tests are also added to ensure correct OTel configuration handling.

OpenTelemetry Integration:

  • Added a new utility (pkg/utils/otel.go) to configure and initialize the OpenTelemetry SDK, supporting both gRPC and HTTP protocols, and allowing configuration via environment variables. This includes helpers for propagators, trace providers, and exporter setup.
  • Updated the main entrypoint (cmd/lfx-access-check/main.go) to initialize the OTel SDK at startup and ensure proper shutdown, using the new utility functions. [1] [2]
  • Instrumented the HTTP server by wrapping the handler with OTel's HTTP middleware (otelhttp.NewHandler), enabling automatic tracing of incoming HTTP requests. [1] [2]

Dependency and Testing Updates:

  • Updated go.mod to add required OpenTelemetry dependencies, update some existing dependencies, and ensure compatibility. [1] [2]
  • Added unit tests for the new OTel utility functions in pkg/utils/otel_test.go, covering environment variable parsing and basic SDK setup/shutdown scenarios.

- Add pkg/utils/otel.go with OTEL SDK setup and configuration
- Add pkg/utils/otel_test.go with unit tests
- Initialize OTEL in main.go with graceful shutdown
- Wrap HTTP handler with otelhttp instrumentation

Issue: LFXV2-612

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Signed-off-by: Trevor Bramwell <[email protected]>
Copilot AI review requested due to automatic review settings January 8, 2026 21:30
@bramwelt bramwelt requested a review from a team as a code owner January 8, 2026 21:30
@coderabbitai
Copy link

coderabbitai bot commented Jan 8, 2026

Walkthrough

Integrates OpenTelemetry distributed tracing into the lfx-access-check application by establishing SDK initialization in main, instrumenting the HTTP server, introducing configuration utilities with environment variable support, updating dependencies to OTEL v1.39.0, and providing comprehensive tests.

Changes

Cohort / File(s) Summary
Application Integration
cmd/lfx-access-check/main.go, cmd/lfx-access-check/server.go
Added OTEL SDK initialization at startup with config derived from environment variables, deferred shutdown handling, and context propagation. HTTP handler wrapped with otelhttp instrumentation for automatic request tracing.
OTEL Utilities
pkg/utils/otel.go
New utility module providing OTelConfig struct, environment-based configuration loader, SDK setup functions supporting both explicit config and environment defaults, propagator initialization, and conditional trace provider creation with OTLP exporter support (gRPC/HTTP).
Test Coverage
pkg/utils/otel_test.go
Comprehensive test suite validating environment variable parsing, SDK initialization with no-exporter mode, and default environment configuration with proper shutdown cleanup.
Dependency Management
go.mod
Updated OpenTelemetry ecosystem to v1.39.0, added otelhttp/otlptrace/otlptracehttp/otlptracegrpc dependencies, added supporting packages (httpsnoop, xxhash, logr/stdr, grpc-gateway), and removed deprecated versions.

Sequence Diagram

sequenceDiagram
    participant App as Application<br/>(main)
    participant Env as Environment<br/>Variables
    participant Config as OTelConfig
    participant Prop as Propagator
    participant TP as TracerProvider
    participant Exp as OTLP Exporter
    participant SDK as OTel SDK

    App->>Env: Read OTEL_*<br/>environment vars
    Env-->>App: Config values
    App->>Config: OTelConfigFromEnv()
    Config-->>App: OTelConfig instance
    App->>SDK: SetupOTelSDKWithConfig(ctx, config)
    SDK->>Prop: newPropagator()
    Prop->>SDK: Install TextMapPropagator<br/>(TraceContext + Baggage)
    alt ExporterEnabled
        SDK->>TP: newTraceProvider(ctx, config)
        TP->>Exp: Create OTLP Exporter<br/>(gRPC or HTTP)
        Exp-->>TP: Exporter instance
        TP->>SDK: Configure with Resource<br/>(service name/version)
        TP-->>SDK: TracerProvider ready
        SDK->>SDK: Register shutdown<br/>function
    end
    SDK-->>App: shutdown function,<br/>error (if any)
    App->>App: Defer shutdown()<br/>on application exit
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive The PR implements most requirements from LFXV2-612: adds tracing library dependencies [go.mod], initializes tracer with environment config [pkg/utils/otel.go, cmd/lfx-access-check/main.go], and instruments HTTP requests [cmd/lfx-access-check/server.go]. However, custom tags/logs for key operations are not evident in the changes provided. Verify that custom tags and logs for key operations (beyond HTTP instrumentation) are present, or clarify if this is deferred to a follow-up task.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically identifies the main change: adding OpenTelemetry tracing support to the service.
Description check ✅ Passed The description is well-detailed and directly related to the changeset, explaining OpenTelemetry integration, instrumentation, and testing additions.
Out of Scope Changes check ✅ Passed All changes are directly related to adding OpenTelemetry tracing support. Updates to go.mod for dependencies, new utility files for OTel configuration, instrumentation in main entry point and HTTP server, and corresponding tests are all within scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bramwelt/otel-implementation

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
pkg/utils/otel.go (2)

95-98: Minor formatting and potential concern with handleErr pattern.

There's a missing space after the comma on line 97. Additionally, this handleErr function is defined but only used when newTraceProvider fails. The pattern is correct for cleanup, but ensure the named return err is intended to be set via closure side-effect.

🔧 Suggested formatting fix
 	handleErr := func(inErr error) {
-		err = errors.Join(inErr,shutdown(ctx))
+		err = errors.Join(inErr, shutdown(ctx))
 	}

128-144: Consider validating the protocol value.

Unknown protocol values (anything other than "http") silently default to gRPC. This could cause confusion if a user misconfigures OTEL_EXPORTER_OTLP_PROTOCOL with an invalid value like "https" or a typo.

Consider either:

  1. Logging when falling back to gRPC
  2. Validating and returning an error for unsupported protocols
♻️ Option: Add explicit protocol validation
 func newTraceProvider(ctx context.Context, config OTelConfig) (*trace.TracerProvider, error) {
 	var traceClient otlptrace.Client
 
-	if config.Protocol == "http" {
+	switch config.Protocol {
+	case "http":
 		opts := []otlptracehttp.Option{
 			otlptracehttp.WithEndpoint(config.Endpoint),
 		}
 		if config.InsecureExporter {
 			opts = append(opts, otlptracehttp.WithInsecure())
 		}
 		traceClient = otlptracehttp.NewClient(opts...)
-	} else {
+	case "grpc", "":
 		opts := []otlptracegrpc.Option{
 			otlptracegrpc.WithEndpoint(config.Endpoint),
 		}
 		if config.InsecureExporter {
 			opts = append(opts, otlptracegrpc.WithInsecure())
 		}
 		traceClient = otlptracegrpc.NewClient(opts...)
+	default:
+		return nil, fmt.Errorf("unsupported OTLP protocol: %s (expected 'grpc' or 'http')", config.Protocol)
 	}
pkg/utils/otel_test.go (2)

62-69: Consider using t.Setenv for automatic cleanup.

The current approach of manually calling os.Unsetenv and os.Setenv doesn't restore the original environment after the test completes. Using t.Setenv (available since Go 1.17) automatically restores the original value when the test completes, providing better isolation.

♻️ Suggested improvement
 		t.Run(tt.name, func(t *testing.T) {
-			// Clear relevant env vars
-			envKeys := []string{
-				"OTEL_SERVICE_NAME",
-				"OTEL_SERVICE_VERSION",
-				"OTEL_EXPORTER_OTLP_ENDPOINT",
-				"OTEL_EXPORTER_OTLP_PROTOCOL",
-				"OTEL_TRACES_EXPORTER",
-				"OTEL_EXPORTER_OTLP_INSECURE",
-			}
-			for _, key := range envKeys {
-				os.Unsetenv(key)
-			}
-
-			// Set test env vars
+			// Set test env vars (t.Setenv auto-restores after test)
 			for key, value := range tt.envVars {
-				os.Setenv(key, value)
+				t.Setenv(key, value)
 			}

Note: For default values test case, you may need to explicitly set empty values or use a helper to ensure vars are unset.


117-135: Test for SetupOTelSDK should ensure complete environment isolation.

This test only unsets OTEL_TRACES_EXPORTER but other OTEL environment variables from previous tests or the system environment could still affect the test. Consider clearing all relevant environment variables for consistent test behavior.

♻️ Suggested improvement
 func TestSetupOTelSDK(t *testing.T) {
-	// Clear env vars to use defaults
-	os.Unsetenv("OTEL_TRACES_EXPORTER")
+	// Clear all OTEL env vars to ensure defaults are used
+	for _, key := range []string{
+		"OTEL_SERVICE_NAME",
+		"OTEL_SERVICE_VERSION",
+		"OTEL_EXPORTER_OTLP_ENDPOINT",
+		"OTEL_EXPORTER_OTLP_PROTOCOL",
+		"OTEL_TRACES_EXPORTER",
+		"OTEL_EXPORTER_OTLP_INSECURE",
+	} {
+		t.Setenv(key, "")
+	}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8943c53 and bec399b.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (5)
  • cmd/lfx-access-check/main.go
  • cmd/lfx-access-check/server.go
  • go.mod
  • pkg/utils/otel.go
  • pkg/utils/otel_test.go
🧰 Additional context used
📓 Path-based instructions (4)
pkg/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

All public packages should be placed under the pkg directory

Files:

  • pkg/utils/otel_test.go
  • pkg/utils/otel.go
cmd/lfx-access-check/{main.go,server.go}

📄 CodeRabbit inference engine (CLAUDE.md)

All requests require valid JWT tokens for authentication

Files:

  • cmd/lfx-access-check/server.go
  • cmd/lfx-access-check/main.go
cmd/lfx-access-check/server.go

📄 CodeRabbit inference engine (CLAUDE.md)

All API endpoints must implement liveness and readiness probes at /livez and /readyz

Files:

  • cmd/lfx-access-check/server.go
{cmd/lfx-access-check/server.go,internal/middleware/*.go}

📄 CodeRabbit inference engine (CLAUDE.md)

All requests must be tracked with unique request IDs for correlation in logs

Files:

  • cmd/lfx-access-check/server.go
🧬 Code graph analysis (2)
pkg/utils/otel_test.go (1)
pkg/utils/otel.go (4)
  • OTelConfig (23-30)
  • OTelConfigFromEnv (34-70)
  • SetupOTelSDKWithConfig (80-116)
  • SetupOTelSDK (74-76)
cmd/lfx-access-check/main.go (1)
pkg/utils/otel.go (2)
  • OTelConfigFromEnv (34-70)
  • SetupOTelSDKWithConfig (80-116)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: CodeQL analysis (go)
  • GitHub Check: Agent
  • GitHub Check: MegaLinter
🔇 Additional comments (5)
cmd/lfx-access-check/server.go (1)

101-103: LGTM! OTel HTTP instrumentation is correctly applied.

The otelhttp.NewHandler wrapper is placed as the outermost layer in the middleware stack, which ensures all incoming requests are traced. The operation name "access-check" provides a reasonable default span name. Request ID middleware is correctly applied before tracing, so trace context will include request correlation.

cmd/lfx-access-check/main.go (1)

28-40: LGTM! OTel SDK initialization follows best practices.

The initialization sequence correctly:

  1. Creates config from environment variables
  2. Sets up the SDK early in the application lifecycle
  3. Defers shutdown with a fresh context.Background() (appropriate since the original context may be cancelled during shutdown)
  4. Exits on setup failure with proper error logging
pkg/utils/otel.go (1)

151-169: LGTM! Resource and TracerProvider configuration follows best practices.

The implementation correctly:

  • Merges default resource attributes with custom service name/version
  • Uses semantic conventions (semconv v1.26.0)
  • Configures a batched exporter with a reasonable 5-second timeout
pkg/utils/otel_test.go (1)

95-115: LGTM! Basic SDK setup test covers the no-exporter path.

The test validates that when TracesExporter is set to "none", the SDK initializes successfully and the shutdown function works correctly.

go.mod (1)

11-15: OpenTelemetry version mismatch may cause compatibility issues.

The OTel dependencies have inconsistent versions:

  • otel/sdk v1.39.0
  • otel v1.39.0
  • otlptrace v1.36.0
  • otlptracegrpc v1.35.0
  • otlptracehttp v1.35.0

OpenTelemetry documentation explicitly recommends keeping exporters and the SDK on the same major/minor release to avoid incompatibilities. The current state pairs SDK v1.39 with exporters at v1.35-1.36. Consider aligning all go.opentelemetry.io/otel/exporters/otlp/otlptrace/* packages to v1.39.0 to match the SDK version.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds comprehensive OpenTelemetry (OTel) tracing support to the lfx-v2-access-check service. The implementation includes SDK configuration from environment variables, automatic HTTP request instrumentation, and proper lifecycle management for the tracing infrastructure.

Key Changes:

  • Introduced new OTel utility package with environment-based configuration and SDK setup functions
  • Integrated OTel initialization in the main entrypoint with proper shutdown handling
  • Instrumented HTTP server with OTel middleware for automatic request tracing

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
pkg/utils/otel.go New utility for OTel SDK configuration and initialization with support for gRPC/HTTP protocols
pkg/utils/otel_test.go Unit tests for OTel configuration parsing and SDK setup/shutdown
cmd/lfx-access-check/main.go Added OTel SDK initialization and shutdown hooks at service startup
cmd/lfx-access-check/server.go Wrapped HTTP handler with OTel instrumentation middleware
go.mod Added OpenTelemetry dependencies and updated related packages
go.sum Updated checksums for new and modified dependencies

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

ServiceName string
ServiceVersion string
Endpoint string
Protocol string // "grpc" or "http"
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The Protocol field accepts any string value without validation. If a value other than "grpc" or "http" is provided (line 50-53 in otel.go), the code will default to gRPC behavior (line 136-143), but this could be confusing for users. Consider adding validation to return an error for invalid protocol values, or at least document the default behavior in the OTelConfig struct comments.

Suggested change
Protocol string // "grpc" or "http"
Protocol string // "grpc" (default) or "http"; any value other than "http" is treated as "grpc"

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +93
func TestOTelConfigFromEnv(t *testing.T) {
tests := []struct {
name string
envVars map[string]string
expected OTelConfig
}{
{
name: "default values",
envVars: map[string]string{},
expected: OTelConfig{
ServiceName: "lfx-v2-access-check",
ServiceVersion: "1.0.0",
Endpoint: "localhost:4317",
Protocol: "grpc",
TracesExporter: "none",
InsecureExporter: false,
},
},
{
name: "custom values",
envVars: map[string]string{
"OTEL_SERVICE_NAME": "custom-service",
"OTEL_SERVICE_VERSION": "2.0.0",
"OTEL_EXPORTER_OTLP_ENDPOINT": "otel-collector:4317",
"OTEL_EXPORTER_OTLP_PROTOCOL": "http",
"OTEL_TRACES_EXPORTER": "otlp",
"OTEL_EXPORTER_OTLP_INSECURE": "true",
},
expected: OTelConfig{
ServiceName: "custom-service",
ServiceVersion: "2.0.0",
Endpoint: "otel-collector:4317",
Protocol: "http",
TracesExporter: "otlp",
InsecureExporter: true,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Clear relevant env vars
envKeys := []string{
"OTEL_SERVICE_NAME",
"OTEL_SERVICE_VERSION",
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_PROTOCOL",
"OTEL_TRACES_EXPORTER",
"OTEL_EXPORTER_OTLP_INSECURE",
}
for _, key := range envKeys {
os.Unsetenv(key)
}

// Set test env vars
for key, value := range tt.envVars {
os.Setenv(key, value)
}

config := OTelConfigFromEnv()

if config.ServiceName != tt.expected.ServiceName {
t.Errorf("ServiceName = %v, want %v", config.ServiceName, tt.expected.ServiceName)
}
if config.ServiceVersion != tt.expected.ServiceVersion {
t.Errorf("ServiceVersion = %v, want %v", config.ServiceVersion, tt.expected.ServiceVersion)
}
if config.Endpoint != tt.expected.Endpoint {
t.Errorf("Endpoint = %v, want %v", config.Endpoint, tt.expected.Endpoint)
}
if config.Protocol != tt.expected.Protocol {
t.Errorf("Protocol = %v, want %v", config.Protocol, tt.expected.Protocol)
}
if config.TracesExporter != tt.expected.TracesExporter {
t.Errorf("TracesExporter = %v, want %v", config.TracesExporter, tt.expected.TracesExporter)
}
if config.InsecureExporter != tt.expected.InsecureExporter {
t.Errorf("InsecureExporter = %v, want %v", config.InsecureExporter, tt.expected.InsecureExporter)
}
})
}
}
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The test suite doesn't cover the scenario where an invalid protocol value is provided (neither "grpc" nor "http"). While the implementation defaults to gRPC in this case (line 136-143 in otel.go), adding a test case would document this behavior and prevent regressions.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +14
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The OpenTelemetry dependency versions are inconsistent across the module. The core SDK and trace packages are at version 1.39.0, while the otlptrace exporters are at mixed versions (v1.36.0 for the base package and v1.35.0 for the gRPC and HTTP variants). This version mismatch could lead to compatibility issues or unexpected behavior. All OpenTelemetry packages from the same release should typically use the same version to ensure compatibility.

Suggested change
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +69
// Clear relevant env vars
envKeys := []string{
"OTEL_SERVICE_NAME",
"OTEL_SERVICE_VERSION",
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_PROTOCOL",
"OTEL_TRACES_EXPORTER",
"OTEL_EXPORTER_OTLP_INSECURE",
}
for _, key := range envKeys {
os.Unsetenv(key)
}

// Set test env vars
for key, value := range tt.envVars {
os.Setenv(key, value)
}
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The test manipulates global environment variables using os.Setenv and os.Unsetenv without proper cleanup, which can cause test pollution and race conditions when tests run in parallel. Consider using t.Setenv() instead, which was introduced in Go 1.17 and automatically handles cleanup and prevents parallel execution of tests that modify the same environment variable.

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +119
// Clear env vars to use defaults
os.Unsetenv("OTEL_TRACES_EXPORTER")
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The test modifies global environment variables without proper cleanup. Similar to TestOTelConfigFromEnv, consider using t.Setenv() instead of os.Unsetenv() for automatic cleanup and better test isolation.

Suggested change
// Clear env vars to use defaults
os.Unsetenv("OTEL_TRACES_EXPORTER")
// Clear env vars to use defaults; t.Setenv ensures cleanup after the test
t.Setenv("OTEL_TRACES_EXPORTER", "")

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +40
defer func() {
if shutdownErr := otelShutdown(context.Background()); shutdownErr != nil {
slog.Error("error shutting down OpenTelemetry SDK", "error", shutdownErr)
}
}()
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The OTel shutdown is deferred to use a new context.Background() instead of the ctx variable that was used for initialization. This means any shutdown timeout or cancellation in the main context won't be respected during shutdown. Consider creating a separate shutdown context with a timeout (e.g., context.WithTimeout) to allow graceful shutdown with a reasonable deadline.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants