more otel
This commit is contained in:
1 parent
9620e3bfa6
commit
21f04d696b
88 files changed
+23419
-192
No files matched your search
+125
-21
@@ -9,7 +9,12 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.tornberg.me/go-otel/internal/auth"
|
||||
"git.tornberg.me/go-otel/internal/collector"
|
||||
"git.tornberg.me/go-otel/internal/honeypot"
|
||||
kube "git.tornberg.me/go-otel/internal/kubernetes"
|
||||
"git.tornberg.me/go-otel/internal/prometheus"
|
||||
"git.tornberg.me/go-otel/internal/sourcemaps"
|
||||
"git.tornberg.me/go-otel/internal/storage"
|
||||
"git.tornberg.me/go-otel/internal/websocket"
|
||||
"go.opentelemetry.io/collector/pdata/plog"
|
||||
@@ -150,14 +155,20 @@ func (s *logsService) Export(ctx context.Context, req *logsgrpc.ExportLogsServic
|
||||
}
|
||||
|
||||
// withCORS allows cross-origin ingestion (e.g. from the otel-rum.js browser
|
||||
// script running on a third-party site) into the OTLP HTTP endpoints. It's
|
||||
// deliberately permissive on origin, since this is a public ingest endpoint
|
||||
// script running on a third-party site like event-agent.k6n.net) into the
|
||||
// OTLP HTTP endpoints. It is deliberately permissive on origin and headers,
|
||||
// scoped by the ingest key/rate limiting instead of same-origin policy.
|
||||
func withCORS(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Otel-Ingest-Key")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
|
||||
reqHeaders := r.Header.Get("Access-Control-Request-Headers")
|
||||
if reqHeaders != "" {
|
||||
w.Header().Set("Access-Control-Allow-Headers", reqHeaders)
|
||||
} else {
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Otel-Ingest-Key, Authorization, Baggage, Traceparent, Tracestate, X-Requested-With")
|
||||
}
|
||||
w.Header().Set("Access-Control-Max-Age", "86400")
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
@@ -166,20 +177,69 @@ func withCORS(next http.HandlerFunc) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// setupStore returns the telemetry store: ClickHouse when OTEL_CLICKHOUSE_ADDR
|
||||
// is set (host:port, native protocol), otherwise the JSONL file store.
|
||||
func setupStore(ctx context.Context) storage.Store {
|
||||
addr := os.Getenv("OTEL_CLICKHOUSE_ADDR")
|
||||
if addr == "" {
|
||||
logPath := os.Getenv("OTELLOG_PATH")
|
||||
if logPath == "" {
|
||||
logPath = "otel-agent.log"
|
||||
}
|
||||
log.Printf("storage: file store at %s (set OTEL_CLICKHOUSE_ADDR for ClickHouse)", logPath)
|
||||
return storage.NewFileStore(logPath)
|
||||
}
|
||||
|
||||
chStore, err := storage.NewClickHouseStore(ctx, storage.ClickHouseConfig{
|
||||
Addr: addr,
|
||||
Database: os.Getenv("OTEL_CLICKHOUSE_DB"),
|
||||
Username: os.Getenv("OTEL_CLICKHOUSE_USER"),
|
||||
Password: os.Getenv("OTEL_CLICKHOUSE_PASSWORD"),
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("storage: ClickHouse unavailable (%v), falling back to file store", err)
|
||||
logPath := os.Getenv("OTELLOG_PATH")
|
||||
if logPath == "" {
|
||||
logPath = "otel-agent.log"
|
||||
}
|
||||
return storage.NewFileStore(logPath)
|
||||
}
|
||||
log.Printf("storage: ClickHouse at %s", addr)
|
||||
return chStore
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
// Configure history size (default 100, can be made configurable)
|
||||
historySize := 100
|
||||
logPath := os.Getenv("OTELLOG_PATH")
|
||||
if logPath == "" {
|
||||
logPath = "otel-agent.log"
|
||||
}
|
||||
store := storage.NewFileStore(logPath)
|
||||
store := setupStore(context.Background())
|
||||
coll := collector.New(nil, historySize, store) // Temporary nil for wsServer
|
||||
kubeWatcher, kubeErr := kube.NewFromEnv(context.Background())
|
||||
if kubeErr != nil {
|
||||
log.Printf("kubernetes: rollout watcher disabled: %v", kubeErr)
|
||||
} else if kubeWatcher != nil {
|
||||
coll.SetKubernetesWatcher(kubeWatcher)
|
||||
log.Printf("kubernetes: rollout watcher enabled")
|
||||
}
|
||||
coll.SetPrometheusClient(prometheus.NewFromEnv())
|
||||
sourceMaps, err := sourcemaps.New(os.Getenv("OTEL_SOURCEMAP_DIR"))
|
||||
if err != nil {
|
||||
log.Fatalf("failed to initialize source maps: %v", err)
|
||||
}
|
||||
coll.SetSourceMaps(sourceMaps)
|
||||
// Aggregate stats endpoints are enabled only for stores that support
|
||||
// them (ClickHouse); they 501 politely on the file store.
|
||||
if sq, ok := store.(storage.StatsQuerier); ok {
|
||||
coll.SetStatsQuerier(sq)
|
||||
}
|
||||
wsServer := websocket.New(coll)
|
||||
honeypotTracker := honeypot.New(honeypot.ConfigFromEnv())
|
||||
// Update collector with wsServer
|
||||
coll.SetBroadcaster(wsServer)
|
||||
coll.SetIngestKey(os.Getenv("OTEL_INGEST_KEY"))
|
||||
ingestKey := os.Getenv("OTEL_INGEST_KEY")
|
||||
coll.SetIngestKey(ingestKey)
|
||||
oidcAuth := auth.NewOidcAuth(ingestKey)
|
||||
log.Printf("auth: OIDC issuer=%s client=%s disabled=%v", oidcAuth.Issuer(), oidcAuth.ClientID(), oidcAuth.IsDisabled())
|
||||
|
||||
// Start gRPC server
|
||||
go func() {
|
||||
@@ -205,8 +265,11 @@ func main() {
|
||||
// OTLP HTTP mux (standard OTel port 4318)
|
||||
otelMux := http.NewServeMux()
|
||||
otelMux.HandleFunc("/v1/traces", withCORS(coll.HandleTraces))
|
||||
otelMux.HandleFunc("/v1/traces/", withCORS(coll.HandleTraces))
|
||||
otelMux.HandleFunc("/v1/metrics", withCORS(coll.HandleMetrics))
|
||||
otelMux.HandleFunc("/v1/metrics/", withCORS(coll.HandleMetrics))
|
||||
otelMux.HandleFunc("/v1/logs", withCORS(coll.HandleLogs))
|
||||
otelMux.HandleFunc("/v1/logs/", withCORS(coll.HandleLogs))
|
||||
otelMux.HandleFunc("/health", coll.HandleHealth)
|
||||
|
||||
// UI and API mux (port 8080)
|
||||
@@ -215,19 +278,43 @@ func main() {
|
||||
// CORS is enabled here since this is the publicly exposed port (via the
|
||||
// ingress) that a browser script such as static/otel-rum.js posts to.
|
||||
uiMux.HandleFunc("/v1/traces", withCORS(coll.HandleTraces))
|
||||
uiMux.HandleFunc("/v1/traces/", withCORS(coll.HandleTraces))
|
||||
uiMux.HandleFunc("/v1/metrics", withCORS(coll.HandleMetrics))
|
||||
uiMux.HandleFunc("/v1/metrics/", withCORS(coll.HandleMetrics))
|
||||
uiMux.HandleFunc("/v1/logs", withCORS(coll.HandleLogs))
|
||||
uiMux.HandleFunc("/v1/logs/", withCORS(coll.HandleLogs))
|
||||
uiMux.HandleFunc("/health", coll.HandleHealth)
|
||||
uiMux.HandleFunc("GET /internal/honeypot/ips", honeypotTracker.HandleIPs)
|
||||
|
||||
uiMux.HandleFunc("/api/history", coll.HandleHistory)
|
||||
uiMux.HandleFunc("/api/history/traces", coll.HandleHistoryByType("traces"))
|
||||
uiMux.HandleFunc("/api/history/metrics", coll.HandleHistoryByType("metrics"))
|
||||
uiMux.HandleFunc("/api/history/logs", coll.HandleHistoryByType("logs"))
|
||||
uiMux.HandleFunc("/api/history/search", coll.HandleSearch)
|
||||
uiMux.HandleFunc("/api/history/wait", coll.HandleWait)
|
||||
uiMux.HandleFunc("GET /api/traces/{traceId}", coll.HandleTraceByID)
|
||||
uiMux.HandleFunc("/api/issues", coll.HandleIssues)
|
||||
uiMux.HandleFunc("GET /api/issues/{id}", coll.HandleIssueByID)
|
||||
// Public OIDC endpoints
|
||||
uiMux.HandleFunc("POST /api/auth/token", oidcAuth.HandleTokenExchange)
|
||||
uiMux.HandleFunc("GET /api/auth/config", oidcAuth.HandleConfig)
|
||||
uiMux.Handle("/api/honeypot/ips", oidcAuth.RequireAuth(http.HandlerFunc(honeypotTracker.HandleIPsForUser)))
|
||||
// Source-map uploads are intentionally public so the release build script
|
||||
// can upload maps without an OIDC session or bearer token.
|
||||
uiMux.HandleFunc("POST /api/sourcemaps", coll.HandleSourceMap)
|
||||
|
||||
// Protected API & WebSocket routes
|
||||
uiMux.Handle("/api/history", oidcAuth.RequireAuth(http.HandlerFunc(coll.HandleHistory)))
|
||||
uiMux.Handle("/api/history/traces", oidcAuth.RequireAuth(coll.HandleHistoryByType("traces")))
|
||||
uiMux.Handle("/api/history/metrics", oidcAuth.RequireAuth(coll.HandleHistoryByType("metrics")))
|
||||
uiMux.Handle("/api/history/logs", oidcAuth.RequireAuth(coll.HandleHistoryByType("logs")))
|
||||
uiMux.Handle("/api/history/search", oidcAuth.RequireAuth(http.HandlerFunc(coll.HandleSearch)))
|
||||
uiMux.Handle("/api/history/wait", oidcAuth.RequireAuth(http.HandlerFunc(coll.HandleWait)))
|
||||
uiMux.Handle("GET /api/traces/{traceId}", oidcAuth.RequireAuth(http.HandlerFunc(coll.HandleTraceByID)))
|
||||
uiMux.Handle("/api/issues", oidcAuth.RequireAuth(http.HandlerFunc(coll.HandleIssues)))
|
||||
uiMux.Handle("GET /api/issues/{id}", oidcAuth.RequireAuth(http.HandlerFunc(coll.HandleIssueByID)))
|
||||
uiMux.Handle("GET /api/issues/{id}/similar", oidcAuth.RequireAuth(http.HandlerFunc(coll.HandleIssueSimilar)))
|
||||
uiMux.Handle("POST /api/issues/{id}/similar", oidcAuth.RequireAuth(http.HandlerFunc(coll.HandleSimilarityFeedback)))
|
||||
uiMux.Handle("GET /api/patterns", oidcAuth.RequireAuth(http.HandlerFunc(coll.HandlePatterns)))
|
||||
uiMux.Handle("GET /api/rollouts", oidcAuth.RequireAuth(http.HandlerFunc(coll.HandleRollouts)))
|
||||
uiMux.Handle("GET /api/rollouts/{id}", oidcAuth.RequireAuth(http.HandlerFunc(coll.HandleRolloutByID)))
|
||||
uiMux.Handle("GET /api/rollouts/{id}/impact", oidcAuth.RequireAuth(http.HandlerFunc(coll.HandleRolloutImpact)))
|
||||
// Aggregate stats (ClickHouse-backed); return 501 with an explanatory
|
||||
// JSON body when only the file store is configured.
|
||||
uiMux.Handle("/api/stats/errors", oidcAuth.RequireAuth(http.HandlerFunc(coll.HandleStatsErrors)))
|
||||
uiMux.Handle("/api/stats/releases", oidcAuth.RequireAuth(http.HandlerFunc(coll.HandleStatsReleases)))
|
||||
uiMux.Handle("/api/stats/services", oidcAuth.RequireAuth(http.HandlerFunc(coll.HandleStatsServices)))
|
||||
uiMux.HandleFunc("/ws", wsServer.HandleConnections)
|
||||
uiMux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
|
||||
// Serve the agent instructions doc as a static route so an AI agent can
|
||||
@@ -235,7 +322,23 @@ func main() {
|
||||
uiMux.HandleFunc("GET /agents_example.md", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "agents_example.md")
|
||||
})
|
||||
uiMux.Handle("/", spaHandler(http.Dir("./dist")))
|
||||
uiMux.HandleFunc("GET /instrumentation.md", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "instrumentation.md")
|
||||
})
|
||||
uiMux.HandleFunc("GET /static/instrumentation.md", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "instrumentation.md")
|
||||
})
|
||||
distDir := "./dist"
|
||||
if _, err := os.Stat(distDir); os.IsNotExist(err) {
|
||||
if _, err := os.Stat("./otel-ui/dist"); err == nil {
|
||||
distDir = "./otel-ui/dist"
|
||||
} else if _, err := os.Stat("./index.html"); err == nil {
|
||||
// Development fallback: serve the checked-in visualizer when no
|
||||
// separately built frontend exists.
|
||||
distDir = "."
|
||||
}
|
||||
}
|
||||
uiMux.Handle("/", spaHandler(http.Dir(distDir)))
|
||||
|
||||
log.Printf("Starting HTTP server on port 8080 (UI/API)")
|
||||
log.Printf("OTLP HTTP endpoints (also on port 8080):")
|
||||
@@ -254,7 +357,8 @@ func main() {
|
||||
}
|
||||
}()
|
||||
|
||||
if err := http.ListenAndServe(":8080", uiMux); err != nil {
|
||||
uiHandler := honeypotTracker.Middleware(uiMux)
|
||||
if err := http.ListenAndServe(":8080", uiHandler); err != nil {
|
||||
log.Fatalf("failed to start HTTP server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user