Skip to content

Conversation

@tobert
Copy link
Owner

@tobert tobert commented Nov 10, 2025

Summary

Adds full OpenTelemetry logs support to otel-cli, bringing it to parity with traces and setting the foundation for metrics support.

Changes

New Command

  • otel-cli log - Send log records via OTLP
    • --body - Log message content
    • --severity - TRACE, DEBUG, INFO, WARN, ERROR, FATAL
    • --service, --attrs, --endpoint - Common flags from traces
    • --logs-endpoint - Signal-specific endpoint (follows OTel spec)

Architecture Improvements

  • Refactored server state - Extracted grpcServerState for shared state across signals
  • Renamed callbacks - CallbackTraceCallback, added LogCallback for clarity
  • HTTP routing - Proper path-based routing for /v1/traces and /v1/logs
  • gRPC services - Separate service structs to avoid method name collisions
  • Ready for metrics - Clean pattern established for future signals

Code Quality Improvements (by Gemini)

  • Moved shared utilities - StringMapAttrsToProtobuf to central location for reuse
  • Renamed files for clarity - Added explicit signal types (_traces, _logs) to filenames
  • Improves readability and prepares for metrics support

Implementation

  • otelcli/log.go - Log command implementation
  • otelcli/config_log.go - Configuration handling
  • otlpclient/protobuf_log.go - Log record creation and severity mapping
  • otlpclient/otlp_client_grpc_logs.go - gRPC logs client
  • otlpserver/ - Updated servers to handle both traces and logs

Documentation Improvements (addressing Copilot feedback)

  • ServeHTTP godoc - Added documentation for backwards compatibility behavior
  • All other Copilot suggestions evaluated and decisions documented in comments

Testing

Dependencies

  • Updated go.opentelemetry.io/proto/otlp to v1.9.0
  • Updated grpc, protobuf, and related dependencies

Test Results

$ go test
PASS
ok  	github.com/tobert/otel-cli	0.962s

All tests passing including:

Example Usage

# Send a log record
./otel-cli log \
  --endpoint localhost:4317 \
  --service my-service \
  --body "User login successful" \
  --severity INFO \
  --attrs "user.id=123,action=login"

# Use signal-specific endpoint
./otel-cli log \
  --logs-endpoint localhost:4318 \
  --service my-service \
  --body "Error occurred" \
  --severity ERROR

Breaking Changes

None. All existing trace functionality remains unchanged.

Copilot Feedback Addressed

✅ Evaluated all 4 pieces of Copilot feedback:

  • ❌ StopWait wrapper - Current pattern is elegant and efficient
  • ❌ Protocol env vars - Pragmatic approach appropriate for CLI tool
  • ❌ path.Join - Actually better than string concatenation (normalizes paths)
  • ✅ ServeHTTP docs - Improved godoc documentation (committed)

Full evaluation documented in PR comments.

Contributors

This PR includes collaborative work from:

  • 🤖 Claude - Core logs implementation, TLS fix integration, Copilot feedback evaluation
  • 💎 Gemini - Code quality refactoring and file organization
  • 🤖 Copilot - Code review and suggestions

Future Work

This refactoring sets up otel-cli to easily add metrics support next, following the same pattern.

Co-Authored-By: Claude [email protected]
Co-Authored-By: Gemini [email protected]
Co-Authored-By: Copilot [email protected]

Copilot AI review requested due to automatic review settings November 10, 2025 03:05
Copy link
Contributor

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 PR adds comprehensive OpenTelemetry logs support to otel-cli, bringing it to feature parity with the existing traces functionality. The implementation follows OTel specifications for signal-specific endpoints and establishes patterns for future metrics support.

Key Changes

  • New otel-cli log command with body, severity, and attribute support
  • Refactored server architecture to handle multiple signals (traces and logs) with shared state
  • Signal-specific endpoint handling following OTel spec (--logs-endpoint)

Reviewed Changes

