From a6876f9f0bfd74c265df9ca19f3bfd6d560a4645 Mon Sep 17 00:00:00 2001 From: matst80 Date: Thu, 6 Nov 2025 16:25:33 +0100 Subject: [PATCH] stuff --- Dockerfile | 25 +++ README.md | 146 ++++++++++++++ cmd/server/main.go | 347 ++++++++++++++++++++++++++++++++ go.mod | 29 +++ go.sum | 82 ++++++++ index.html | 232 +++++++++++++++++++++ internal/collector/collector.go | 143 +++++++++++++ internal/websocket/server.go | 154 ++++++++++++++ k8s/deployment.yaml | 29 +++ k8s/ingress.yaml | 23 +++ k8s/service.yaml | 17 ++ test-client/go.mod | 37 ++++ test-client/go.sum | 87 ++++++++ test-client/main.go | 224 +++++++++++++++++++++ test.sh | 36 ++++ 15 files changed, 1611 insertions(+) create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 cmd/server/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 index.html create mode 100644 internal/collector/collector.go create mode 100644 internal/websocket/server.go create mode 100644 k8s/deployment.yaml create mode 100644 k8s/ingress.yaml create mode 100644 k8s/service.yaml create mode 100644 test-client/go.mod create mode 100644 test-client/go.sum create mode 100644 test-client/main.go create mode 100755 test.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..14461f9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# Build stage +FROM golang:1.25-alpine AS builder + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -o /go-otel ./cmd/server + +# Final stage +FROM alpine:latest + +RUN apk --no-cache add ca-certificates + +WORKDIR /root/ + +COPY --from=builder /go-otel . +COPY --from=builder /app/index.html . + +EXPOSE 8080 + +CMD ["./go-otel"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..71ac08b --- /dev/null +++ b/README.md @@ -0,0 +1,146 @@ +# Go OTel Debugger + +This is a debug OpenTelemetry receiver that accepts traces, metrics, and logs via OTLP HTTP protocol and broadcasts them over WebSocket connections for real-time visualization. + +## Features + +- Receives OTLP traces, metrics, and logs over HTTP +- Broadcasts received telemetry data over WebSocket connections +- Supports both JSON and Protocol Buffer formats +- Real-time debugging and visualization + +## Getting Started + +### Prerequisites + +- Go 1.21+ +- Docker (optional) + +### Running Locally + +1. Clone the repository: + ```sh + git clone https://git.tornberg.me/go-otel.git + cd go-otel + ``` + +2. Run the server: + ```sh + go run ./cmd/server + ``` + +The server will be running on port 8080. + +### Running with Docker + +1. Build the Docker image: + ```sh + docker build -t go-otel . + ``` + +2. Run the Docker container: + ```sh + docker run -p 8080:8080 go-otel + ``` + +## API + +### OTLP HTTP Endpoints + +- **POST /v1/traces**: Receive OTLP traces +- **POST /v1/metrics**: Receive OTLP metrics +- **POST /v1/logs**: Receive OTLP logs + +Supported Content-Types: +- `application/json` +- `application/x-protobuf` + +### OTLP gRPC Endpoints + +- **gRPC service**: `otlptracegrpc.TraceService.Export` +- **gRPC service**: `otlptracegrpc.MetricsService.Export` +- **gRPC service**: `otlptracegrpc.LogsService.Export` + +gRPC server runs on port 4317 (standard OTLP gRPC port). + +### WebSocket + +- **GET /ws**: Connect to receive real-time telemetry data + +## Usage + +Send OTLP data to the appropriate endpoints. The data will be broadcast to all connected WebSocket clients for visualization. + +Open `http://localhost:8080` in your browser to access the web-based visualizer. + +### HTTP Examples + +Example with curl (JSON): +```sh +curl -X POST http://localhost:8080/v1/traces \ + -H "Content-Type: application/json" \ + -d @trace-data.json +``` + +### gRPC Examples + +Use OTLP gRPC clients or libraries to send data to `localhost:4317`. + +### WebSocket + +Connect to WebSocket to receive the data: +```javascript +const ws = new WebSocket('ws://localhost:8080/ws'); +ws.onmessage = (event) => { + console.log('Received:', JSON.parse(event.data)); +}; +``` + +## Web Interface + +The web interface provides real-time visualization of: +- Traces +- Metrics +- Logs + +Features: +- Live data streaming via WebSocket +- Separate sections for each telemetry type +- Message counters +- Clear buttons for each section +- Connection status indicator + +## Testing + +A test client is included to verify that the receiver works correctly. + +### Run the Test + +1. Start the receiver: + ```sh + go run ./cmd/server + ``` + +2. In another terminal, run the test client: + ```sh + cd test-client && go run . + ``` + +Or use the provided test script: +```sh +./test.sh +``` + +The test client will send sample traces via both HTTP and gRPC protocols. + +### Manual Testing + +#### HTTP Testing +```sh +curl -X POST http://localhost:8080/v1/traces \ + -H "Content-Type: application/json" \ + -d '{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"test-service"}}]},"scopeSpans":[{"scope":{"name":"test-scope"},"spans":[{"traceId":"0102030405060708090a0b0c0d0e0f10","spanId":"0102030405060708","name":"test-span","startTimeUnixNano":"1640995200000000000","endTimeUnixNano":"1640995260000000000","attributes":[{"key":"test.key","value":{"stringValue":"test-value"}}]}]}]}]}' +``` + +#### Web Interface +Open `http://localhost:8080` in your browser to see the real-time visualization. \ No newline at end of file diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..1eafae0 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,347 @@ +package main + +import ( + "context" + "encoding/json" + "io" + "log" + "net" + "net/http" + + "git.tornberg.me/go-otel/internal/collector" + "git.tornberg.me/go-otel/internal/websocket" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/pdata/ptrace" + logsgrpc "go.opentelemetry.io/proto/otlp/collector/logs/v1" + metricsgrpc "go.opentelemetry.io/proto/otlp/collector/metrics/v1" + tracegrpc "go.opentelemetry.io/proto/otlp/collector/trace/v1" + "google.golang.org/grpc" + "google.golang.org/protobuf/proto" +) + +type traceService struct { + tracegrpc.UnimplementedTraceServiceServer + collector *collector.Collector +} + +func (s *traceService) Export(ctx context.Context, req *tracegrpc.ExportTraceServiceRequest) (*tracegrpc.ExportTraceServiceResponse, error) { + log.Printf("Received gRPC traces %d", len(req.ResourceSpans)) + + // Convert protobuf to bytes then to pdata + data, err := proto.Marshal(req) + if err != nil { + log.Printf("failed to marshal trace request: %v", err) + return nil, err + } + + unmarshaler := &ptrace.ProtoUnmarshaler{} + traces, err := unmarshaler.UnmarshalTraces(data) + if err != nil { + log.Printf("failed to unmarshal traces: %v", err) + return nil, err + } + + if err := s.collector.ConsumeTraces(ctx, traces); err != nil { + log.Printf("failed to consume traces: %v", err) + return nil, err + } + + return &tracegrpc.ExportTraceServiceResponse{}, nil +} + +type metricsService struct { + metricsgrpc.UnimplementedMetricsServiceServer + collector *collector.Collector +} + +func (s *metricsService) Export(ctx context.Context, req *metricsgrpc.ExportMetricsServiceRequest) (*metricsgrpc.ExportMetricsServiceResponse, error) { + log.Printf("Received gRPC metrics %d", len(req.ResourceMetrics)) + + // Convert protobuf to bytes then to pdata + data, err := proto.Marshal(req) + if err != nil { + log.Printf("failed to marshal metrics request: %v", err) + return nil, err + } + + unmarshaler := &pmetric.ProtoUnmarshaler{} + metrics, err := unmarshaler.UnmarshalMetrics(data) + if err != nil { + log.Printf("failed to unmarshal metrics: %v", err) + return nil, err + } + + if err := s.collector.ConsumeMetrics(ctx, metrics); err != nil { + log.Printf("failed to consume metrics: %v", err) + return nil, err + } + + return &metricsgrpc.ExportMetricsServiceResponse{}, nil +} + +type logsService struct { + logsgrpc.UnimplementedLogsServiceServer + collector *collector.Collector +} + +func (s *logsService) Export(ctx context.Context, req *logsgrpc.ExportLogsServiceRequest) (*logsgrpc.ExportLogsServiceResponse, error) { + log.Printf("Received gRPC logs %d", len(req.ResourceLogs)) + + // Convert protobuf to bytes then to pdata + data, err := proto.Marshal(req) + if err != nil { + log.Printf("failed to marshal logs request: %v", err) + return nil, err + } + + unmarshaler := &plog.ProtoUnmarshaler{} + logs, err := unmarshaler.UnmarshalLogs(data) + if err != nil { + log.Printf("failed to unmarshal logs: %v", err) + return nil, err + } + + if err := s.collector.ConsumeLogs(ctx, logs); err != nil { + log.Printf("failed to consume logs: %v", err) + return nil, err + } + + return &logsgrpc.ExportLogsServiceResponse{}, nil +} + +func main() { + + // Configure history size (default 100, can be made configurable) + historySize := 100 + coll := collector.New(nil, historySize) // Temporary nil for wsServer + wsServer := websocket.New(coll) + // Update collector with wsServer + coll.SetBroadcaster(wsServer) + + // Start gRPC server + go func() { + lis, err := net.Listen("tcp", ":4317") + if err != nil { + log.Fatal("failed to listen for gRPC", err) + } + + s := grpc.NewServer() + tracegrpc.RegisterTraceServiceServer(s, &traceService{collector: coll}) + metricsgrpc.RegisterMetricsServiceServer(s, &metricsService{collector: coll}) + logsgrpc.RegisterLogsServiceServer(s, &logsService{collector: coll}) + + log.Print("Starting gRPC server on port 4317") + if err := s.Serve(lis); err != nil { + log.Fatalf("failed to serve gRPC: %v", err) + } + }() + + // OTLP HTTP endpoints + http.HandleFunc("/v1/traces", handleTraces(coll)) + http.HandleFunc("/v1/metrics", handleMetrics(coll)) + http.HandleFunc("/v1/logs", handleLogs(coll)) + + // History API endpoints + http.HandleFunc("/api/history", handleHistory(coll)) + http.HandleFunc("/api/history/traces", handleHistoryByType(coll, "traces")) + http.HandleFunc("/api/history/metrics", handleHistoryByType(coll, "metrics")) + http.HandleFunc("/api/history/logs", handleHistoryByType(coll, "logs")) + + // WebSocket server for visualization + http.HandleFunc("/ws", wsServer.HandleConnections) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OTel Debug Receiver is running")) + }) + + log.Printf("Starting HTTP server on port 8080") + log.Printf("OTLP HTTP endpoints:") + log.Printf(" Traces: http://localhost:8080/v1/traces") + log.Printf(" Metrics: http://localhost:8080/v1/metrics") + log.Printf(" Logs: http://localhost:8080/v1/logs") + log.Printf("OTLP gRPC endpoints:") + log.Printf(" Traces: localhost:4317") + log.Printf(" Metrics: localhost:4317") + log.Printf(" Logs: localhost:4317") + log.Printf("WebSocket: ws://localhost:8080/ws") + + if err := http.ListenAndServe(":8080", nil); err != nil { + log.Fatalf("failed to start HTTP server: %v", err) + } +} + +func handleTraces(coll *collector.Collector) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + log.Printf("Received traces request: method=%s, content-type=%s", r.Method, r.Header.Get("Content-Type")) + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + log.Printf("failed to read request body: %v", err) + http.Error(w, "failed to read request body", http.StatusInternalServerError) + return + } + + log.Printf("Received traces body, size=%d", len(body)) + + var unmarshaler ptrace.Unmarshaler + contentType := r.Header.Get("Content-Type") + switch contentType { + case "application/x-protobuf": + unmarshaler = &ptrace.ProtoUnmarshaler{} + case "application/json": + unmarshaler = &ptrace.JSONUnmarshaler{} + default: + unmarshaler = &ptrace.JSONUnmarshaler{} // default to JSON + } + + traces, err := unmarshaler.UnmarshalTraces(body) + if err != nil { + log.Printf("failed to unmarshal traces: %v", err) + http.Error(w, "failed to unmarshal traces", http.StatusBadRequest) + return + } + + log.Printf("Successfully unmarshaled traces, span_count=%d", traces.SpanCount()) + + if err := coll.ConsumeTraces(context.Background(), traces); err != nil { + log.Printf("failed to consume traces: %v", err) + http.Error(w, "failed to consume traces", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + } +} + +func handleMetrics(coll *collector.Collector) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + log.Printf("failed to read request body: %v", err) + http.Error(w, "failed to read request body", http.StatusInternalServerError) + return + } + + var unmarshaler pmetric.Unmarshaler + contentType := r.Header.Get("Content-Type") + switch contentType { + case "application/x-protobuf": + unmarshaler = &pmetric.ProtoUnmarshaler{} + case "application/json": + unmarshaler = &pmetric.JSONUnmarshaler{} + default: + unmarshaler = &pmetric.JSONUnmarshaler{} // default to JSON + } + + metrics, err := unmarshaler.UnmarshalMetrics(body) + if err != nil { + log.Printf("failed to unmarshal metrics: %v", err) + http.Error(w, "failed to unmarshal metrics", http.StatusBadRequest) + return + } + + if err := coll.ConsumeMetrics(context.Background(), metrics); err != nil { + log.Printf("failed to consume metrics: %v", err) + http.Error(w, "failed to consume metrics", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + } +} + +func handleLogs(coll *collector.Collector) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + log.Printf("failed to read request body: %v", err) + http.Error(w, "failed to read request body", http.StatusInternalServerError) + return + } + + var unmarshaler plog.Unmarshaler + contentType := r.Header.Get("Content-Type") + switch contentType { + case "application/x-protobuf": + unmarshaler = &plog.ProtoUnmarshaler{} + case "application/json": + unmarshaler = &plog.JSONUnmarshaler{} + default: + unmarshaler = &plog.JSONUnmarshaler{} // default to JSON + } + + logs, err := unmarshaler.UnmarshalLogs(body) + if err != nil { + log.Printf("failed to unmarshal logs: %v", err) + http.Error(w, "failed to unmarshal logs", http.StatusBadRequest) + return + } + + if err := coll.ConsumeLogs(context.Background(), logs); err != nil { + log.Printf("failed to consume logs: %v", err) + http.Error(w, "failed to consume logs", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + } +} + +func handleHistory(coll *collector.Collector) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + history := coll.GetHistory() + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(history); err != nil { + log.Printf("failed to encode history: %v", err) + http.Error(w, "failed to encode history", http.StatusInternalServerError) + return + } + } +} + +func handleHistoryByType(coll *collector.Collector, dataType string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + history := coll.GetHistory() + + // Filter by type + var filteredHistory []websocket.HistoryEntry + for _, entry := range history { + if string(entry.Type) == dataType { + filteredHistory = append(filteredHistory, entry) + } + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(filteredHistory); err != nil { + log.Printf("failed to encode filtered history: %v", err) + http.Error(w, "failed to encode history", http.StatusInternalServerError) + return + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..59d55de --- /dev/null +++ b/go.mod @@ -0,0 +1,29 @@ +module git.tornberg.me/go-otel + +go 1.25.1 + +require ( + github.com/gorilla/websocket v1.5.3 + go.opentelemetry.io/collector/consumer v1.45.0 + go.opentelemetry.io/collector/pdata v1.45.0 + go.opentelemetry.io/proto/otlp v1.9.0 + google.golang.org/grpc v1.76.0 + google.golang.org/protobuf v1.36.10 +) + +require ( + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + go.opentelemetry.io/collector/featuregate v1.45.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ba16a2a --- /dev/null +++ b/go.sum @@ -0,0 +1,82 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/collector/consumer v1.45.0 h1:TtqXxgW+1GSCwdoohq0fzqnfqrZBKbfo++1XRj8mrEA= +go.opentelemetry.io/collector/consumer v1.45.0/go.mod h1:pJzqTWBubwLt8mVou+G4/Hs23b3m425rVmld3LqOYpY= +go.opentelemetry.io/collector/featuregate v1.45.0 h1:D06hpf1F2KzKC+qXLmVv5e8IZpgCyZVeVVC8iOQxVmw= +go.opentelemetry.io/collector/featuregate v1.45.0/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= +go.opentelemetry.io/collector/pdata v1.45.0 h1:q4XaISpeX640BcwXwb2mKOVw/gb67r22HjGWl8sbWsk= +go.opentelemetry.io/collector/pdata v1.45.0/go.mod h1:5q2f001YhwMQO8QvpFhCOa4Cq/vtwX9W4HRMsXkU/nE= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.opentelemetry.io/proto/slim/otlp v1.8.0 h1:afcLwp2XOeCbGrjufT1qWyruFt+6C9g5SOuymrSPUXQ= +go.opentelemetry.io/proto/slim/otlp v1.8.0/go.mod h1:Yaa5fjYm1SMCq0hG0x/87wV1MP9H5xDuG/1+AhvBcsI= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.1.0 h1:Uc+elixz922LHx5colXGi1ORbsW8DTIGM+gg+D9V7HE= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.1.0/go.mod h1:VyU6dTWBWv6h9w/+DYgSZAPMabWbPTFTuxp25sM8+s0= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.1.0 h1:i8YpvWGm/Uq1koL//bnbJ/26eV3OrKWm09+rDYo7keU= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.1.0/go.mod h1:pQ70xHY/ZVxNUBPn+qUWPl8nwai87eWdqL3M37lNi9A= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101 h1:vk5TfqZHNn0obhPIYeS+cxIFKFQgser/M2jnI+9c6MM= +google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101/go.mod h1:E17fc4PDhkr22dE3RgnH2hEubUaky6ZwW4VhANxyspg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/index.html b/index.html new file mode 100644 index 0000000..00e424e --- /dev/null +++ b/index.html @@ -0,0 +1,232 @@ + + + + + + OTel Debug Visualizer + + + +
+

