Add frontend error collector, CORS/ingest guards, and static doc routes (Phase 3)
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Failing after 5s

- Add static/otel-rum.js: a framework-free browser script that captures
  uncaught errors, unhandled promise rejections and failed resource loads,
  translating them into OTLP log records POSTed to the existing /v1/logs
  endpoint (source: frontend attribute) so they flow through the same
  history/search/issue pipeline as backend telemetry.
- Add correlation-id support: traceHeaders() (manual) and same-origin
  fetch auto-instrumentation inject a W3C traceparent header, so a
  frontend error and the backend spans it triggered share a trace id and
  can be found together via GET /api/traces/{traceId}.
- Add CORS handling (withCORS in cmd/server/main.go) on the OTLP HTTP
  endpoints, required for any browser-origin POST.
- Add an optional shared ingest key (OTEL_INGEST_KEY, via '?key=' or
  X-Otel-Ingest-Key) and a per-IP rate limit (20 rps/burst 50) on OTLP
  HTTP ingestion, to scope/protect a public endpoint fed by a browser
  script. Verified end-to-end (CORS preflight, cross-origin POST, ingest
  key enforcement, rate limiting) against a running server.
- Serve static/otel-rum.js and agents_example.md as static HTTP routes so
  both the script and the agent instructions doc are fetchable directly
  from the base URL.
- Ignore the separately-cloned otel-ui/ frontend repo (has its own .git;
  not meant to be nested into this repo's history).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
developerandClaude Sonnet 5 committed 2026-09-17 10:20:42 +00:00
1 parent 0d274ff89c
commit c20a0b77f3
10 files changed
+489 -17

No files matched your search

+59
View File
@@ -1,6 +1,7 @@
package collector
import (
"bytes"
"context"
"encoding/json"
"net/http"
@@ -318,3 +319,61 @@ func TestHandleIssuesAndByID(t *testing.T) {
t.Fatalf("expected 404 for unknown issue id, got %d", rec.Code)
}
}
func TestHandleLogsIngestKey(t *testing.T) {
coll := New(noopBroadcaster{}, 10, nil)
coll.SetIngestKey("secret")
body := []byte(`{"resourceLogs":[]}`)
cases := []struct {
name string
url string
header string
wantStatus int
}{
{"no key", "/v1/logs", "", http.StatusUnauthorized},
{"wrong key", "/v1/logs?key=wrong", "", http.StatusUnauthorized},
{"correct key in query", "/v1/logs?key=secret", "", http.StatusOK},
{"correct key in header", "/v1/logs", "secret", http.StatusOK},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, tc.url, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
if tc.header != "" {
req.Header.Set("X-Otel-Ingest-Key", tc.header)
}
// Shared IP across subtests; the ingest limiter's burst of 50 is
// well above this test's request count.
req.RemoteAddr = "198.51.100.1:1234"
rec := httptest.NewRecorder()
coll.HandleLogs(rec, req)
if rec.Code != tc.wantStatus {
t.Errorf("expected status %d, got %d (body=%s)", tc.wantStatus, rec.Code, rec.Body.String())
}
})
}
}
func TestHandleLogsIngestRateLimited(t *testing.T) {
coll := New(noopBroadcaster{}, 10, nil)
body := []byte(`{"resourceLogs":[]}`)
var lastCode int
for i := 0; i < 80; i++ {
req := httptest.NewRequest(http.MethodPost, "/v1/logs", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.RemoteAddr = "198.51.100.2:1234"
rec := httptest.NewRecorder()
coll.HandleLogs(rec, req)
lastCode = rec.Code
if lastCode == http.StatusTooManyRequests {
break
}
}
if lastCode != http.StatusTooManyRequests {
t.Fatalf("expected ingest rate limiting to eventually return 429, last code was %d", lastCode)
}
}