Copilot reviewed 23 out of 24 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
otelcli/log.go Implements the new log command with body and severity parameters
otelcli/config_log.go Log record creation and configuration mapping
otelcli/config.go Adds logs endpoint parsing and log-specific config fields
otelcli/root.go Registers log command and adds service parameter refactoring
otlpclient/protobuf_log.go Log record creation and severity level mapping
otlpclient/otlp_client_grpc_logs.go gRPC client implementation for logs signal
otlpclient/otlp_client.go Adds GetLogsEndpoint interface method
otlpserver/server.go Refactored callbacks (TraceCallback/LogCallback) and added log processing
otlpserver/grpcserver.go Extracted shared state, added separate logs service
otlpserver/httpserver.go Path-based routing for /v1/traces and /v1/logs
data_for_test.go Added log test fixtures and results tracking
main_test.go Updated test harness to handle log records
go.mod Updated OTel proto and dependencies
demos/02-simple-log.sh Demo script showing log command usage
otelcli/*.go Import ordering cleanup (subjective style changes)

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

Comment on lines +148 to +151
// need to call StopWait on the GrpcServer, not directly on state
// so we create a temporary GrpcServer wrapper
gs := &GrpcServer{state: ls.state}
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

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

Creating a temporary GrpcServer wrapper to call StopWait is unnecessarily complex. Consider adding a stopWait() method to grpcServerState that handles the shutdown logic directly, then call it from both GrpcServer.StopWait() and here. This eliminates the wrapper creation and makes the shutdown path clearer.

Copilot uses AI. Check for mistakes.
TracesEndpoint string `json:"traces_endpoint" env:"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"`
Protocol string `json:"protocol" env:"OTEL_EXPORTER_OTLP_PROTOCOL,OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"`
LogsEndpoint string `json:"logs_endpoint" env:"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"`
Protocol string `json:"protocol" env:"OTEL_EXPORTER_OTLP_PROTOCOL,OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,OTEL_EXPORTER_OTLP_LOGS_PROTOCOL"`
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

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

The env tag includes both traces and logs protocol environment variables, but per OTel spec, signal-specific protocol variables should only apply when using signal-specific endpoints. The general OTEL_EXPORTER_OTLP_PROTOCOL should be the only variable here, with OTEL_EXPORTER_OTLP_TRACES_PROTOCOL and OTEL_EXPORTER_OTLP_LOGS_PROTOCOL handled separately in their respective endpoint parsing logic if needed.

Suggested change
Protocol string `json:"protocol" env:"OTEL_EXPORTER_OTLP_PROTOCOL,OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,OTEL_EXPORTER_OTLP_LOGS_PROTOCOL"`
Protocol string `json:"protocol" env:"OTEL_EXPORTER_OTLP_PROTOCOL"`

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +48
// For backwards compatibility, treat unspecified paths as traces
hs.handleTraces(rw, req)
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

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

This backwards compatibility behavior should be documented in the function's godoc comment, not just in an inline comment. Users of the API need to know that non-standard paths will be treated as trace endpoints.

Copilot uses AI. Check for mistakes.
// Per spec, /v1/logs is the default, appended to any url passed
// to the general endpoint
if strings.HasPrefix(epUrl.Scheme, "http") && source != "signal" && !strings.HasSuffix(epUrl.Path, "/v1/logs") {
epUrl.Path = path.Join(epUrl.Path, "/v1/logs")
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

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

The path.Join function cleans the path and can remove trailing slashes or collapse multiple slashes. Per OTel spec, the path should be appended with a forward slash separator. Use epUrl.Path = strings.TrimSuffix(epUrl.Path, \"/\") + \"/v1/logs\" instead to ensure consistent path handling.

Suggested change
epUrl.Path = path.Join(epUrl.Path, "/v1/logs")
epUrl.Path = strings.TrimSuffix(epUrl.Path, "/") + "/v1/logs"

Copilot uses AI. Check for mistakes.
@tobert
Copy link
Owner Author

tobert commented Nov 11, 2025

Copilot Feedback Evaluation

I've critically evaluated all 4 pieces of feedback from Copilot. Here's my analysis:


1. StopWait wrapper pattern (grpcserver.go:148-151)

Copilot says: Creating a temporary GrpcServer wrapper is unnecessarily complex. Add a stopWait() method to grpcServerState.

Evaluation:REJECT

  • Current pattern is elegant - lightweight facade (just a pointer) that reuses existing StopWait method
  • Adding separate stopWait() to grpcServerState would duplicate logic
  • Wrapper is stack-allocated and extremely cheap (just a pointer)
  • Maintains consistency - both trace and log services call shutdown the same way
  • Comment clearly explains why this is done

Decision: Keep current code as-is.


2. Protocol env variable handling (config.go:82)

Copilot says: Signal-specific protocol variables should only apply when using signal-specific endpoints, not be included in the general Protocol field.

Evaluation: ⚠️ NEEDS INVESTIGATION

  • Subtle point about OTel spec compliance
  • Question: Should OTEL_EXPORTER_OTLP_TRACES_PROTOCOL and OTEL_EXPORTER_OTLP_LOGS_PROTOCOL populate the general Protocol field?
  • Need to check OTel spec for correct behavior
  • For a CLI tool used in shell scripts, current behavior may be intentionally user-friendly

Decision: Will research OTel spec and make decision based on spec vs. usability tradeoff.


3. ServeHTTP backwards compatibility docs (httpserver.go:36)

Copilot says: Document the backwards compatibility behavior in the godoc comment.

Evaluation:ACCEPT

  • Good documentation practice
  • Backwards compatibility behavior (treating non-standard paths as traces) should be in function's godoc
  • Currently only mentioned in inline comment
  • Important API behavior users should know about

Decision: Will add godoc comment explaining backwards compatibility.


4. path.Join vs string concatenation (config.go:409)

Copilot says: Use strings.TrimSuffix(epUrl.Path, "/") + "/v1/logs" instead of path.Join because path.Join cleans paths.

Evaluation: 🤔 NEEDS TESTING

  • path.Join does clean paths (removes trailing slashes, collapses separators)
  • For URL paths, cleaning is often desired behavior
  • Same pattern used for /v1/traces on line 354, so we'd need to fix both if this is an issue
  • Need to test if path.Join causes actual problems

Decision: Will test path.Join with various inputs. If it causes issues, fix both traces and logs. Otherwise, keep current code.


Plan

  1. ✅ Evaluate all feedback critically (done)
  2. 🔲 Test path.Join behavior
  3. 🔲 Research OTel spec for protocol env vars
  4. 🔲 Improve ServeHTTP godoc if needed
  5. 🔲 Push any necessary fixes

🤖 Claude [email protected]

@tobert
Copy link
Owner Author

tobert commented Nov 11, 2025

✅ path.Join evaluation complete

Tested path.Join vs string concatenation with various inputs:

Input: ""                    path.Join: "/v1/logs"            concat: "/v1/logs"          
Input: "/"                   path.Join: "/v1/logs"            concat: "/v1/logs"          
Input: "/api"                path.Join: "/api/v1/logs"        concat: "/api/v1/logs"      
Input: "/api/"               path.Join: "/api/v1/logs"        concat: "/api/v1/logs"      
Input: "//double//slash//"   path.Join: "/double/slash/v1/logs"  concat: "//double//slash//v1/logs"

Result: path.Join is actually better - it correctly normalizes malformed paths by cleaning duplicate slashes. The only difference is with malformed input, where path.Join does the right thing.

Decision:REJECT Copilot's suggestion. Keep path.Join - it's more robust and handles edge cases correctly.

🤖 Claude [email protected]

@tobert
Copy link
Owner Author

tobert commented Nov 11, 2025

❌ Protocol env vars evaluation complete

Reviewed the OTel spec for protocol configuration.

Current code:

Protocol string `json:"protocol" env:"OTEL_EXPORTER_OTLP_PROTOCOL,OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,OTEL_EXPORTER_OTLP_LOGS_PROTOCOL"`

The spec says (line 13): "Each configuration option MUST be overridable by a signal specific option."

This means signal-specific protocol vars should override the general one for that specific signal only, not populate a shared Protocol field.

Analysis: otel-cli currently has a single Protocol field used for all signals. Copilot is technically correct that this violates the spec's intent, but fixing it properly would require per-signal protocol handling.

Decision:REJECT - Keep current behavior

Reasoning:

  • Works for the common use case (same protocol for all signals)
  • otel-cli is a CLI tool, not a full SDK - pragmatic approach is appropriate
  • Fixing properly requires significant refactoring for a feature unlikely to be used
  • Will address if users actually need per-signal protocol support

🤖 Claude [email protected]

@tobert
Copy link
Owner Author

tobert commented Nov 11, 2025

✅ ServeHTTP documentation improved

Copilot's feedback: Document backwards compatibility behavior in godoc comment.

Change made:

// ServeHTTP routes requests to the appropriate handler based on URL path.
// Routes /v1/traces to trace handler and /v1/logs to log handler per OTLP spec.
// For backwards compatibility, all other paths are treated as trace endpoints.

Decision:ACCEPT - Good documentation practice. Important API behavior now documented in godoc where users will see it.

🤖 Claude [email protected]

tobert added a commit that referenced this pull request Nov 11, 2025
Why: Copilot feedback on PR #18 correctly noted that backwards compatibility
behavior should be documented in godoc, not just inline comments
Approach: Added two lines to godoc explaining OTLP spec routing and backwards
compatibility behavior for non-standard paths
Learned: API behavior documentation belongs in godoc where users see it
Next: Push changes and complete Copilot feedback evaluation

Co-Authored-By: Claude <[email protected]>
Co-Authored-By: Copilot <[email protected]>
tobert and others added 2 commits November 11, 2025 23:21
Add full support for OpenTelemetry logs alongside existing traces:

**CLI Changes:**
- New `otel-cli log` command with --body and --severity flags
- Support for --logs-endpoint (follows OTel spec for signal-specific endpoints)
- Reuses common flags: --service, --attrs, --endpoint, etc.

**Client Implementation:**
- otlpclient/protobuf_log.go: Log record creation and severity mapping
- otlpclient/otlp_client_grpc_logs.go: gRPC logs client with OTLP export
- otelcli/config_log.go: Configuration handling for log records

**Server Refactoring:**
- Extracted grpcServerState to share state across signal services
- Renamed Callback -> TraceCallback for clarity
- Added LogCallback type for log-specific handling
- Both gRPC and HTTP servers now support logs via proper routing
  - gRPC: Separate service registration for logs
  - HTTP: Routes /v1/traces and /v1/logs to appropriate handlers

**Testing:**
- Added 2 integration tests for log command (gRPC)
- Tests verify log record transmission and severity handling
- Updated test harness to handle both spans and logs
- Both log tests pass successfully

**Demo:**
- demos/02-simple-log.sh: Example usage with multiple severity levels

**Architecture Notes:**
- Clean separation of concerns with shared state pattern
- Easy to extend for future signals (metrics, etc.)
- Follows OpenTelemetry specification for endpoint paths
- Maintains backward compatibility for traces

Related: #17 (documents pre-existing TLS test failures)

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

Co-Authored-By: Claude <[email protected]>
Why: Copilot feedback on PR #18 correctly noted that backwards compatibility
behavior should be documented in godoc, not just inline comments
Approach: Added two lines to godoc explaining OTLP spec routing and backwards
compatibility behavior for non-standard paths
Learned: API behavior documentation belongs in godoc where users see it
Next: Push changes and complete Copilot feedback evaluation

Co-Authored-By: Claude <[email protected]>
Co-Authored-By: Copilot <[email protected]>
@tobert tobert force-pushed the feat/add-logs-and-metrics branch from 36c3c35 to 7f25e6e Compare November 11, 2025 23:22
Why: To improve clarity and consistency in the otlpclient package by explicitly naming files with their respective signal types (traces). This prepares the codebase for future additions like metrics.
Approach: Renamed otlp_client_grpc.go, otlp_client_grpc_test.go, otlp_client_http.go, otlp_client_http_test.go, protobuf_span.go, and protobuf_span_test.go to include '_traces' or '_trace' in their names.
Learned: Explicit naming conventions enhance code readability and maintainability, especially in projects with multiple signal types.
Next: Update imports in affected files.

💎 Gemini <[email protected]>
Why: To make the `StringMapAttrsToProtobuf` function available to all signals (traces, logs, and future metrics) by moving it to a central, shared location.
Approach: Moved the function from `otlpclient/protobuf_trace.go` to `otlpclient/otlp_client.go`.
Learned: Centralizing common utility functions improves code reusability and maintainability.
Next: Continue with PR review.

💎 Gemini <[email protected]>
@tobert
Copy link
Owner Author

tobert commented Nov 11, 2025

I've completed a review of this PR, and the implementation of logging support is very well done.

Findings

  • High Consistency: The new code for logs closely mirrors the existing implementation for traces. The patterns for CLI commands, configuration, OTLP clients, and the test harness are all consistent with the established codebase.
  • Clear Structure: The separation of concerns between the otelcli, otlpclient, and otlpserver packages is maintained effectively.
  • Thorough Testing: The new functional tests in main_test.go and data_for_test.go are comprehensive and follow the existing data-driven testing style.

Refinements

To further improve clarity and prepare for future signals like metrics, we made two small refactoring changes:

  1. File Renaming: Renamed signal-specific files in the otlpclient package to be more explicit (e.g., otlpclient/otlp_client_grpc_traces.go).
  2. Generic Function: Moved the StringMapAttrsToProtobuf function to a shared location (otlpclient/otlp_client.go) so it can be reused by logs and future signals.

Conclusion

This is a solid contribution that significantly enhances otel-cli. The changes are well-architected and consistent. I'm confident in merging this.

💎 Gemini [email protected]

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