OpenTelemetry Debug Visualizer

+ +
+ Disconnected from server +
+ +
+
+

Traces

+

0

+
+
+

Metrics

+

0

+
+
+

Logs

+

0

+
+
+ +
+

Traces

+
+ +
+ +
+

Metrics

+
+ +
+ +
+

Logs

+
+ +
+
+ + + + diff --git a/internal/collector/collector.go b/internal/collector/collector.go new file mode 100644 index 0000000..8aba6d2 --- /dev/null +++ b/internal/collector/collector.go @@ -0,0 +1,143 @@ +package collector + +import ( + "container/ring" + "context" + "encoding/json" + "log" + "sync" + "time" + + "git.tornberg.me/go-otel/internal/websocket" + + "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/pdata/ptrace" +) + +type Collector struct { + broadcaster websocket.Broadcaster + historySize int + history *ring.Ring + historyMutex sync.RWMutex +} + +func New(broadcaster websocket.Broadcaster, historySize int) *Collector { + return &Collector{ + broadcaster: broadcaster, + historySize: historySize, + history: ring.New(historySize), + } +} + +func (c *Collector) SetBroadcaster(broadcaster websocket.Broadcaster) { + c.broadcaster = broadcaster +} + +func (c *Collector) addToHistory(dataType websocket.DataType, data interface{}) { + c.historyMutex.Lock() + defer c.historyMutex.Unlock() + + c.history.Value = websocket.HistoryEntry{ + Type: string(dataType), + Timestamp: time.Now().UnixNano(), + Data: data, + } + c.history = c.history.Next() +} + +func (c *Collector) GetHistory() []websocket.HistoryEntry { + c.historyMutex.RLock() + defer c.historyMutex.RUnlock() + + var history []websocket.HistoryEntry + c.history.Do(func(v interface{}) { + if entry, ok := v.(websocket.HistoryEntry); ok && entry.Type != "" { + history = append(history, entry) + } + }) + return history +} + +func (c *Collector) ConsumeTraces(ctx context.Context, td ptrace.Traces) error { + marshaler := &ptrace.JSONMarshaler{} + buf, err := marshaler.MarshalTraces(td) + if err != nil { + log.Printf("failed to marshal traces: %v", err) + return err + } + + // Parse to interface{} for history storage + var data interface{} + if err := json.Unmarshal(buf, &data); err != nil { + log.Printf("failed to unmarshal traces for history: %v", err) + } else { + c.addToHistory(websocket.DataTypeTraces, data) + } + + entry := websocket.Entry{ + Type: string(websocket.DataTypeTraces), + Data: data, + } + + c.broadcaster.Broadcast(&entry) + return nil +} + +func (c *Collector) ConsumeMetrics(ctx context.Context, md pmetric.Metrics) error { + marshaler := &pmetric.JSONMarshaler{} + buf, err := marshaler.MarshalMetrics(md) + if err != nil { + log.Printf("failed to marshal metrics: %v", err) + return err + } + + // Parse to interface{} for history storage + var data interface{} + if err := json.Unmarshal(buf, &data); err != nil { + log.Printf("failed to unmarshal metrics for history: %v", err) + } else { + c.addToHistory(websocket.DataTypeMetrics, data) + } + + entry := websocket.Entry{ + Type: string(websocket.DataTypeMetrics), + Data: data, + } + + c.broadcaster.Broadcast(&entry) + return nil +} + +func (c *Collector) ConsumeLogs(ctx context.Context, ld plog.Logs) error { + marshaler := &plog.JSONMarshaler{} + buf, err := marshaler.MarshalLogs(ld) + if err != nil { + log.Printf("failed to marshal logs: %v", err) + return err + } + + // Parse to interface{} for history storage + var data interface{} + if err := json.Unmarshal(buf, &data); err != nil { + log.Printf("failed to unmarshal logs for history: %v", err) + } else { + c.addToHistory(websocket.DataTypeLogs, data) + } + + entry := websocket.Entry{ + Type: string(websocket.DataTypeLogs), + Data: data, + } + + c.broadcaster.Broadcast(&entry) + + //c.broadcaster.Broadcast(buf) + return nil +} + +// Implement consumer interfaces +func (c *Collector) Capabilities() consumer.Capabilities { + return consumer.Capabilities{MutatesData: false} +} diff --git a/internal/websocket/server.go b/internal/websocket/server.go new file mode 100644 index 0000000..cbf266c --- /dev/null +++ b/internal/websocket/server.go @@ -0,0 +1,154 @@ +package websocket + +import ( + "encoding/json" + "log" + "net/http" + "sync" + + "github.com/gorilla/websocket" +) + +type DataType string + +const ( + DataTypeTraces DataType = "traces" + DataTypeMetrics DataType = "metrics" + DataTypeLogs DataType = "logs" +) + +type HistoryProvider interface { + GetHistory() []HistoryEntry +} + +type HistoryEntry struct { + Type string `json:"type"` + Timestamp int64 `json:"timestamp"` + Data interface{} `json:"data"` +} + +type Entry struct { + Type string `json:"type"` + Data interface{} `json:"data"` +} + +type Broadcaster interface { + Broadcast(data *Entry) +} + +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +type Server struct { + clients map[*websocket.Conn]bool + mutex sync.Mutex + historyProvider HistoryProvider +} + +func New(historyProvider HistoryProvider) *Server { + return &Server{ + + clients: make(map[*websocket.Conn]bool), + historyProvider: historyProvider, + } +} + +func (s *Server) HandleConnections(w http.ResponseWriter, r *http.Request) { + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("failed to upgrade connection: %v", err) + return + } + defer ws.Close() + + s.mutex.Lock() + s.clients[ws] = true + s.mutex.Unlock() + + log.Printf("client connected") + + // // Send history to new client + // if s.historyProvider != nil { + // history := s.historyProvider.GetHistory() + // for _, entry := range history { + // if data, err := json.Marshal(entry); err == nil { + // if err := ws.WriteMessage(websocket.TextMessage, data); err != nil { + // log.Printf("failed to send history to client: %v", err) + // break + // } + // } + // } + // } + + for { + // Read message from browser + _, _, err := ws.ReadMessage() + if err != nil { + log.Printf("read error: %v", err) + break + } + } + + s.mutex.Lock() + delete(s.clients, ws) + s.mutex.Unlock() + log.Printf("client disconnected") +} + +func (s *Server) Broadcast(entry *Entry) { + s.mutex.Lock() + defer s.mutex.Unlock() + msg, err := json.Marshal(entry) + if err != nil { + log.Printf("failed to marshal log entry: %v", err) + } else { + for client := range s.clients { + if err := client.WriteMessage(websocket.TextMessage, msg); err != nil { + log.Printf("write error: %v", err) + } + } + } +} + +func (s *Server) SendHistory(ws *websocket.Conn) { + s.mutex.Lock() + defer s.mutex.Unlock() + + history := s.historyProvider.GetHistory() // Assuming GetHistory() returns the history data + for _, entry := range history { + data, err := json.Marshal(entry) + if err != nil { + log.Printf("failed to marshal history entry: %v", err) + continue + } + if err := ws.WriteMessage(websocket.TextMessage, data); err != nil { + log.Printf("write error: %v", err) + } + } +} + +func (s *Server) HandleMessage(ws *websocket.Conn, msg []byte) { + var message map[string]interface{} + if err := json.Unmarshal(msg, &message); err != nil { + log.Printf("error unmarshaling message: %v", err) + return + } + + // Handle the message (this is just an example, adjust according to your needs) + switch message["type"] { + case "ping": + log.Printf("received ping") + s.SendPong(ws) + default: + log.Printf("unknown message type : %v", message) + } +} + +func (s *Server) SendPong(ws *websocket.Conn) { + if err := ws.WriteMessage(websocket.PongMessage, nil); err != nil { + log.Printf("error sending pong", err) + } +} diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 0000000..e92abaf --- /dev/null +++ b/k8s/deployment.yaml @@ -0,0 +1,29 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: otel-debug-receiver +spec: + replicas: 1 + selector: + matchLabels: + app: otel-debug-receiver + template: + metadata: + labels: + app: otel-debug-receiver + spec: + containers: + - name: otel-debug + image: matst80/otel-debug:latest + ports: + - containerPort: 8080 + name: http + - containerPort: 4317 + name: grpc + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "500m" \ No newline at end of file diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml new file mode 100644 index 0000000..099b24a --- /dev/null +++ b/k8s/ingress.yaml @@ -0,0 +1,23 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: otel-debug-ingress + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: nginx # Assuming nginx ingress controller + tls: + - hosts: + - otel.tornberg.me + secretName: otel-tls + rules: + - host: otel.tornberg.me + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: otel-debug-service + port: + number: 80 \ No newline at end of file diff --git a/k8s/service.yaml b/k8s/service.yaml new file mode 100644 index 0000000..a9ad91f --- /dev/null +++ b/k8s/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: otel-debug-service +spec: + selector: + app: otel-debug-receiver + ports: + - name: http + port: 80 + targetPort: 8080 + protocol: TCP + - name: grpc + port: 4317 + targetPort: 4317 + protocol: TCP + type: ClusterIP \ No newline at end of file diff --git a/test-client/go.mod b/test-client/go.mod new file mode 100644 index 0000000..795188a --- /dev/null +++ b/test-client/go.mod @@ -0,0 +1,37 @@ +module test-client + +go 1.25.1 + +require ( + go.opentelemetry.io/collector/pdata v1.45.0 + go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 + go.opentelemetry.io/otel/sdk v1.38.0 + go.opentelemetry.io/otel/trace v1.38.0 +) + +require ( + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/collector/featuregate v1.45.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect +) diff --git a/test-client/go.sum b/test-client/go.sum new file mode 100644 index 0000000..42acc43 --- /dev/null +++ b/test-client/go.sum @@ -0,0 +1,87 @@ +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/collector/featuregate v1.45.0 h1:D06hpf1F2KzKC+qXLmVv5e8IZpgCyZVeVVC8iOQxVmw= +go.opentelemetry.io/collector/featuregate v1.45.0/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= +go.opentelemetry.io/collector/pdata v1.45.0 h1:q4XaISpeX640BcwXwb2mKOVw/gb67r22HjGWl8sbWsk= +go.opentelemetry.io/collector/pdata v1.45.0/go.mod h1:5q2f001YhwMQO8QvpFhCOa4Cq/vtwX9W4HRMsXkU/nE= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.opentelemetry.io/proto/slim/otlp v1.8.0 h1:afcLwp2XOeCbGrjufT1qWyruFt+6C9g5SOuymrSPUXQ= +go.opentelemetry.io/proto/slim/otlp v1.8.0/go.mod h1:Yaa5fjYm1SMCq0hG0x/87wV1MP9H5xDuG/1+AhvBcsI= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.1.0 h1:Uc+elixz922LHx5colXGi1ORbsW8DTIGM+gg+D9V7HE= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.1.0/go.mod h1:VyU6dTWBWv6h9w/+DYgSZAPMabWbPTFTuxp25sM8+s0= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.1.0 h1:i8YpvWGm/Uq1koL//bnbJ/26eV3OrKWm09+rDYo7keU= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.1.0/go.mod h1:pQ70xHY/ZVxNUBPn+qUWPl8nwai87eWdqL3M37lNi9A= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test-client/main.go b/test-client/main.go new file mode 100644 index 0000000..bb52575 --- /dev/null +++ b/test-client/main.go @@ -0,0 +1,224 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "log" + "net/http" + "time" + + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" + "go.opentelemetry.io/otel/trace" +) + +func main() { + ctx := context.Background() + + fmt.Println("Testing OTLP Debug Receiver...") + fmt.Println("Make sure the receiver is running on ports 8080 (HTTP) and 4317 (gRPC)") + fmt.Println() + + // Test HTTP endpoint + fmt.Println("Testing HTTP endpoint...") + testHTTPTraces(ctx) + + time.Sleep(1 * time.Second) + + // Test gRPC endpoint + fmt.Println("Testing gRPC endpoint...") + testGRPCTraces(ctx) + + time.Sleep(1 * time.Second) + + // Test HTTP metrics + fmt.Println("Testing HTTP metrics...") + testHTTPMetrics() + + time.Sleep(1 * time.Second) + + // Test HTTP logs + fmt.Println("Testing HTTP logs...") + testHTTPLogs() + + fmt.Println() + fmt.Println("Test completed! Check the receiver logs and web interface at http://localhost:8080") + fmt.Println("You should see traces, metrics, and logs from both HTTP and gRPC clients.") +} + +func testHTTPTraces(ctx context.Context) { + // Create OTLP HTTP exporter + exporter, err := otlptracehttp.New(ctx, + otlptracehttp.WithEndpoint("localhost:8080"), + otlptracehttp.WithInsecure(), + ) + if err != nil { + log.Fatalf("failed to create HTTP exporter: %v", err) + } + + // Create tracer provider + tp := sdktrace.NewTracerProvider( + sdktrace.WithBatcher(exporter), + sdktrace.WithResource(resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String("test-client-http"), + semconv.ServiceVersionKey.String("1.0.0"), + )), + ) + defer func() { + if err := tp.Shutdown(ctx); err != nil { + log.Printf("failed to shutdown tracer provider: %v", err) + } + }() + + otel.SetTracerProvider(tp) + + // Create tracer + tracer := tp.Tracer("test-tracer") + + // Create some sample spans + for i := 0; i < 3; i++ { + _, span := tracer.Start(ctx, fmt.Sprintf("http-test-span-%d", i), + trace.WithAttributes( + attribute.String("test.key", fmt.Sprintf("test-value-%d", i)), + attribute.Int("test.number", i), + ), + ) + + // Add an event + span.AddEvent("test event", trace.WithAttributes( + attribute.String("event.key", "event.value"), + )) + + // Simulate some work + time.Sleep(10 * time.Millisecond) + + span.End() + } + + // Force flush + if err := tp.ForceFlush(ctx); err != nil { + log.Printf("failed to flush: %v", err) + } + + fmt.Println("HTTP traces sent successfully") +} + +func testGRPCTraces(ctx context.Context) { + // Create OTLP gRPC exporter + exporter, err := otlptracegrpc.New(ctx, + otlptracegrpc.WithEndpoint("localhost:4317"), + otlptracegrpc.WithInsecure(), + ) + if err != nil { + log.Fatalf("failed to create gRPC exporter: %v", err) + } + + // Create tracer provider + tp := sdktrace.NewTracerProvider( + sdktrace.WithBatcher(exporter), + sdktrace.WithResource(resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String("test-client-grpc"), + semconv.ServiceVersionKey.String("1.0.0"), + )), + ) + defer func() { + if err := tp.Shutdown(ctx); err != nil { + log.Printf("failed to shutdown tracer provider: %v", err) + } + }() + + otel.SetTracerProvider(tp) + + // Create tracer + tracer := tp.Tracer("test-tracer-grpc") + + // Create some sample spans + for i := 0; i < 3; i++ { + _, span := tracer.Start(ctx, fmt.Sprintf("grpc-test-span-%d", i), + trace.WithAttributes( + attribute.String("test.key", fmt.Sprintf("grpc-test-value-%d", i)), + attribute.Int("test.number", i*10), + ), + ) + + // Add an event + span.AddEvent("grpc test event", trace.WithAttributes( + attribute.String("grpc.event.key", "grpc.event.value"), + )) + + // Simulate some work + time.Sleep(10 * time.Millisecond) + + span.End() + } + + // Force flush + if err := tp.ForceFlush(ctx); err != nil { + log.Printf("failed to flush: %v", err) + } + + fmt.Println("gRPC traces sent successfully") +} + +func testHTTPMetrics() { + md := pmetric.NewMetrics() + resourceMetrics := md.ResourceMetrics().AppendEmpty() + resourceMetrics.Resource().Attributes().PutStr("service.name", "test-client-metric") + scopeMetrics := resourceMetrics.ScopeMetrics().AppendEmpty() + scopeMetrics.Scope().SetName("test-meter") + metric := scopeMetrics.Metrics().AppendEmpty() + metric.SetName("test_counter") + gauge := metric.SetEmptyGauge() + dataPoint := gauge.DataPoints().AppendEmpty() + dataPoint.SetIntValue(42) + dataPoint.Attributes().PutStr("test.key", "test-value") + marshaler := &pmetric.ProtoMarshaler{} + buf, err := marshaler.MarshalMetrics(md) + if err != nil { + log.Printf("failed to marshal metrics: %v", err) + return + } + resp, err := http.Post("http://localhost:8080/v1/metrics", "application/x-protobuf", bytes.NewReader(buf)) + if err != nil { + log.Printf("failed to post metrics: %v", err) + return + } + resp.Body.Close() + fmt.Println("HTTP metrics sent successfully") +} + +func testHTTPLogs() { + ld := plog.NewLogs() + resourceLogs := ld.ResourceLogs().AppendEmpty() + resourceLogs.Resource().Attributes().PutStr("service.name", "test-client-log") + scopeLogs := resourceLogs.ScopeLogs().AppendEmpty() + scopeLogs.Scope().SetName("test-logger") + logRecord := scopeLogs.LogRecords().AppendEmpty() + logRecord.Body().SetStr("test log message") + logRecord.SetTimestamp(pcommon.NewTimestampFromTime(time.Now())) + logRecord.Attributes().PutStr("test.key", "test-value") + marshaler := &plog.ProtoMarshaler{} + buf, err := marshaler.MarshalLogs(ld) + if err != nil { + log.Printf("failed to marshal logs: %v", err) + return + } + resp, err := http.Post("http://localhost:8080/v1/logs", "application/x-protobuf", bytes.NewReader(buf)) + if err != nil { + log.Printf("failed to post logs: %v", err) + return + } + resp.Body.Close() + fmt.Println("HTTP logs sent successfully") +} diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..f9b21ef --- /dev/null +++ b/test.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +echo "=== OTLP Debug Receiver Test ===" +echo "This script will start the receiver and run the test client." +echo + +echo "Building receiver..." +cd /Users/mats/git.tornberg.me/go-otel +go build -o server ./cmd/server + +echo "Building test client..." +cd test-client +go build -o test-client . + +echo +echo "Starting OTLP Debug Receiver..." +cd .. +./server & +SERVER_PID=$! + +echo "Waiting for server to start..." +sleep 3 + +echo +echo "Running test client..." +cd test-client +./test-client + +echo +echo "Test completed. Stopping server..." +kill $SERVER_PID + +echo +echo "=== Test Complete ===" +echo "Check the output above for any errors." +echo "Open http://localhost:8080 in your browser to see the web interface." \ No newline at end of file