From 03ce5ec1ba6516ce484fc683761498dcddd0cf17 Mon Sep 17 00:00:00 2001 From: jstojiljkovic Date: Thu, 11 Jun 2026 14:21:54 +0200 Subject: [PATCH 01/24] feat: restructure skills into installable agent-skills format (#190) --- skills/traceway-debug/SKILL.md | 125 +++++++++++++++++ skills/traceway-install-cli/SKILL.md | 126 ++++++++++++++++++ .../SKILL.md} | 67 ++++++++-- .../hono.md} | 0 .../nestjs.md} | 0 .../nextjs.md} | 0 6 files changed, 305 insertions(+), 13 deletions(-) create mode 100644 skills/traceway-debug/SKILL.md create mode 100644 skills/traceway-install-cli/SKILL.md rename skills/{add-traceway.md => traceway-setup-project/SKILL.md} (63%) rename skills/{add-traceway-to-hono-project.md => traceway-setup-project/hono.md} (100%) rename skills/{add-traceway-to-nestjs-project.md => traceway-setup-project/nestjs.md} (100%) rename skills/{add-traceway-to-nextjs-project.md => traceway-setup-project/nextjs.md} (100%) diff --git a/skills/traceway-debug/SKILL.md b/skills/traceway-debug/SKILL.md new file mode 100644 index 00000000..4a38b81e --- /dev/null +++ b/skills/traceway-debug/SKILL.md @@ -0,0 +1,125 @@ +--- +name: traceway-debug +description: Investigate a bug or production issue using observability data from a Traceway instance — exceptions, logs, endpoint stats, and metrics queried via the traceway CLI, correlated with the codebase. Use when the user describes a bug, error, crash, slowness, or anomaly and a Traceway instance is monitoring the affected app, e.g. "/traceway-debug users report 500s on checkout since this morning". +--- + +# Debug with Traceway + +Investigate a described bug using telemetry from a Traceway instance, then correlate findings with the codebase to find the root cause. + +## Prerequisites + +The `traceway` CLI must be installed, authenticated, and pointed at the right project: + +```bash +traceway version # installed? +traceway projects list # authenticated? right instance? +traceway projects use # select the project for the affected app +``` + +If the CLI is missing, install it first (see the `traceway-install-cli` skill, or https://github.com/tracewayapp/traceway/releases). If authentication fails (exit code 4), ask the user to run `traceway login --url https://`. + +**CLI behavior for agents:** piped output defaults to JSON (one record per line); `--fields a,b,c` trims responses; errors emit `{"error":"","message":"...","hint":"...","exit_code":N}` on stderr. Time ranges: `--since 1h|24h|7d` or `--from/--to` (RFC3339). All list commands paginate with `--page` / `--page-size` (default 50). + +## Step 1: Frame the Investigation + +From the user's bug description, extract: the symptom (error, wrong behavior, slowness, crash), the affected endpoint/feature, and the time window. Default to `--since 24h` if no timeframe was given; widen later if needed. + +## Step 2: Look for Exceptions + +Most bugs surface as grouped exceptions (Issues): + +```bash +# Recent exception groups, most impactful first +traceway exceptions list --since 24h + +# Search for terms from the bug description (error message, type, file) +traceway exceptions list --since 24h --search "checkout" +traceway exceptions list --since 24h --search "NullPointer" --search-type regex + +# Sort by what matters: lastSeen (default), firstSeen (regressions), count (volume) +traceway exceptions list --since 7d --order-by firstSeen + +# Full detail for a group: stack trace, occurrences, tags +traceway exceptions show +``` + +`exceptions show` is the high-value call — it returns the full stack trace and occurrence tags (user IDs, app versions, request context). Use `firstSeen` to correlate with deploys: a group that first appeared right after a release points at that release's diff. + +## Step 3: Query Logs Around the Failure + +```bash +# Errors and worse, in the window +traceway logs query --since 24h --min-severity 17 + +# Search log bodies for terms from the bug report +traceway logs query --since 24h --search "payment declined" + +# Narrow by service in multi-service projects +traceway logs query --since 24h --service checkout-api --min-severity 13 +``` + +Severity numbers are OTel-standard: 1=TRACE, 5=DEBUG, 9=INFO, 13=WARN, 17=ERROR, 21=FATAL. + +**Correlate by trace:** if a log record or exception includes a trace ID, pull every log line from that exact request: + +```bash +traceway logs query --since 24h --trace-id +``` + +This reconstructs the request timeline — usually the fastest route to a root cause. + +## Step 4: Check Endpoint Health (for Slowness / Error Rates) + +```bash +# Per-endpoint p50/p95/p99, error counts — sorted by impact +traceway endpoints list --since 24h + +# Find the affected endpoint +traceway endpoints list --since 24h --search "checkout" + +# Worst latency first +traceway endpoints list --since 24h --order-by p95 +``` + +Compare windows to find when a regression started, e.g. `--since 1h` vs `--since 7d`, or two explicit `--from/--to` ranges around a suspected deploy. + +## Step 5: Check Metrics (for Resource / Systemic Issues) + +```bash +# Latency over time +traceway metrics query --name http.server.duration --aggregation p95 --since 24h + +# Resource saturation (Go SDK default metrics) +traceway metrics query --name cpu.used_pcnt --aggregation avg --since 24h +traceway metrics query --name mem.used_pcnt --aggregation max --since 24h + +# Group a metric by tag, filter by tag +traceway metrics query --name http.server.duration --aggregation p95 --group-by endpoint --since 6h +traceway metrics query --name queue.depth --aggregation max --tag queue=email --since 6h +``` + +Aggregations: `avg, sum, count, min, max, p50, p95, p99`. Use `--interval-minutes` to control bucket size (0 = auto). Spikes that line up with the exception's `firstSeen` time confirm a systemic cause (OOM, CPU saturation, downstream slowness) rather than a code bug. + +## Step 6: Correlate with the Code + +With a stack trace, trace timeline, or regression window in hand: + +1. Open the files/lines named in the stack trace and read the failing path. +2. If the issue started at a known time, check what shipped then: `git log --since "" --until ""` or the deploy history. +3. Form a hypothesis that explains **all** observations (error message, affected endpoint, timing, volume) — not just the first stack frame. +4. Propose or implement the fix per the user's instruction. + +## Step 7: Report and Clean Up + +Summarize: symptom → evidence (exception hashes, log excerpts, metric anomalies) → root cause → fix. Include `traceway exceptions show ` references so the user can verify. + +After a fix is deployed and verified, the exception group can be archived — this mutates server state, so only do it when the user asks: + +```bash +traceway exceptions archive --yes +``` + +## If There Is No Telemetry + +Empty results usually mean the wrong project, wrong time window, or the app is not instrumented. Check `traceway projects list`, widen `--since`, and if the app was never connected to Traceway, set it up first (see the `traceway-setup-project` skill). diff --git a/skills/traceway-install-cli/SKILL.md b/skills/traceway-install-cli/SKILL.md new file mode 100644 index 00000000..a317e7ec --- /dev/null +++ b/skills/traceway-install-cli/SKILL.md @@ -0,0 +1,126 @@ +--- +name: traceway-install-cli +description: Install the traceway CLI, authenticate against a Traceway instance, and select a project. Use when the user wants the Traceway command-line client set up on their machine, or when another Traceway skill needs the CLI and it is not installed yet. +--- + +# Install the Traceway CLI + +The `traceway` CLI queries a Traceway observability instance from the terminal — exceptions, logs, endpoints, and metrics. It is designed to be first-class for both LLM agents (stable JSON output, stable error identifiers, no hung prompts) and humans. + +## Step 1: Check for an Existing Install + +```bash +traceway version +``` + +If this prints a version, skip to Step 3 (authenticate). Source builds report `dev`. + +## Step 2: Install + +### Option A: Prebuilt binary (preferred) + +Binaries are published on the [tracewayapp/traceway releases page](https://github.com/tracewayapp/traceway/releases) under `CLI vX.Y.Z` tags (git tag format: `cli/vX.Y.Z`). Assets are named `traceway___.tar.gz` (`.zip` on Windows), with `os` ∈ `darwin`, `linux`, `windows` and `arch` ∈ `arm64`, `x86_64`. + +With `gh` (handles the fact that the latest release may be a Backend release, not a CLI one): + +```bash +OS=$(uname -s | tr '[:upper:]' '[:lower:]') +ARCH=$(uname -m); [ "$ARCH" = "aarch64" ] && ARCH=arm64 +TAG=$(gh release list --repo tracewayapp/traceway --limit 20 --json tagName --jq '[.[].tagName | select(startswith("cli/"))][0]') +gh release download "$TAG" --repo tracewayapp/traceway --pattern "traceway_*_${OS}_${ARCH}.tar.gz" --output - | tar -xz traceway +install -m 755 traceway ~/.local/bin/traceway && rm traceway +``` + +Without `gh`, resolve the download URL via the GitHub API: + +```bash +OS=$(uname -s | tr '[:upper:]' '[:lower:]') +ARCH=$(uname -m); [ "$ARCH" = "aarch64" ] && ARCH=arm64 +URL=$(curl -s "https://api.github.com/repos/tracewayapp/traceway/releases?per_page=20" \ + | grep -o "https://[^\"]*traceway_[^\"]*_${OS}_${ARCH}\.tar\.gz" | head -1) +curl -sL "$URL" | tar -xz traceway +install -m 755 traceway ~/.local/bin/traceway && rm traceway +``` + +Make sure `~/.local/bin` is on `PATH` (or install to `/usr/local/bin` instead). + +### Option B: Build from source + +Requires Go (see `cli/go.mod` for the minimum version): + +```bash +git clone https://github.com/tracewayapp/traceway +cd traceway/cli +go build -o bin/traceway ./cmd/traceway +install -m 755 bin/traceway ~/.local/bin/traceway +``` + +The repo also ships a Nix dev shell: `nix develop` then `just build`. + +### Verify + +```bash +traceway version +``` + +## Step 3: Authenticate + +```bash +traceway login --url https:// +``` + +This prompts for email and password, then stores the JWT. Config (URL, username) goes to `$XDG_CONFIG_HOME/traceway/config.json`; credentials and the active project go to `$XDG_STATE_HOME/traceway/state.json`. + +Login prompts interactively for the password — when running as an agent, ask the user to run the login command themselves. For non-interactive contexts where the user has the password in a secret store: + +```bash +printf '%s' "$TRACEWAY_PASSWORD" | traceway login --url https:// --username you@example.com --password-stdin +``` + +Never echo a password into the command line or shell history. + +Multiple instances/accounts coexist via profiles: + +```bash +traceway login --url https://traceway.example.com --profile work +traceway profiles list +traceway profiles use work +``` + +## Step 4: Select a Project + +```bash +traceway projects list +traceway projects use +``` + +The selected project is used implicitly by all subsequent commands. + +## Step 5: Smoke-Check + +```bash +traceway exceptions list --since 24h +traceway endpoints list --since 1h +``` + +## Usage Notes for Agents + +- **Output**: `--output table|json|yaml`. Default is `table` on a TTY and `json` otherwise — piped/scripted calls always get machine-readable output. `--fields a,b,c` projects list responses to just those keys. +- **Exit codes**: `0` success, `1` generic/API error, `2` usage error, `3` connection failure, `4` auth failure, `5` not found, `6` rate limited, `7` server 5xx. +- **Error envelope** (stderr, JSON mode): `{"error":"token_expired","message":"...","hint":"traceway login","exit_code":4}`. The `error` field is a stable snake_case identifier — branch on it. +- **Mutations** (`exceptions archive` / `unarchive`) require `--yes` (or `TRACEWAY_ASSUME_YES=1`) in non-TTY contexts; without it they fail fast with exit 2 instead of hanging on a prompt. +- Run `traceway --help` for full per-command flags. + +## Command Reference + +| Command | Purpose | +|---|---| +| `traceway login` / `logout` | Authenticate / forget the stored JWT | +| `traceway profiles {list,use}` | Manage multiple instances/accounts | +| `traceway projects {list,use}` | List or select the active project | +| `traceway exceptions list` | Recent grouped exceptions | +| `traceway exceptions show ` | A single exception group + occurrences | +| `traceway exceptions archive/unarchive ...` | Mutating; needs `--yes` non-interactively | +| `traceway logs query` | Query logs with severity / service / search filters | +| `traceway endpoints list` | Per-endpoint p50/p95/p99 stats | +| `traceway metrics query` | Time-series metric queries | diff --git a/skills/add-traceway.md b/skills/traceway-setup-project/SKILL.md similarity index 63% rename from skills/add-traceway.md rename to skills/traceway-setup-project/SKILL.md index ab1870fa..9ce75b87 100644 --- a/skills/add-traceway.md +++ b/skills/traceway-setup-project/SKILL.md @@ -1,6 +1,22 @@ -# Add Traceway to a Project +--- +name: traceway-setup-project +description: Connect a project to a Traceway instance so it reports endpoints, spans, errors, and metrics. Use when the user wants to add Traceway (or OpenTelemetry tracing that exports to Traceway) to a backend, frontend, or mobile project. Accepts a project token and instance URL, e.g. "/traceway-setup-project with token abc123". +--- -Add OpenTelemetry tracing to an existing project so it reports to a Traceway instance. +# Set Up Traceway in a Project + +Connect an existing project to a Traceway instance so it reports endpoints, spans, errors, and metrics. + +## Step 0: Gather Connection Info + +Two values are required: + +| Value | Example | Where to find it | +|---|---|---| +| **Instance URL** | `https://traceway.example.com` | The URL of the Traceway dashboard | +| **Project token** | `abc123...` | Traceway dashboard → Connection page | + +Both may be provided in the invocation (e.g. `/traceway-setup-project with token abc123 and url https://traceway.example.com`). If either is missing, check for existing `TRACEWAY_URL` / `TRACEWAY_TOKEN` environment variables or `.env` entries in the project — otherwise ask the user before proceeding. Never invent placeholder values in committed code; wire everything through environment variables. ## What Traceway Needs @@ -25,26 +41,26 @@ For the integration to work correctly, the instrumentation MUST capture: ## Step 1: Identify the Framework -Detect the framework by reading `package.json` (Node.js), `go.mod` (Go), `composer.json` (PHP), or asking the user. +Detect the framework by reading `package.json` (Node.js), `go.mod` (Go), `composer.json` (PHP), `requirements.txt`/`pyproject.toml` (Python), `pubspec.yaml` (Flutter), `build.gradle` (Android), or asking the user. ## Step 2: Follow the Framework-Specific Guide ### Hono (Node.js) -Follow `skills/add-traceway-to-hono-project.md`. Uses `@hono/otel` middleware — do NOT use `@opentelemetry/instrumentation-http` (it doesn't work with Hono's ESM imports on Node 22+). +Follow `hono.md` in this skill directory. Uses `@hono/otel` middleware — do NOT use `@opentelemetry/instrumentation-http` (it doesn't work with Hono's ESM imports on Node 22+). - Endpoints: `@hono/otel` sets `http.route` automatically - Status codes: `@hono/otel` sets them automatically - Exceptions: `@hono/otel` records thrown errors automatically - Tasks: No built-in scheduler — use `SpanKind.CONSUMER` manually for background work ### NestJS (Node.js) -Follow `skills/add-traceway-to-nestjs-project.md`. Simplest integration — Express auto-instrumentation handles everything. +Follow `nestjs.md` in this skill directory. Simplest integration — Express auto-instrumentation handles everything. - Endpoints: `instrumentation-express` sets `http.route` automatically - Status codes: `instrumentation-http` sets them automatically - Exceptions: Express error handling records them automatically - Tasks: Wrap `@nestjs/schedule` cron jobs and `@nestjs/bull` queue consumers with `SpanKind.CONSUMER` spans ### Next.js (Node.js) -Follow `skills/add-traceway-to-nextjs-project.md`. Requires `withRoute()` wrapper for API routes and `@prisma/instrumentation` for database tracing. +Follow `nextjs.md` in this skill directory. Requires `withRoute()` wrapper for API routes and `@prisma/instrumentation` for database tracing. - Endpoints: `withRoute()` helper must be added manually to every API route handler - Status codes: Set by the HTTP instrumentation - Exceptions: `withRoute()` catches and records thrown errors @@ -56,14 +72,18 @@ Follow `skills/add-traceway-to-nextjs-project.md`. Requires `withRoute()` wrappe - No app code changes needed — auto-instrumentation captures routes, status codes, errors - Start with `node --import ./instrumentation.js server.js` - Tasks: Use `SpanKind.CONSUMER` manually for background work -- Full docs: `docs/pages/client/node-sdk/index.mdx` +- Full docs: https://docs.tracewayapp.com/client/node-sdk ### Gin / Chi / Fiber / FastHTTP / stdlib (Go) - Install the framework-specific middleware: `go get go.tracewayapp.com/tracewaygin` (or `tracewaychi`, `tracewayfiber`, `tracewayfasthttp`, `tracewayhttp`) - Add middleware: `r.Use(tracewaygin.New("token@http://traceway:8082/api/report"))` - Reports via Traceway's native protocol (`/api/report`), not OTel - Endpoints, status codes, exceptions, and tasks are all handled by the Go SDK automatically -- Full docs: `docs/pages/client/gin-middleware/index.mdx` (or the corresponding framework directory) +- Full docs: https://docs.tracewayapp.com/client/gin-middleware (or the corresponding framework page) + +### Django (Python) +- Uses OTel auto-instrumentation for Django +- Full docs: https://docs.tracewayapp.com/client/django ### Symfony (PHP) - Install: `composer require traceway/opentelemetry-symfony open-telemetry/exporter-otlp php-http/guzzle7-adapter` @@ -71,17 +91,25 @@ Follow `skills/add-traceway-to-nextjs-project.md`. Requires `withRoute()` wrappe - Add `\OpenTelemetry\SDK\SdkAutoloader::autoload()` to `public/index.php` - Endpoints and status codes: handled by Symfony OTel auto-instrumentation - Tasks: Symfony Messenger consumers are auto-instrumented as Tasks -- Full docs: `docs/pages/client/symfony/index.mdx` +- Full docs: https://docs.tracewayapp.com/client/symfony + +### Laravel (PHP) +- Full docs: https://docs.tracewayapp.com/client/laravel -### React / Vue / Svelte / jQuery (Frontend) +### React / Vue / Svelte / jQuery / plain JS (Frontend) - Install the framework-specific Traceway SDK: `npm install @tracewayapp/react` (or `@tracewayapp/vue`, `@tracewayapp/svelte`, `@tracewayapp/jquery`) - These are client-side SDKs that report to `/api/report`, not OTel -- Full docs: `docs/pages/client/react/index.mdx` (or the corresponding framework directory) +- They capture JS errors (as Issues), page loads, and web vitals; upload source maps for readable stack traces from minified bundles +- Full docs: https://docs.tracewayapp.com/client/react (or `vue`, `svelte`, `jquery`, `js-sdk` for plain JavaScript) + +### React Native / Flutter / Android (Mobile) +- Install the platform Traceway SDK and initialize it with the instance URL + project token at app startup +- Full docs: https://docs.tracewayapp.com/client/react-native, https://docs.tracewayapp.com/client/flutter, https://docs.tracewayapp.com/client/android ### Cloudflare Workers - Uses Cloudflare's built-in OTLP export, not the Node SDK - Scheduled handlers (`scheduled` event) create root spans automatically -- Full docs: `docs/pages/client/cloudflare/index.mdx` +- Full docs: https://docs.tracewayapp.com/client/cloudflare ### Any Other Language (Generic OTel) - Use any OpenTelemetry SDK for the language @@ -89,7 +117,7 @@ Follow `skills/add-traceway-to-nextjs-project.md`. Requires `withRoute()` wrappe - Set `Authorization: Bearer ` header - Ensure `http.route` is set on root SERVER spans (not just `url.path`) - Use `SpanKind.CONSUMER` for background/scheduled work -- Full docs: `docs/pages/client/otel/index.mdx` +- Full docs: https://docs.tracewayapp.com/client/otel ## Instrumenting Background Tasks (All Frameworks) @@ -129,3 +157,16 @@ Without `SpanKind.CONSUMER`, the span would either be classified as an Endpoint - **Environment variables**: `TRACEWAY_URL` and `TRACEWAY_TOKEN` (or standard `OTEL_*` vars) - **Auto-instrumented child spans** (CJS packages only): `pg`, `mysql2`, `mongodb`, `ioredis`, `redis`, Prisma (with `@prisma/instrumentation`), outgoing `fetch()` via `instrumentation-undici` - **Not auto-instrumented**: SQLite (`better-sqlite3`), custom business logic — use `tracer.startActiveSpan()` manually + +## Step 3: Verify + +1. Start the app and hit a few endpoints (or trigger an error on purpose). +2. Check the Traceway dashboard: + - **Endpoints page** — routes appear grouped by pattern (e.g. `GET /api/users/:id`), not by literal URL + - **Issues page** — thrown errors appear with stack traces + - **Endpoint detail → Spans tab** — database queries and outgoing calls appear as children +3. If the `traceway` CLI is installed and authenticated, verify from the terminal instead: + ```bash + traceway endpoints list --since 15m + traceway exceptions list --since 15m + ``` diff --git a/skills/add-traceway-to-hono-project.md b/skills/traceway-setup-project/hono.md similarity index 100% rename from skills/add-traceway-to-hono-project.md rename to skills/traceway-setup-project/hono.md diff --git a/skills/add-traceway-to-nestjs-project.md b/skills/traceway-setup-project/nestjs.md similarity index 100% rename from skills/add-traceway-to-nestjs-project.md rename to skills/traceway-setup-project/nestjs.md diff --git a/skills/add-traceway-to-nextjs-project.md b/skills/traceway-setup-project/nextjs.md similarity index 100% rename from skills/add-traceway-to-nextjs-project.md rename to skills/traceway-setup-project/nextjs.md From b42d1cdf2184809e7d1ea15c844d293658a0a5d0 Mon Sep 17 00:00:00 2001 From: jstojiljkovic Date: Thu, 11 Jun 2026 16:30:27 +0200 Subject: [PATCH 02/24] chore: working on correct data-model --- skills/traceway-setup-project/data-model.md | 233 ++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 skills/traceway-setup-project/data-model.md diff --git a/skills/traceway-setup-project/data-model.md b/skills/traceway-setup-project/data-model.md new file mode 100644 index 00000000..c895ad1e --- /dev/null +++ b/skills/traceway-setup-project/data-model.md @@ -0,0 +1,233 @@ +# How Traceway Interprets OpenTelemetry Data + +This is the framework-agnostic reference for connecting any OpenTelemetry project to Traceway. It explains what Traceway shows in the dashboard, how it classifies incoming OTel spans, and the quirks the instrumentation MUST respect for the data to display correctly. Read this before following any framework-specific guide. + +## Sending Data + +| What | Value | +|---|---| +| Traces | `POST https:///api/otel/v1/traces` | +| Metrics | `POST https:///api/otel/v1/metrics` | +| Logs | `POST https:///api/otel/v1/logs` | +| Auth | `Authorization: Bearer ` header on every request | +| Encoding | OTLP/HTTP — both `application/x-protobuf` and `application/json` are accepted. OTLP/gRPC is NOT supported. | +| Compression | `Content-Encoding: gzip` is supported (exporters default to no compression; enabling `compression: gzip` is fine). Max request body: 10 MB. | + +## What the Dashboard Shows + +Traceway turns OTel spans into five distinct concepts, each with its own dashboard page: + +| Concept | Dashboard page | Built from | +|---|---|---| +| **Endpoint** | Endpoints (P50/P95/P99, error rates, Apdex) | Root spans that look like HTTP requests | +| **Task** | Tasks (background jobs, cron, consumers) | `CONSUMER`-kind spans | +| **Span** | Endpoint/Task detail → Spans tab (waterfall) | Child spans (DB queries, outgoing calls, custom work) | +| **Issue** | Issues (grouped errors with stack traces) | `exception` events on spans | +| **AI Trace** | AI Traces (tokens, cost, model) | Spans with `gen_ai.*` attributes | + +## Span Classification Rules (exact) + +For every incoming span, Traceway applies these rules in order: + +1. **Endpoint** — `SpanKind` is `SERVER` or `INTERNAL`, the span has at least one HTTP attribute (`http.request.method`, `http.method`, `http.route`, or `url.path`), AND it is either a root span (no parent) or its parent span is not present in the same export batch (cross-process tracing, e.g. behind a proxy that injects `traceparent`). +2. **Task** — `SpanKind` is `CONSUMER`. This applies to ANY consumer span, root or not. +3. **Task** — a root `INTERNAL` span with a `console.command` attribute (CLI command instrumentation, e.g. Laravel/Symfony console). +4. **AI Trace** — the span has any attribute starting with `gen_ai.`. +5. **Child span** — anything else with a parent: stored as a generic span, attached to the nearest promoted ancestor (Endpoint/Task/AI Trace) by walking up the parent chain. +6. **Dropped** — anything else that is a root span. A root span that matches none of the rules above is silently discarded, along with any exception events on it. + +The consequences of rule 6 are the most common integration bug: a custom root span created with `tracer.startActiveSpan("my-job")` and default `SpanKind.INTERNAL` produces NOTHING in Traceway. Background work must use `SpanKind.CONSUMER` (see Tasks below). + +## Endpoints: Name and Route Parameters + +This is where most integrations go wrong. The endpoint name displayed and grouped in the dashboard is built as: + +``` + +``` + +e.g. `GET /api/users/:id`. The pieces come from span attributes, with these exact rules: + +- **Method**: `http.request.method` (current semconv), falling back to `http.method` (old semconv). +- **Route**: `http.route`, falling back to `url.path`. A `http.route` value that does not start with `/` is ignored entirely (treated as absent). +- If the method is present but no route can be resolved, the span name is used as the route. If neither is present, the raw span name becomes the endpoint name. + +### The cardinality quirk — `http.route` is mandatory + +`http.route` must contain the **route pattern with parameter placeholders**, never the concrete URL: + +``` +correct: http.route = /api/users/:id -> one endpoint: "GET /api/users/:id" +correct: http.route = /api/users/{id} -> one endpoint: "GET /api/users/{id}" +wrong: http.route missing, url.path only -> thousands of endpoints: "GET /api/users/1", "GET /api/users/2", ... +``` + +The placeholder style (`:id`, `{id}`, ``) does not matter — Traceway uses the route string as-is. What matters is that the string is **identical for every request hitting that route**. If `http.route` is missing, Traceway falls back to `url.path`, which contains real IDs, and the Endpoints page explodes into one row per unique URL. Express, Hono (via `@hono/otel`), and Django instrumentations set `http.route` to the path template automatically — but verify it in the dashboard after setup; it's the #1 thing to check. + +Symfony caveat: the stock `open-telemetry/opentelemetry-auto-symfony` package sets `http.route` to the Symfony route *name* (e.g. `app_user_show`), not a path template. Traceway deliberately ignores `http.route` values that don't start with `/` and falls back to `url.path` — so endpoint grouping for raw Symfony auto-instrumentation degrades to literal paths. Use the Traceway Symfony integration (see the Symfony guide) instead. + +### Other endpoint attributes Traceway reads + +| Attribute (current semconv) | Legacy fallback also read | Used for | +|---|---|---| +| `http.response.status_code` | `http.status_code` | Status, error rate, 4xx/5xx breakdown | +| `http.response.body.size` | `http.response_content_length` | Response size | +| `client.address` | `net.peer.ip` | Client IP | + +Quirks: + +- **404 responses are renamed to `UNMATCHED`.** Any endpoint span with status 404 is grouped under a single `UNMATCHED` endpoint regardless of its route — bot scans and typo'd URLs don't pollute the endpoint list. Don't be surprised when a legitimate 404-returning route doesn't appear under its own name. +- **Missing status code becomes `0`.** Without `http.response.status_code`, error tracking and Apdex for that endpoint are meaningless. Make sure the instrumentation sets it. +- **Streaming endpoints** (long-lived responses that would otherwise look like terrible P99s) are detected when: status is `101` (WebSocket upgrade), the captured response header `http.response.header.content-type` contains `text/event-stream` (SSE), or the vendor attribute `traceway.is_stream` (boolean) is `true`. OTel clients don't capture response headers by default — for SSE endpoints either enable capture of `content-type` or set `traceway.is_stream` manually. Streaming endpoints keep their request count and error rate, but latency percentiles and Apdex are zeroed (connection lifetime is not request latency). +- **Health-check endpoints are filtered at ingestion** according to the project's health-check configuration, so `GET /health`-style routes don't pollute the endpoint list or stats. +- **Apdex thresholds are fixed**: satisfied at duration up to 750 ms, tolerating up to 1.5 s, bad above that or on any 5xx response. + +## Tasks: Scheduled Jobs, Cron, Queue Consumers + +Background work appears on the **Tasks** page only when the span has `SpanKind.CONSUMER`: + +```typescript +import { trace, SpanKind, SpanStatusCode } from "@opentelemetry/api"; + +const tracer = trace.getTracer("my-app"); + +async function runScheduledJob() { + await tracer.startActiveSpan( + "cleanup-expired-sessions", // becomes the Task name, must be stable + { kind: SpanKind.CONSUMER }, // without this the span is DROPPED + async (span) => { + try { + await doWork(); + span.setStatus({ code: SpanStatusCode.OK }); + } catch (error) { + span.recordException(error); // becomes an Issue, linked to this Task + span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); + throw error; + } finally { + span.end(); + } + } + ); +} +``` + +Quirks: + +- **The span name IS the task name, and Traceway groups tasks by name.** Use a stable identifier like `cleanup-expired-sessions` or `process-email-queue`. Never put job IDs, timestamps, or user IDs in the span name — each unique name becomes a separate task group. +- **Every `CONSUMER` span becomes a Task, even non-root ones.** If a queue library's auto-instrumentation already emits `CONSUMER` spans (e.g. Kafka/RabbitMQ consumers, Symfony Messenger), do NOT wrap them in another `CONSUMER` span — you'd get duplicate Task entries. +- A root `INTERNAL` span with `SpanKind.INTERNAL` (the default!) is **dropped silently** — this is the most common reason "my cron job doesn't show up". The kind must be `CONSUMER`. +- Per-job context (job ID, batch size) belongs in span **attributes**, where it shows on the task detail page without affecting grouping. + +## Database Queries and Child Spans + +Work inside an endpoint or task — DB queries, cache hits, outgoing HTTP calls, custom business logic — should be **child spans** of the entity's root span. They render in the waterfall on the Endpoint/Task detail page. + +How Traceway handles them: + +- **The span name is replaced by the SQL text when present**: if the span has `db.query.text` (current semconv) or `db.statement` (old semconv), that value is displayed instead of the span name. So a `pg.query` span carrying `db.statement = "SELECT * FROM users WHERE id = $1"` shows the query itself in the waterfall — this is what makes auto-instrumented DB clients (`pg`, `mysql2`, `mongodb`, `ioredis`, Prisma via `@prisma/instrumentation`) useful out of the box. +- **Child spans attach to the nearest promoted ancestor.** The parent chain is walked until an Endpoint/Task/AI Trace is found. This means DB spans must be created **inside the active context** of the request/task span (`startActiveSpan`, or the framework middleware's context). A DB span created outside any active context becomes a root `INTERNAL` span — and is dropped. +- Spans whose parent lives in another process/batch and never resolves to a promoted entity are kept but linked by the raw OTel trace ID instead. +- For databases without auto-instrumentation (e.g. SQLite), create a manual child span and set `db.system` + `db.statement` attributes so it renders like the auto-instrumented ones. + +## Issues: Exceptions + +Errors become **Issues** when recorded as a span **event named `exception`** (`span.recordException(error)` does exactly this) with the standard attributes: + +- `exception.type` — error class, e.g. `TypeError` +- `exception.message` — error message +- `exception.stacktrace` — full stack trace text + +Quirks: + +- **Exceptions on dropped spans are dropped too.** An exception recorded on an unpromoted root span (see classification rule 6) never reaches the Issues page. Record exceptions on spans that live inside an Endpoint/Task, or fix the span kind. +- Traceway also understands Honeycomb-style structured stack traces (`exception.structured_stacktrace.urls` / `.functions` / `.lines` / `.columns` array attributes) — used by some browser/edge SDKs. +- **Grouping is automatic.** Before hashing (SHA-256, truncated to 16 hex chars), Traceway normalizes the stack trace: error messages are stripped (only the error type is kept, including on `Caused by:` lines), JS function-name lines collapse to ``, absolute paths reduce to `filename:line`, dependency version suffixes (`@v1.2.3`) are removed, and runtime values are replaced with placeholders — hex addresses → ``, UUIDs → ``, numbers of 5+ digits → ``, emails → ``, IPs → ``, goroutine IDs → `goroutine `. JVM frames additionally lose their line numbers (`(Foo.java:123)` → `(Foo.java)`) and `... N more` becomes `... more`. The same logical error therefore groups into one Issue even when runtime values differ — don't try to pre-group errors client-side. +- **JS projects get source-map symbolication.** Triggered when the `telemetry.sdk.language` resource attribute is a JS value (`nodejs`, `webjs`, `javascript`, `typescript` — OTel SDKs set this automatically), or when the instrumentation scope name is npm-scoped (`@scope/pkg`) or `next.js`. Minified frames are resolved server-side **before** grouping, so uploading source maps improves grouping too. +- **Source maps are matched by filename (and debug ID when embedded), not by version.** Upload via `POST /api/sourcemaps/upload` (multipart `files` field, `.map`/`.js`/`.cjs`/`.mjs`, max 50 MB per file) using a token from `POST /api/projects/source-map-token`. A frame referencing `app.min.js` resolves against the uploaded `app.min.js.map` by name — keep bundle filenames unique per release (content hashes do this) or rely on debug IDs. + +## Metrics + +OTLP metrics sent to `/api/otel/v1/metrics` are stored under their **original names** — there is no renaming or namespacing. Every incoming metric is auto-registered (name, unit, type) in the metric registry, becomes discoverable in the dashboard's metric explorer, and can be charted in custom widgets or queried via `traceway metrics query --name `. + +Conversion rules per OTLP metric type: + +| OTLP type | Stored as | Registered type | +|---|---|---| +| Gauge | ``, value as-is | `gauge` | +| Sum (monotonic) | ``, value as-is | `counter` | +| Sum (non-monotonic) | ``, value as-is | `gauge` | +| Histogram | TWO series: `.avg` (sum/count per export) and `.count` | `gauge` + `counter` | +| ExponentialHistogram, Summary | **silently dropped** | — | + +Quirks: + +- **Histograms lose their buckets.** Only the average and count survive — you cannot compute true percentiles from an OTLP histogram in Traceway. +- **No percentile aggregations on metrics at all.** Metric queries support `avg`, `sum`, `count`, `min`, and `max` only; anything else silently falls back to `avg`. Latency percentiles (P50/P95/P99) exist for Endpoints and Tasks because those are computed from raw span durations — if you need percentiles on a measurement, model it as a span, not a metric. +- **Tags come from data-point attributes only.** Resource attributes are NOT copied onto metric points, with two exceptions: `service.name` becomes the `server_name` tag, and the process-scraper allowlist (`process.pid`, `process.executable.name`, `process.command_line`, `process.owner`) is lifted so hostmetrics per-process series stay distinguishable. Any per-series dimension must be a data-point attribute, and keep its cardinality low. + +### Built-in metric names + +The dashboard's built-in system charts (Metrics page, default widget suggestions) read these **exact hardcoded names** — they are emitted natively by the Traceway Go SDK: + +| Name | Meaning | Unit | +|---|---|---| +| `cpu.used_pcnt` | CPU usage | percent (0–100) | +| `mem.used` | Memory used | MB | +| `mem.total` | Memory total | MB | +| `go.go_routines` | Goroutine count | count | +| `go.heap_objects` | Heap objects | count | +| `go.num_gc` | Total GC cycles | count | +| `go.gc_pause` | Last GC pause | nanoseconds | + +An OTel project's metrics (e.g. hostmetrics' `system.cpu.utilization`) do NOT populate those built-in charts — they appear as custom metrics under their own names, chartable via custom widgets. To fill the built-in CPU/memory charts from an OTel pipeline, the metrics must be emitted under the exact names and units above. + +## Logs + +OTLP logs sent to `/api/otel/v1/logs` appear on the **Logs** page. From each LogRecord, Traceway reads: timestamp (falling back to observed timestamp), severity number and text, body, trace ID and span ID, and three separate attribute maps — resource, scope, and log-record attributes — each independently filterable. + +What the Logs page (and `traceway logs query`) can filter by: minimum severity, service name (from the `service.name` resource attribute), trace ID, free-text body search, and attribute key/value filters scoped to resource, scope, or log attributes. + +Quirks: + +- **Emit logs inside the active span context.** The trace ID on a log record is what links it to the Endpoint/Task that produced it — OTel log bridges do this automatically when a span is active. +- **Body search over ranges longer than 24 hours requires at least one additional filter** (service, severity, trace, or attribute) — unbounded full-text scans are rejected. +- A JS `exception.stacktrace` attribute on a log record is source-map symbolicated, same as span exceptions. +- Logs are retained for 30 days (database TTL); other telemetry has no fixed expiry on ClickHouse deployments. + +## Resource Attributes + +Set these once on the OTel `Resource` — they tag everything the service exports: + +| Resource attribute | Shown in Traceway as | Notes | +|---|---|---| +| `service.name` | Server Name | Set via `OTEL_SERVICE_NAME` or SDK config. Distinguishes instances/services within a project. | +| `service.version` | App Version | Enables release-over-release comparison. On Cloudflare Workers, derived from `cloudflare.script_version.id` automatically. | +| `telemetry.sdk.language` | — | Drives JS symbolication; set automatically by every OTel SDK. | + +## Vendor Extension Attributes + +Traceway recognizes these non-standard span attributes: + +| Attribute | Type | Purpose | +|---|---|---| +| `traceway.is_stream` | boolean | Mark an endpoint span as streaming (SSE/long-poll) so its duration is excluded from latency percentiles. | +| `traceway.distributed_trace_id` | string (UUID) | Override the distributed trace ID used to link entities across services. Rarely needed — the OTel trace ID is used by default. | + +## Verification Checklist + +After wiring up any project, confirm in the dashboard (or via `traceway` CLI): + +1. **Endpoints page** — routes are grouped by pattern (`GET /api/users/:id`), NOT one row per concrete URL. If you see raw IDs, `http.route` is not being set. +2. **Endpoints page** — status codes are non-zero. +3. **Tasks page** — each scheduled job appears under one stable name after triggering it. (Dashboard only — the CLI has no `tasks` command.) +4. **Issues page** — a deliberately thrown error shows up with a readable stack trace. +5. **Endpoint detail → Spans tab** — DB queries appear as children showing the SQL text. + +With the `traceway` CLI (`--since` takes relative ranges like `1h`, `24h`, `7d`): + +```bash +traceway endpoints list --since 1h +traceway exceptions list --since 1h +traceway logs query --since 1h --min-severity 17 +traceway metrics query --name --since 1h +``` From 641b469882940ce6283ba5213d0dd3b342e87669 Mon Sep 17 00:00:00 2001 From: ddux Date: Thu, 11 Jun 2026 10:49:16 -0500 Subject: [PATCH 03/24] progress --- examples/devtesting-embedded/go.mod | 12 +- examples/devtesting-embedded/go.sum | 24 + .../components/setup/ai-setup-steps.svelte | 49 ++ .../setup/copyable-code-block.svelte | 108 ++++ .../components/setup/copyable-inline.svelte | 25 + .../setup/otel-exporter-config.svelte | 33 + .../components/setup/otel-setup-steps.svelte | 126 ++++ .../components/setup/setup-mode-tabs.svelte | 24 + .../setup/source-map-upload-card.svelte | 83 +++ frontend/src/lib/utils/ai-setup.ts | 5 + frontend/src/lib/utils/otel-sdks.ts | 42 ++ frontend/src/lib/utils/otel-setup.ts | 566 ++++++++++++++++++ frontend/src/lib/utils/setup-storage.ts | 68 +++ frontend/src/routes/+page.svelte | 130 +--- frontend/src/routes/connection/+page.svelte | 214 +------ skills/traceway-debug/SKILL.md | 2 +- .../SKILL.md | 16 +- .../hono.md | 0 .../nestjs.md | 0 .../nextjs.md | 0 20 files changed, 1227 insertions(+), 300 deletions(-) create mode 100644 frontend/src/lib/components/setup/ai-setup-steps.svelte create mode 100644 frontend/src/lib/components/setup/copyable-code-block.svelte create mode 100644 frontend/src/lib/components/setup/copyable-inline.svelte create mode 100644 frontend/src/lib/components/setup/otel-exporter-config.svelte create mode 100644 frontend/src/lib/components/setup/otel-setup-steps.svelte create mode 100644 frontend/src/lib/components/setup/setup-mode-tabs.svelte create mode 100644 frontend/src/lib/components/setup/source-map-upload-card.svelte create mode 100644 frontend/src/lib/utils/ai-setup.ts create mode 100644 frontend/src/lib/utils/otel-sdks.ts create mode 100644 frontend/src/lib/utils/otel-setup.ts create mode 100644 frontend/src/lib/utils/setup-storage.ts rename skills/{traceway-setup-project => traceway-setup}/SKILL.md (86%) rename skills/{traceway-setup-project => traceway-setup}/hono.md (100%) rename skills/{traceway-setup-project => traceway-setup}/nestjs.md (100%) rename skills/{traceway-setup-project => traceway-setup}/nextjs.md (100%) diff --git a/examples/devtesting-embedded/go.mod b/examples/devtesting-embedded/go.mod index 1f17c539..c1f68995 100644 --- a/examples/devtesting-embedded/go.mod +++ b/examples/devtesting-embedded/go.mod @@ -48,6 +48,7 @@ require ( github.com/cloudwego/base64x v0.1.6 // indirect github.com/coreos/go-systemd/v22 v22.6.0 // indirect github.com/dop251/goja v0.0.0-20260607120635-348e6bea910d // indirect + github.com/ebitengine/purego v0.10.1 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gin-contrib/sse v1.1.1 // indirect github.com/go-chi/chi/v5 v5.2.2 // indirect @@ -55,6 +56,7 @@ require ( github.com/go-faster/errors v0.7.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.2 // indirect @@ -74,6 +76,7 @@ require ( github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lib/pq v1.10.9 // indirect + github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect github.com/markbates/goth v1.82.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -81,20 +84,27 @@ require ( github.com/paulmach/orb v0.12.0 // indirect github.com/pelletier/go-toml/v2 v2.3.0 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect github.com/segmentio/asm v1.2.1 // indirect + github.com/shirou/gopsutil/v4 v4.26.4 // indirect github.com/shopspring/decimal v1.4.0 // indirect + github.com/tklauser/go-sysconf v0.4.0 // indirect + github.com/tklauser/numcpus v0.12.0 // indirect github.com/tracewayapp/lit/v2 v2.0.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/host v0.69.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect - go.tracewayapp.com v1.0.3 // indirect + go.tracewayapp.com v1.0.4 // indirect go.tracewayapp.com/tracewaygin v1.0.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.25.0 // indirect diff --git a/examples/devtesting-embedded/go.sum b/examples/devtesting-embedded/go.sum index b1ba1322..af1d5491 100644 --- a/examples/devtesting-embedded/go.sum +++ b/examples/devtesting-embedded/go.sum @@ -91,6 +91,8 @@ github.com/dop251/goja v0.0.0-20260607120635-348e6bea910d/go.mod h1:Sc+QOu1Wruva github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY= +github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= @@ -110,6 +112,9 @@ 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/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -176,6 +181,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/markbates/goth v1.82.0 h1:8j/c34AjBSTNzO7zTsOyP5IYCQCMBTRBHAbBt/PI0bQ= github.com/markbates/goth v1.82.0/go.mod h1:/DRlcq0pyqkKToyZjsL2KgiA1zbF1HIjE7u2uC79rUk= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= @@ -213,6 +220,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= @@ -223,6 +232,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/shirou/gopsutil/v4 v4.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbprAOWY= +github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -238,6 +249,10 @@ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= +github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI= +github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4= +github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= github.com/tracewayapp/lit/v2 v2.0.2 h1:ZE8UlRWUOX3OZvY2UfbYSDcDUpBoTNLXl70Qujs0mqs= github.com/tracewayapp/lit/v2 v2.0.2/go.mod h1:i3zPBL5NtFgZxpxT9PvC2w4gm3S9IzDv4K5JSALtlfs= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= @@ -252,6 +267,8 @@ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3i github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= @@ -259,6 +276,8 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.68.0 h1:5FXSL2s6afUC1bzNzl1iedZZ8yqR7GOhbCoEXtyeK6Q= go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.68.0/go.mod h1:MdHW7tLtkeGJnR4TyOrnd5D0zUGZQB1l84uHCe8hRpE= +go.opentelemetry.io/contrib/instrumentation/host v0.69.0 h1:N/wKAdIOKd9U4bwiwY3j95e2ZR48onTMR/NI/SZQ/a4= +go.opentelemetry.io/contrib/instrumentation/host v0.69.0/go.mod h1:rE1Bxmu3mwcmyTJV4EQdJ/C6zCUnuT9jCiinhRXLAXc= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/contrib/propagators/b3 v1.43.0 h1:CETqV3QLLPTy5yNrqyMr41VnAOOD4lsRved7n4QG00A= @@ -277,6 +296,8 @@ go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIE go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA= +go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk= go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= @@ -324,13 +345,16 @@ golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/frontend/src/lib/components/setup/ai-setup-steps.svelte b/frontend/src/lib/components/setup/ai-setup-steps.svelte new file mode 100644 index 00000000..9cbd8250 --- /dev/null +++ b/frontend/src/lib/components/setup/ai-setup-steps.svelte @@ -0,0 +1,49 @@ + + +
+
+
+
+ 1 +
+

Install the Traceway Skill

+
+

+ Add the Traceway setup skill to your coding agent. Works with Claude Code, Cursor, and any + agent that supports agent skills. +

+
+
+ +
+
+ +
+
+
+
+ 2 +
+

Run the Setup Prompt

+
+

+ Paste this prompt into your agent. Your instance URL and project token are already filled in. +

+
+
+ +
+
diff --git a/frontend/src/lib/components/setup/copyable-code-block.svelte b/frontend/src/lib/components/setup/copyable-code-block.svelte new file mode 100644 index 00000000..4cb8d83b --- /dev/null +++ b/frontend/src/lib/components/setup/copyable-code-block.svelte @@ -0,0 +1,108 @@ + + +
+
+ +
+
+ +
+
+ + diff --git a/frontend/src/lib/components/setup/copyable-inline.svelte b/frontend/src/lib/components/setup/copyable-inline.svelte new file mode 100644 index 00000000..1ce79592 --- /dev/null +++ b/frontend/src/lib/components/setup/copyable-inline.svelte @@ -0,0 +1,25 @@ + + +
+ {value} + +
diff --git a/frontend/src/lib/components/setup/otel-exporter-config.svelte b/frontend/src/lib/components/setup/otel-exporter-config.svelte new file mode 100644 index 00000000..1c1d8906 --- /dev/null +++ b/frontend/src/lib/components/setup/otel-exporter-config.svelte @@ -0,0 +1,33 @@ + + +
+
+

OTLP Endpoint

+

+ Your SDK or Collector will append /v1/traces + and + /v1/metrics automatically. +

+ +
+
+

Authorization Header

+ +
+
+

Example: OTel Collector (optional)

+ +
+
diff --git a/frontend/src/lib/components/setup/otel-setup-steps.svelte b/frontend/src/lib/components/setup/otel-setup-steps.svelte new file mode 100644 index 00000000..b6cab16b --- /dev/null +++ b/frontend/src/lib/components/setup/otel-setup-steps.svelte @@ -0,0 +1,126 @@ + + +
+

Language

+ + + {#each OTEL_TARGETS as t (t.id)} + {t.label} + {/each} + + + {#if targetDef.frameworks.length > 1} +

Framework

+ + + {#each targetDef.frameworks as f (f.id)} + {f.label} + {/each} + + + {/if} +
+ +{#each steps as step, i (targetDef.id + activeFramework + step.title)} +
+
+
+
+ {i + 1} +
+

{step.title}

+
+ {#if step.description} +

{step.description}

+ {/if} +
+ {#if step.code} +
+ + {#if step.link} +

+ {step.link.label} +

+ {/if} +
+ {/if} +
+{/each} + +{#if target === 'nodejs'} + +{/if} diff --git a/frontend/src/lib/components/setup/setup-mode-tabs.svelte b/frontend/src/lib/components/setup/setup-mode-tabs.svelte new file mode 100644 index 00000000..363919c2 --- /dev/null +++ b/frontend/src/lib/components/setup/setup-mode-tabs.svelte @@ -0,0 +1,24 @@ + + + + + AI + Manual + + diff --git a/frontend/src/lib/components/setup/source-map-upload-card.svelte b/frontend/src/lib/components/setup/source-map-upload-card.svelte new file mode 100644 index 00000000..f0d7cbfd --- /dev/null +++ b/frontend/src/lib/components/setup/source-map-upload-card.svelte @@ -0,0 +1,83 @@ + + +{#if projectWithToken && !isReadonly} + + + + + Source Map Upload + + + Upload source maps to see original file names and line numbers in stack traces from + minified code. + + + + {#if sourceMapToken} +
+
+

Upload Token

+ +
+
+

Usage

+ +
+
+ {:else} +

+ Generate an upload token to start uploading source maps as part of your build process. +

+ + {/if} +
+
+{/if} diff --git a/frontend/src/lib/utils/ai-setup.ts b/frontend/src/lib/utils/ai-setup.ts new file mode 100644 index 00000000..f34e6d68 --- /dev/null +++ b/frontend/src/lib/utils/ai-setup.ts @@ -0,0 +1,5 @@ +export const SKILL_INSTALL_COMMAND = 'npx skills add tracewayapp/traceway'; + +export function getSetupPrompt(backendUrl: string, token: string): string { + return `/traceway-setup with token ${token} and url ${backendUrl}`; +} diff --git a/frontend/src/lib/utils/otel-sdks.ts b/frontend/src/lib/utils/otel-sdks.ts new file mode 100644 index 00000000..827fb3db --- /dev/null +++ b/frontend/src/lib/utils/otel-sdks.ts @@ -0,0 +1,42 @@ +export type OtelSdkId = 'nodejs' | 'go' | 'python' | 'java' | 'dotnet' | 'php'; + +export type OtelSdk = { + id: OtelSdkId; + label: string; + installCommand: string; +}; + +export const OTEL_SDKS: OtelSdk[] = [ + { + id: 'nodejs', + label: 'Node.js', + installCommand: + 'npm install @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-http @opentelemetry/exporter-metrics-otlp-http' + }, + { + id: 'go', + label: 'Go', + installCommand: 'go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp' + }, + { + id: 'python', + label: 'Python', + installCommand: 'pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http' + }, + { + id: 'java', + label: 'Java', + installCommand: "implementation 'io.opentelemetry:opentelemetry-exporter-otlp'" + }, + { + id: 'dotnet', + label: '.NET', + installCommand: 'dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol' + }, + { + id: 'php', + label: 'PHP', + installCommand: + 'composer require open-telemetry/sdk open-telemetry/exporter-otlp php-http/guzzle7-adapter' + } +]; diff --git a/frontend/src/lib/utils/otel-setup.ts b/frontend/src/lib/utils/otel-setup.ts new file mode 100644 index 00000000..9180102b --- /dev/null +++ b/frontend/src/lib/utils/otel-setup.ts @@ -0,0 +1,566 @@ +export type OtelTargetId = + | 'collector' + | 'nodejs' + | 'go' + | 'python' + | 'java' + | 'dotnet' + | 'php' + | 'ruby' + | 'other'; + +export type OtelFramework = { + id: string; + label: string; +}; + +export type OtelTarget = { + id: OtelTargetId; + label: string; + frameworks: OtelFramework[]; +}; + +export type OtelStepLanguage = + | 'bash' + | 'go' + | 'javascript' + | 'typescript' + | 'python' + | 'gradle' + | 'csharp' + | 'ruby' + | 'yaml'; + +export type OtelStep = { + title: string; + description?: string; + code?: string; + codeLanguage?: OtelStepLanguage; + link?: { label: string; href: string }; +}; + +export const OTEL_TARGETS: OtelTarget[] = [ + { id: 'collector', label: 'Collector', frameworks: [] }, + { + id: 'nodejs', + label: 'Node.js', + frameworks: [ + { id: 'express', label: 'Express' }, + { id: 'nestjs', label: 'NestJS' }, + { id: 'fastify', label: 'Fastify' }, + { id: 'nextjs', label: 'Next.js' }, + { id: 'koa', label: 'Koa' }, + { id: 'other', label: 'Other' } + ] + }, + { + id: 'go', + label: 'Go', + frameworks: [ + { id: 'gin', label: 'Gin' }, + { id: 'echo', label: 'Echo' }, + { id: 'chi', label: 'Chi' }, + { id: 'fiber', label: 'Fiber' }, + { id: 'mux', label: 'gorilla/mux' }, + { id: 'nethttp', label: 'net/http' } + ] + }, + { + id: 'python', + label: 'Python', + frameworks: [ + { id: 'django', label: 'Django' }, + { id: 'flask', label: 'Flask' }, + { id: 'fastapi', label: 'FastAPI' }, + { id: 'other', label: 'Other' } + ] + }, + { + id: 'java', + label: 'Java', + frameworks: [ + { id: 'agent', label: 'Any framework' }, + { id: 'spring', label: 'Spring Boot' } + ] + }, + { id: 'dotnet', label: '.NET', frameworks: [] }, + { + id: 'php', + label: 'PHP', + frameworks: [ + { id: 'symfony', label: 'Symfony' }, + { id: 'laravel', label: 'Laravel' }, + { id: 'slim', label: 'Slim' }, + { id: 'other', label: 'Other' } + ] + }, + { + id: 'ruby', + label: 'Ruby', + frameworks: [ + { id: 'rails', label: 'Rails' }, + { id: 'other', label: 'Other' } + ] + }, + { id: 'other', label: 'Other', frameworks: [] } +]; + +function envBlock(backendUrl: string, token: string, extra: string[] = []): string { + return [ + 'OTEL_SERVICE_NAME=my-service', + `OTEL_EXPORTER_OTLP_ENDPOINT=${backendUrl}/api/otel`, + `OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${token}`, + ...extra + ].join('\n'); +} + +function envStep(backendUrl: string, token: string, extra: string[] = [], note = ''): OtelStep { + return { + title: 'Configure the Exporter', + description: `Set these environment variables in your shell, .env file, or deployment config. The SDK appends /v1/traces and /v1/metrics to the endpoint automatically.${note ? ' ' + note : ''}`, + code: envBlock(backendUrl, token, extra), + codeLanguage: 'bash' + }; +} + +function collectorConfig(backendUrl: string, token: string): string { + return `exporters: + otlphttp: + endpoint: "${backendUrl}/api/otel" + headers: + Authorization: "Bearer ${token}" + +service: + pipelines: + traces: + exporters: [otlphttp] + metrics: + exporters: [otlphttp]`; +} + +const GO_BOOTSTRAP = `import ( + "context" + "log" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/propagation" + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +func initTracer(ctx context.Context) *sdktrace.TracerProvider { + exp, err := otlptracehttp.New(ctx) + if err != nil { + log.Fatal(err) + } + tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exp)) + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.TraceContext{}) + return tp +}`; + +const GO_FRAMEWORKS: Record = { + gin: { + lib: 'go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin', + snippet: `import "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" + +r := gin.Default() +r.Use(otelgin.Middleware("my-service"))` + }, + echo: { + lib: 'go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho', + snippet: `import "go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho" + +e := echo.New() +e.Use(otelecho.Middleware("my-service"))` + }, + chi: { + lib: 'github.com/riandyrn/otelchi', + snippet: `import "github.com/riandyrn/otelchi" + +r := chi.NewRouter() +r.Use(otelchi.Middleware("my-service", otelchi.WithChiRoutes(r)))`, + note: 'WithChiRoutes lets the middleware resolve the route pattern so endpoints group correctly.' + }, + fiber: { + lib: 'github.com/gofiber/contrib/v3/otel', + snippet: `import fiberotel "github.com/gofiber/contrib/v3/otel" + +app := fiber.New() +app.Use(fiberotel.Middleware())`, + note: 'For Fiber v2 use github.com/gofiber/contrib/otelfiber/v2 and otelfiber.Middleware() instead.' + }, + mux: { + lib: 'go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux', + snippet: `import "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux" + +r := mux.NewRouter() +r.Use(otelmux.Middleware("my-service"))` + }, + nethttp: { + lib: 'go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp', + snippet: `import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + +mux := http.NewServeMux() +mux.Handle("GET /users/{id}", otelhttp.NewHandler(http.HandlerFunc(getUser), "GET /users/{id}")) +http.ListenAndServe(":8080", mux)`, + note: 'Wrap each route individually with Go 1.22+ method patterns so the route is set on spans and endpoints group by pattern instead of raw URL.' + } +}; + +const NODE_ZERO_CODE_INSTALL = 'npm install @opentelemetry/api @opentelemetry/auto-instrumentations-node'; + +function nodeZeroCodeSteps( + backendUrl: string, + token: string, + entrypoint: string, + installNote: string, + runNote: string +): OtelStep[] { + return [ + { + title: 'Install the SDK', + description: installNote, + code: NODE_ZERO_CODE_INSTALL, + codeLanguage: 'bash' + }, + envStep(backendUrl, token), + { + title: 'Run with Instrumentation', + description: runNote, + code: `node --require @opentelemetry/auto-instrumentations-node/register ${entrypoint}`, + codeLanguage: 'bash' + } + ]; +} + +export function getOtelSteps( + target: OtelTargetId, + framework: string, + backendUrl: string, + token: string +): OtelStep[] { + switch (target) { + case 'collector': + return [ + { + title: 'Add the Traceway Exporter', + description: + 'Merge this into your OpenTelemetry Collector configuration. Any pipeline that lists the otlphttp exporter will be forwarded to Traceway.', + code: collectorConfig(backendUrl, token), + codeLanguage: 'yaml' + }, + { + title: 'Restart the Collector', + description: + 'Restart the Collector to apply the configuration. Traces and metrics flowing through its pipelines will appear in Traceway.' + } + ]; + + case 'nodejs': { + if (framework === 'fastify') { + return [ + { + title: 'Install the SDK', + description: + 'Fastify is instrumented by the @fastify/otel package maintained by the Fastify team.', + code: 'npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @fastify/otel', + codeLanguage: 'bash' + }, + { + title: 'Create instrumentation.js', + description: 'Add this file at the project root.', + code: `const { NodeSDK } = require('@opentelemetry/sdk-node'); +const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); +const { FastifyOtelInstrumentation } = require('@fastify/otel'); + +new NodeSDK({ + instrumentations: [ + getNodeAutoInstrumentations(), + new FastifyOtelInstrumentation({ registerOnInitialization: true }), + ], +}).start();`, + codeLanguage: 'javascript' + }, + envStep(backendUrl, token), + { + title: 'Run with Instrumentation', + code: 'node --require ./instrumentation.js app.js', + codeLanguage: 'bash' + } + ]; + } + if (framework === 'nextjs') { + return [ + { + title: 'Install the SDK', + code: 'npm install @vercel/otel', + codeLanguage: 'bash' + }, + { + title: 'Create instrumentation.ts', + description: + 'Add this file at the project root (next to package.json). Next.js calls register() automatically on startup.', + code: `import { registerOTel } from '@vercel/otel' + +export function register() { + registerOTel({ serviceName: 'my-service' }) +}`, + codeLanguage: 'typescript' + }, + envStep( + backendUrl, + token, + [], + 'Start your app normally with next start; no extra flags are needed.' + ) + ]; + } + if (framework === 'nestjs') { + return nodeZeroCodeSteps( + backendUrl, + token, + 'dist/main.js', + 'Auto-instrumentation captures NestJS routes, status codes, and errors through the default Express adapter with no code changes. If you use the Fastify adapter, follow the Fastify setup instead.', + 'Routes group by pattern automatically.' + ); + } + if (framework === 'koa') { + return nodeZeroCodeSteps( + backendUrl, + token, + 'app.js', + 'Auto-instrumentation captures Koa requests, status codes, and errors with no code changes.', + 'Route patterns are captured when routing with @koa/router.' + ); + } + return nodeZeroCodeSteps( + backendUrl, + token, + 'app.js', + 'Auto-instrumentation captures routes, status codes, and errors with no code changes.', + 'For ESM apps, add --experimental-loader=@opentelemetry/instrumentation/hook.mjs and use --import instead of --require.' + ); + } + + case 'go': { + const fw = GO_FRAMEWORKS[framework] ?? GO_FRAMEWORKS.gin; + return [ + { + title: 'Install the SDK', + code: `go get go.opentelemetry.io/otel go.opentelemetry.io/otel/sdk go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp ${fw.lib}`, + codeLanguage: 'bash' + }, + { + title: 'Initialize the SDK', + description: + 'Call initTracer at startup and defer tp.Shutdown(ctx) before exit. The exporter reads the environment variables from the next step.', + code: GO_BOOTSTRAP, + codeLanguage: 'go' + }, + { + title: 'Add the Middleware', + description: fw.note, + code: fw.snippet, + codeLanguage: 'go' + }, + envStep(backendUrl, token) + ]; + } + + case 'python': { + const runByFramework: Record = { + django: { + cmd: 'opentelemetry-instrument python manage.py runserver --noreload', + note: 'The --noreload flag is required with runserver; the autoreloader breaks instrumentation. It is not needed under gunicorn or other production servers.' + }, + flask: { cmd: 'opentelemetry-instrument flask run' }, + fastapi: { + cmd: 'opentelemetry-instrument uvicorn main:app', + note: 'Avoid --reload and --workers with zero-code instrumentation; for multi-worker production use gunicorn with uvicorn workers.' + }, + other: { cmd: 'opentelemetry-instrument python app.py' } + }; + const run = runByFramework[framework] ?? runByFramework.other; + return [ + { + title: 'Install the SDK', + description: + 'opentelemetry-bootstrap detects your installed packages and adds the matching instrumentation.', + code: 'pip install opentelemetry-distro opentelemetry-exporter-otlp-proto-http\nopentelemetry-bootstrap -a install', + codeLanguage: 'bash' + }, + envStep( + backendUrl, + token, + ['OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf'], + 'The protocol variable is required; the Python SDK defaults to gRPC.' + ), + { + title: 'Run with Instrumentation', + description: run.note, + code: run.cmd, + codeLanguage: 'bash' + } + ]; + } + + case 'java': { + if (framework === 'spring') { + return [ + { + title: 'Add the Starter', + description: 'Add the OpenTelemetry Spring Boot starter to your Gradle build (a Maven dependency works the same way).', + code: `implementation(platform("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:2.28.1")) +implementation("io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter")`, + codeLanguage: 'gradle' + }, + envStep( + backendUrl, + token, + [], + 'Start your app normally; the starter reads these variables and reports routes, status codes, and exceptions.' + ) + ]; + } + return [ + { + title: 'Download the Java Agent', + description: + 'The agent instruments Spring, JAX-RS, and most Java frameworks with zero code changes.', + code: 'curl -L -O https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar', + codeLanguage: 'bash' + }, + envStep(backendUrl, token), + { + title: 'Run with the Agent', + code: 'java -javaagent:./opentelemetry-javaagent.jar -jar myapp.jar', + codeLanguage: 'bash' + } + ]; + } + + case 'dotnet': + return [ + { + title: 'Install the Packages', + code: `dotnet add package OpenTelemetry.Extensions.Hosting +dotnet add package OpenTelemetry.Instrumentation.AspNetCore +dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol`, + codeLanguage: 'bash' + }, + { + title: 'Add to Program.cs', + description: + 'Keep AddOtlpExporter() empty so the exporter is driven entirely by the environment variables in the next step.', + code: `builder.Services.AddOpenTelemetry() + .WithTracing(t => t + .AddAspNetCoreInstrumentation() + .AddOtlpExporter());`, + codeLanguage: 'csharp' + }, + envStep( + backendUrl, + token, + ['OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf'], + 'The protocol variable is required; the .NET exporter defaults to gRPC.' + ) + ]; + + case 'php': { + const autoPackages: Record = { + symfony: ' open-telemetry/opentelemetry-auto-symfony', + laravel: ' open-telemetry/opentelemetry-auto-laravel', + slim: ' open-telemetry/opentelemetry-auto-slim', + other: '' + }; + const autoPackage = autoPackages[framework] ?? ''; + return [ + { + title: 'Install the SDK', + description: + 'Auto-instrumentation needs the opentelemetry PECL extension; enable it with extension=opentelemetry in php.ini.' + + (framework === 'other' + ? ' Find auto-instrumentation packages for your framework in the OpenTelemetry registry.' + : ''), + code: `pecl install opentelemetry +composer require open-telemetry/sdk open-telemetry/exporter-otlp php-http/guzzle7-adapter${autoPackage}`, + codeLanguage: 'bash', + link: + framework === 'other' + ? { + label: 'Browse PHP instrumentation packages', + href: 'https://opentelemetry.io/ecosystem/registry/?language=php&component=instrumentation' + } + : undefined + }, + envStep( + backendUrl, + token, + ['OTEL_PHP_AUTOLOAD_ENABLED=true'], + 'These must be real process environment variables; the extension does not read framework .env files. Use env[...] in php-fpm pool config or SetEnv in Apache.' + ) + ]; + } + + case 'ruby': { + if (framework === 'rails') { + return [ + { + title: 'Install the Gems', + code: 'bundle add opentelemetry-sdk opentelemetry-exporter-otlp opentelemetry-instrumentation-rails', + codeLanguage: 'bash' + }, + { + title: 'Create the Initializer', + description: 'Add config/initializers/opentelemetry.rb.', + code: `require 'opentelemetry/sdk' +require 'opentelemetry/exporter/otlp' +require 'opentelemetry/instrumentation/rails' + +OpenTelemetry::SDK.configure do |c| + c.use 'OpenTelemetry::Instrumentation::Rails' +end`, + codeLanguage: 'ruby' + }, + envStep(backendUrl, token) + ]; + } + return [ + { + title: 'Install the Gems', + code: 'bundle add opentelemetry-sdk opentelemetry-exporter-otlp opentelemetry-instrumentation-all', + codeLanguage: 'bash' + }, + { + title: 'Configure the SDK', + description: 'Run this once at startup, before your app starts handling requests.', + code: `require 'opentelemetry/sdk' +require 'opentelemetry/exporter/otlp' +require 'opentelemetry/instrumentation/all' + +OpenTelemetry::SDK.configure do |c| + c.use_all +end`, + codeLanguage: 'ruby' + }, + envStep(backendUrl, token) + ]; + } + + default: + return [ + { + title: 'Configure any OpenTelemetry SDK', + description: + 'Any language with an OTLP/HTTP exporter works. Set these environment variables; the protocol variable matters for SDKs that default to gRPC. Make sure http.route is set on root server spans so endpoints group by route pattern, and use SpanKind CONSUMER for background jobs.', + code: envBlock(backendUrl, token, ['OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf']), + codeLanguage: 'bash', + link: { + label: 'View all supported languages', + href: 'https://opentelemetry.io/docs/languages/' + } + } + ]; + } +} diff --git a/frontend/src/lib/utils/setup-storage.ts b/frontend/src/lib/utils/setup-storage.ts new file mode 100644 index 00000000..de8d5ab4 --- /dev/null +++ b/frontend/src/lib/utils/setup-storage.ts @@ -0,0 +1,68 @@ +import { browser } from '$app/environment'; +import { OTEL_TARGETS, type OtelTargetId } from './otel-setup'; + +export type SetupMode = 'ai' | 'manual'; + +const MODE_KEY = 'traceway_setup_mode'; +const OTEL_TARGET_KEY = 'traceway_otel_language'; +const OTEL_FRAMEWORK_KEY = 'traceway_otel_framework'; + +export function getSetupMode(): SetupMode { + if (!browser) return 'ai'; + + try { + const stored = localStorage.getItem(MODE_KEY); + if (stored === 'ai' || stored === 'manual') { + return stored; + } + } catch {} + + return 'ai'; +} + +export function setSetupMode(mode: SetupMode): void { + if (!browser) return; + + try { + localStorage.setItem(MODE_KEY, mode); + } catch {} +} + +export function getOtelTarget(): OtelTargetId { + if (!browser) return OTEL_TARGETS[0].id; + + try { + const stored = localStorage.getItem(OTEL_TARGET_KEY); + if (stored && OTEL_TARGETS.some((t) => t.id === stored)) { + return stored as OtelTargetId; + } + } catch {} + + return OTEL_TARGETS[0].id; +} + +export function setOtelTarget(id: OtelTargetId): void { + if (!browser) return; + + try { + localStorage.setItem(OTEL_TARGET_KEY, id); + } catch {} +} + +export function getOtelFramework(): string | null { + if (!browser) return null; + + try { + return localStorage.getItem(OTEL_FRAMEWORK_KEY); + } catch {} + + return null; +} + +export function setOtelFramework(id: string): void { + if (!browser) return; + + try { + localStorage.setItem(OTEL_FRAMEWORK_KEY, id); + } catch {} +} diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 39047a0d..c995b5d4 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -42,7 +42,6 @@ import php from 'svelte-highlight/languages/php'; import python from 'svelte-highlight/languages/python'; import { themeState } from '$lib/state/theme.svelte'; - import yaml from 'svelte-highlight/languages/yaml'; import 'svelte-highlight/styles/github-dark.css'; import { getFrameworkCode, @@ -54,6 +53,12 @@ } from '$lib/utils/framework-code'; import { toast } from 'svelte-sonner'; import { goto } from '$app/navigation'; + import { OTEL_SDKS } from '$lib/utils/otel-sdks'; + import { getSetupMode } from '$lib/utils/setup-storage'; + import SetupModeTabs from '$lib/components/setup/setup-mode-tabs.svelte'; + import AiSetupSteps from '$lib/components/setup/ai-setup-steps.svelte'; + import OtelSetupSteps from '$lib/components/setup/otel-setup-steps.svelte'; + import OtelExporterConfig from '$lib/components/setup/otel-exporter-config.svelte'; const timezone = $derived(getTimezone()); @@ -115,6 +120,7 @@ const impactfulEndpoints = $derived(data?.worstEndpoints?.filter((e) => e.impact >= 0.25) ?? []); let projectWithToken = $derived(projectsState.currentProject); + let setupMode = $state(getSetupMode()); let copiedInstall = $state(false); let copiedCode = $state(false); let copiedTesting = $state(false); @@ -176,6 +182,7 @@ let copiedCfDeploy = $state(false); const isOtel = $derived(projectWithToken ? isOtelFramework(projectWithToken.framework) : false); + const isOtelGeneric = $derived(projectWithToken?.framework === 'opentelemetry'); const otelBaseEndpoint = $derived( projectWithToken ? `${projectWithToken.backendUrl}/api/otel` : '' ); @@ -197,21 +204,7 @@ service: : '' ); - const otelSdks = [ - { - lang: 'Node.js', - cmd: 'npm install @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-http @opentelemetry/exporter-metrics-otlp-http' - }, - { lang: 'Python', cmd: 'pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http' }, - { lang: 'Go', cmd: 'go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp' }, - { lang: 'Java', cmd: "implementation 'io.opentelemetry:opentelemetry-exporter-otlp'" }, - { lang: '.NET', cmd: 'dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol' } - ]; - let copiedSdkLang = $state(null); - let copiedOtelEndpoint = $state(false); - let copiedOtelAuth = $state(false); - let copiedOtelCollector = $state(false); async function copyCfEndpoint() { await navigator.clipboard.writeText(cfOtelEndpoint); @@ -243,24 +236,6 @@ service: setTimeout(() => (copiedSdkLang = null), 2000); } - async function copyOtelEndpoint() { - await navigator.clipboard.writeText(otelBaseEndpoint); - copiedOtelEndpoint = true; - setTimeout(() => (copiedOtelEndpoint = false), 2000); - } - - async function copyOtelAuth() { - await navigator.clipboard.writeText(otelAuthHeader); - copiedOtelAuth = true; - setTimeout(() => (copiedOtelAuth = false), 2000); - } - - async function copyOtelCollector() { - await navigator.clipboard.writeText(otelCollectorConfig); - copiedOtelCollector = true; - setTimeout(() => (copiedOtelCollector = false), 2000); - } - async function copyInstall() { await navigator.clipboard.writeText(installCommand); copiedInstall = true; @@ -395,7 +370,10 @@ service: {#if projectWithToken} - {#if isCloudflare} + (setupMode = m)} /> + {#if setupMode === 'ai'} + + {:else if isCloudflare}
@@ -515,6 +493,11 @@ service:
+ {:else if isOtelGeneric} + {:else if isOtel}
@@ -533,18 +516,18 @@ service:

- {#each otelSdks as sdk} + {#each OTEL_SDKS as sdk (sdk.id)}
- {sdk.lang} + {sdk.label} {sdk.cmd}{sdk.installCommand}
-
-
-

OTLP Endpoint

-

- Your SDK or Collector will append /v1/traces - and - /v1/metrics automatically. -

-
- {otelBaseEndpoint} - -
-
-
-

Authorization Header

-
- {otelAuthHeader} - -
-
-
-

Example: OTel Collector (optional)

-
-
- -
-
- -
-
-
+
+
@@ -812,7 +740,7 @@ service:

- {#if isOtel || isCloudflare} + {#if setupMode === 'ai' || isOtel || isCloudflare} Once you've completed the steps above and sent some traffic through your application, click below to verify. {:else} diff --git a/frontend/src/routes/connection/+page.svelte b/frontend/src/routes/connection/+page.svelte index 79eefe6b..8b867fc9 100644 --- a/frontend/src/routes/connection/+page.svelte +++ b/frontend/src/routes/connection/+page.svelte @@ -7,7 +7,7 @@ CardTitle } from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; - import { Copy, Check, KeyRound } from 'lucide-svelte'; + import { Copy, Check } from 'lucide-svelte'; import { projectsState, type ProjectWithToken, @@ -15,8 +15,6 @@ isOtelFramework, isCloudflareFramework } from '$lib/state/projects.svelte'; - import { authState } from '$lib/state/auth.svelte'; - import { LoadingCircle } from '$lib/components/ui/loading-circle'; import FrameworkIcon from '$lib/components/framework-icon.svelte'; import Highlight from 'svelte-highlight'; import go from 'svelte-highlight/languages/go'; @@ -24,7 +22,6 @@ import bash from 'svelte-highlight/languages/bash'; import php from 'svelte-highlight/languages/php'; import python from 'svelte-highlight/languages/python'; - import yaml from 'svelte-highlight/languages/yaml'; import { themeState } from '$lib/state/theme.svelte'; import 'svelte-highlight/styles/github-dark.css'; import { @@ -32,22 +29,24 @@ getInstallCommand, getFrameworkLabel } from '$lib/utils/framework-code'; + import { getSetupMode } from '$lib/utils/setup-storage'; + import SetupModeTabs from '$lib/components/setup/setup-mode-tabs.svelte'; + import AiSetupSteps from '$lib/components/setup/ai-setup-steps.svelte'; + import OtelSetupSteps from '$lib/components/setup/otel-setup-steps.svelte'; + import OtelExporterConfig from '$lib/components/setup/otel-exporter-config.svelte'; + import SourceMapUploadCard from '$lib/components/setup/source-map-upload-card.svelte'; let projectWithToken = $derived(projectsState.currentProject); + let setupMode = $state(getSetupMode()); let copiedCode = $state(false); let copiedInstall = $state(false); - let copiedToken = $state(false); - let copiedCommand = $state(false); - let generatingToken = $state(false); - let copiedOtelEndpoint = $state(false); - let copiedOtelAuth = $state(false); - let copiedOtelCollector = $state(false); let copiedCfEndpoint = $state(false); let copiedCfAuth = $state(false); let copiedCfWrangler = $state(false); let copiedCfDeploy = $state(false); const isOtel = $derived(projectWithToken ? isOtelFramework(projectWithToken.framework) : false); + const isOtelGeneric = $derived(projectWithToken?.framework === 'opentelemetry'); const isCloudflare = $derived( projectWithToken ? isCloudflareFramework(projectWithToken.framework) : false ); @@ -116,17 +115,6 @@ service: }); const isJs = $derived(projectWithToken ? isJsFramework(projectWithToken.framework) : false); - const isReadonly = $derived( - authState.getRoleForOrganization(projectsState.currentProject?.organizationId ?? 0) === - 'readonly' - ); - const sourceMapToken = $derived(projectWithToken?.sourceMapToken ?? null); - - const uploadCommand = $derived( - projectWithToken && sourceMapToken - ? `npx @tracewayapp/sourcemap-upload --url ${projectWithToken.backendUrl} --token ${sourceMapToken} --version YOUR_VERSION --directory dist/assets` - : '' - ); async function copyCode() { await navigator.clipboard.writeText(sdkCode); @@ -140,28 +128,6 @@ service: setTimeout(() => (copiedInstall = false), 2000); } - async function generateToken() { - generatingToken = true; - try { - await projectsState.generateSourceMapToken(); - } finally { - generatingToken = false; - } - } - - async function copyToken() { - if (!sourceMapToken) return; - await navigator.clipboard.writeText(sourceMapToken); - copiedToken = true; - setTimeout(() => (copiedToken = false), 2000); - } - - async function copyUploadCommand() { - await navigator.clipboard.writeText(uploadCommand); - copiedCommand = true; - setTimeout(() => (copiedCommand = false), 2000); - } - async function copyCfEndpoint() { await navigator.clipboard.writeText(cloudflareOtelEndpoint); copiedCfEndpoint = true; @@ -185,24 +151,6 @@ service: copiedCfDeploy = true; setTimeout(() => (copiedCfDeploy = false), 2000); } - - async function copyOtelEndpoint() { - await navigator.clipboard.writeText(otelEndpoint); - copiedOtelEndpoint = true; - setTimeout(() => (copiedOtelEndpoint = false), 2000); - } - - async function copyOtelAuth() { - await navigator.clipboard.writeText(otelAuthHeader); - copiedOtelAuth = true; - setTimeout(() => (copiedOtelAuth = false), 2000); - } - - async function copyOtelCollector() { - await navigator.clipboard.writeText(otelCollectorConfig); - copiedOtelCollector = true; - setTimeout(() => (copiedOtelCollector = false), 2000); - }

@@ -212,7 +160,12 @@ service:
{#if projectWithToken} - {#if isOtel} + (setupMode = m)} /> + {#if setupMode === 'ai'} + + {:else if isOtelGeneric} + + {:else if isOtel} @@ -224,65 +177,11 @@ service: -
-
-

OTLP Endpoint

-

- Your SDK or Collector will append /v1/traces and - /v1/metrics automatically. -

-
- {otelEndpoint} - -
-
-
-

Authorization Header

-
- {otelAuthHeader} - -
-
-
-

Example: OTel Collector (optional)

-
-
- -
-
- -
-
-
-
+
{:else if isCloudflare} @@ -443,77 +342,8 @@ service: - {#if isJs && !isReadonly} - - - - - Source Map Upload - - - Upload source maps to see original file names and line numbers in stack traces from - minified code. - - - - {#if sourceMapToken} -
-
-

Upload Token

-
- {sourceMapToken} - -
-
-
-

Usage

-
-
- -
-
- -
-
-
-
- {:else} -

- Generate an upload token to start uploading source maps as part of your build - process. -

- - {/if} -
-
+ {#if isJs} + {/if} {/if} {:else} diff --git a/skills/traceway-debug/SKILL.md b/skills/traceway-debug/SKILL.md index 4a38b81e..b0a11709 100644 --- a/skills/traceway-debug/SKILL.md +++ b/skills/traceway-debug/SKILL.md @@ -122,4 +122,4 @@ traceway exceptions archive --yes ## If There Is No Telemetry -Empty results usually mean the wrong project, wrong time window, or the app is not instrumented. Check `traceway projects list`, widen `--since`, and if the app was never connected to Traceway, set it up first (see the `traceway-setup-project` skill). +Empty results usually mean the wrong project, wrong time window, or the app is not instrumented. Check `traceway projects list`, widen `--since`, and if the app was never connected to Traceway, set it up first (see the `traceway-setup` skill). diff --git a/skills/traceway-setup-project/SKILL.md b/skills/traceway-setup/SKILL.md similarity index 86% rename from skills/traceway-setup-project/SKILL.md rename to skills/traceway-setup/SKILL.md index 9ce75b87..c5955992 100644 --- a/skills/traceway-setup-project/SKILL.md +++ b/skills/traceway-setup/SKILL.md @@ -1,6 +1,6 @@ --- -name: traceway-setup-project -description: Connect a project to a Traceway instance so it reports endpoints, spans, errors, and metrics. Use when the user wants to add Traceway (or OpenTelemetry tracing that exports to Traceway) to a backend, frontend, or mobile project. Accepts a project token and instance URL, e.g. "/traceway-setup-project with token abc123". +name: traceway-setup +description: Connect a project to a Traceway instance so it reports endpoints, spans, errors, and metrics. Use when the user wants to add Traceway (or OpenTelemetry tracing that exports to Traceway) to a backend, frontend, or mobile project. Accepts a project token and instance URL, e.g. "/traceway-setup with token abc123". --- # Set Up Traceway in a Project @@ -16,7 +16,7 @@ Two values are required: | **Instance URL** | `https://traceway.example.com` | The URL of the Traceway dashboard | | **Project token** | `abc123...` | Traceway dashboard → Connection page | -Both may be provided in the invocation (e.g. `/traceway-setup-project with token abc123 and url https://traceway.example.com`). If either is missing, check for existing `TRACEWAY_URL` / `TRACEWAY_TOKEN` environment variables or `.env` entries in the project — otherwise ask the user before proceeding. Never invent placeholder values in committed code; wire everything through environment variables. +Both may be provided in the invocation (e.g. `/traceway-setup with token abc123 and url https://traceway.example.com`). If either is missing, check for existing `TRACEWAY_URL` / `TRACEWAY_TOKEN` environment variables or `.env` entries in the project — otherwise ask the user before proceeding. Never invent placeholder values in committed code; wire everything through environment variables. ## What Traceway Needs @@ -39,9 +39,15 @@ For the integration to work correctly, the instrumentation MUST capture: | Non-root span | Has a parent span ID | **Span** | | Exception event | Event named `"exception"` on any span | **Issue** | -## Step 1: Identify the Framework +## Step 1: Analyze the Architecture -Detect the framework by reading `package.json` (Node.js), `go.mod` (Go), `composer.json` (PHP), `requirements.txt`/`pyproject.toml` (Python), `pubspec.yaml` (Flutter), `build.gradle` (Android), or asking the user. +Before changing anything, build a picture of what needs instrumenting: + +1. **Frameworks and languages**: detect them by reading `package.json` (Node.js), `go.mod` (Go), `composer.json` (PHP), `requirements.txt`/`pyproject.toml` (Python), `pubspec.yaml` (Flutter), `build.gradle` (Android), or asking the user. +2. **Services and entry points**: in a monorepo, list each deployable service and its entry point. Each service that should report to Traceway needs its own integration, and usually its own project token (ask the user before reusing one token across services). +3. **Background work**: find cron jobs, queue consumers, schedulers, and long-running workers. These must be instrumented as Tasks (root spans with `SpanKind.CONSUMER`), not Endpoints. + +Then follow the framework-specific guide for each service that needs instrumenting. ## Step 2: Follow the Framework-Specific Guide diff --git a/skills/traceway-setup-project/hono.md b/skills/traceway-setup/hono.md similarity index 100% rename from skills/traceway-setup-project/hono.md rename to skills/traceway-setup/hono.md diff --git a/skills/traceway-setup-project/nestjs.md b/skills/traceway-setup/nestjs.md similarity index 100% rename from skills/traceway-setup-project/nestjs.md rename to skills/traceway-setup/nestjs.md diff --git a/skills/traceway-setup-project/nextjs.md b/skills/traceway-setup/nextjs.md similarity index 100% rename from skills/traceway-setup-project/nextjs.md rename to skills/traceway-setup/nextjs.md From 8d55c974d60fe189cf6e3b6fca3c79774f41edad Mon Sep 17 00:00:00 2001 From: ddux Date: Thu, 11 Jun 2026 10:52:50 -0500 Subject: [PATCH 04/24] progress --- .../components/setup/ai-setup-steps.svelte | 13 +++--- .../components/setup/copyable-prompt.svelte | 42 +++++++++++++++++++ frontend/src/lib/utils/ai-setup.ts | 35 +++++++++++++++- skills/traceway-setup/SKILL.md | 9 ++++ 4 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 frontend/src/lib/components/setup/copyable-prompt.svelte diff --git a/frontend/src/lib/components/setup/ai-setup-steps.svelte b/frontend/src/lib/components/setup/ai-setup-steps.svelte index 9cbd8250..a5bb1596 100644 --- a/frontend/src/lib/components/setup/ai-setup-steps.svelte +++ b/frontend/src/lib/components/setup/ai-setup-steps.svelte @@ -1,12 +1,14 @@
@@ -40,10 +42,11 @@

Run the Setup Prompt

- Paste this prompt into your agent. Your instance URL and project token are already filled in. + Paste this prompt into your agent. Your instance URL and project token are already filled + in{sourceMapToken ? ', along with your source map upload token' : ''}.

- +
diff --git a/frontend/src/lib/components/setup/copyable-prompt.svelte b/frontend/src/lib/components/setup/copyable-prompt.svelte new file mode 100644 index 00000000..af270fac --- /dev/null +++ b/frontend/src/lib/components/setup/copyable-prompt.svelte @@ -0,0 +1,42 @@ + + +
+
+ +
+ + {#each parts as part, i (i)} + {#if part.bold} + {part.text} + {:else} + {part.text} + {/if} + {/each} + +
diff --git a/frontend/src/lib/utils/ai-setup.ts b/frontend/src/lib/utils/ai-setup.ts index f34e6d68..c21700d7 100644 --- a/frontend/src/lib/utils/ai-setup.ts +++ b/frontend/src/lib/utils/ai-setup.ts @@ -1,5 +1,36 @@ export const SKILL_INSTALL_COMMAND = 'npx skills add tracewayapp/traceway'; -export function getSetupPrompt(backendUrl: string, token: string): string { - return `/traceway-setup with token ${token} and url ${backendUrl}`; +export type PromptPart = { + text: string; + bold: boolean; +}; + +export function getSetupPromptParts( + backendUrl: string, + token: string, + sourceMapToken: string | null = null +): PromptPart[] { + const parts: PromptPart[] = [ + { text: '/traceway-setup with token ', bold: false }, + { text: token, bold: true }, + { text: ' and url ', bold: false }, + { text: backendUrl, bold: true } + ]; + if (sourceMapToken) { + parts.push( + { text: ' and source map upload token ', bold: false }, + { text: sourceMapToken, bold: true } + ); + } + return parts; +} + +export function getSetupPrompt( + backendUrl: string, + token: string, + sourceMapToken: string | null = null +): string { + return getSetupPromptParts(backendUrl, token, sourceMapToken) + .map((p) => p.text) + .join(''); } diff --git a/skills/traceway-setup/SKILL.md b/skills/traceway-setup/SKILL.md index c5955992..a17c8411 100644 --- a/skills/traceway-setup/SKILL.md +++ b/skills/traceway-setup/SKILL.md @@ -15,9 +15,18 @@ Two values are required: |---|---|---| | **Instance URL** | `https://traceway.example.com` | The URL of the Traceway dashboard | | **Project token** | `abc123...` | Traceway dashboard → Connection page | +| **Source map upload token** (optional) | `def456...` | Traceway dashboard → Connection page → Source Map Upload | Both may be provided in the invocation (e.g. `/traceway-setup with token abc123 and url https://traceway.example.com`). If either is missing, check for existing `TRACEWAY_URL` / `TRACEWAY_TOKEN` environment variables or `.env` entries in the project — otherwise ask the user before proceeding. Never invent placeholder values in committed code; wire everything through environment variables. +If a **source map upload token** is provided and the project produces minified/bundled JavaScript, add source map upload to the build or deploy step so stack traces resolve to original files and lines: + +```bash +npx @tracewayapp/sourcemap-upload --url --token --version --directory +``` + +Do not commit the upload token; keep it in CI secrets or an untracked env file. + ## What Traceway Needs For the integration to work correctly, the instrumentation MUST capture: From c1dcc42ad4f50a36c74bb5a2892e67b143fa5058 Mon Sep 17 00:00:00 2001 From: ddux Date: Thu, 11 Jun 2026 10:58:22 -0500 Subject: [PATCH 05/24] Merge branch 'main' into feat/traceway-llm-skill --- .../setup/source-map-upload-card.svelte | 49 +------------------ 1 file changed, 2 insertions(+), 47 deletions(-) diff --git a/frontend/src/lib/components/setup/source-map-upload-card.svelte b/frontend/src/lib/components/setup/source-map-upload-card.svelte index f0d7cbfd..a6f7b416 100644 --- a/frontend/src/lib/components/setup/source-map-upload-card.svelte +++ b/frontend/src/lib/components/setup/source-map-upload-card.svelte @@ -6,38 +6,17 @@ CardHeader, CardTitle } from '$lib/components/ui/card'; - import { Button } from '$lib/components/ui/button'; import { KeyRound } from 'lucide-svelte'; - import { LoadingCircle } from '$lib/components/ui/loading-circle'; - import bash from 'svelte-highlight/languages/bash'; import { projectsState } from '$lib/state/projects.svelte'; import { authState } from '$lib/state/auth.svelte'; - import CopyableInline from './copyable-inline.svelte'; - import CopyableCodeBlock from './copyable-code-block.svelte'; + import SourceMapSetup from '$lib/components/source-map-setup.svelte'; let projectWithToken = $derived(projectsState.currentProject); - let generatingToken = $state(false); const isReadonly = $derived( authState.getRoleForOrganization(projectsState.currentProject?.organizationId ?? 0) === 'readonly' ); - const sourceMapToken = $derived(projectWithToken?.sourceMapToken ?? null); - - const uploadCommand = $derived( - projectWithToken && sourceMapToken - ? `npx @tracewayapp/sourcemap-upload --url ${projectWithToken.backendUrl} --token ${sourceMapToken} --version YOUR_VERSION --directory dist/assets` - : '' - ); - - async function generateToken() { - generatingToken = true; - try { - await projectsState.generateSourceMapToken(); - } finally { - generatingToken = false; - } - } {#if projectWithToken && !isReadonly} @@ -53,31 +32,7 @@ - {#if sourceMapToken} -
-
-

Upload Token

- -
-
-

Usage

- -
-
- {:else} -

- Generate an upload token to start uploading source maps as part of your build process. -

- - {/if} +
{/if} From ba05c82b5be88fdbf556ee6e4c0493c59cfd4974 Mon Sep 17 00:00:00 2001 From: ddux Date: Thu, 11 Jun 2026 11:23:54 -0500 Subject: [PATCH 06/24] fix: sourcemap setup progress --- frontend/src/lib/components/setup/copyable-prompt.svelte | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/components/setup/copyable-prompt.svelte b/frontend/src/lib/components/setup/copyable-prompt.svelte index af270fac..2f6200de 100644 --- a/frontend/src/lib/components/setup/copyable-prompt.svelte +++ b/frontend/src/lib/components/setup/copyable-prompt.svelte @@ -29,11 +29,13 @@ {#each parts as part, i (i)} {#if part.bold} - {part.text} + {part.text} {:else} {part.text} {/if} From c5bb0aecd9918d9e7ce9b1439557b3968dfa494d Mon Sep 17 00:00:00 2001 From: ddux Date: Thu, 11 Jun 2026 11:53:00 -0500 Subject: [PATCH 07/24] fixes --- frontend/src/lib/components/setup/copyable-prompt.svelte | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/components/setup/copyable-prompt.svelte b/frontend/src/lib/components/setup/copyable-prompt.svelte index 2f6200de..b5470c71 100644 --- a/frontend/src/lib/components/setup/copyable-prompt.svelte +++ b/frontend/src/lib/components/setup/copyable-prompt.svelte @@ -29,13 +29,11 @@ {#each parts as part, i (i)} {#if part.bold} - {part.text} + {part.text} {:else} {part.text} {/if} From fc300a885a908b0952bde32001da792b971ae1b1 Mon Sep 17 00:00:00 2001 From: ddux Date: Thu, 11 Jun 2026 13:40:52 -0500 Subject: [PATCH 08/24] traceway setup skill --- skills/traceway-setup/SKILL.md | 338 ++++++++++++------ .../data-model.md | 0 skills/traceway-setup/hono.md | 277 -------------- skills/traceway-setup/nestjs.md | 177 --------- skills/traceway-setup/nextjs.md | 331 ----------------- 5 files changed, 221 insertions(+), 902 deletions(-) rename skills/{traceway-setup-project => traceway-setup}/data-model.md (100%) delete mode 100644 skills/traceway-setup/hono.md delete mode 100644 skills/traceway-setup/nestjs.md delete mode 100644 skills/traceway-setup/nextjs.md diff --git a/skills/traceway-setup/SKILL.md b/skills/traceway-setup/SKILL.md index a17c8411..2229c759 100644 --- a/skills/traceway-setup/SKILL.md +++ b/skills/traceway-setup/SKILL.md @@ -1,52 +1,51 @@ --- name: traceway-setup -description: Connect a project to a Traceway instance so it reports endpoints, spans, errors, and metrics. Use when the user wants to add Traceway (or OpenTelemetry tracing that exports to Traceway) to a backend, frontend, or mobile project. Accepts a project token and instance URL, e.g. "/traceway-setup with token abc123". +description: Connect a project to a Traceway instance so it reports endpoints, spans, errors, background tasks, AI traces, and metrics. Backends use OpenTelemetry over OTLP/HTTP, frontends and mobile apps use the Traceway SDKs, and host metrics use the Traceway OTel Agent. Use when the user wants to add Traceway (or OpenTelemetry tracing that exports to Traceway) to a backend, frontend, full-stack, or mobile project. Accepts a project token and instance URL, e.g. "/traceway-setup with token abc123". --- # Set Up Traceway in a Project -Connect an existing project to a Traceway instance so it reports endpoints, spans, errors, and metrics. +Connect an existing project to a Traceway instance so it reports endpoints, spans, errors, background tasks, AI traces, and metrics. ## Step 0: Gather Connection Info -Two values are required: - | Value | Example | Where to find it | |---|---|---| | **Instance URL** | `https://traceway.example.com` | The URL of the Traceway dashboard | | **Project token** | `abc123...` | Traceway dashboard → Connection page | -| **Source map upload token** (optional) | `def456...` | Traceway dashboard → Connection page → Source Map Upload | - -Both may be provided in the invocation (e.g. `/traceway-setup with token abc123 and url https://traceway.example.com`). If either is missing, check for existing `TRACEWAY_URL` / `TRACEWAY_TOKEN` environment variables or `.env` entries in the project — otherwise ask the user before proceeding. Never invent placeholder values in committed code; wire everything through environment variables. - -If a **source map upload token** is provided and the project produces minified/bundled JavaScript, add source map upload to the build or deploy step so stack traces resolve to original files and lines: +| **Source map upload token** (optional, frontend only) | `def456...` | Traceway dashboard → Connection page → Source Maps | -```bash -npx @tracewayapp/sourcemap-upload --url --token --version --directory -``` +Instance URL and project token may be provided in the invocation (e.g. `/traceway-setup with token abc123 and url https://traceway.example.com`). If either is missing, check for existing `TRACEWAY_URL` / `TRACEWAY_TOKEN` environment variables or `.env` entries in the project, otherwise ask the user before proceeding. Never invent placeholder values in committed code; wire everything through environment variables. -Do not commit the upload token; keep it in CI secrets or an untracked env file. +## Integration Paths -## What Traceway Needs +Pick the path by project type. This is not negotiable per framework; it is how Traceway is designed to receive data: -For the integration to work correctly, the instrumentation MUST capture: +| Project type | Path | +|---|---| +| **Backend** (any language) | OpenTelemetry, exporting OTLP/HTTP to `/api/otel/v1/*`. Always, including Go. The native Traceway Go SDK is used only when the user explicitly asks for it. | +| **Frontend** (browser SPA) | Traceway `@tracewayapp/` SDK + bundler plugin + source map upload (see "Frontend and Mobile" below). | +| **Full-stack JS** (Next.js, SvelteKit, Remix) | BOTH: server side via OpenTelemetry AND browser side via the frontend SDK. | +| **Mobile** (Flutter, React Native, Android) | The Traceway platform SDK only. Never OTel. | -1. **Endpoints grouped by route pattern** — `GET /api/users/1` and `GET /api/users/2` must appear as a single `GET /api/users/:id` endpoint, NOT as separate entries. This requires `http.route` to be set on the root span. Without it, the Traceway dashboard explodes with thousands of unique URL entries. +Two hard rules apply to every backend integration: -2. **Status codes** — `http.response.status_code` must be set on spans so Traceway can track error rates, 4xx/5xx breakdowns, and Apdex scores. - -3. **Exceptions with stack traces** — Thrown errors must be recorded as span events with `exception.type`, `exception.message`, and `exception.stacktrace` attributes. These appear as **Issues** in Traceway. - -4. **Scheduled/long-running tasks** — Background jobs (cron, queues, consumers) must create root spans with `SpanKind.CONSUMER`. This is how Traceway distinguishes Tasks from Endpoints. Without the correct span kind, background work either gets misclassified as an Endpoint or dropped entirely. +1. **Endpoints MUST arrive parametrized.** `http.route` must be set to the route pattern (`/api/users/:id`), never the concrete URL. Traceway uses the value as-is; when it is missing it falls back to `url.path`, and the Endpoints page explodes into one row per unique URL. +2. **Background work MUST use `SpanKind.CONSUMER`.** A root span with the default `INTERNAL` kind and no HTTP attributes is silently dropped. ### How Traceway classifies spans | OTel Span | Condition | Traceway Concept | |---|---|---| | Root span | `SpanKind = SERVER` or `INTERNAL` with HTTP attributes | **Endpoint** | -| Root span | `SpanKind = CONSUMER` | **Task** | -| Non-root span | Has a parent span ID | **Span** | +| Any span | `SpanKind = CONSUMER` | **Task** | +| Root span | `SpanKind = INTERNAL` with a `console.command` attribute | **Task** (CLI command) | +| Any span | Has any `gen_ai.*` attribute | **AI Trace** | +| Non-root span | Has a parent span ID, matched none of the above | **Span** (child) | | Exception event | Event named `"exception"` on any span | **Issue** | +| Root span | Matched none of the above | **Dropped silently** | + +For the exact classification rules, endpoint naming, metric conversion, and all the quirks, read `data-model.md` in this skill directory. It is the authoritative reference. ## Step 1: Analyze the Architecture @@ -54,89 +53,109 @@ Before changing anything, build a picture of what needs instrumenting: 1. **Frameworks and languages**: detect them by reading `package.json` (Node.js), `go.mod` (Go), `composer.json` (PHP), `requirements.txt`/`pyproject.toml` (Python), `pubspec.yaml` (Flutter), `build.gradle` (Android), or asking the user. 2. **Services and entry points**: in a monorepo, list each deployable service and its entry point. Each service that should report to Traceway needs its own integration, and usually its own project token (ask the user before reusing one token across services). -3. **Background work**: find cron jobs, queue consumers, schedulers, and long-running workers. These must be instrumented as Tasks (root spans with `SpanKind.CONSUMER`), not Endpoints. - -Then follow the framework-specific guide for each service that needs instrumenting. - -## Step 2: Follow the Framework-Specific Guide - -### Hono (Node.js) -Follow `hono.md` in this skill directory. Uses `@hono/otel` middleware — do NOT use `@opentelemetry/instrumentation-http` (it doesn't work with Hono's ESM imports on Node 22+). -- Endpoints: `@hono/otel` sets `http.route` automatically -- Status codes: `@hono/otel` sets them automatically -- Exceptions: `@hono/otel` records thrown errors automatically -- Tasks: No built-in scheduler — use `SpanKind.CONSUMER` manually for background work - -### NestJS (Node.js) -Follow `nestjs.md` in this skill directory. Simplest integration — Express auto-instrumentation handles everything. -- Endpoints: `instrumentation-express` sets `http.route` automatically -- Status codes: `instrumentation-http` sets them automatically -- Exceptions: Express error handling records them automatically -- Tasks: Wrap `@nestjs/schedule` cron jobs and `@nestjs/bull` queue consumers with `SpanKind.CONSUMER` spans - -### Next.js (Node.js) -Follow `nextjs.md` in this skill directory. Requires `withRoute()` wrapper for API routes and `@prisma/instrumentation` for database tracing. -- Endpoints: `withRoute()` helper must be added manually to every API route handler -- Status codes: Set by the HTTP instrumentation -- Exceptions: `withRoute()` catches and records thrown errors -- Tasks: No built-in scheduler — use `SpanKind.CONSUMER` manually for background work - -### Express (Node.js) -- Install: `@opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-http @opentelemetry/exporter-metrics-otlp-http @opentelemetry/api` -- Create `instrumentation.js` at project root with `NodeSDK` + `getNodeAutoInstrumentations()` -- No app code changes needed — auto-instrumentation captures routes, status codes, errors -- Start with `node --import ./instrumentation.js server.js` -- Tasks: Use `SpanKind.CONSUMER` manually for background work -- Full docs: https://docs.tracewayapp.com/client/node-sdk - -### Gin / Chi / Fiber / FastHTTP / stdlib (Go) -- Install the framework-specific middleware: `go get go.tracewayapp.com/tracewaygin` (or `tracewaychi`, `tracewayfiber`, `tracewayfasthttp`, `tracewayhttp`) -- Add middleware: `r.Use(tracewaygin.New("token@http://traceway:8082/api/report"))` -- Reports via Traceway's native protocol (`/api/report`), not OTel -- Endpoints, status codes, exceptions, and tasks are all handled by the Go SDK automatically -- Full docs: https://docs.tracewayapp.com/client/gin-middleware (or the corresponding framework page) - -### Django (Python) -- Uses OTel auto-instrumentation for Django -- Full docs: https://docs.tracewayapp.com/client/django - -### Symfony (PHP) -- Install: `composer require traceway/opentelemetry-symfony open-telemetry/exporter-otlp php-http/guzzle7-adapter` -- Configure via `.env` with `OTEL_*` variables -- Add `\OpenTelemetry\SDK\SdkAutoloader::autoload()` to `public/index.php` -- Endpoints and status codes: handled by Symfony OTel auto-instrumentation -- Tasks: Symfony Messenger consumers are auto-instrumented as Tasks -- Full docs: https://docs.tracewayapp.com/client/symfony - -### Laravel (PHP) -- Full docs: https://docs.tracewayapp.com/client/laravel - -### React / Vue / Svelte / jQuery / plain JS (Frontend) -- Install the framework-specific Traceway SDK: `npm install @tracewayapp/react` (or `@tracewayapp/vue`, `@tracewayapp/svelte`, `@tracewayapp/jquery`) -- These are client-side SDKs that report to `/api/report`, not OTel -- They capture JS errors (as Issues), page loads, and web vitals; upload source maps for readable stack traces from minified bundles -- Full docs: https://docs.tracewayapp.com/client/react (or `vue`, `svelte`, `jquery`, `js-sdk` for plain JavaScript) - -### React Native / Flutter / Android (Mobile) -- Install the platform Traceway SDK and initialize it with the instance URL + project token at app startup -- Full docs: https://docs.tracewayapp.com/client/react-native, https://docs.tracewayapp.com/client/flutter, https://docs.tracewayapp.com/client/android - -### Cloudflare Workers -- Uses Cloudflare's built-in OTLP export, not the Node SDK -- Scheduled handlers (`scheduled` event) create root spans automatically -- Full docs: https://docs.tracewayapp.com/client/cloudflare - -### Any Other Language (Generic OTel) -- Use any OpenTelemetry SDK for the language -- Export via OTLP/HTTP to `https:///api/otel/v1/traces` and `/v1/metrics` -- Set `Authorization: Bearer ` header -- Ensure `http.route` is set on root SERVER spans (not just `url.path`) -- Use `SpanKind.CONSUMER` for background/scheduled work -- Full docs: https://docs.tracewayapp.com/client/otel - -## Instrumenting Background Tasks (All Frameworks) - -For any framework, background work (cron jobs, queue consumers, scheduled tasks) must create a root span with `SpanKind.CONSUMER` to appear as a **Task** in Traceway: +3. **Background work**: find cron jobs, queue consumers, schedulers, CLI commands, and long-running workers. These must be instrumented as Tasks (Step 3). +4. **AI/LLM usage**: check dependencies for `openai`, `@anthropic-ai/sdk`, `anthropic`, `langchain` / `@langchain/*`, `ai` (Vercel AI SDK), `litellm`, `google-generativeai`, `cohere`, `openrouter`. If any are present, Step 4 applies. +5. **Deployment signals**: note Dockerfiles, `docker-compose.yml`, Kubernetes manifests, Helm charts, deploy/provisioning scripts, `fly.toml`, `vercel.json`, Procfiles. You will use these in Step 5 to set up server metrics. + +## Step 2: Backend OTel Setup + +The same shape in every language: + +1. Install the language's OpenTelemetry SDK plus the auto-instrumentation for the web framework and database clients. +2. Point the OTLP/HTTP exporter at Traceway with the project token as a Bearer header. +3. Set `service.name` (becomes the Server Name in Traceway) and `service.version` (enables release comparison) on the resource. +4. Verify endpoint grouping (the hard rule above) before anything else. + +### Exporter configuration + +Where the SDK supports the standard env vars, prefer them; they work identically across languages: + +```bash +OTEL_SERVICE_NAME=my-service +OTEL_EXPORTER_OTLP_ENDPOINT=https:///api/otel +OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf +OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer " +OTEL_TRACES_EXPORTER=otlp +OTEL_METRICS_EXPORTER=otlp +OTEL_LOGS_EXPORTER=otlp +``` + +SDKs append `/v1/traces`, `/v1/metrics`, `/v1/logs` to the endpoint automatically. When configuring in code instead, the full URLs are `https:///api/otel/v1/traces` (and `/v1/metrics`, `/v1/logs`) with header `Authorization: Bearer `. + +Constraints: OTLP/HTTP only (protobuf or JSON), OTLP/gRPC is NOT supported; gzip is fine; max request body 10 MB. + +### Node.js example + +Create `instrumentation.js` at the project root and load it before the app (`node --import ./instrumentation.js server.js`): + +```javascript +import { NodeSDK } from "@opentelemetry/sdk-node"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; +import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http"; +import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; +import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node"; + +const url = process.env.TRACEWAY_URL; +const headers = { Authorization: `Bearer ${process.env.TRACEWAY_TOKEN}` }; + +const sdk = new NodeSDK({ + traceExporter: new OTLPTraceExporter({ url: `${url}/api/otel/v1/traces`, headers }), + metricReader: new PeriodicExportingMetricReader({ + exporter: new OTLPMetricExporter({ url: `${url}/api/otel/v1/metrics`, headers }), + exportIntervalMillis: 30_000, + }), + instrumentations: [getNodeAutoInstrumentations()], +}); + +sdk.start(); +``` + +Auto-instrumentation covers Express routes (sets `http.route`), status codes, errors, and CJS database clients (`pg`, `mysql2`, `mongodb`, `ioredis`). SQLite and custom business logic need manual `tracer.startActiveSpan()` child spans. + +### Per-language notes + +- **Go**: use the contrib middleware for the framework: `otelgin`, `otelchi` (with `otelchi.WithChiRoutes(r)`), `otelfiber`; they set `http.route` from the route pattern. For stdlib `net/http`, current `otelhttp` derives `http.route` from Go 1.23+ `ServeMux` method+pattern routes (`GET /api/users/{id}`); for other routers set `http.route` manually on the active span. Exporter: `otlptracehttp.WithEndpointURL(...)` + `WithHeaders`. +- **Python**: `opentelemetry-distro` + `opentelemetry-bootstrap -a install`, run under `opentelemetry-instrument` with the env vars above. Django/Flask/FastAPI instrumentations set `http.route`. +- **PHP**: Symfony and Laravel via their OTel packages and `OTEL_*` env vars. Symfony caveat: the stock auto-instrumentation sets `http.route` to the route NAME, which Traceway ignores; use the Traceway Symfony integration (`composer require traceway/opentelemetry-symfony`). See `data-model.md`. +- **Java / .NET / anything else**: the standard OTel agent or SDK with the env vars above works as-is. +- Full per-framework docs: https://docs.tracewayapp.com/client/otel + +### Verify endpoint grouping (do this first) + +Hit a parametrized route a few times with different IDs and check the Traceway Endpoints page: you must see ONE row (`GET /api/users/:id`), not one row per ID. If you see raw IDs, the instrumentation is not setting `http.route`; fix that before continuing. Last resort, set it manually in a middleware that knows the matched route pattern: + +```javascript +import { trace } from "@opentelemetry/api"; + +trace.getActiveSpan()?.setAttribute("http.route", matchedRoutePattern); +``` + +### Errors + +Thrown errors must be recorded as exception events to appear as Issues. Auto-instrumentation handles uncaught errors; for caught-and-handled ones: + +```javascript +import { trace, SpanStatusCode } from "@opentelemetry/api"; + +const span = trace.getActiveSpan(); +span?.recordException(error); +span?.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); +``` + +(Go: `span.RecordError(err, trace.WithStackTrace(true))`; the stack trace option is what produces the `exception.stacktrace` attribute.) + +### If the user explicitly asks for the Traceway Go SDK + +Only on explicit request, instead of OTel: `go get go.tracewayapp.com/tracewaygin` (or `tracewaychi`, `tracewayfiber`, `tracewayfasthttp`, `tracewayhttp`), add the middleware with connection string `@https:///api/report`. Trade-off: the Go SDK natively emits the built-in system metric names (`cpu.used_pcnt`, `mem.used`, `go.*`) that populate the dashboard's built-in charts; on the OTel path those charts stay empty and host metrics come from the Traceway OTel Agent (Step 5). + +## Step 3: Background Tasks (Boundaries and Labeling) + +If Step 1 found background work, instrument it as Tasks. The rules: + +- **Boundary: one Task = one execution of a unit of background work.** A whole cron job run is one task. One queue message or job is one task. One CLI command invocation is one task. Per-item work inside a run (each email in a batch, each row in an import) is a child span of the task span, never a separate `CONSUMER` span. +- **Do not double-wrap.** If a library's auto-instrumentation already emits `CONSUMER` spans (Kafka, RabbitMQ, Symfony Messenger consumers), wrapping them again creates duplicate Task entries. +- **Labeling: the span name IS the task name and the grouping key.** Use a stable identifier like `cleanup-expired-sessions` or `process-email-queue`. Never embed job IDs, timestamps, or user IDs in the name; each unique name becomes a separate task group. Dynamic context (job ID, batch size) belongs in span attributes, where it shows on the task detail page without affecting grouping. +- **The kind must be `CONSUMER`.** A root span with the default `SpanKind.INTERNAL` is dropped silently; this is the most common reason "my cron job doesn't show up". CLI commands may alternatively be root `INTERNAL` spans with a `console.command` attribute (Laravel/Symfony console instrumentation does this). ```typescript import { trace, SpanKind, SpanStatusCode } from "@opentelemetry/api"; @@ -163,25 +182,110 @@ async function runScheduledJob() { } ``` -Without `SpanKind.CONSUMER`, the span would either be classified as an Endpoint (wrong) or dropped. +(Go: `tracer.Start(ctx, "cleanup-expired-sessions", trace.WithSpanKind(trace.SpanKindConsumer))`.) + +## Step 4: AI Traces + +If Step 1 found AI/LLM dependencies, instrument the model calls. Any span carrying at least one `gen_ai.*` attribute is promoted to an **AI Trace**; since calls usually happen inside a request or task, the span is naturally a child and stays linked to its Endpoint/Task by trace ID. + +Boundaries: **one span per model call** (one provider API request = one AI Trace row). A multi-step agent run is multiple spans sharing a stable `trace.name` (same labeling discipline as task names: no IDs or timestamps). For streaming, end the span when the stream finishes. Set `user.id` to break down usage and cost per user. + +Attributes Traceway reads (all optional, set what is available): + +| Attribute | Meaning | +|---|---| +| `gen_ai.request.model` / `gen_ai.response.model` | Requested / serving model | +| `gen_ai.system` or `gen_ai.provider.name` | Provider (`openai`, `anthropic`, ...) | +| `gen_ai.operation.name` | Operation (`chat`, `embeddings`, ...) | +| `gen_ai.usage.input_tokens` / `.output_tokens` / `.total_tokens` | Token counts | +| `gen_ai.usage.input_tokens.cached` / `gen_ai.usage.output_tokens.reasoning` | Cached / reasoning tokens | +| `gen_ai.usage.input_cost` / `.output_cost` / `.total_cost` | Cost, when you compute pricing | +| `trace.name` | Agent/workflow grouping name | +| `user.id` | End-user attribution | +| `gen_ai.response.finish_reason` | Why generation stopped | +| `gen_ai.prompt` / `gen_ai.completion` | Conversation content, shown on the trace detail page (skip if content must not leave the app) | + +```typescript +return tracer.startActiveSpan("chat-completion", async (span) => { + const response = await openai.chat.completions.create({ model: "gpt-4o", messages }); + span.setAttributes({ + "gen_ai.system": "openai", + "gen_ai.request.model": "gpt-4o", + "gen_ai.usage.input_tokens": response.usage.prompt_tokens, + "gen_ai.usage.output_tokens": response.usage.completion_tokens, + "trace.name": "support-agent", + }); + span.end(); + return response; +}); +``` + +Zero-code path for OpenRouter users: in OpenRouter Settings → Observability, add an OpenTelemetry Collector destination pointing at `https:///api/otel/v1/traces` with header `{"Authorization": "Bearer "}`. Docs: https://docs.tracewayapp.com/client/openrouter + +## Frontend and Mobile + +Frontend and mobile projects do NOT use OTel; they use the Traceway SDKs reporting to `/api/report` with connection string `@https:///api/report`. + +**Browser** (React / Vue / Svelte / jQuery / plain JS), three pieces, all expected: + +1. **SDK**: `npm install @tracewayapp/react` (or `vue`, `svelte`, `jquery`, `frontend` for plain JS) and initialize with the connection string (React: wrap the app in ``). Captures errors, web vitals, and session replay. +2. **Bundler plugin**: `npm install -D @tracewayapp/bundler-plugin`, then add `tracewayDebugIds()` from `@tracewayapp/bundler-plugin/vite` (or `/rollup`, or `TracewayDebugIdsWebpackPlugin` from `/webpack`) to the bundler config, with source maps enabled (`build.sourcemap: true` / `devtool: "source-map"`). +3. **Source map upload**: `npm install -D @tracewayapp/sourcemap-upload`, then run `traceway-sourcemaps --url --token --directory ./dist` as a postbuild or CI step (env vars: `TRACEWAY_URL`, `TRACEWAY_SOURCEMAP_TOKEN`). The upload token comes from Step 0 and is a CI secret, never committed. + +Full docs: https://docs.tracewayapp.com/client/react (or `vue`, `svelte`, `jquery`, `js-sdk`). + +**Full-stack JS** (Next.js, SvelteKit, Remix): both sides. Server side follows Step 2 (verify `http.route` grouping; set it manually in a server hook where the auto-instrumentation does not know the router). Browser side follows the three pieces above. + +**Mobile**, always the platform SDK, never OTel: + +- **Flutter**: `flutter pub add traceway`, then `Traceway.run(connectionString: '@https:///api/report', child: MyApp())`. Docs: https://docs.tracewayapp.com/client/flutter +- **React Native**: `npm install @tracewayapp/react-native`, wrap the app in `TracewayProvider`. Docs: https://docs.tracewayapp.com/client/react-native +- **Android**: `implementation("com.tracewayapp:traceway:")`, call `Traceway.init(...)` at startup. Docs: https://docs.tracewayapp.com/client/android + +## Step 5: Deployment and Server Metrics + +After the code-side integration is in place, ask the user two questions (pre-fill your best guess from the deployment signals found in Step 1 and let them confirm): + +1. **How is this project deployed?** Docker on a VM / directly on a VM or bare metal / Kubernetes / serverless or PaaS. +2. **Do you want server (host) metrics tracked in Traceway?** CPU, memory, disk, filesystem, network of the machine running the app. + +| Deployment | Wants host metrics | What to do | +|---|---|---| +| Docker on a VM, or directly on a VM/host | Yes | Install the **Traceway OTel Agent** on the host (below). For Docker deploys this is the default; the agent goes on the host, not in a container. | +| Kubernetes | Any | Agent not applicable (host service, no Docker image or K8s manifests by design). In-process app metrics still flow via the OTLP metrics exporter from Step 2. | +| Serverless / PaaS | Any | No host to install on; skip. | +| Anything | No | Skip. | + +The agent is a tiny pre-configured OTel Collector that scrapes host metrics every 60s and ships them with the project token. Install on the host (Linux systemd / macOS launchd; PowerShell installer exists for Windows): + +```bash +curl -fsSL https://install.tracewayapp.com/install.sh | \ + TRACEWAY_TOKEN= \ + TRACEWAY_ENDPOINT=https:///api/otel \ + TRACEWAY_SERVICE_NAME= \ + bash +``` -## Common Across All Node.js Frameworks +- `TRACEWAY_ENDPOINT` ends in `/api/otel` and is required for self-hosted instances; omit only for Traceway Cloud. +- Optional: `TRACEWAY_LOG_PATHS` (comma-separated globs to tail as logs), `TRACEWAY_PROCESS_NAMES` (per-process metrics). +- Re-running the installer upgrades in place, so the command is safe to keep in provisioning scripts. +- If the repo has host provisioning or deploy scripts (cloud-init, Ansible, Terraform `user_data`, `deploy.sh`), add the command there with the token referenced from a secret. Otherwise hand the operator the filled-in one-liner; do not modify the repo. -- **Traceway URL**: `https:///api/otel/v1/traces` and `/v1/metrics` -- **Auth header**: `Authorization: Bearer ` -- **Environment variables**: `TRACEWAY_URL` and `TRACEWAY_TOKEN` (or standard `OTEL_*` vars) -- **Auto-instrumented child spans** (CJS packages only): `pg`, `mysql2`, `mongodb`, `ioredis`, `redis`, Prisma (with `@prisma/instrumentation`), outgoing `fetch()` via `instrumentation-undici` -- **Not auto-instrumented**: SQLite (`better-sqlite3`), custom business logic — use `tracer.startActiveSpan()` manually +Metrics arrive within ~60s under their hostmetrics names (`system.cpu.utilization`, `system.memory.usage`, ...), tagged with `service.name` and `host.name`. They are chartable via custom widgets; they do NOT populate the built-in CPU/memory charts (those read the Go SDK's exact names). Agent repo: https://github.com/tracewayapp/traceway-otel-agent -## Step 3: Verify +## Step 6: Verify 1. Start the app and hit a few endpoints (or trigger an error on purpose). 2. Check the Traceway dashboard: - - **Endpoints page** — routes appear grouped by pattern (e.g. `GET /api/users/:id`), not by literal URL - - **Issues page** — thrown errors appear with stack traces - - **Endpoint detail → Spans tab** — database queries and outgoing calls appear as children + - **Endpoints page**: routes appear grouped by pattern (e.g. `GET /api/users/:id`), not by literal URL, and status codes are non-zero. + - **Issues page**: thrown errors appear with stack traces. For frontend projects, stack traces are symbolicated to original files and lines. + - **Endpoint detail → Spans tab**: database queries and outgoing calls appear as children. + - **Tasks page**: after triggering each background job once, it appears under one stable name. + - **AI Traces page** (if Step 4 applied): model calls appear with token counts and cost. + - **Metrics** (if the agent was installed): host metrics arrive within about 60 seconds. 3. If the `traceway` CLI is installed and authenticated, verify from the terminal instead: ```bash traceway endpoints list --since 15m traceway exceptions list --since 15m + traceway metrics query --name system.cpu.utilization --since 15m ``` diff --git a/skills/traceway-setup-project/data-model.md b/skills/traceway-setup/data-model.md similarity index 100% rename from skills/traceway-setup-project/data-model.md rename to skills/traceway-setup/data-model.md diff --git a/skills/traceway-setup/hono.md b/skills/traceway-setup/hono.md deleted file mode 100644 index daf5a953..00000000 --- a/skills/traceway-setup/hono.md +++ /dev/null @@ -1,277 +0,0 @@ -# Add Traceway to a Hono Project - -Add OpenTelemetry tracing to an existing Hono (Node.js) project so it reports endpoints, spans, and errors to a Traceway instance. - -## Prerequisites - -- Hono app running on Node.js via `@hono/node-server` -- A Traceway instance URL and project token (get these from your Traceway dashboard → Connection page) - -## Step 1: Install Dependencies - -```bash -npm install @hono/otel \ - @opentelemetry/sdk-node \ - @opentelemetry/auto-instrumentations-node \ - @opentelemetry/exporter-trace-otlp-http \ - @opentelemetry/exporter-metrics-otlp-http \ - @opentelemetry/api -``` - -`hono` and `@hono/node-server` should already be in the project. - -## Step 2: Create the Instrumentation File - -Create `instrumentation.js` (or `.ts`) at the project root — next to `package.json`, NOT inside `src/`: - -```javascript -import { NodeSDK } from "@opentelemetry/sdk-node"; -import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; -import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http"; -import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; -import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node"; - -const tracewayUrl = process.env.TRACEWAY_URL || "https://your-traceway-instance.com"; -const tracewayToken = process.env.TRACEWAY_TOKEN || "your-project-token"; - -const sdk = new NodeSDK({ - traceExporter: new OTLPTraceExporter({ - url: `${tracewayUrl}/api/otel/v1/traces`, - headers: { Authorization: `Bearer ${tracewayToken}` }, - }), - - metricReader: new PeriodicExportingMetricReader({ - exporter: new OTLPMetricExporter({ - url: `${tracewayUrl}/api/otel/v1/metrics`, - headers: { Authorization: `Bearer ${tracewayToken}` }, - }), - exportIntervalMillis: 30_000, - }), - - // IMPORTANT: Disable instrumentation-http. - // @hono/node-server imports Node's `http` via ESM. On Node 22+, OTel's - // import-in-the-middle hook cannot intercept ESM imports of built-in modules, - // so instrumentation-http never patches the server. @hono/otel handles HTTP - // spans at the middleware level instead. - instrumentations: [ - getNodeAutoInstrumentations({ - "@opentelemetry/instrumentation-http": { enabled: false }, - }), - ], -}); - -sdk.start(); -``` - -### Key decisions in this file - -- **No `serviceName` / `serviceVersion` in SDK config.** Set these via `OTEL_SERVICE_NAME` env var or pass them to `otel()` in step 3. -- **`instrumentation-http` is disabled.** `@hono/otel` replaces it. Without this, you get either duplicate spans or zero spans (depending on Node version). -- **Other auto-instrumentations stay enabled.** CJS npm packages (`pg`, `mysql2`, `mongodb`, `ioredis`) are automatically traced. Only ESM-imported Node.js built-in modules have the patching issue. - -## Step 3: Add the OTel Middleware - -In your main server file, add two lines — import `otel` and register it as middleware **before** your routes: - -```javascript -import { otel } from "@hono/otel"; - -// Add BEFORE route definitions -app.use(otel()); -``` - -Full example: - -```javascript -import { serve } from "@hono/node-server"; -import { Hono } from "hono"; -import { otel } from "@hono/otel"; - -const app = new Hono(); - -app.use(otel()); // ← this is the only change to your app code - -// ... existing routes unchanged ... -app.get("/api/users", (c) => c.json({ users: [] })); -app.get("/api/users/:id", (c) => c.json({ id: c.req.param("id") })); - -serve({ fetch: app.fetch, port: 3000 }); -``` - -### What `otel()` does automatically - -- Creates a root SERVER span for every request -- Sets `http.route` to the matched route pattern (e.g., `/api/users/:id`) so Traceway groups endpoints correctly -- Sets `http.request.method`, `http.response.status_code`, `url.full` -- Records thrown exceptions as span events with `exception.type`, `exception.message`, `exception.stacktrace` — these appear as Issues in Traceway -- Sets span status to ERROR on exceptions - -### Optional: pass config to `otel()` - -```javascript -app.use(otel({ - serviceName: "my-app", - serviceVersion: "1.0.0", - captureRequestHeaders: ["user-agent", "x-request-id"], - captureResponseHeaders: ["x-trace-id"], -})); -``` - -## Step 4: Update the Start Script - -The instrumentation file must load **before** the app code. Update `package.json`: - -```json -{ - "scripts": { - "start": "node --import ./instrumentation.js server.js" - } -} -``` - -If using CommonJS: - -```json -{ - "scripts": { - "start": "node --require ./instrumentation.js server.js" - } -} -``` - -Or set via environment variable: - -```bash -export NODE_OPTIONS="--import ./instrumentation.js" -``` - -## Step 5: Set Environment Variables - -```bash -TRACEWAY_URL=https://your-traceway-instance.com -TRACEWAY_TOKEN=your-project-token -``` - -## What Gets Traced Automatically - -After completing steps 1-5, the following produces traces with zero additional code: - -| What | How | Span type in Traceway | -|------|-----|----------------------| -| Every incoming HTTP request | `@hono/otel` middleware | **Endpoint** (root SERVER span) | -| Route grouping (`/users/1` + `/users/2` → `/users/:id`) | `@hono/otel` sets `http.route` | Endpoint attribute | -| Thrown errors | `@hono/otel` calls `span.recordException()` | **Issue** | -| Outgoing `fetch()` calls | `instrumentation-undici` (auto) | **Span** (child) | -| PostgreSQL queries (`pg`) | `instrumentation-pg` (auto) | **Span** (child) | -| MySQL queries (`mysql2`) | `instrumentation-mysql2` (auto) | **Span** (child) | -| MongoDB queries | `instrumentation-mongodb` (auto) | **Span** (child) | -| Redis operations (`ioredis`) | `instrumentation-ioredis` (auto) | **Span** (child) | -| DNS lookups | `instrumentation-dns` (auto) | **Span** (child) | - -## Adding Manual Spans (Optional) - -For operations without auto-instrumentation (SQLite, custom business logic, external APIs via non-fetch clients): - -```javascript -import { trace, SpanStatusCode } from "@opentelemetry/api"; - -const tracer = trace.getTracer("my-app"); - -// Wrap any operation in a span -function dbSpan(name, query, fn) { - return tracer.startActiveSpan(name, (span) => { - span.setAttribute("db.system", "sqlite"); - span.setAttribute("db.statement", query); - try { - const result = fn(); - span.end(); - return result; - } catch (error) { - span.recordException(error); - span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); - span.end(); - throw error; - } - }); -} - -// Use in a route handler — automatically becomes a child of the @hono/otel root span -app.get("/api/users", (c) => { - const users = dbSpan("db.query", "SELECT * FROM users", () => - db.prepare("SELECT * FROM users").all() - ); - return c.json(users); -}); -``` - -## Instrumenting Background Tasks (Optional) - -Hono has no built-in scheduler. If your app runs cron jobs or queue consumers (via `node-cron`, `bullmq`, etc.), wrap them with `SpanKind.CONSUMER` so they appear as **Tasks** in Traceway: - -```javascript -import { trace, SpanKind, SpanStatusCode } from "@opentelemetry/api"; - -const tracer = trace.getTracer("my-app"); - -// In your cron/queue handler -async function processQueueJob(job) { - await tracer.startActiveSpan( - "process-email-queue", - { kind: SpanKind.CONSUMER }, - async (span) => { - try { - span.setAttribute("job.id", job.id); - await sendEmail(job.data); - span.setStatus({ code: SpanStatusCode.OK }); - } catch (error) { - span.recordException(error); - span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); - throw error; - } finally { - span.end(); - } - } - ); -} -``` - -Without `SpanKind.CONSUMER`, background work would be dropped or misclassified as an Endpoint. - -## Recording Caught Exceptions (Optional) - -`@hono/otel` auto-records thrown errors. For errors you catch and handle: - -```javascript -import { trace, SpanStatusCode } from "@opentelemetry/api"; - -app.get("/api/checkout", async (c) => { - const span = trace.getActiveSpan(); - try { - await processPayment(); - } catch (error) { - if (span) { - span.recordException(error); - span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); - } - return c.json({ error: "Payment failed" }, 500); - } - return c.json({ status: "ok" }); -}); -``` - -## Verification - -After starting your app, hit a few endpoints and check Traceway: - -1. **Endpoints page** — routes should appear grouped by pattern (e.g., `GET /api/users/:id`), not by literal URL -2. **Issues page** — any thrown errors should appear with stack traces -3. **Endpoint detail → Spans tab** — database queries, outgoing fetch calls, and manual spans should appear as children of the HTTP request - -## Checklist - -- [ ] `@hono/otel` and OTel packages installed -- [ ] `instrumentation.js` created at project root with `instrumentation-http` disabled -- [ ] `app.use(otel())` added before route definitions -- [ ] Start script uses `--import ./instrumentation.js` (ESM) or `--require` (CJS) -- [ ] `TRACEWAY_URL` and `TRACEWAY_TOKEN` env vars set -- [ ] Verified endpoints appear in Traceway dashboard diff --git a/skills/traceway-setup/nestjs.md b/skills/traceway-setup/nestjs.md deleted file mode 100644 index ec07ee8e..00000000 --- a/skills/traceway-setup/nestjs.md +++ /dev/null @@ -1,177 +0,0 @@ -# Add Traceway to a NestJS Project - -Add OpenTelemetry tracing to an existing NestJS project so it reports endpoints, spans, and errors to a Traceway instance. - -## Prerequisites - -- NestJS application (Express or Fastify adapter) -- A Traceway instance URL and project token (get these from your Traceway dashboard → Connection page) - -## Step 1: Install Dependencies - -```bash -npm install @opentelemetry/sdk-node \ - @opentelemetry/auto-instrumentations-node \ - @opentelemetry/exporter-trace-otlp-http \ - @opentelemetry/exporter-metrics-otlp-http \ - @opentelemetry/api -``` - -If the project uses Prisma, also install its OTel instrumentation: - -```bash -npm install @prisma/instrumentation -``` - -## Step 2: Create the Instrumentation File - -Create `instrumentation.ts` at the project root — next to `package.json`, NOT inside `src/`: - -```typescript -import { NodeSDK } from "@opentelemetry/sdk-node"; -import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; -import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http"; -import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; -import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node"; -import { Resource } from "@opentelemetry/resources"; - -const tracewayUrl = process.env.TRACEWAY_URL || "https://your-traceway-instance.com"; -const tracewayToken = process.env.TRACEWAY_TOKEN || "your-project-token"; - -const sdk = new NodeSDK({ - resource: new Resource({ - "service.name": "my-nestjs-app", - "service.version": "1.0.0", - }), - - traceExporter: new OTLPTraceExporter({ - url: `${tracewayUrl}/api/otel/v1/traces`, - headers: { Authorization: `Bearer ${tracewayToken}` }, - }), - - metricReader: new PeriodicExportingMetricReader({ - exporter: new OTLPMetricExporter({ - url: `${tracewayUrl}/api/otel/v1/metrics`, - headers: { Authorization: `Bearer ${tracewayToken}` }, - }), - exportIntervalMillis: 30_000, - }), - - instrumentations: [getNodeAutoInstrumentations()], -}); - -sdk.start(); -``` - -If using Prisma, add `import { PrismaInstrumentation } from "@prisma/instrumentation"` and include `new PrismaInstrumentation()` in the `instrumentations` array. - -### Why NestJS is simpler than Hono/Next.js - -- **No `instrumentation-http` disable needed.** NestJS uses Express (CJS) internally, so OTel patches it successfully. -- **No route helper needed.** `@opentelemetry/instrumentation-express` automatically sets `http.route` — endpoint grouping works out of the box. -- **No error handler needed.** Express instrumentation records exceptions automatically. - -## Step 3: Update Start Scripts - -The instrumentation file must load **before** NestJS boots. Update `package.json`: - -```json -{ - "scripts": { - "start": "node --require ./instrumentation.js dist/main.js", - "start:dev": "node --require ts-node/register --require ./instrumentation.ts src/main.ts" - } -} -``` - -Requires `ts-node` as a dev dependency for development mode: - -```bash -npm install -D ts-node -``` - -If using the NestJS CLI (`nest start`), set via environment variable instead: - -```bash -export NODE_OPTIONS="--require ./instrumentation.js" -npm run start -``` - -## Step 4: Set Environment Variables - -```bash -TRACEWAY_URL=https://your-traceway-instance.com -TRACEWAY_TOKEN=your-project-token -``` - -## Step 5: Instrument Background Tasks (Optional) - -NestJS doesn't have built-in scheduled tasks that auto-instrument. If your app uses `@nestjs/schedule` (cron jobs) or `@nestjs/bull` (queues), wrap them in manual spans with `SpanKind.CONSUMER` so they appear as **Tasks** in Traceway: - -```typescript -import { trace, SpanKind, SpanStatusCode } from "@opentelemetry/api"; - -const tracer = trace.getTracer("my-nestjs-app"); - -@Injectable() -export class TasksService { - @Cron("0 * * * *") - async hourlyCleanup() { - await tracer.startActiveSpan( - "hourly-cleanup", - { kind: SpanKind.CONSUMER }, - async (span) => { - try { - await this.cleanupExpiredSessions(); - span.setStatus({ code: SpanStatusCode.OK }); - } catch (error) { - span.recordException(error); - span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); - throw error; - } finally { - span.end(); - } - } - ); - } -} -``` - -The `SpanKind.CONSUMER` is what tells Traceway to classify this as a **Task** instead of an Endpoint. - -## What Gets Traced Automatically - -After completing steps 1-4, the following produces traces with zero additional code: - -| What | How | Span type in Traceway | -|------|-----|----------------------| -| Every incoming HTTP request | `instrumentation-http` + `instrumentation-express` | **Endpoint** (root SERVER span) | -| Route grouping (`/users/1` + `/users/2` → `/users/:id`) | `instrumentation-express` sets `http.route` | Endpoint attribute | -| Status codes (200, 404, 500) | `instrumentation-http` sets `http.response.status_code` | Endpoint attribute | -| Thrown errors | Express error handling records exceptions | **Issue** | -| NestJS handler spans | `instrumentation-nestjs-core` | **Span** (child) | -| Prisma queries | `@prisma/instrumentation` (if installed) | **Span** (child) | -| Outgoing `fetch()` calls | `instrumentation-undici` (auto) | **Span** (child) | -| Outgoing HTTP (`axios`) | `instrumentation-http` (auto) | **Span** (child) | -| PostgreSQL queries (`pg`) | `instrumentation-pg` (auto) | **Span** (child) | -| MySQL queries (`mysql2`) | `instrumentation-mysql2` (auto) | **Span** (child) | -| MongoDB queries | `instrumentation-mongodb` (auto) | **Span** (child) | -| Redis operations (`ioredis`) | `instrumentation-ioredis` (auto) | **Span** (child) | -| Cron jobs / queue consumers | Manual with `SpanKind.CONSUMER` (step 5) | **Task** | - -## Verification - -After starting your app, hit a few endpoints and check Traceway: - -1. **Endpoints page** — routes should appear grouped by pattern (e.g., `GET /api/users/:id`), not by literal URL -2. **Issues page** — any thrown errors should appear with stack traces -3. **Endpoint detail → Spans tab** — database queries, outgoing fetch calls, and NestJS handler spans should appear as children - -## Checklist - -- [ ] OTel packages installed (+ `@prisma/instrumentation` if using Prisma) -- [ ] `instrumentation.ts` created at project root -- [ ] Start scripts updated to use `--require ./instrumentation.ts` (dev) or `--require ./instrumentation.js` (prod) -- [ ] `TRACEWAY_URL` and `TRACEWAY_TOKEN` env vars set -- [ ] Cron jobs / queue consumers wrapped with `SpanKind.CONSUMER` spans (if applicable) -- [ ] Verified endpoints appear in Traceway dashboard diff --git a/skills/traceway-setup/nextjs.md b/skills/traceway-setup/nextjs.md deleted file mode 100644 index fb681bb4..00000000 --- a/skills/traceway-setup/nextjs.md +++ /dev/null @@ -1,331 +0,0 @@ -# Add Traceway to a Next.js Project - -Add OpenTelemetry tracing to an existing Next.js (App Router) project so it reports endpoints, spans, and errors to a Traceway instance. - -## Prerequisites - -- Next.js 13.4+ with App Router -- A Traceway instance URL and project token (get these from your Traceway dashboard → Connection page) - -## Step 1: Install Dependencies - -```bash -npm install @opentelemetry/sdk-node \ - @opentelemetry/auto-instrumentations-node \ - @opentelemetry/exporter-trace-otlp-http \ - @opentelemetry/exporter-metrics-otlp-http \ - @opentelemetry/api -``` - -If the project uses Prisma, also install its OTel instrumentation for automatic query tracing: - -```bash -npm install @prisma/instrumentation -``` - -## Step 2: Create the Instrumentation File - -Create `instrumentation.ts` at the project root — next to `next.config.js`, NOT inside `src/` or `app/`. Next.js loads this file automatically during server startup. No `--require` or `--import` flag needed. - -```typescript -export async function register() { - if (process.env.NEXT_RUNTIME === "nodejs") { - const { NodeSDK } = await import("@opentelemetry/sdk-node"); - const { OTLPTraceExporter } = await import( - "@opentelemetry/exporter-trace-otlp-http" - ); - const { OTLPMetricExporter } = await import( - "@opentelemetry/exporter-metrics-otlp-http" - ); - const { PeriodicExportingMetricReader } = await import( - "@opentelemetry/sdk-metrics" - ); - const { getNodeAutoInstrumentations } = await import( - "@opentelemetry/auto-instrumentations-node" - ); - const { Resource } = await import("@opentelemetry/resources"); - const { PrismaInstrumentation } = await import( - "@prisma/instrumentation" - ); - - const tracewayUrl = process.env.TRACEWAY_URL || "https://your-traceway-instance.com"; - const tracewayToken = process.env.TRACEWAY_TOKEN || "your-project-token"; - - const sdk = new NodeSDK({ - resource: new Resource({ - "service.name": "my-nextjs-app", - "service.version": "1.0.0", - }), - - traceExporter: new OTLPTraceExporter({ - url: `${tracewayUrl}/api/otel/v1/traces`, - headers: { Authorization: `Bearer ${tracewayToken}` }, - }), - - metricReader: new PeriodicExportingMetricReader({ - exporter: new OTLPMetricExporter({ - url: `${tracewayUrl}/api/otel/v1/metrics`, - headers: { Authorization: `Bearer ${tracewayToken}` }, - }), - exportIntervalMillis: 30_000, - }), - - instrumentations: [ - getNodeAutoInstrumentations(), - new PrismaInstrumentation(), - ], - }); - - sdk.start(); - } -} -``` - -### Key decisions in this file - -- **`process.env.NEXT_RUNTIME === "nodejs"` guard is required.** Next.js runs `instrumentation.ts` in both Node.js and Edge runtimes. The OTel Node SDK only works in Node.js. -- **All imports are dynamic `import()`.** This prevents OTel packages from being bundled into the Edge runtime. -- **`PrismaInstrumentation` is included.** If the project doesn't use Prisma, remove the import and the `new PrismaInstrumentation()` line. -- **`getNodeAutoInstrumentations()` is kept with defaults.** Unlike Hono, `instrumentation-http` is NOT disabled — it may capture some spans depending on the Node.js version. - -## Step 3: Create the Route Helper - -Next.js (App Router) does not use Express or Fastify, so OTel auto-instrumentation cannot set `http.route` automatically. Without it, Traceway creates a separate endpoint for every unique URL path instead of grouping them. - -Create `lib/with-route.ts`: - -```typescript -import { trace, SpanStatusCode } from "@opentelemetry/api"; - -type RouteHandler = ( - req: Request, - context: { params: Promise> } -) => Response | Promise; - -export function withRoute(route: string, handler: RouteHandler): RouteHandler { - return async (req, context) => { - const span = trace.getActiveSpan(); - if (span) { - span.setAttribute("http.route", route); - } - try { - return await handler(req, context); - } catch (error) { - if (span) { - span.recordException(error as Error); - span.setStatus({ - code: SpanStatusCode.ERROR, - message: (error as Error).message, - }); - } - throw error; - } - }; -} -``` - -### What `withRoute` does - -- Sets `http.route` to the parameterized route pattern so Traceway groups endpoints correctly (e.g., `/api/users/[id]` instead of `/api/users/1`, `/api/users/2`) -- Catches thrown exceptions, records them as span events (appear as Issues in Traceway), then re-throws - -## Step 4: Wrap Every API Route Handler - -Wrap each exported handler with `withRoute`, passing the route pattern that matches the file path: - -```typescript -// app/api/users/route.ts -import { withRoute } from "@/lib/with-route"; -import { prisma } from "@/lib/db"; - -export const GET = withRoute("/api/users", async () => { - const users = await prisma.user.findMany(); - return Response.json(users); -}); - -export const POST = withRoute("/api/users", async (req) => { - const body = await req.json(); - const user = await prisma.user.create({ data: body }); - return Response.json(user, { status: 201 }); -}); -``` - -```typescript -// app/api/users/[id]/route.ts -import { withRoute } from "@/lib/with-route"; -import { prisma } from "@/lib/db"; - -export const GET = withRoute("/api/users/[id]", async (req, { params }) => { - const { id } = await params; - const user = await prisma.user.findUnique({ where: { id: parseInt(id) } }); - if (!user) { - return Response.json({ error: "User not found" }, { status: 404 }); - } - return Response.json(user); -}); -``` - -### Route pattern convention - -The `route` string passed to `withRoute` should match the file-system path with Next.js bracket notation: - -| File path | Route string | -|---|---| -| `app/api/users/route.ts` | `"/api/users"` | -| `app/api/users/[id]/route.ts` | `"/api/users/[id]"` | -| `app/api/posts/[slug]/comments/route.ts` | `"/api/posts/[slug]/comments"` | - -## Step 5: Enable Instrumentation Hook (Next.js 13.4–14.x only) - -For Next.js versions before 15, add to `next.config.js`: - -```javascript -/** @type {import('next').NextConfig} */ -const nextConfig = { - experimental: { - instrumentationHook: true, - }, -}; - -module.exports = nextConfig; -``` - -Not needed for Next.js 15+. - -## Step 6: Set Environment Variables - -```bash -TRACEWAY_URL=https://your-traceway-instance.com -TRACEWAY_TOKEN=your-project-token -``` - -## What Gets Traced Automatically - -After completing steps 1-6, the following produces traces with zero additional code: - -| What | How | Span type in Traceway | -|------|-----|----------------------| -| Every incoming HTTP request | `withRoute` sets `http.route` | **Endpoint** (root SERVER span) | -| Route grouping (`/users/1` + `/users/2` → `/users/[id]`) | `withRoute` sets `http.route` | Endpoint attribute | -| Thrown errors | `withRoute` calls `span.recordException()` | **Issue** | -| Prisma queries | `@prisma/instrumentation` (auto) | **Span** (child) | -| Outgoing `fetch()` calls | `instrumentation-undici` (auto) | **Span** (child) | -| PostgreSQL queries (`pg`) | `instrumentation-pg` (auto) | **Span** (child) | -| MySQL queries (`mysql2`) | `instrumentation-mysql2` (auto) | **Span** (child) | -| MongoDB queries | `instrumentation-mongodb` (auto) | **Span** (child) | -| Redis operations (`ioredis`) | `instrumentation-ioredis` (auto) | **Span** (child) | - -## Instrumenting Background Tasks (Optional) - -Next.js has no built-in scheduler. If your app runs cron jobs or queue consumers (via `node-cron`, `bullmq`, or triggered by an external scheduler like Vercel Cron), wrap them with `SpanKind.CONSUMER` so they appear as **Tasks** in Traceway: - -```typescript -import { trace, SpanKind, SpanStatusCode } from "@opentelemetry/api"; - -const tracer = trace.getTracer("my-nextjs-app"); - -// e.g., in an API route triggered by a cron scheduler -export const POST = withRoute("/api/cron/cleanup", async () => { - return tracer.startActiveSpan( - "cleanup-expired-sessions", - { kind: SpanKind.CONSUMER }, - async (span) => { - try { - await prisma.session.deleteMany({ - where: { expiresAt: { lt: new Date() } }, - }); - span.setStatus({ code: SpanStatusCode.OK }); - span.end(); - return Response.json({ status: "ok" }); - } catch (error) { - span.recordException(error as Error); - span.setStatus({ code: SpanStatusCode.ERROR, message: (error as Error).message }); - span.end(); - throw error; - } - } - ); -}); -``` - -Without `SpanKind.CONSUMER`, background work would be classified as an Endpoint or dropped. - -## Adding Manual Spans (Optional) - -For operations without auto-instrumentation (SQLite, Server Components, custom business logic): - -```typescript -import { trace, SpanStatusCode } from "@opentelemetry/api"; - -const tracer = trace.getTracer("my-nextjs-app"); - -// In an API route handler -export const GET = withRoute("/api/report", async () => { - return tracer.startActiveSpan("generate-report", async (span) => { - try { - span.setAttribute("report.type", "monthly"); - const data = await generateReport(); - span.end(); - return Response.json(data); - } catch (error) { - span.recordException(error as Error); - span.setStatus({ code: SpanStatusCode.ERROR, message: (error as Error).message }); - span.end(); - throw error; - } - }); -}); - -// In a Server Component -export default async function DashboardPage() { - return tracer.startActiveSpan("DashboardPage.render", async (span) => { - try { - const stats = await fetchStats(); - span.setAttribute("stats.count", stats.length); - return ; - } finally { - span.end(); - } - }); -} -``` - -## Recording Caught Exceptions (Optional) - -`withRoute` auto-records thrown errors. For errors you catch and handle: - -```typescript -import { trace, SpanStatusCode } from "@opentelemetry/api"; - -export const POST = withRoute("/api/checkout", async (req) => { - const span = trace.getActiveSpan(); - try { - await processPayment(await req.json()); - } catch (error) { - if (span) { - span.recordException(error as Error); - span.setStatus({ code: SpanStatusCode.ERROR, message: (error as Error).message }); - } - return Response.json({ error: "Payment failed" }, { status: 500 }); - } - return Response.json({ status: "ok" }); -}); -``` - -## Verification - -After starting your app, hit a few endpoints and check Traceway: - -1. **Endpoints page** — routes should appear grouped by pattern (e.g., `GET /api/users/[id]`), not by literal URL -2. **Issues page** — any thrown errors should appear with stack traces -3. **Endpoint detail → Spans tab** — Prisma queries, outgoing fetch calls, and manual spans should appear as children of the HTTP request - -## Checklist - -- [ ] OTel packages installed (+ `@prisma/instrumentation` if using Prisma) -- [ ] `instrumentation.ts` created at project root with `register()` function and runtime guard -- [ ] `lib/with-route.ts` created -- [ ] Every API route handler wrapped with `withRoute("/api/path/[param]", handler)` -- [ ] `experimental.instrumentationHook: true` added (Next.js 13.4-14.x only) -- [ ] `TRACEWAY_URL` and `TRACEWAY_TOKEN` env vars set -- [ ] Verified endpoints appear in Traceway dashboard From b0b3ad6db552ccd8ae88cc34bd3beb0d22a3d794 Mon Sep 17 00:00:00 2001 From: ddux Date: Thu, 11 Jun 2026 13:56:22 -0500 Subject: [PATCH 09/24] progress --- skills/traceway-debug/SKILL.md | 125 ------- skills/traceway-install-cli/SKILL.md | 126 ------- skills/traceway/SKILL.md | 200 +++++++++++ website/app/page.tsx | 30 ++ website/app/product/agent-skills/page.tsx | 341 +++++++++++++++++++ website/components/agent-debug-terminal.tsx | 64 ++++ website/components/site-footer.tsx | 1 + website/components/site-header.tsx | 7 + website/components/skill-install-command.tsx | 49 +++ 9 files changed, 692 insertions(+), 251 deletions(-) delete mode 100644 skills/traceway-debug/SKILL.md delete mode 100644 skills/traceway-install-cli/SKILL.md create mode 100644 skills/traceway/SKILL.md create mode 100644 website/app/product/agent-skills/page.tsx create mode 100644 website/components/agent-debug-terminal.tsx create mode 100644 website/components/skill-install-command.tsx diff --git a/skills/traceway-debug/SKILL.md b/skills/traceway-debug/SKILL.md deleted file mode 100644 index b0a11709..00000000 --- a/skills/traceway-debug/SKILL.md +++ /dev/null @@ -1,125 +0,0 @@ ---- -name: traceway-debug -description: Investigate a bug or production issue using observability data from a Traceway instance — exceptions, logs, endpoint stats, and metrics queried via the traceway CLI, correlated with the codebase. Use when the user describes a bug, error, crash, slowness, or anomaly and a Traceway instance is monitoring the affected app, e.g. "/traceway-debug users report 500s on checkout since this morning". ---- - -# Debug with Traceway - -Investigate a described bug using telemetry from a Traceway instance, then correlate findings with the codebase to find the root cause. - -## Prerequisites - -The `traceway` CLI must be installed, authenticated, and pointed at the right project: - -```bash -traceway version # installed? -traceway projects list # authenticated? right instance? -traceway projects use # select the project for the affected app -``` - -If the CLI is missing, install it first (see the `traceway-install-cli` skill, or https://github.com/tracewayapp/traceway/releases). If authentication fails (exit code 4), ask the user to run `traceway login --url https://`. - -**CLI behavior for agents:** piped output defaults to JSON (one record per line); `--fields a,b,c` trims responses; errors emit `{"error":"","message":"...","hint":"...","exit_code":N}` on stderr. Time ranges: `--since 1h|24h|7d` or `--from/--to` (RFC3339). All list commands paginate with `--page` / `--page-size` (default 50). - -## Step 1: Frame the Investigation - -From the user's bug description, extract: the symptom (error, wrong behavior, slowness, crash), the affected endpoint/feature, and the time window. Default to `--since 24h` if no timeframe was given; widen later if needed. - -## Step 2: Look for Exceptions - -Most bugs surface as grouped exceptions (Issues): - -```bash -# Recent exception groups, most impactful first -traceway exceptions list --since 24h - -# Search for terms from the bug description (error message, type, file) -traceway exceptions list --since 24h --search "checkout" -traceway exceptions list --since 24h --search "NullPointer" --search-type regex - -# Sort by what matters: lastSeen (default), firstSeen (regressions), count (volume) -traceway exceptions list --since 7d --order-by firstSeen - -# Full detail for a group: stack trace, occurrences, tags -traceway exceptions show -``` - -`exceptions show` is the high-value call — it returns the full stack trace and occurrence tags (user IDs, app versions, request context). Use `firstSeen` to correlate with deploys: a group that first appeared right after a release points at that release's diff. - -## Step 3: Query Logs Around the Failure - -```bash -# Errors and worse, in the window -traceway logs query --since 24h --min-severity 17 - -# Search log bodies for terms from the bug report -traceway logs query --since 24h --search "payment declined" - -# Narrow by service in multi-service projects -traceway logs query --since 24h --service checkout-api --min-severity 13 -``` - -Severity numbers are OTel-standard: 1=TRACE, 5=DEBUG, 9=INFO, 13=WARN, 17=ERROR, 21=FATAL. - -**Correlate by trace:** if a log record or exception includes a trace ID, pull every log line from that exact request: - -```bash -traceway logs query --since 24h --trace-id -``` - -This reconstructs the request timeline — usually the fastest route to a root cause. - -## Step 4: Check Endpoint Health (for Slowness / Error Rates) - -```bash -# Per-endpoint p50/p95/p99, error counts — sorted by impact -traceway endpoints list --since 24h - -# Find the affected endpoint -traceway endpoints list --since 24h --search "checkout" - -# Worst latency first -traceway endpoints list --since 24h --order-by p95 -``` - -Compare windows to find when a regression started, e.g. `--since 1h` vs `--since 7d`, or two explicit `--from/--to` ranges around a suspected deploy. - -## Step 5: Check Metrics (for Resource / Systemic Issues) - -```bash -# Latency over time -traceway metrics query --name http.server.duration --aggregation p95 --since 24h - -# Resource saturation (Go SDK default metrics) -traceway metrics query --name cpu.used_pcnt --aggregation avg --since 24h -traceway metrics query --name mem.used_pcnt --aggregation max --since 24h - -# Group a metric by tag, filter by tag -traceway metrics query --name http.server.duration --aggregation p95 --group-by endpoint --since 6h -traceway metrics query --name queue.depth --aggregation max --tag queue=email --since 6h -``` - -Aggregations: `avg, sum, count, min, max, p50, p95, p99`. Use `--interval-minutes` to control bucket size (0 = auto). Spikes that line up with the exception's `firstSeen` time confirm a systemic cause (OOM, CPU saturation, downstream slowness) rather than a code bug. - -## Step 6: Correlate with the Code - -With a stack trace, trace timeline, or regression window in hand: - -1. Open the files/lines named in the stack trace and read the failing path. -2. If the issue started at a known time, check what shipped then: `git log --since "" --until ""` or the deploy history. -3. Form a hypothesis that explains **all** observations (error message, affected endpoint, timing, volume) — not just the first stack frame. -4. Propose or implement the fix per the user's instruction. - -## Step 7: Report and Clean Up - -Summarize: symptom → evidence (exception hashes, log excerpts, metric anomalies) → root cause → fix. Include `traceway exceptions show ` references so the user can verify. - -After a fix is deployed and verified, the exception group can be archived — this mutates server state, so only do it when the user asks: - -```bash -traceway exceptions archive --yes -``` - -## If There Is No Telemetry - -Empty results usually mean the wrong project, wrong time window, or the app is not instrumented. Check `traceway projects list`, widen `--since`, and if the app was never connected to Traceway, set it up first (see the `traceway-setup` skill). diff --git a/skills/traceway-install-cli/SKILL.md b/skills/traceway-install-cli/SKILL.md deleted file mode 100644 index a317e7ec..00000000 --- a/skills/traceway-install-cli/SKILL.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -name: traceway-install-cli -description: Install the traceway CLI, authenticate against a Traceway instance, and select a project. Use when the user wants the Traceway command-line client set up on their machine, or when another Traceway skill needs the CLI and it is not installed yet. ---- - -# Install the Traceway CLI - -The `traceway` CLI queries a Traceway observability instance from the terminal — exceptions, logs, endpoints, and metrics. It is designed to be first-class for both LLM agents (stable JSON output, stable error identifiers, no hung prompts) and humans. - -## Step 1: Check for an Existing Install - -```bash -traceway version -``` - -If this prints a version, skip to Step 3 (authenticate). Source builds report `dev`. - -## Step 2: Install - -### Option A: Prebuilt binary (preferred) - -Binaries are published on the [tracewayapp/traceway releases page](https://github.com/tracewayapp/traceway/releases) under `CLI vX.Y.Z` tags (git tag format: `cli/vX.Y.Z`). Assets are named `traceway___.tar.gz` (`.zip` on Windows), with `os` ∈ `darwin`, `linux`, `windows` and `arch` ∈ `arm64`, `x86_64`. - -With `gh` (handles the fact that the latest release may be a Backend release, not a CLI one): - -```bash -OS=$(uname -s | tr '[:upper:]' '[:lower:]') -ARCH=$(uname -m); [ "$ARCH" = "aarch64" ] && ARCH=arm64 -TAG=$(gh release list --repo tracewayapp/traceway --limit 20 --json tagName --jq '[.[].tagName | select(startswith("cli/"))][0]') -gh release download "$TAG" --repo tracewayapp/traceway --pattern "traceway_*_${OS}_${ARCH}.tar.gz" --output - | tar -xz traceway -install -m 755 traceway ~/.local/bin/traceway && rm traceway -``` - -Without `gh`, resolve the download URL via the GitHub API: - -```bash -OS=$(uname -s | tr '[:upper:]' '[:lower:]') -ARCH=$(uname -m); [ "$ARCH" = "aarch64" ] && ARCH=arm64 -URL=$(curl -s "https://api.github.com/repos/tracewayapp/traceway/releases?per_page=20" \ - | grep -o "https://[^\"]*traceway_[^\"]*_${OS}_${ARCH}\.tar\.gz" | head -1) -curl -sL "$URL" | tar -xz traceway -install -m 755 traceway ~/.local/bin/traceway && rm traceway -``` - -Make sure `~/.local/bin` is on `PATH` (or install to `/usr/local/bin` instead). - -### Option B: Build from source - -Requires Go (see `cli/go.mod` for the minimum version): - -```bash -git clone https://github.com/tracewayapp/traceway -cd traceway/cli -go build -o bin/traceway ./cmd/traceway -install -m 755 bin/traceway ~/.local/bin/traceway -``` - -The repo also ships a Nix dev shell: `nix develop` then `just build`. - -### Verify - -```bash -traceway version -``` - -## Step 3: Authenticate - -```bash -traceway login --url https:// -``` - -This prompts for email and password, then stores the JWT. Config (URL, username) goes to `$XDG_CONFIG_HOME/traceway/config.json`; credentials and the active project go to `$XDG_STATE_HOME/traceway/state.json`. - -Login prompts interactively for the password — when running as an agent, ask the user to run the login command themselves. For non-interactive contexts where the user has the password in a secret store: - -```bash -printf '%s' "$TRACEWAY_PASSWORD" | traceway login --url https:// --username you@example.com --password-stdin -``` - -Never echo a password into the command line or shell history. - -Multiple instances/accounts coexist via profiles: - -```bash -traceway login --url https://traceway.example.com --profile work -traceway profiles list -traceway profiles use work -``` - -## Step 4: Select a Project - -```bash -traceway projects list -traceway projects use -``` - -The selected project is used implicitly by all subsequent commands. - -## Step 5: Smoke-Check - -```bash -traceway exceptions list --since 24h -traceway endpoints list --since 1h -``` - -## Usage Notes for Agents - -- **Output**: `--output table|json|yaml`. Default is `table` on a TTY and `json` otherwise — piped/scripted calls always get machine-readable output. `--fields a,b,c` projects list responses to just those keys. -- **Exit codes**: `0` success, `1` generic/API error, `2` usage error, `3` connection failure, `4` auth failure, `5` not found, `6` rate limited, `7` server 5xx. -- **Error envelope** (stderr, JSON mode): `{"error":"token_expired","message":"...","hint":"traceway login","exit_code":4}`. The `error` field is a stable snake_case identifier — branch on it. -- **Mutations** (`exceptions archive` / `unarchive`) require `--yes` (or `TRACEWAY_ASSUME_YES=1`) in non-TTY contexts; without it they fail fast with exit 2 instead of hanging on a prompt. -- Run `traceway --help` for full per-command flags. - -## Command Reference - -| Command | Purpose | -|---|---| -| `traceway login` / `logout` | Authenticate / forget the stored JWT | -| `traceway profiles {list,use}` | Manage multiple instances/accounts | -| `traceway projects {list,use}` | List or select the active project | -| `traceway exceptions list` | Recent grouped exceptions | -| `traceway exceptions show ` | A single exception group + occurrences | -| `traceway exceptions archive/unarchive ...` | Mutating; needs `--yes` non-interactively | -| `traceway logs query` | Query logs with severity / service / search filters | -| `traceway endpoints list` | Per-endpoint p50/p95/p99 stats | -| `traceway metrics query` | Time-series metric queries | diff --git a/skills/traceway/SKILL.md b/skills/traceway/SKILL.md new file mode 100644 index 00000000..5ec14b93 --- /dev/null +++ b/skills/traceway/SKILL.md @@ -0,0 +1,200 @@ +--- +name: traceway +description: Operate a Traceway observability instance through the traceway CLI: log in, query exceptions, logs, endpoints, and metrics, and debug production issues down to root cause. Use when the user invokes /traceway with a subcommand, e.g. "/traceway login", "/traceway debug issue ", "/traceway what's broken in prod", or whenever they want to investigate errors, crashes, slowness, or logs from an app monitored by Traceway. +--- + +# Traceway + +Drive a Traceway instance from the terminal with the `traceway` CLI. The first word of the argument decides the flow: + +| Invocation | Flow | +|---|---| +| `/traceway login` | **Login**: install the CLI if missing, authenticate, select a project | +| `/traceway debug ` | **Debug**: resolve the issue and investigate to root cause | +| `/traceway ` | **Query**: answer the observability question with CLI reads | +| `/traceway` (no argument) | Ask what they want: log in, debug an issue, or run a query | + +> The CLI is under active development. If a flag documented here does not appear in `traceway --help`, trust the binary. + +## Ground Rules (All Flows) + +- **Reads are safe**: any `list` / `show` / `query` subcommand may run freely; they never mutate server state. +- **Writes require explicit user instruction**: `exceptions archive` / `unarchive` are the only mutating data commands; only run them when the user asks by name, with `--yes` in non-interactive contexts. "Look at this error" means read it, not archive it. +- **Output**: piped output defaults to JSON (table on a TTY). Prefer JSON + `jq`, and `--fields a,b,c` to trim responses. Keep `--page-size` at 10 to 20 for triage. +- **Time windows**: always bound queries, default `--since 1h` for "now" questions, `--since 24h` otherwise. `--since` accepts `s`, `m`, `h`, lowercase `Nd` (no `1w`, no `7d2h`). Absolute windows via `--from` / `--to` (RFC3339). +- **Exit codes**: 0 ok, 1 generic/API, 2 usage, 3 connection, 4 auth, 5 not found, 6 rate limited, 7 server 5xx. Errors emit `{"error":"","message":"...","hint":"...","exit_code":N}` on stderr; branch on the `error` field. +- On exit code 4 (auth), do not run `traceway login` yourself; switch to the Login flow and let the user enter credentials. + +## Flow: Login + +### 1. Check for an existing install + +```bash +traceway version +``` + +If it prints a version, skip to authentication. + +### 2. Install if missing + +Prebuilt binaries are on the [tracewayapp/traceway releases page](https://github.com/tracewayapp/traceway/releases) under `cli/vX.Y.Z` tags (the latest release may be a Backend release, so filter for CLI tags): + +```bash +OS=$(uname -s | tr '[:upper:]' '[:lower:]') +ARCH=$(uname -m); [ "$ARCH" = "aarch64" ] && ARCH=arm64 +URL=$(curl -s "https://api.github.com/repos/tracewayapp/traceway/releases?per_page=20" \ + | grep -o "https://[^\"]*traceway_[^\"]*_${OS}_${ARCH}\.tar\.gz" | head -1) +curl -sL "$URL" | tar -xz traceway +install -m 755 traceway ~/.local/bin/traceway && rm traceway +``` + +Make sure `~/.local/bin` is on `PATH` (or install to `/usr/local/bin`). Fallback, build from source (requires Go): + +```bash +git clone https://github.com/tracewayapp/traceway && cd traceway/cli +go build -o bin/traceway ./cmd/traceway && install -m 755 bin/traceway ~/.local/bin/traceway +``` + +Verify with `traceway version`. + +### 3. Authenticate + +Login prompts for the password interactively, so ask the user to run it themselves (in Claude Code, suggest typing `! traceway login --url https://` so the output lands in the session): + +```bash +traceway login --url https:// +``` + +Non-interactive alternative when the password is in a secret store (never echo a password into the command line or shell history): + +```bash +printf '%s' "$TRACEWAY_PASSWORD" | traceway login --url https:// --username you@example.com --password-stdin +``` + +Multiple instances or accounts coexist via profiles: `traceway login --url ... --profile work`, then `traceway profiles list` / `traceway profiles use work`. + +### 4. Select a project and smoke-check + +```bash +traceway projects list +traceway projects use +traceway exceptions list --since 24h +``` + +The selected project is used implicitly by all subsequent commands. + +## Flow: Debug + +`/traceway debug issue X` or `/traceway debug `. + +### 1. Resolve the issue reference + +`X` can be several things; resolve it to an exception hash (16 hex chars): + +| Reference looks like | How to resolve | +|---|---| +| Dashboard URL (`https:///issues/`, `/issues//events`, `/issues//`) | Take the path segment right after `/issues/`; that is the hash | +| Bare 16-char hex string | Already the hash | +| Anything else (title, error message, type, file name) | Search: `traceway exceptions list --since 7d --search ""`; widen to `--since 30d` (and `--include-archived`) if empty | +| No issue reference, just a bug description | Skip to triage below | + +When a search returns multiple groups, show a shortlist (hash, count, lastSeen, first stack line) and ask the user which one before drilling in. + +```bash +traceway exceptions list --since 7d --search "checkout" --output json \ + | jq '.data[]? | {hash: .exceptionHash, count, lastSeen, top: (.stackTrace | split("\n")[0])}' +``` + +### 2. Drill into the issue + +```bash +traceway exceptions show +``` + +This is the high-value call: full stack trace, occurrence list with `recordedAt`, `attributes` (user IDs, app versions, request context), and optional `distributedTraceId` / `sessionId` per occurrence. `firstSeen` correlates with deploys: a group that first appeared right after a release points at that release's diff. A bogus hash exits 5 with `not_found`; fall back to search. + +### 3. Triage and correlate (also the entry point for free-form bug descriptions) + +From the description extract symptom, affected endpoint/feature, and time window, then read several signals before forming a hypothesis: + +```bash +traceway exceptions list --since 24h --order-by lastSeen # what is erroring (firstSeen for regressions, count for volume) +traceway logs query --since 24h --min-severity 17 # errors and worse +traceway logs query --since 24h --search "payment declined" # search log bodies +traceway logs query --since 24h --service checkout-api --min-severity 13 +traceway endpoints list --since 24h --search "checkout" # latency p50/p95/p99 and error counts, --order-by impact|count|p95|lastSeen +``` + +Severity is an OTel number, not a name: 1 TRACE, 5 DEBUG, 9 INFO, 13 WARN, 17 ERROR, 21 FATAL. The flag is `--min-severity 17`, never `--severity error`. + +**Correlate by trace**: when an occurrence or log line carries a trace ID, pull the whole request timeline; this is usually the fastest route to a root cause: + +```bash +traceway exceptions show $HASH --output json | jq -r '.occurrences[0].distributedTraceId' \ + | xargs -I{} traceway logs query --trace-id {} --output json +``` + +**Check metrics for systemic causes** (spikes lining up with `firstSeen` suggest saturation rather than a code bug): + +```bash +traceway metrics query --name system.cpu.utilization --aggregation max --since 24h +traceway metrics query --name --aggregation avg|sum|count|min|max|p50|p95|p99 [--tag key=value] [--group-by ] +``` + +There is no `metrics list`; a bogus name returns an empty `series: {}` cleanly, so probing names is safe. Host metrics from the Traceway OTel Agent live under `system.*` names. + +### 4. Correlate with the code + +1. Open the files and lines named in the stack trace and read the failing path. +2. If the issue started at a known time, check what shipped then: `git log --since "" --until ""` or the deploy history. +3. Form a hypothesis that explains ALL observations (error message, affected endpoint, timing, volume), not just the first stack frame. +4. Propose or implement the fix per the user's instruction. + +### 5. Report and clean up + +Summarize: symptom, evidence (exception hashes, log excerpts, metric anomalies), root cause, fix. Include `traceway exceptions show ` references so the user can verify. After a fix is deployed and verified, archive only when the user asks: + +```bash +traceway exceptions archive --yes +``` + +## Flow: Query + +For free-form requests ("what's broken in prod?", "is /api/checkout slow?", "show errors for service X"), use the read commands directly. + +### Command reference + +| Command | Purpose | +|---|---| +| `traceway projects {list,use}` | List or select the active project | +| `traceway exceptions list` | Grouped exceptions; `--search`, `--search-type text\|regex`, `--order-by lastSeen\|firstSeen\|count`, `--include-archived` | +| `traceway exceptions show ` | One group: full stack trace + occurrences | +| `traceway exceptions archive/unarchive ...` | Mutating; explicit user request + `--yes` only | +| `traceway logs query` | Logs; `--search` (`--search-type body\|attribute`), `--service`, `--min-severity `, `--trace-id` | +| `traceway endpoints list` | Per-endpoint p50/p95/p99 and counts; `--search`, `--order-by impact\|count\|p95\|lastSeen` | +| `traceway metrics query --name ` | Time series; `--aggregation`, `--tag`, `--group-by`, `--interval-minutes` | +| `traceway profiles {list,use}`, `login`, `logout`, `version` | Profile and session management | + +Not implemented yet (do not fabricate flags; point the user at the web UI): `traces show`, `sessions list/show`, `ai-traces list/show`, `endpoints show`, `metrics list/discover`. Tasks have no CLI command either; the dashboard's Tasks page covers them. + +### Recipes + +```bash +# What's broken right now +traceway exceptions list --since 1h --order-by lastSeen --page-size 10 --output json \ + | jq '.data[]? | {hash: .exceptionHash, count, lastSeen}' + +# Did anything NEW break since a deploy at 13:00 UTC +traceway exceptions list --from 2026-06-11T13:00:00Z --to "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --order-by firstSeen --output json \ + | jq '.data[]? | select(.firstSeen >= "2026-06-11T13:00:00Z") | {hash: .exceptionHash, firstSeen, count}' + +# Worst endpoint by latency +traceway endpoints list --since 1h --order-by p95 --page-size 1 --output json | jq '.data[0]' + +# Errors for one service (exceptions --search is free text, not a service filter; use logs) +traceway logs query --service checkout-api --min-severity 17 --since 1h --output json \ + | jq '.data[]? | {timestamp, body, traceId}' +``` + +Empty results (`data: null`) are not errors: widen the window, re-check the active project (`traceway projects list`), and if the app was never connected to Traceway, set it up first (the `traceway-setup` skill). diff --git a/website/app/page.tsx b/website/app/page.tsx index fbcec123..fe3d709e 100644 --- a/website/app/page.tsx +++ b/website/app/page.tsx @@ -15,6 +15,8 @@ import { Eyebrow } from "@/components/eyebrow"; import { DiscordIcon } from "@/components/discord-icon"; import { FinalCTA } from "@/components/final-cta"; import { Terminal } from "@/components/terminal"; +import { SkillInstallCommand } from "@/components/skill-install-command"; +import { AgentDebugTerminal } from "@/components/agent-debug-terminal"; import { StatsStrip } from "@/components/stats-strip"; import { HeroEmailCTA } from "@/components/hero-email-cta"; import { getCalendlyUrl } from "@/lib/calendly"; @@ -130,6 +132,34 @@ export default function Home() { + {/* AI-FIRST: install the agent skills, agent session terminal */} +
+
+
+ AI-first +

+ Your agents can fix production. Hand them the telemetry. +

+

+ One command installs the Traceway skills into Claude Code, + Cursor, or any agent that reads SKILL.md. Your agent queries + exceptions, logs, and metrics through the traceway CLI and + walks a bug to its root cause. +

+
+ +
+
+ + Explore Agent Skills + + +
+
+ +
+
+ {/* WHITE BAND: community, deploy, detect/resolve, cost render on white */}
{/* COMMUNITY: built in the open */} diff --git a/website/app/product/agent-skills/page.tsx b/website/app/product/agent-skills/page.tsx new file mode 100644 index 00000000..77c88e30 --- /dev/null +++ b/website/app/product/agent-skills/page.tsx @@ -0,0 +1,341 @@ +import Link from "next/link"; +import type { Metadata } from "next"; +import { Bot, Bug, Github, Plug, SquareTerminal } from "lucide-react"; + +import { Chip } from "@/components/chip"; +import { SectionHead } from "@/components/section-head"; +import { FeatureRow } from "@/components/feature-row"; +import { FaqList } from "@/components/faq-list"; +import { FinalCTA } from "@/components/final-cta"; +import { AuroraBackground } from "@/components/aurora-background"; +import { Eyebrow } from "@/components/eyebrow"; +import { Terminal } from "@/components/terminal"; +import { SkillInstallCommand } from "@/components/skill-install-command"; +import { AgentDebugTerminal } from "@/components/agent-debug-terminal"; +import { GITHUB_URL } from "@/lib/links"; + +export const metadata: Metadata = { + title: "Agent Skills · Traceway", + description: + "Install the Traceway agent skills and your coding agent can instrument your app, query production telemetry through the agent-first traceway CLI, and debug issues end to end.", +}; + +const SKILLS = [ + { + icon: Plug, + name: "/traceway-setup", + description: + "Instruments a project from scratch. Reads the repo, wires OpenTelemetry exporters for the backend and Traceway SDKs for web and mobile, then verifies that clean, grouped data arrives.", + }, + { + icon: Bug, + name: "/traceway-debug", + description: + "Investigates a bug end to end. Pulls grouped exceptions, queries logs around the failure, checks endpoint health and metrics, and correlates it all with your code.", + }, + { + icon: SquareTerminal, + name: "/traceway-install-cli", + description: + "Installs the traceway CLI, authenticates against your instance, cloud or self-hosted, and selects the project so every other skill can query it.", + }, +]; + +const AGENTS = [ + "Claude Code", + "Cursor", + "Codex", + "OpenCode", + "Gemini CLI", + "Copilot", +]; + +export default function AgentSkillsPage() { + return ( +
+
+ +
+ + + Agent Skills + +

+ AI-first observability. Your agent does the debugging. +

+

+ Install the Traceway skills once and your coding agent can + instrument your app, query production telemetry through the + agent-first traceway CLI, and take a bug from report to root + cause. +

+
+ + + + View on GitHub + +
+

+ Works with Claude Code, Cursor, Codex, and any agent that reads + SKILL.md. +

+
+
+ +
+ + Three skills, one install. + + } + description="Skills are plain Markdown playbooks your agent loads on demand. Each one encodes how a Traceway engineer would do the job, so your agent does it the same way." + /> +
+ {SKILLS.map((skill) => ( +
+
+ + + + + {skill.name} + +
+
+ {skill.description} +
+
+ ))} +
+
+ +
+
+
+
+ Debug +

+ From bug report to root cause +

+

+ Describe the bug the way a user reported it. The skill walks + your agent through the same investigation a senior engineer + would run: exceptions first, then logs around the failure, + endpoint health, and metrics. It ends in your code, not in a + dashboard. +

+
    +
  • Grouped exceptions with full stack traces and tags
  • +
  • Trace-correlated logs reconstruct the failing request
  • +
  • First-seen timestamps line up regressions with deploys
  • +
  • The agent reads the failing code and proposes the fix
  • +
+
+ +
+
+ +
+
+
+ The CLI +

+ A command line designed for agents first +

+

+ Most CLIs assume a human is typing. The traceway CLI assumes + an agent is: machine-readable output, stable error envelopes + with hints, and nothing that blocks a session waiting for + input. Humans still get tables on a TTY. +

+
    +
  • JSON by default when piped, tables when watched
  • +
  • --fields trims responses to exactly what was asked
  • +
  • Stable error identifiers and exit codes to branch on
  • +
  • Mutations fail fast without --yes, no hung prompts
  • +
+
+ + $ traceway exceptions list + --since 24h | head -1 + + ), + }, + { + ln: "2", + type: "mute", + content: + '{"hash":"82b58892","type":"TypeError","count":412}', + }, + { + ln: "3", + type: "tx", + content: ( + <> + $ traceway exceptions show + 82b58892 --fields type,stacktrace + + ), + }, + { + ln: "4", + type: "mute", + content: + '{"type":"TypeError","stacktrace":"src/checkout/session.ts:42 …"}', + }, + { + ln: "5", + type: "tx", + content: ( + <> + $ traceway exceptions + archive 82b58892 + + ), + }, + { + ln: "6", + type: "mute", + content: + '{"error":"confirmation_required","hint":"re-run with --yes","exit_code":2}', + }, + { + ln: "7", + type: "ok", + content: "# ✓ predictable for agents, readable for humans", + }, + ]} + /> +
+
+ +
+ + Set up by the agent, not by the docs + + } + description="/traceway-setup reads your repo and picks the right integration path: OpenTelemetry for any backend, Traceway SDKs for browser and mobile. It enforces the rules that keep data clean, then checks that telemetry is actually arriving." + bullets={[ + "Detects frameworks, services, and background jobs from the repo", + "OTel for backends, Traceway SDKs for web and mobile", + "Wires tasks, AI traces, and source maps, not just HTTP", + "Finishes by verifying grouped endpoints in your dashboard", + ]} + image={{ + src: "/images/performance-percentiles-overview.png", + alt: "Traceway endpoints grouped by route pattern with percentiles", + }} + /> +
+ +
+
+ Compatibility +

One format, every agent

+

+ Skills are plain Markdown in the open SKILL.md format. If your + agent can read instructions, it can run Traceway. No plugin + marketplace, no vendor lock-in, and the skills live in the same + MIT-licensed repo as the rest of Traceway. +

+
+ {AGENTS.map((agent) => ( + + {agent} + + ))} +
+
+
+
+ + + Give your agent production context. + + } + description="Install the skills, point the CLI at your instance, and let the agent take the next bug." + primary={{ + label: "Star on GitHub", + href: GITHUB_URL, + external: true, + }} + secondary={{ + label: "Start for free", + href: "https://cloud.tracewayapp.com/register", + }} + /> + +
+
+ +
+ +

+ Any agent that understands the SKILL.md convention: + Claude Code, Cursor, Codex, OpenCode, Gemini CLI, + Copilot, and more. +

+

+ npx skills add tracewayapp/traceway{" "} + detects the agents on your machine and installs the + skills where each one expects them. +

+ + ), + }, + { + q: "Can an agent damage my production data?", + a: "No. The CLI is read-only except for archiving exception groups, and every mutation requires an explicit --yes flag in non-interactive contexts. An agent can query telemetry freely but cannot change it by accident.", + }, + { + q: "Does this work with self-hosted Traceway?", + a: "Yes. The CLI authenticates against any instance with traceway login --url, and profiles let one machine talk to several instances, cloud or self-hosted.", + }, + { + q: "Do I need the CLI installed before the skills are useful?", + a: "No. The /traceway-install-cli skill handles that: it downloads the right binary for the platform, authenticates, and selects a project. The debug skill calls it automatically when the CLI is missing.", + }, + ]} + /> +
+
+
+
+ ); +} diff --git a/website/components/agent-debug-terminal.tsx b/website/components/agent-debug-terminal.tsx new file mode 100644 index 00000000..49f2f46c --- /dev/null +++ b/website/components/agent-debug-terminal.tsx @@ -0,0 +1,64 @@ +import { Terminal } from "@/components/terminal"; + +export function AgentDebugTerminal({ className }: { className?: string }) { + return ( + + /traceway-debug users report 500s + on checkout since this morning + + ), + }, + { ln: "2", type: "mute", content: "# querying production telemetry…" }, + { + ln: "3", + type: "tx", + content: ( + <> + $ traceway exceptions list --since + 24h --search checkout + + ), + }, + { + ln: "4", + type: "tx", + content: "TypeError: cart is null · 412 events · first seen 09:14", + }, + { + ln: "5", + type: "tx", + content: ( + <> + $ traceway logs query --trace-id + 9f2c41d8 + + ), + }, + { + ln: "6", + type: "mute", + content: "# reading src/checkout/session.ts:42", + }, + { + ln: "7", + type: "ok", + content: "# ✓ root cause: repeat purchase reuses an expired cart", + }, + { + ln: "8", + type: "ok", + content: "# ✓ fix ready for review in src/checkout/session.ts", + }, + ]} + showCursor + /> + ); +} diff --git a/website/components/site-footer.tsx b/website/components/site-footer.tsx index 21ffb224..69612c0c 100644 --- a/website/components/site-footer.tsx +++ b/website/components/site-footer.tsx @@ -23,6 +23,7 @@ const COLUMNS: Column[] = [ { heading: "Specialized", links: [ + { label: "Agent Skills", href: "/product/agent-skills" }, { label: "AI Tracing", href: "/product/ai-tracing" }, { label: "Performance", href: "/product/performance" }, { label: "Flutter Session Replay", href: "/product/flutter-session-replay" }, diff --git a/website/components/site-header.tsx b/website/components/site-header.tsx index 341ccca9..8845ee94 100644 --- a/website/components/site-header.tsx +++ b/website/components/site-header.tsx @@ -14,6 +14,7 @@ import { Workflow, Activity, Smartphone, + Bot, } from "lucide-react"; import { MobileNav } from "@/components/mobile-nav"; import { DiscordIcon } from "@/components/discord-icon"; @@ -61,6 +62,12 @@ const PILLARS: NavItem[] = [ ]; const SPECIALIZED: NavItem[] = [ + { + title: "Agent Skills", + description: "Your AI agent debugs with Traceway.", + href: "/product/agent-skills", + icon: Bot, + }, { title: "AI Tracing", description: "LLM cost, tokens, latency, conversations.", diff --git a/website/components/skill-install-command.tsx b/website/components/skill-install-command.tsx new file mode 100644 index 00000000..7d33ffe0 --- /dev/null +++ b/website/components/skill-install-command.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { useState } from "react"; +import { Check, Copy } from "lucide-react"; +import { cn } from "@/lib/utils"; + +const COMMAND = "npx skills add tracewayapp/traceway"; + +export function SkillInstallCommand({ className }: { className?: string }) { + const [copied, setCopied] = useState(false); + + async function copyCommand() { + try { + await navigator.clipboard.writeText(COMMAND); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + setCopied(false); + } + } + + return ( + + ); +} From a6cdc3a9c8a56feb728cfd5ec382b15d1dbd2075 Mon Sep 17 00:00:00 2001 From: jstojiljkovic Date: Thu, 11 Jun 2026 21:14:46 +0200 Subject: [PATCH 10/24] docs: align skills and README with verified data model --- cli/.claude/skills/traceway-cli.md | 2 ++ skills/traceway-setup/SKILL.md | 6 +++--- skills/traceway/SKILL.md | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cli/.claude/skills/traceway-cli.md b/cli/.claude/skills/traceway-cli.md index 7814ef99..3224e83a 100644 --- a/cli/.claude/skills/traceway-cli.md +++ b/cli/.claude/skills/traceway-cli.md @@ -193,6 +193,8 @@ traceway metrics query --name `--name` is required; omitting it exits with code 2 and `usage_error`. +**Percentile caveat:** the CLI accepts `p50`/`p95`/`p99` but the server has no quantile aggregation for metric points — it silently computes `avg` for them. Do not present percentile results from `metrics query` to a user; latency percentiles come from `traceway endpoints list` (computed from raw request durations). + **There is no `metrics list` / `metrics discover` subcommand.** The CLI cannot enumerate available metric names — the user has to provide one, or you have to guess from OpenTelemetry semantic conventions. Names observed live: `system.cpu.utilization`, `system.network.io`, `system.network.errors`, `system.network.dropped`. If the user is checking system telemetry, those four are the likeliest starting points; if they're checking app-level metrics, ask. A bogus metric name returns exit 0 with `series: {}` (clean empty, not an error). Use that to probe whether a name exists. diff --git a/skills/traceway-setup/SKILL.md b/skills/traceway-setup/SKILL.md index 2229c759..4e375215 100644 --- a/skills/traceway-setup/SKILL.md +++ b/skills/traceway-setup/SKILL.md @@ -37,13 +37,13 @@ Two hard rules apply to every backend integration: | OTel Span | Condition | Traceway Concept | |---|---|---| -| Root span | `SpanKind = SERVER` or `INTERNAL` with HTTP attributes | **Endpoint** | +| Root span (or span whose parent lives in another service) | `SpanKind = SERVER` or `INTERNAL` with HTTP attributes | **Endpoint** | | Any span | `SpanKind = CONSUMER` | **Task** | | Root span | `SpanKind = INTERNAL` with a `console.command` attribute | **Task** (CLI command) | | Any span | Has any `gen_ai.*` attribute | **AI Trace** | | Non-root span | Has a parent span ID, matched none of the above | **Span** (child) | -| Exception event | Event named `"exception"` on any span | **Issue** | -| Root span | Matched none of the above | **Dropped silently** | +| Exception event | Event named `"exception"` on any span above | **Issue** | +| Root span | Matched none of the above | **Dropped silently** (including its exception events) | For the exact classification rules, endpoint naming, metric conversion, and all the quirks, read `data-model.md` in this skill directory. It is the authoritative reference. diff --git a/skills/traceway/SKILL.md b/skills/traceway/SKILL.md index 5ec14b93..6a74cf87 100644 --- a/skills/traceway/SKILL.md +++ b/skills/traceway/SKILL.md @@ -138,10 +138,10 @@ traceway exceptions show $HASH --output json | jq -r '.occurrences[0].distribute ```bash traceway metrics query --name system.cpu.utilization --aggregation max --since 24h -traceway metrics query --name --aggregation avg|sum|count|min|max|p50|p95|p99 [--tag key=value] [--group-by ] +traceway metrics query --name --aggregation avg|sum|count|min|max [--tag key=value] [--group-by ] ``` -There is no `metrics list`; a bogus name returns an empty `series: {}` cleanly, so probing names is safe. Host metrics from the Traceway OTel Agent live under `system.*` names. +The CLI also accepts `p50|p95|p99`, but the server has no quantile aggregation for metric points and silently computes `avg` for them — never present those as percentiles. Latency percentiles come from `traceway endpoints list`, computed from raw request durations. There is no `metrics list`; a bogus name returns an empty `series: {}` cleanly, so probing names is safe. Host metrics from the Traceway OTel Agent live under `system.*` names, and OTLP histogram metrics are stored as two series, `.avg` and `.count`. ### 4. Correlate with the code From a498b382a84b760782762b1726ff50260ae281c0 Mon Sep 17 00:00:00 2001 From: ddux Date: Thu, 11 Jun 2026 14:15:54 -0500 Subject: [PATCH 11/24] more progress --- skills/traceway-setup/SKILL.md | 9 +- website/app/page.tsx | 55 ++++++------ website/app/product/agent-skills/page.tsx | 94 +++++++++++--------- website/components/agent-debug-terminal.tsx | 4 +- website/components/skill-install-command.tsx | 24 +++-- 5 files changed, 107 insertions(+), 79 deletions(-) diff --git a/skills/traceway-setup/SKILL.md b/skills/traceway-setup/SKILL.md index 2229c759..82647026 100644 --- a/skills/traceway-setup/SKILL.md +++ b/skills/traceway-setup/SKILL.md @@ -15,7 +15,14 @@ Connect an existing project to a Traceway instance so it reports endpoints, span | **Project token** | `abc123...` | Traceway dashboard → Connection page | | **Source map upload token** (optional, frontend only) | `def456...` | Traceway dashboard → Connection page → Source Maps | -Instance URL and project token may be provided in the invocation (e.g. `/traceway-setup with token abc123 and url https://traceway.example.com`). If either is missing, check for existing `TRACEWAY_URL` / `TRACEWAY_TOKEN` environment variables or `.env` entries in the project, otherwise ask the user before proceeding. Never invent placeholder values in committed code; wire everything through environment variables. +Instance URL and project token may be provided in the invocation (e.g. `/traceway-setup with token abc123 and url https://traceway.example.com`). If either is missing: + +1. Check for existing `TRACEWAY_URL` / `TRACEWAY_TOKEN` environment variables or `.env` entries in the project. +2. Still missing: ask the user whether they already have a Traceway account. + - **Yes**: ask them to open their Traceway dashboard, go to the project's **Connection** page, and paste the instance URL and project token here. If no project exists yet for this app, have them create one first, picking the framework that matches this codebase. + - **No**: send them to the register page to create an account: https://cloud.tracewayapp.com/register (or `https:///register` if they are self-hosting). After registering and creating a project, the Connection page shows the token; ask them to paste the URL and token here. + +Do not proceed without real values. Never invent placeholder values in committed code; wire everything through environment variables. ## Integration Paths diff --git a/website/app/page.tsx b/website/app/page.tsx index fe3d709e..f13d4d4b 100644 --- a/website/app/page.tsx +++ b/website/app/page.tsx @@ -132,36 +132,37 @@ export default function Home() {
- {/* AI-FIRST: install the agent skills, agent session terminal */} -
-
-
- AI-first -

- Your agents can fix production. Hand them the telemetry. -

-

- One command installs the Traceway skills into Claude Code, - Cursor, or any agent that reads SKILL.md. Your agent queries - exceptions, logs, and metrics through the traceway CLI and - walks a bug to its root cause. -

-
- -
-
- - Explore Agent Skills - - + {/* WHITE BAND: AI-first, community, deploy, detect/resolve, cost render on white */} +
+ {/* AI-FIRST: install the agent skills, agent session terminal */} +
+
+
+ AI-first +

+ Your agents can fix production.{" "} + Hand them the telemetry. +

+

+ One command installs the Traceway skills into Claude Code, + Cursor, or any agent that reads SKILL.md. Your agent queries + exceptions, logs, and metrics through the traceway CLI and + walks a bug to its root cause. +

+
+ +
+
+ + Explore Agent Skills + + +
+
- -
-
+ - {/* WHITE BAND: community, deploy, detect/resolve, cost render on white */} -
{/* COMMUNITY: built in the open */}
diff --git a/website/app/product/agent-skills/page.tsx b/website/app/product/agent-skills/page.tsx index 77c88e30..889842e4 100644 --- a/website/app/product/agent-skills/page.tsx +++ b/website/app/product/agent-skills/page.tsx @@ -1,6 +1,6 @@ import Link from "next/link"; import type { Metadata } from "next"; -import { Bot, Bug, Github, Plug, SquareTerminal } from "lucide-react"; +import { Bot, Plug, SquareTerminal } from "lucide-react"; import { Chip } from "@/components/chip"; import { SectionHead } from "@/components/section-head"; @@ -26,18 +26,14 @@ const SKILLS = [ name: "/traceway-setup", description: "Instruments a project from scratch. Reads the repo, wires OpenTelemetry exporters for the backend and Traceway SDKs for web and mobile, then verifies that clean, grouped data arrives.", - }, - { - icon: Bug, - name: "/traceway-debug", - description: - "Investigates a bug end to end. Pulls grouped exceptions, queries logs around the failure, checks endpoint health and metrics, and correlates it all with your code.", + tags: ["OTel backends", "Web + mobile SDKs", "Source maps", "Verification"], }, { icon: SquareTerminal, - name: "/traceway-install-cli", + name: "/traceway", description: - "Installs the traceway CLI, authenticates against your instance, cloud or self-hosted, and selects the project so every other skill can query it.", + "Operates Traceway day to day. Installs and authenticates the traceway CLI, queries exceptions, logs, endpoints, and metrics, and walks a bug from report to root cause.", + tags: ["CLI install", "Telemetry queries", "Debugging", "Issue triage"], }, ]; @@ -56,35 +52,36 @@ export default function AgentSkillsPage() {
- - - Agent Skills - -

- AI-first observability. Your agent does the debugging. -

-

- Install the Traceway skills once and your coding agent can - instrument your app, query production telemetry through the - agent-first traceway CLI, and take a bug from report to root - cause. -

-
- - - - View on GitHub - +
+ + + Agent Skills + +

+ AI-first observability. Your agent does the debugging. +

+

+ Your agent sets up Traceway, queries production telemetry, and + finds the root cause. +

+
+ +
+

+ Works with Claude Code, Cursor, Codex, and any agent that reads + SKILL.md. +

+

+ + View on GitHub → + +

-

- Works with Claude Code, Cursor, Codex, and any agent that reads - SKILL.md. -

@@ -93,12 +90,12 @@ export default function AgentSkillsPage() { eyebrow="The skills" title={ <> - Three skills, one install. + Two skills, one install. } - description="Skills are plain Markdown playbooks your agent loads on demand. Each one encodes how a Traceway engineer would do the job, so your agent does it the same way." + description="Skills are plain Markdown playbooks your agent loads on demand. One sets your project up; the other runs the day-to-day investigation." /> -
+
{SKILLS.map((skill) => (
@@ -112,8 +109,17 @@ export default function AgentSkillsPage() { {skill.name}
-
- {skill.description} +
+

+ {skill.description} +

+
+ {skill.tags.map((tag) => ( + + {tag} + + ))} +
))} @@ -300,7 +306,7 @@ export default function AgentSkillsPage() { items={[ { q: "What exactly is an agent skill?", - a: "A skill is a Markdown playbook (SKILL.md) that compatible coding agents load when a task matches its description. Traceway ships three: one to instrument a project, one to investigate bugs with production telemetry, and one to install and authenticate the traceway CLI. They are versioned in the open-source repo like any other code.", + a: "A skill is a Markdown playbook (SKILL.md) that compatible coding agents load when a task matches its description. Traceway ships two: one that instruments a project, and one that operates Traceway from the terminal, from installing the CLI to querying telemetry and investigating bugs. They are versioned in the open-source repo like any other code.", }, { q: "Which agents are supported?", @@ -329,7 +335,7 @@ export default function AgentSkillsPage() { }, { q: "Do I need the CLI installed before the skills are useful?", - a: "No. The /traceway-install-cli skill handles that: it downloads the right binary for the platform, authenticates, and selects a project. The debug skill calls it automatically when the CLI is missing.", + a: "No. The /traceway skill installs it: it downloads the right binary for the platform, authenticates against your instance, and selects a project before running its first query.", }, ]} /> diff --git a/website/components/agent-debug-terminal.tsx b/website/components/agent-debug-terminal.tsx index 49f2f46c..4ef3c16e 100644 --- a/website/components/agent-debug-terminal.tsx +++ b/website/components/agent-debug-terminal.tsx @@ -11,8 +11,8 @@ export function AgentDebugTerminal({ className }: { className?: string }) { type: "tx", content: ( <> - /traceway-debug users report 500s - on checkout since this morning + /traceway users report 500s on + checkout since this morning ), }, diff --git a/website/components/skill-install-command.tsx b/website/components/skill-install-command.tsx index 7d33ffe0..6d617b86 100644 --- a/website/components/skill-install-command.tsx +++ b/website/components/skill-install-command.tsx @@ -6,7 +6,13 @@ import { cn } from "@/lib/utils"; const COMMAND = "npx skills add tracewayapp/traceway"; -export function SkillInstallCommand({ className }: { className?: string }) { +export function SkillInstallCommand({ + className, + size = "default", +}: { + className?: string; + size?: "default" | "lg"; +}) { const [copied, setCopied] = useState(false); async function copyCommand() { @@ -24,22 +30,30 @@ export function SkillInstallCommand({ className }: { className?: string }) { type="button" onClick={copyCommand} className={cn( - "group inline-flex max-w-full items-center gap-3 rounded-lg border border-hair-2 bg-ink-1 py-2.5 pl-4 pr-2.5 text-left font-mono text-xs sm:text-[0.8125rem] transition-colors hover:bg-ink-2", + "group inline-flex max-w-full items-center rounded-lg border border-hair-2 bg-ink-1 text-left font-mono transition-colors hover:bg-ink-2", + size === "lg" + ? "gap-4 py-4 pl-6 pr-3.5 text-sm sm:text-base" + : "gap-3 py-2.5 pl-4 pr-2.5 text-xs sm:text-[0.8125rem]", className )} > $ - {COMMAND} + {COMMAND} - {copied ? : } + {copied ? ( + + ) : ( + + )} {copied ? "Copied to clipboard" : "Copy install command"} From 91b00fad3a85d23726c4a6deff4abc353d31cdde Mon Sep 17 00:00:00 2001 From: ddux Date: Thu, 11 Jun 2026 14:17:32 -0500 Subject: [PATCH 12/24] Update page.tsx --- website/app/product/agent-skills/page.tsx | 47 +++++++++++------------ 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/website/app/product/agent-skills/page.tsx b/website/app/product/agent-skills/page.tsx index 889842e4..1d8b6bdd 100644 --- a/website/app/product/agent-skills/page.tsx +++ b/website/app/product/agent-skills/page.tsx @@ -25,14 +25,14 @@ const SKILLS = [ icon: Plug, name: "/traceway-setup", description: - "Instruments a project from scratch. Reads the repo, wires OpenTelemetry exporters for the backend and Traceway SDKs for web and mobile, then verifies that clean, grouped data arrives.", + "Reads your repo and wires it up: OpenTelemetry for the backend, Traceway SDKs for web and mobile. Verifies data arrives clean and grouped.", tags: ["OTel backends", "Web + mobile SDKs", "Source maps", "Verification"], }, { icon: SquareTerminal, name: "/traceway", description: - "Operates Traceway day to day. Installs and authenticates the traceway CLI, queries exceptions, logs, endpoints, and metrics, and walks a bug from report to root cause.", + "Installs the traceway CLI, then uses it: exceptions, logs, endpoints, and metrics, from bug report to root cause.", tags: ["CLI install", "Telemetry queries", "Debugging", "Issue triage"], }, ]; @@ -93,7 +93,7 @@ export default function AgentSkillsPage() { Two skills, one install. } - description="Skills are plain Markdown playbooks your agent loads on demand. One sets your project up; the other runs the day-to-day investigation." + description="Plain Markdown playbooks your agent loads on demand. One sets up your project, the other queries and debugs it." />
{SKILLS.map((skill) => ( @@ -135,17 +135,15 @@ export default function AgentSkillsPage() { From bug report to root cause

- Describe the bug the way a user reported it. The skill walks - your agent through the same investigation a senior engineer - would run: exceptions first, then logs around the failure, - endpoint health, and metrics. It ends in your code, not in a - dashboard. + Paste the bug report as written. The agent pulls the matching + exceptions, the logs around the failure, and endpoint stats, + then opens the failing code.

    -
  • Grouped exceptions with full stack traces and tags
  • -
  • Trace-correlated logs reconstruct the failing request
  • -
  • First-seen timestamps line up regressions with deploys
  • -
  • The agent reads the failing code and proposes the fix
  • +
  • Grouped exceptions with full stack traces
  • +
  • Logs correlated by trace id
  • +
  • First-seen times point at the breaking deploy
  • +
  • Ends with a proposed fix in your code
@@ -160,16 +158,15 @@ export default function AgentSkillsPage() { A command line designed for agents first

- Most CLIs assume a human is typing. The traceway CLI assumes - an agent is: machine-readable output, stable error envelopes - with hints, and nothing that blocks a session waiting for - input. Humans still get tables on a TTY. + Built for non-interactive use: JSON output, stable errors, and + nothing that hangs waiting for input. Humans on a TTY still + get tables.

    -
  • JSON by default when piped, tables when watched
  • -
  • --fields trims responses to exactly what was asked
  • -
  • Stable error identifiers and exit codes to branch on
  • -
  • Mutations fail fast without --yes, no hung prompts
  • +
  • JSON when piped, tables on a TTY
  • +
  • --fields trims responses to what was asked
  • +
  • Stable error identifiers and exit codes
  • +
  • Mutations require --yes, nothing hangs
not by the docs } - description="/traceway-setup reads your repo and picks the right integration path: OpenTelemetry for any backend, Traceway SDKs for browser and mobile. It enforces the rules that keep data clean, then checks that telemetry is actually arriving." + description="/traceway-setup picks the right path for each part of the stack: OTel for backends, Traceway SDKs for browser and mobile. Then it verifies data is actually arriving." bullets={[ - "Detects frameworks, services, and background jobs from the repo", - "OTel for backends, Traceway SDKs for web and mobile", - "Wires tasks, AI traces, and source maps, not just HTTP", - "Finishes by verifying grouped endpoints in your dashboard", + "Detects frameworks and services from the repo", + "OTel for backends, SDKs for web and mobile", + "Covers background tasks, AI traces, and source maps", + "Verifies grouped endpoints in the dashboard", ]} image={{ src: "/images/performance-percentiles-overview.png", From 40922960e1c2c686a3a308ef6cb7ca9cb8040280 Mon Sep 17 00:00:00 2001 From: ddux Date: Thu, 11 Jun 2026 14:23:11 -0500 Subject: [PATCH 13/24] website progress --- website/app/page.tsx | 7 +++-- website/app/product/agent-skills/page.tsx | 28 +++++++++----------- website/components/skill-install-command.tsx | 4 +-- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/website/app/page.tsx b/website/app/page.tsx index f13d4d4b..7a70c9ab 100644 --- a/website/app/page.tsx +++ b/website/app/page.tsx @@ -144,10 +144,9 @@ export default function Home() { Hand them the telemetry.

- One command installs the Traceway skills into Claude Code, - Cursor, or any agent that reads SKILL.md. Your agent queries - exceptions, logs, and metrics through the traceway CLI and - walks a bug to its root cause. + One command installs the skills into Claude Code, Cursor, or + any agent that reads SKILL.md. From there your agent queries + exceptions, logs, and metrics itself.

diff --git a/website/app/product/agent-skills/page.tsx b/website/app/product/agent-skills/page.tsx index 1d8b6bdd..3c4440a5 100644 --- a/website/app/product/agent-skills/page.tsx +++ b/website/app/product/agent-skills/page.tsx @@ -257,10 +257,9 @@ export default function AgentSkillsPage() { Compatibility

One format, every agent

- Skills are plain Markdown in the open SKILL.md format. If your - agent can read instructions, it can run Traceway. No plugin - marketplace, no vendor lock-in, and the skills live in the same - MIT-licensed repo as the rest of Traceway. + Skills are plain Markdown in the open SKILL.md format. No + marketplace, no lock-in. They live in the same MIT-licensed + repo as Traceway itself.

{AGENTS.map((agent) => ( @@ -279,7 +278,7 @@ export default function AgentSkillsPage() { Give your agent production context. } - description="Install the skills, point the CLI at your instance, and let the agent take the next bug." + description="Install the skills and let your agent take the next bug." primary={{ label: "Star on GitHub", href: GITHUB_URL, @@ -303,36 +302,35 @@ export default function AgentSkillsPage() { items={[ { q: "What exactly is an agent skill?", - a: "A skill is a Markdown playbook (SKILL.md) that compatible coding agents load when a task matches its description. Traceway ships two: one that instruments a project, and one that operates Traceway from the terminal, from installing the CLI to querying telemetry and investigating bugs. They are versioned in the open-source repo like any other code.", + a: "A SKILL.md file: Markdown instructions your agent loads when a task matches. Traceway ships two, one that sets up a project and one that queries and debugs it. Both live in the open-source repo.", }, { q: "Which agents are supported?", a: ( <>

- Any agent that understands the SKILL.md convention: - Claude Code, Cursor, Codex, OpenCode, Gemini CLI, - Copilot, and more. + Anything that reads SKILL.md: Claude Code, Cursor, + Codex, OpenCode, Gemini CLI, Copilot, and more.

npx skills add tracewayapp/traceway{" "} - detects the agents on your machine and installs the - skills where each one expects them. + installs the skills for every agent it finds on your + machine.

), }, { q: "Can an agent damage my production data?", - a: "No. The CLI is read-only except for archiving exception groups, and every mutation requires an explicit --yes flag in non-interactive contexts. An agent can query telemetry freely but cannot change it by accident.", + a: "No. The CLI is read-only apart from archiving exception groups, and that requires an explicit --yes flag.", }, { q: "Does this work with self-hosted Traceway?", - a: "Yes. The CLI authenticates against any instance with traceway login --url, and profiles let one machine talk to several instances, cloud or self-hosted.", + a: "Yes. traceway login --url works against any instance, and profiles let one machine use several.", }, { - q: "Do I need the CLI installed before the skills are useful?", - a: "No. The /traceway skill installs it: it downloads the right binary for the platform, authenticates against your instance, and selects a project before running its first query.", + q: "Do I need the CLI installed first?", + a: "No. The /traceway skill installs and authenticates it before running its first query.", }, ]} /> diff --git a/website/components/skill-install-command.tsx b/website/components/skill-install-command.tsx index 6d617b86..6ff5f005 100644 --- a/website/components/skill-install-command.tsx +++ b/website/components/skill-install-command.tsx @@ -32,7 +32,7 @@ export function SkillInstallCommand({ className={cn( "group inline-flex max-w-full items-center rounded-lg border border-hair-2 bg-ink-1 text-left font-mono transition-colors hover:bg-ink-2", size === "lg" - ? "gap-4 py-4 pl-6 pr-3.5 text-sm sm:text-base" + ? "gap-3 py-3 pl-4 pr-2.5 text-xs sm:gap-4 sm:py-4 sm:pl-6 sm:pr-3.5 sm:text-base" : "gap-3 py-2.5 pl-4 pr-2.5 text-xs sm:text-[0.8125rem]", className )} @@ -44,7 +44,7 @@ export function SkillInstallCommand({ Date: Thu, 11 Jun 2026 15:55:16 -0500 Subject: [PATCH 14/24] making the website niceeee --- website/app/globals.css | 28 +++- website/app/product/agent-skills/page.tsx | 82 +++++------ website/components/agent-debug-terminal.tsx | 151 ++++++++++++-------- website/public/images/claudebot.png | Bin 0 -> 880 bytes 4 files changed, 154 insertions(+), 107 deletions(-) create mode 100644 website/public/images/claudebot.png diff --git a/website/app/globals.css b/website/app/globals.css index c4be141c..588d81ee 100644 --- a/website/app/globals.css +++ b/website/app/globals.css @@ -19,7 +19,7 @@ --ink-4: #1d2430; --ink-5: #283041; --hair: rgba(255, 255, 255, 0.06); - --hair-2: rgba(255, 255, 255, 0.10); + --hair-2: rgba(255, 255, 255, 0.1); /* Text */ --fg-0: #f4f6fb; @@ -290,7 +290,7 @@ --ink-1: #0a0d14; --ink-3: #151a24; --hair: rgba(255, 255, 255, 0.06); - --hair-2: rgba(255, 255, 255, 0.10); + --hair-2: rgba(255, 255, 255, 0.1); --fg-0: #f4f6fb; --fg-1: #c9d0dd; --fg-2: #8a93a6; @@ -306,9 +306,12 @@ /* Terminal stays a dark object inside the light band. */ .band-light .term { --ink-1: #0a0d14; + --ink-2: #0f131c; --hair: rgba(255, 255, 255, 0.06); - --hair-2: rgba(255, 255, 255, 0.10); + --hair-2: rgba(255, 255, 255, 0.1); + --fg-0: #f4f6fb; --fg-1: #c9d0dd; + --fg-2: #8a93a6; --fg-3: #5a6374; --a2: #00d4ff; --ok: #22e0a8; @@ -351,7 +354,10 @@ font-weight: 500; font-size: 14px; letter-spacing: -0.01em; - transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease; + transition: + background 0.15s ease, + border-color 0.15s ease, + color 0.15s ease; border: 1px solid transparent; white-space: nowrap; cursor: pointer; @@ -360,7 +366,9 @@ .btn-primary { background: var(--fg-0); color: var(--ink-0); - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.6) inset, 0 0 0 1px rgba(255, 255, 255, 0.12); + box-shadow: + 0 1px 0 rgba(255, 255, 255, 0.6) inset, + 0 0 0 1px rgba(255, 255, 255, 0.12); } .btn-primary:hover { background: color-mix(in oklab, var(--fg-0) 92%, white); @@ -394,7 +402,9 @@ border-radius: var(--radius-lg); padding: 20px 22px; text-decoration: none; - transition: background 0.15s ease, border-color 0.15s ease; + transition: + background 0.15s ease, + border-color 0.15s ease; } .blog-card:hover { background: var(--ink-3); @@ -419,7 +429,10 @@ color: var(--fg-3); text-decoration: none; border: 1px solid transparent; - transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease; + transition: + background 0.15s ease, + border-color 0.15s ease, + color 0.15s ease; } .blog-tab:hover { color: var(--fg-1); @@ -581,6 +594,7 @@ font-family: var(--font-mono); font-size: 13px; line-height: 1.75; + padding-top: 12px; } .term-line { display: grid; diff --git a/website/app/product/agent-skills/page.tsx b/website/app/product/agent-skills/page.tsx index 3c4440a5..a76fb376 100644 --- a/website/app/product/agent-skills/page.tsx +++ b/website/app/product/agent-skills/page.tsx @@ -85,47 +85,6 @@ export default function AgentSkillsPage() {
-
- - Two skills, one install. - - } - description="Plain Markdown playbooks your agent loads on demand. One sets up your project, the other queries and debugs it." - /> -
- {SKILLS.map((skill) => ( -
-
- - - - - {skill.name} - -
-
-

- {skill.description} -

-
- {skill.tags.map((tag) => ( - - {tag} - - ))} -
-
-
- ))} -
-
-
@@ -150,6 +109,47 @@ export default function AgentSkillsPage() {
+
+ + Two skills, one install. + + } + description="Plain Markdown playbooks your agent loads on demand. One sets up your project, the other queries and debugs it." + /> +
+ {SKILLS.map((skill) => ( +
+
+ + + + + {skill.name} + +
+
+

+ {skill.description} +

+
+ {skill.tags.map((tag) => ( + + {tag} + + ))} +
+
+
+ ))} +
+
+
diff --git a/website/components/agent-debug-terminal.tsx b/website/components/agent-debug-terminal.tsx index 4ef3c16e..34b100cf 100644 --- a/website/components/agent-debug-terminal.tsx +++ b/website/components/agent-debug-terminal.tsx @@ -1,64 +1,97 @@ -import { Terminal } from "@/components/terminal"; +import Image from "next/image"; +import { cn } from "@/lib/utils"; export function AgentDebugTerminal({ className }: { className?: string }) { return ( - - /traceway users report 500s on - checkout since this morning - - ), - }, - { ln: "2", type: "mute", content: "# querying production telemetry…" }, - { - ln: "3", - type: "tx", - content: ( - <> - $ traceway exceptions list --since - 24h --search checkout - - ), - }, - { - ln: "4", - type: "tx", - content: "TypeError: cart is null · 412 events · first seen 09:14", - }, - { - ln: "5", - type: "tx", - content: ( - <> - $ traceway logs query --trace-id - 9f2c41d8 - - ), - }, - { - ln: "6", - type: "mute", - content: "# reading src/checkout/session.ts:42", - }, - { - ln: "7", - type: "ok", - content: "# ✓ root cause: repeat purchase reuses an expired cart", - }, - { - ln: "8", - type: "ok", - content: "# ✓ fix ready for review in src/checkout/session.ts", - }, - ]} - showCursor - /> +
+
+
+ Claude +
+
+ Claude Code{" "} + v2.1.173 +
+
Fable 5 · Claude Max
+
~/shop-api
+
+
+ +
+ + + /traceway users report 500s on checkout since this morning + +
+ +
+ + + Bash + + (traceway exceptions list --since 24h --search checkout) + + +
+
+ + + {"⎿ TypeError: cart is null · 412 events · first seen 09:14"} + +
+ +
+ + + Bash + + (traceway logs query --trace-id 9f2c41d8) + + +
+
+ + + {"⎿ 12 log records · cart expired 09:13, reused 09:14"} + +
+ +
+ + + Read + (src/checkout/session.ts) + +
+
+ + + {"⎿ Read 86 lines"} + +
+ +
+ + + Root cause: repeat purchase reuses an expired cart. Fix ready for + review in src/checkout/session.ts:42. + +
+ +
+ + + + +
+
+
); } diff --git a/website/public/images/claudebot.png b/website/public/images/claudebot.png new file mode 100644 index 0000000000000000000000000000000000000000..c83a2f133360a6d340aaeeb25d33359301b55a09 GIT binary patch literal 880 zcmeAS@N?(olHy`uVBq!ia0vp^Gk`degAGV(#bmmtT}V`<;yx0|V1vPZ!6KiaBrZp7$0>lsW$K zy|HnGph!qWrIw8gr|hCl-X-2jO@4R(T%0c;H_6Lo%VYt^=K;-Kd>7*dSQ53O+6}B+ zoB~`*SMrqJom*+eZuoi4+w@y+{#w<~sO8I_VSDDB)%(4zGvwc_oe}AMK|=cOn~M`q z3M(G=s1^EipMxieg-w&GS)|di>wto5!UQD)4?&3~_=H!c)NOblc=5#Vk~5Z3I>FEO z)<#D0-Fts;TWH0suij_={5%vYa)0{)`F4{-b6>K~o)N`yYu>vX>CEX(b@dwYU{RBy;#URwG_eEy0W?W?nF z>;CFh)PF13J@vcEjI+Cr=%s{~Z_2narE%fQkJBH)UF5S76wmOKbLh*2~7ZH2ziwN literal 0 HcmV?d00001 From 3a76824ac20413ba6051dceeeaf69c1d47bc2cb1 Mon Sep 17 00:00:00 2001 From: ddux Date: Thu, 11 Jun 2026 16:05:57 -0500 Subject: [PATCH 15/24] Update page.tsx --- website/app/product/agent-skills/page.tsx | 85 +++++++++++------------ 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/website/app/product/agent-skills/page.tsx b/website/app/product/agent-skills/page.tsx index a76fb376..1083cbfa 100644 --- a/website/app/product/agent-skills/page.tsx +++ b/website/app/product/agent-skills/page.tsx @@ -1,6 +1,6 @@ import Link from "next/link"; import type { Metadata } from "next"; -import { Bot, Plug, SquareTerminal } from "lucide-react"; +import { Bot } from "lucide-react"; import { Chip } from "@/components/chip"; import { SectionHead } from "@/components/section-head"; @@ -22,14 +22,14 @@ export const metadata: Metadata = { const SKILLS = [ { - icon: Plug, + label: "01 · Setup", name: "/traceway-setup", description: "Reads your repo and wires it up: OpenTelemetry for the backend, Traceway SDKs for web and mobile. Verifies data arrives clean and grouped.", tags: ["OTel backends", "Web + mobile SDKs", "Source maps", "Verification"], }, { - icon: SquareTerminal, + label: "02 · Debug", name: "/traceway", description: "Installs the traceway CLI, then uses it: exceptions, logs, endpoints, and metrics, from bug report to root cause.", @@ -109,47 +109,6 @@ export default function AgentSkillsPage() {
-
- - Two skills, one install. - - } - description="Plain Markdown playbooks your agent loads on demand. One sets up your project, the other queries and debugs it." - /> -
- {SKILLS.map((skill) => ( -
-
- - - - - {skill.name} - -
-
-

- {skill.description} -

-
- {skill.tags.map((tag) => ( - - {tag} - - ))} -
-
-
- ))} -
-
-
@@ -252,6 +211,44 @@ export default function AgentSkillsPage() { />
+
+ The skills +

+ Two skills, one install. +

+

+ Plain Markdown playbooks your agent loads on demand. One sets up + your project, the other queries and debugs it. +

+ +
+ {SKILLS.map((skill) => ( +
+
+

+ {skill.label} +

+

+ {skill.name} +

+
+
+

+ {skill.description} +

+
+ {skill.tags.map((tag) => ( + + {tag} + + ))} +
+
+
+ ))} +
+
+
Compatibility From c2a7baa6251ef992a9ed13a4a2fc579b960155e5 Mon Sep 17 00:00:00 2001 From: ddux Date: Thu, 11 Jun 2026 18:00:27 -0500 Subject: [PATCH 16/24] website progress --- .../product/javascript-symbolication/page.tsx | 337 ++++++++++++++++++ website/components/otel-pipeline-tabs.tsx | 111 ++++++ website/components/site-footer.tsx | 1 + website/components/site-header.tsx | 7 + .../components/symbolication-before-after.tsx | 116 ++++++ 5 files changed, 572 insertions(+) create mode 100644 website/app/product/javascript-symbolication/page.tsx create mode 100644 website/components/otel-pipeline-tabs.tsx create mode 100644 website/components/symbolication-before-after.tsx diff --git a/website/app/product/javascript-symbolication/page.tsx b/website/app/product/javascript-symbolication/page.tsx new file mode 100644 index 00000000..7cdbe98d --- /dev/null +++ b/website/app/product/javascript-symbolication/page.tsx @@ -0,0 +1,337 @@ +import Link from "next/link"; +import Image from "next/image"; +import type { Metadata } from "next"; +import { ArrowRight, Github, BookOpen, Workflow } from "lucide-react"; + +import { Eyebrow } from "@/components/eyebrow"; +import { SectionHead } from "@/components/section-head"; +import { StatsStrip } from "@/components/stats-strip"; +import { FeatureRow } from "@/components/feature-row"; +import { FaqList } from "@/components/faq-list"; +import { FinalCTA } from "@/components/final-cta"; +import { AuroraBackground } from "@/components/aurora-background"; +import { OtelPipelineTabs } from "@/components/otel-pipeline-tabs"; +import { SymbolicationBeforeAfter } from "@/components/symbolication-before-after"; +import { GITHUB_URL } from "@/lib/links"; + +export const metadata: Metadata = { + title: "JavaScript Stack Trace Symbolication · Traceway", + description: + "Open-source, OpenTelemetry-compatible source map symbolication. Resolve minified production errors back to the original file, line, and function at ingest. Pure Go, built to be fast.", +}; + +const BUNDLERS = [ + "Vite", + "webpack", + "esbuild", + "Rollup", + "Metro", + "Next.js", + "SvelteKit", + "Cloudflare Workers", +]; + +export default function JavaScriptSymbolicationPage() { + return ( +
+
+ +
+
+
+ OpenTelemetry +
+

+ + app.min.js:1:63 + +
+ tells you nothing. +

+

+ Minified bundles reduce every production error to single letters + on line one. Traceway resolves each frame back to the file, line, + and function you wrote, the moment the error arrives. +

+
+ + Upload your source maps + + + + View on GitHub + +
+

+ Open source · Pure Go · OpenTelemetry compatible +

+
+
+
+ +
+ + +
+ + Symbolication that keeps up with ingest. + + } + description="Most symbolicators re-parse source maps on every restart and hold them in RAM until the process dies. Ours compiles each map once, then memory-maps it from disk. Numbers below come from the benchmark workflows in the repo." + /> + <1µs", label: "To open a compiled source map" }, + { num: "<1ms", label: "p99 lookup with a cold cache" }, + { num: "3×", label: "Faster bundle parsing than SWC" }, + { num: "0", label: "Maps re-parsed after a restart" }, + ]} + /> +
+
+

+ Why it stays fast +

+
    +
  • + Parse once, compile to .tw.{" "} + Each map and bundle compiles into a binary format on first + use. Every lookup after that opens the file in under a + microsecond and binary-searches it. +
  • +
  • + Disk is the budget, not RAM.{" "} + Compiled maps are memory-mapped, so resident memory tracks + the hot set. A corpus of thousands of bundles costs disk + space, not heap. +
  • +
  • + Cold lookups don’t hurt.{" "} + An in-RAM LRU churns the garbage collector every time an old + release throws. The mmap cache holds p99 under a millisecond + on the long tail. +
  • +
  • + Restarts warm from disk.{" "} + Nothing is re-parsed when the process comes back. The cache + is already there. +
  • +
+
+
+

+ The fine print +

+
    +
  • + Two parser engines.{" "} + The default build is pure Go and parses bundles with{" "} + dop251/goja. A build flag swaps in{" "} + oxc, which{" "} + + parses 3x faster than SWC + + . +
  • +
  • + Two cache modes.{" "} + Hold parsed maps in memory, or compile them to{" "} + .tw files and memory-map them from disk. +
  • +
  • + Not memory bound.{" "} + With oxc and the disk cache, the corpus is a disk budget: the + cache holds as many maps as the disk fits. Competing + symbolicators cache parsed maps in RAM, so their corpus caps + out at what the heap can hold. +
  • +
  • + Benchmarked in the open.{" "} + Parser numbers come from go test -bench over + webpack, Metro, and Preact fixtures plus 1 MB and + 5 MB synthetic bundles. Cache numbers come from a sweep + of 1,000 to 12,000 bundles on a 2 vCPU box. Both + workflows are in the repo; run them on your fork. +
  • +
  • + MIT licensed, actually open source.{" "} + The parsers, the cache, the collector processor, and the + whole backend live in one public repo. No open core, no + enterprise tier where symbolication really lives. +
  • +
+
+
+
+ +
+
+
+ OpenTelemetry +

+ Bring your own pipeline. +

+

+ The same engine ships as an OpenTelemetry Collector processor. + It’s a drop-in replacement for Honeycomb’s{" "} + source_map_symbolicator: same component type, same + attribute contract, same config keys. Existing pipelines and + web instrumentation work unchanged. +

+
    +
  • Symbolicates spans, span events, and log records
  • +
  • Cache bounded by disk size, not an entry count in RAM
  • +
  • Pure Go, so no glibc base image required
  • +
  • Works in any collector build, with or without Traceway behind it
  • +
+
+ +
+
+ +
+
+
+
+ Built in the open +

+ Read the parser. Run the benchmarks. Self-host all of it. +

+

+ The symbolicator, the benchmark harnesses, and the rest of + Traceway live in one MIT-licensed repo. Nothing about how + your stack traces get resolved is a black box. +

+
+ {BUNDLERS.map((b) => ( + + {b} + + ))} +
+
+
+ + + traceway → + + + + OTel processor → + + + + Documentation → + +
+
+
+
+
+ + + Stop debugging app.min.js. + + } + description="Upload your source maps once. Every production error after that points at the code you wrote." + primary={{ + label: "Get Started", + href: "https://docs.tracewayapp.com/client/js-sdk/sourcemap-upload", + }} + secondary={{ + label: "Star on GitHub", + href: GITHUB_URL, + external: true, + }} + /> + +
+
+ +
+ +
+
+
+
+ ); +} diff --git a/website/components/otel-pipeline-tabs.tsx b/website/components/otel-pipeline-tabs.tsx new file mode 100644 index 00000000..1e95a972 --- /dev/null +++ b/website/components/otel-pipeline-tabs.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { useState } from "react"; + +const TABS = [ + { + filename: "builder-config.yaml", + code: ( + <> + + # compile the processor into your collector (ocb manifest) + + {"\n"} + processors: + {"\n"} + {" - "} + gomod:{" "} + + github.com/tracewayapp/traceway/backend v1.8.0 + + {"\n"} + {" "} + import:{" "} + + github.com/tracewayapp/traceway/backend/app/symbolicator/otelprocessor + + {"\n\n"} + ${" "} + + ocb --config builder-config.yaml + + + ), + }, + { + filename: "otel-collector.yaml", + code: ( + <> + + # then reference it in the collector config + + {"\n"} + processors: + {"\n"} + {" "} + source_map_symbolicator: + {"\n"} + + {" source_map_store: file_store\n"} + {" local_source_maps:\n"} + {" path: /sourcemaps\n"} + {" cache_dir: /var/cache/symbolicator\n"} + {" cache_max_disk_pct: 50"} + + {"\n\n"} + + {"service:\n"} + {" pipelines:\n"} + {" traces:"} + + {"\n"} + + {" processors: [source_map_symbolicator]"} + + + ), + }, +]; + +export function OtelPipelineTabs() { + const [activeTab, setActiveTab] = useState(0); + + return ( +
+
+ + + +
+ {TABS.map((tab, i) => ( + + ))} +
+
+
+
+          {TABS[activeTab].code}
+        
+
+
+ ); +} diff --git a/website/components/site-footer.tsx b/website/components/site-footer.tsx index 69612c0c..bf6897b0 100644 --- a/website/components/site-footer.tsx +++ b/website/components/site-footer.tsx @@ -27,6 +27,7 @@ const COLUMNS: Column[] = [ { label: "AI Tracing", href: "/product/ai-tracing" }, { label: "Performance", href: "/product/performance" }, { label: "Flutter Session Replay", href: "/product/flutter-session-replay" }, + { label: "JavaScript Symbolication", href: "/product/javascript-symbolication" }, ], }, { diff --git a/website/components/site-header.tsx b/website/components/site-header.tsx index 8845ee94..1cb33a81 100644 --- a/website/components/site-header.tsx +++ b/website/components/site-header.tsx @@ -15,6 +15,7 @@ import { Activity, Smartphone, Bot, + Braces, } from "lucide-react"; import { MobileNav } from "@/components/mobile-nav"; import { DiscordIcon } from "@/components/discord-icon"; @@ -86,6 +87,12 @@ const SPECIALIZED: NavItem[] = [ href: "/product/flutter-session-replay", icon: Smartphone, }, + { + title: "JavaScript Symbolication", + description: "Minified stack traces, resolved to your source.", + href: "/product/javascript-symbolication", + icon: Braces, + }, ]; export function SiteHeader() { diff --git a/website/components/symbolication-before-after.tsx b/website/components/symbolication-before-after.tsx new file mode 100644 index 00000000..7a5abb6e --- /dev/null +++ b/website/components/symbolication-before-after.tsx @@ -0,0 +1,116 @@ +import { MoveRight, MoveDown } from "lucide-react"; + +const RAW_LINES = [ + "n()", + "", + "app.min.js:1:63", + "t()", + "", + "app.min.js:1:129", + "", + "app.min.js:1:146", + "", + "app.min.js:1:164", +]; + +const RESOLVED_FRAMES = [ + { fn: "validateUser", loc: "../src/user.ts:8:11", top: true }, + { fn: "handleSignup", loc: "../src/index.ts:4:10" }, + { fn: "", loc: "../src/index.ts:7:1" }, + { fn: "", loc: "../src/index.ts:7:29" }, +]; + +const CARD_SHADOW = "0 18px 40px -24px rgba(10, 14, 24, 0.25)"; + +function CardLabel({ text }: { text: string }) { + return ( +

+ {text} +

+ ); +} + +export function SymbolicationBeforeAfter() { + return ( +
+
+
+ +
+
Error: user has no name
+
+ {RAW_LINES.map((line, i) => ( +
+ {line} +
+ ))} +
+
+
+ +
+ + +
+ +
+ +
+
+ Error: user has no name +
+ {RESOLVED_FRAMES.map((f, i) => ( +
0 ? "1px solid var(--hair)" : "none", + borderLeft: `3px solid ${f.top ? "var(--a2)" : "var(--ink-5)"}`, + overflowWrap: "anywhere", + }} + > + {f.fn} + {f.loc} +
+ ))} +
+
+
+
+ ); +} From 141a0259a0b27fc57e3852b1c34f646b707aa337 Mon Sep 17 00:00:00 2001 From: ddux Date: Thu, 11 Jun 2026 19:25:48 -0500 Subject: [PATCH 17/24] testing otel collector grafana --- .../.gitignore | 3 ++ .../config.yaml | 42 ++++++++++++++++++ .../manifest.yaml | 16 +++++++ .../otel-collector-grafana-honeycomb/run.sh | 15 +++++++ .../sourcemaps/app.mjs | 3 ++ .../sourcemaps/app.mjs.map | 1 + .../otel-collector-grafana/.gitignore | 4 ++ .../otel-collector-grafana/config.yaml | 44 +++++++++++++++++++ .../otel-collector-grafana/manifest.yaml | 19 ++++++++ .../otel-collector-grafana/run.sh | 15 +++++++ .../otel-collector-grafana/sourcemaps/app.mjs | 3 ++ .../sourcemaps/app.mjs.map | 1 + 12 files changed, 166 insertions(+) create mode 100644 testing/otel-collector-grafana-honeycomb/.gitignore create mode 100644 testing/otel-collector-grafana-honeycomb/config.yaml create mode 100644 testing/otel-collector-grafana-honeycomb/manifest.yaml create mode 100755 testing/otel-collector-grafana-honeycomb/run.sh create mode 100644 testing/otel-collector-grafana-honeycomb/sourcemaps/app.mjs create mode 100644 testing/otel-collector-grafana-honeycomb/sourcemaps/app.mjs.map create mode 100644 testing/symbolication/otel-collector-grafana/.gitignore create mode 100644 testing/symbolication/otel-collector-grafana/config.yaml create mode 100644 testing/symbolication/otel-collector-grafana/manifest.yaml create mode 100755 testing/symbolication/otel-collector-grafana/run.sh create mode 100644 testing/symbolication/otel-collector-grafana/sourcemaps/app.mjs create mode 100644 testing/symbolication/otel-collector-grafana/sourcemaps/app.mjs.map diff --git a/testing/otel-collector-grafana-honeycomb/.gitignore b/testing/otel-collector-grafana-honeycomb/.gitignore new file mode 100644 index 00000000..6303ccf1 --- /dev/null +++ b/testing/otel-collector-grafana-honeycomb/.gitignore @@ -0,0 +1,3 @@ +bin/ +build/ +.env diff --git a/testing/otel-collector-grafana-honeycomb/config.yaml b/testing/otel-collector-grafana-honeycomb/config.yaml new file mode 100644 index 00000000..319567b1 --- /dev/null +++ b/testing/otel-collector-grafana-honeycomb/config.yaml @@ -0,0 +1,42 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:5317 + http: + endpoint: 0.0.0.0:5318 + cors: + allowed_origins: + - "*" + allowed_headers: + - "*" + +processors: + source_map_symbolicator: + source_map_store: file_store + local_source_maps: + path: ./sourcemaps + batch: + timeout: 2s + +exporters: + otlphttp/grafana: + endpoint: https://otlp-gateway-prod-us-east-3.grafana.net/otlp + headers: + Authorization: "Basic ${env:GRAFANA_OTLP_BASIC}" + debug: + verbosity: detailed + +service: + telemetry: + metrics: + level: none + pipelines: + traces: + receivers: [otlp] + processors: [source_map_symbolicator, batch] + exporters: [otlphttp/grafana, debug] + logs: + receivers: [otlp] + processors: [source_map_symbolicator, batch] + exporters: [otlphttp/grafana, debug] diff --git a/testing/otel-collector-grafana-honeycomb/manifest.yaml b/testing/otel-collector-grafana-honeycomb/manifest.yaml new file mode 100644 index 00000000..276d9e3e --- /dev/null +++ b/testing/otel-collector-grafana-honeycomb/manifest.yaml @@ -0,0 +1,16 @@ +dist: + name: otelcol-honeycomb-symbolicator + description: OTel Collector with the Honeycomb source map symbolicator + output_path: ./build + cgo_enabled: true + +receivers: + - gomod: go.opentelemetry.io/collector/receiver/otlpreceiver v0.143.0 + +processors: + - gomod: go.opentelemetry.io/collector/processor/batchprocessor v0.143.0 + - gomod: github.com/honeycombio/opentelemetry-collector-symbolicator/sourcemapprocessor v1.0.3 + +exporters: + - gomod: go.opentelemetry.io/collector/exporter/debugexporter v0.143.0 + - gomod: go.opentelemetry.io/collector/exporter/otlphttpexporter v0.143.0 diff --git a/testing/otel-collector-grafana-honeycomb/run.sh b/testing/otel-collector-grafana-honeycomb/run.sh new file mode 100755 index 00000000..f5d0cee4 --- /dev/null +++ b/testing/otel-collector-grafana-honeycomb/run.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +source .env + +export GRAFANA_OTLP_BASIC="$(printf '%s:%s' "$GRAFANA_OTLP_INSTANCE_ID" "$GRAFANA_CLOUD_TOKEN" | base64)" + +if [ ! -x build/otelcol-honeycomb-symbolicator ]; then + echo "build/otelcol-honeycomb-symbolicator not found, run ./bin/builder --config manifest.yaml first" >&2 + exit 1 +fi + +exec ./build/otelcol-honeycomb-symbolicator --config config.yaml diff --git a/testing/otel-collector-grafana-honeycomb/sourcemaps/app.mjs b/testing/otel-collector-grafana-honeycomb/sourcemaps/app.mjs new file mode 100644 index 00000000..0449178c --- /dev/null +++ b/testing/otel-collector-grafana-honeycomb/sourcemaps/app.mjs @@ -0,0 +1,3 @@ +import{trace as e,SpanKind as t,SpanStatusCode as r}from"@opentelemetry/api";import{NodeTracerProvider as o,BatchSpanProcessor as s}from"@opentelemetry/sdk-trace-node";import{OTLPTraceExporter as n}from"@opentelemetry/exporter-trace-otlp-http";import{defaultResource as a,resourceFromAttributes as c}from"@opentelemetry/resources";const i=new Map([["ord_1042",2]]);function p(e){const t=function(e,t){const r=i.get(e)??0;if(t>r)throw new RangeError(`cannot reserve ${t} units for order ${e}, only ${r} in stock`);return i.set(e,r-t),{orderId:e,units:t}}(e.orderId,e.units);return{orderId:e.orderId,reservation:t}}const d=process.env.TW_TOKEN||"5638c2de607f45169bcf98aa8774fe5c",l=process.env.TW_OTLP_URL||"http://localhost:8082/api/otel/v1/traces",u=process.argv[2]||"event";(async function(){const i=new o({resource:a().merge(c({"service.name":"symbolication-node-app"})),spanProcessors:[new s(new n({url:l,headers:{Authorization:`Bearer ${d}`}}))]});i.register();const m=e.getTracer("symbolication-node-app"),f=m.startSpan("POST /orders/fulfill",{kind:t.SERVER,attributes:{"http.request.method":"POST","http.route":"/orders/fulfill","url.path":"/orders/fulfill"}});try{const e=p({orderId:"ord_1042",units:3});f.setAttribute("http.response.status_code",200),f.setStatus({code:r.OK}),console.log("order fulfilled:",e)}catch(e){if(f.setAttribute("http.response.status_code",500),f.setStatus({code:r.ERROR,message:e.message}),"span"===u){m.startSpan("exception",{attributes:{"exception.type":e.name,"exception.message":e.message,"exception.stacktrace":e.stack}}).end()}else f.recordException(e);console.error(`reported ${e.name} via mode=${u}: ${e.message}`)}f.end(),await i.shutdown(),console.log(`flushed to ${l}`)})().catch(e=>{console.error("fatal:",e),process.exit(1)}); + +//# sourceMappingURL=app.mjs.map diff --git a/testing/otel-collector-grafana-honeycomb/sourcemaps/app.mjs.map b/testing/otel-collector-grafana-honeycomb/sourcemaps/app.mjs.map new file mode 100644 index 00000000..4d784be8 --- /dev/null +++ b/testing/otel-collector-grafana-honeycomb/sourcemaps/app.mjs.map @@ -0,0 +1 @@ +{"version":3,"file":"app.mjs","sources":["../src/inventory.js","../src/order.js","../src/index.js"],"sourcesContent":["const stockLevels = new Map([[\"ord_1042\", 2]]);\n\nexport function reserveStock(orderId, units) {\n const available = stockLevels.get(orderId) ?? 0;\n if (units > available) {\n throw new RangeError(\n `cannot reserve ${units} units for order ${orderId}, only ${available} in stock`,\n );\n }\n stockLevels.set(orderId, available - units);\n return { orderId, units };\n}\n","import { reserveStock } from \"./inventory.js\";\n\nexport function fulfillOrder(order) {\n const reservation = reserveStock(order.orderId, order.units);\n return { orderId: order.orderId, reservation };\n}\n","import { trace, SpanKind, SpanStatusCode } from \"@opentelemetry/api\";\nimport {\n NodeTracerProvider,\n BatchSpanProcessor,\n} from \"@opentelemetry/sdk-trace-node\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport {\n defaultResource,\n resourceFromAttributes,\n} from \"@opentelemetry/resources\";\nimport { fulfillOrder } from \"./order.js\";\n\nconst token = process.env.TW_TOKEN || \"5638c2de607f45169bcf98aa8774fe5c\";\nconst url =\n process.env.TW_OTLP_URL || \"http://localhost:8082/api/otel/v1/traces\";\nconst mode = process.argv[2] || \"event\";\n\nasync function main() {\n const provider = new NodeTracerProvider({\n resource: defaultResource().merge(\n resourceFromAttributes({ \"service.name\": \"symbolication-node-app\" }),\n ),\n spanProcessors: [\n new BatchSpanProcessor(\n new OTLPTraceExporter({\n url,\n headers: { Authorization: `Bearer ${token}` },\n }),\n ),\n ],\n });\n provider.register();\n\n const tracer = trace.getTracer(\"symbolication-node-app\");\n\n const span = tracer.startSpan(\"POST /orders/fulfill\", {\n kind: SpanKind.SERVER,\n attributes: {\n \"http.request.method\": \"POST\",\n \"http.route\": \"/orders/fulfill\",\n \"url.path\": \"/orders/fulfill\",\n },\n });\n try {\n const result = fulfillOrder({ orderId: \"ord_1042\", units: 3 });\n span.setAttribute(\"http.response.status_code\", 200);\n span.setStatus({ code: SpanStatusCode.OK });\n console.log(\"order fulfilled:\", result);\n } catch (err) {\n span.setAttribute(\"http.response.status_code\", 500);\n span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });\n if (mode === \"span\") {\n const exceptionSpan = tracer.startSpan(\"exception\", {\n attributes: {\n \"exception.type\": err.name,\n \"exception.message\": err.message,\n \"exception.stacktrace\": err.stack,\n },\n });\n exceptionSpan.end();\n } else {\n span.recordException(err);\n }\n console.error(`reported ${err.name} via mode=${mode}: ${err.message}`);\n }\n span.end();\n\n await provider.shutdown();\n console.log(`flushed to ${url}`);\n}\n\nmain().catch((err) => {\n console.error(\"fatal:\", err);\n process.exit(1);\n});\n"],"names":["stockLevels","Map","fulfillOrder","order","reservation","orderId","units","available","get","RangeError","set","reserveStock","token","process","env","TW_TOKEN","url","TW_OTLP_URL","mode","argv","async","provider","NodeTracerProvider","resource","defaultResource","merge","resourceFromAttributes","spanProcessors","BatchSpanProcessor","OTLPTraceExporter","headers","Authorization","register","tracer","trace","getTracer","span","startSpan","kind","SpanKind","SERVER","attributes","result","setAttribute","setStatus","code","SpanStatusCode","OK","console","log","err","ERROR","message","name","stack","end","recordException","error","shutdown","main","catch","exit"],"mappings":"2UAAA,MAAMA,EAAc,IAAIC,IAAI,CAAC,CAAC,WAAY,KCEnC,SAASC,EAAaC,GAC3B,MAAMC,EDDD,SAAsBC,EAASC,GACpC,MAAMC,EAAYP,EAAYQ,IAAIH,IAAY,EAC9C,GAAIC,EAAQC,EACV,MAAM,IAAIE,WACR,kBAAkBH,qBAAyBD,WAAiBE,cAIhE,OADAP,EAAYU,IAAIL,EAASE,EAAYD,GAC9B,CAAED,UAASC,QACpB,CCRsBK,CAAaR,EAAME,QAASF,EAAMG,OACtD,MAAO,CAAED,QAASF,EAAME,QAASD,cACnC,CCOA,MAAMQ,EAAQC,QAAQC,IAAIC,UAAY,mCAChCC,EACJH,QAAQC,IAAIG,aAAe,2CACvBC,EAAOL,QAAQM,KAAK,IAAM,SAEhCC,iBACE,MAAMC,EAAW,IAAIC,EAAmB,CACtCC,SAAUC,IAAkBC,MAC1BC,EAAuB,CAAE,eAAgB,4BAE3CC,eAAgB,CACd,IAAIC,EACF,IAAIC,EAAkB,CACpBb,MACAc,QAAS,CAAEC,cAAe,UAAUnB,WAK5CS,EAASW,WAET,MAAMC,EAASC,EAAMC,UAAU,0BAEzBC,EAAOH,EAAOI,UAAU,uBAAwB,CACpDC,KAAMC,EAASC,OACfC,WAAY,CACV,sBAAuB,OACvB,aAAc,kBACd,WAAY,qBAGhB,IACE,MAAMC,EAASxC,EAAa,CAAEG,QAAS,WAAYC,MAAO,IAC1D8B,EAAKO,aAAa,4BAA6B,KAC/CP,EAAKQ,UAAU,CAAEC,KAAMC,EAAeC,KACtCC,QAAQC,IAAI,mBAAoBP,EAClC,CAAE,MAAOQ,GAGP,GAFAd,EAAKO,aAAa,4BAA6B,KAC/CP,EAAKQ,UAAU,CAAEC,KAAMC,EAAeK,MAAOC,QAASF,EAAIE,UAC7C,SAATlC,EAAiB,CACGe,EAAOI,UAAU,YAAa,CAClDI,WAAY,CACV,iBAAkBS,EAAIG,KACtB,oBAAqBH,EAAIE,QACzB,uBAAwBF,EAAII,SAGlBC,KAChB,MACEnB,EAAKoB,gBAAgBN,GAEvBF,QAAQS,MAAM,YAAYP,EAAIG,iBAAiBnC,MAASgC,EAAIE,UAC9D,CACAhB,EAAKmB,YAEClC,EAASqC,WACfV,QAAQC,IAAI,cAAcjC,IAC5B,EAEA2C,GAAOC,MAAOV,IACZF,QAAQS,MAAM,SAAUP,GACxBrC,QAAQgD,KAAK"} \ No newline at end of file diff --git a/testing/symbolication/otel-collector-grafana/.gitignore b/testing/symbolication/otel-collector-grafana/.gitignore new file mode 100644 index 00000000..67c14ca0 --- /dev/null +++ b/testing/symbolication/otel-collector-grafana/.gitignore @@ -0,0 +1,4 @@ +bin/ +build/ +twcache/ +.env diff --git a/testing/symbolication/otel-collector-grafana/config.yaml b/testing/symbolication/otel-collector-grafana/config.yaml new file mode 100644 index 00000000..44367ba4 --- /dev/null +++ b/testing/symbolication/otel-collector-grafana/config.yaml @@ -0,0 +1,44 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + cors: + allowed_origins: + - "*" + allowed_headers: + - "*" + +processors: + source_map_symbolicator: + source_map_store: file_store + local_source_maps: + path: ./sourcemaps + cache_dir: ./twcache + cache_max_mb: 512 + batch: + timeout: 2s + +exporters: + otlphttp/grafana: + endpoint: https://otlp-gateway-prod-us-east-3.grafana.net/otlp + headers: + Authorization: "Basic ${env:GRAFANA_OTLP_BASIC}" + debug: + verbosity: detailed + +service: + telemetry: + metrics: + level: none + pipelines: + traces: + receivers: [otlp] + processors: [source_map_symbolicator, batch] + exporters: [otlphttp/grafana, debug] + logs: + receivers: [otlp] + processors: [source_map_symbolicator, batch] + exporters: [otlphttp/grafana, debug] diff --git a/testing/symbolication/otel-collector-grafana/manifest.yaml b/testing/symbolication/otel-collector-grafana/manifest.yaml new file mode 100644 index 00000000..a898d05e --- /dev/null +++ b/testing/symbolication/otel-collector-grafana/manifest.yaml @@ -0,0 +1,19 @@ +dist: + name: otelcol-symbolicator + description: OTel Collector with the Traceway source map symbolicator + output_path: ./build + +receivers: + - gomod: go.opentelemetry.io/collector/receiver/otlpreceiver v0.154.0 + +processors: + - gomod: go.opentelemetry.io/collector/processor/batchprocessor v0.154.0 + - gomod: github.com/tracewayapp/traceway/backend v1.8.0 + import: github.com/tracewayapp/traceway/backend/app/symbolicator/otelprocessor + +exporters: + - gomod: go.opentelemetry.io/collector/exporter/debugexporter v0.154.0 + - gomod: go.opentelemetry.io/collector/exporter/otlphttpexporter v0.154.0 + +replaces: + - github.com/tracewayapp/traceway/backend => /Users/dusanstanojevic/Documents/workspace/traceway/backend diff --git a/testing/symbolication/otel-collector-grafana/run.sh b/testing/symbolication/otel-collector-grafana/run.sh new file mode 100755 index 00000000..5be739a5 --- /dev/null +++ b/testing/symbolication/otel-collector-grafana/run.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +source .env + +export GRAFANA_OTLP_BASIC="$(printf '%s:%s' "$GRAFANA_OTLP_INSTANCE_ID" "$GRAFANA_CLOUD_TOKEN" | base64)" + +if [ ! -x build/otelcol-symbolicator ]; then + echo "build/otelcol-symbolicator not found, run ./bin/builder --config manifest.yaml first" >&2 + exit 1 +fi + +exec ./build/otelcol-symbolicator --config config.yaml diff --git a/testing/symbolication/otel-collector-grafana/sourcemaps/app.mjs b/testing/symbolication/otel-collector-grafana/sourcemaps/app.mjs new file mode 100644 index 00000000..0449178c --- /dev/null +++ b/testing/symbolication/otel-collector-grafana/sourcemaps/app.mjs @@ -0,0 +1,3 @@ +import{trace as e,SpanKind as t,SpanStatusCode as r}from"@opentelemetry/api";import{NodeTracerProvider as o,BatchSpanProcessor as s}from"@opentelemetry/sdk-trace-node";import{OTLPTraceExporter as n}from"@opentelemetry/exporter-trace-otlp-http";import{defaultResource as a,resourceFromAttributes as c}from"@opentelemetry/resources";const i=new Map([["ord_1042",2]]);function p(e){const t=function(e,t){const r=i.get(e)??0;if(t>r)throw new RangeError(`cannot reserve ${t} units for order ${e}, only ${r} in stock`);return i.set(e,r-t),{orderId:e,units:t}}(e.orderId,e.units);return{orderId:e.orderId,reservation:t}}const d=process.env.TW_TOKEN||"5638c2de607f45169bcf98aa8774fe5c",l=process.env.TW_OTLP_URL||"http://localhost:8082/api/otel/v1/traces",u=process.argv[2]||"event";(async function(){const i=new o({resource:a().merge(c({"service.name":"symbolication-node-app"})),spanProcessors:[new s(new n({url:l,headers:{Authorization:`Bearer ${d}`}}))]});i.register();const m=e.getTracer("symbolication-node-app"),f=m.startSpan("POST /orders/fulfill",{kind:t.SERVER,attributes:{"http.request.method":"POST","http.route":"/orders/fulfill","url.path":"/orders/fulfill"}});try{const e=p({orderId:"ord_1042",units:3});f.setAttribute("http.response.status_code",200),f.setStatus({code:r.OK}),console.log("order fulfilled:",e)}catch(e){if(f.setAttribute("http.response.status_code",500),f.setStatus({code:r.ERROR,message:e.message}),"span"===u){m.startSpan("exception",{attributes:{"exception.type":e.name,"exception.message":e.message,"exception.stacktrace":e.stack}}).end()}else f.recordException(e);console.error(`reported ${e.name} via mode=${u}: ${e.message}`)}f.end(),await i.shutdown(),console.log(`flushed to ${l}`)})().catch(e=>{console.error("fatal:",e),process.exit(1)}); + +//# sourceMappingURL=app.mjs.map diff --git a/testing/symbolication/otel-collector-grafana/sourcemaps/app.mjs.map b/testing/symbolication/otel-collector-grafana/sourcemaps/app.mjs.map new file mode 100644 index 00000000..4d784be8 --- /dev/null +++ b/testing/symbolication/otel-collector-grafana/sourcemaps/app.mjs.map @@ -0,0 +1 @@ +{"version":3,"file":"app.mjs","sources":["../src/inventory.js","../src/order.js","../src/index.js"],"sourcesContent":["const stockLevels = new Map([[\"ord_1042\", 2]]);\n\nexport function reserveStock(orderId, units) {\n const available = stockLevels.get(orderId) ?? 0;\n if (units > available) {\n throw new RangeError(\n `cannot reserve ${units} units for order ${orderId}, only ${available} in stock`,\n );\n }\n stockLevels.set(orderId, available - units);\n return { orderId, units };\n}\n","import { reserveStock } from \"./inventory.js\";\n\nexport function fulfillOrder(order) {\n const reservation = reserveStock(order.orderId, order.units);\n return { orderId: order.orderId, reservation };\n}\n","import { trace, SpanKind, SpanStatusCode } from \"@opentelemetry/api\";\nimport {\n NodeTracerProvider,\n BatchSpanProcessor,\n} from \"@opentelemetry/sdk-trace-node\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport {\n defaultResource,\n resourceFromAttributes,\n} from \"@opentelemetry/resources\";\nimport { fulfillOrder } from \"./order.js\";\n\nconst token = process.env.TW_TOKEN || \"5638c2de607f45169bcf98aa8774fe5c\";\nconst url =\n process.env.TW_OTLP_URL || \"http://localhost:8082/api/otel/v1/traces\";\nconst mode = process.argv[2] || \"event\";\n\nasync function main() {\n const provider = new NodeTracerProvider({\n resource: defaultResource().merge(\n resourceFromAttributes({ \"service.name\": \"symbolication-node-app\" }),\n ),\n spanProcessors: [\n new BatchSpanProcessor(\n new OTLPTraceExporter({\n url,\n headers: { Authorization: `Bearer ${token}` },\n }),\n ),\n ],\n });\n provider.register();\n\n const tracer = trace.getTracer(\"symbolication-node-app\");\n\n const span = tracer.startSpan(\"POST /orders/fulfill\", {\n kind: SpanKind.SERVER,\n attributes: {\n \"http.request.method\": \"POST\",\n \"http.route\": \"/orders/fulfill\",\n \"url.path\": \"/orders/fulfill\",\n },\n });\n try {\n const result = fulfillOrder({ orderId: \"ord_1042\", units: 3 });\n span.setAttribute(\"http.response.status_code\", 200);\n span.setStatus({ code: SpanStatusCode.OK });\n console.log(\"order fulfilled:\", result);\n } catch (err) {\n span.setAttribute(\"http.response.status_code\", 500);\n span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });\n if (mode === \"span\") {\n const exceptionSpan = tracer.startSpan(\"exception\", {\n attributes: {\n \"exception.type\": err.name,\n \"exception.message\": err.message,\n \"exception.stacktrace\": err.stack,\n },\n });\n exceptionSpan.end();\n } else {\n span.recordException(err);\n }\n console.error(`reported ${err.name} via mode=${mode}: ${err.message}`);\n }\n span.end();\n\n await provider.shutdown();\n console.log(`flushed to ${url}`);\n}\n\nmain().catch((err) => {\n console.error(\"fatal:\", err);\n process.exit(1);\n});\n"],"names":["stockLevels","Map","fulfillOrder","order","reservation","orderId","units","available","get","RangeError","set","reserveStock","token","process","env","TW_TOKEN","url","TW_OTLP_URL","mode","argv","async","provider","NodeTracerProvider","resource","defaultResource","merge","resourceFromAttributes","spanProcessors","BatchSpanProcessor","OTLPTraceExporter","headers","Authorization","register","tracer","trace","getTracer","span","startSpan","kind","SpanKind","SERVER","attributes","result","setAttribute","setStatus","code","SpanStatusCode","OK","console","log","err","ERROR","message","name","stack","end","recordException","error","shutdown","main","catch","exit"],"mappings":"2UAAA,MAAMA,EAAc,IAAIC,IAAI,CAAC,CAAC,WAAY,KCEnC,SAASC,EAAaC,GAC3B,MAAMC,EDDD,SAAsBC,EAASC,GACpC,MAAMC,EAAYP,EAAYQ,IAAIH,IAAY,EAC9C,GAAIC,EAAQC,EACV,MAAM,IAAIE,WACR,kBAAkBH,qBAAyBD,WAAiBE,cAIhE,OADAP,EAAYU,IAAIL,EAASE,EAAYD,GAC9B,CAAED,UAASC,QACpB,CCRsBK,CAAaR,EAAME,QAASF,EAAMG,OACtD,MAAO,CAAED,QAASF,EAAME,QAASD,cACnC,CCOA,MAAMQ,EAAQC,QAAQC,IAAIC,UAAY,mCAChCC,EACJH,QAAQC,IAAIG,aAAe,2CACvBC,EAAOL,QAAQM,KAAK,IAAM,SAEhCC,iBACE,MAAMC,EAAW,IAAIC,EAAmB,CACtCC,SAAUC,IAAkBC,MAC1BC,EAAuB,CAAE,eAAgB,4BAE3CC,eAAgB,CACd,IAAIC,EACF,IAAIC,EAAkB,CACpBb,MACAc,QAAS,CAAEC,cAAe,UAAUnB,WAK5CS,EAASW,WAET,MAAMC,EAASC,EAAMC,UAAU,0BAEzBC,EAAOH,EAAOI,UAAU,uBAAwB,CACpDC,KAAMC,EAASC,OACfC,WAAY,CACV,sBAAuB,OACvB,aAAc,kBACd,WAAY,qBAGhB,IACE,MAAMC,EAASxC,EAAa,CAAEG,QAAS,WAAYC,MAAO,IAC1D8B,EAAKO,aAAa,4BAA6B,KAC/CP,EAAKQ,UAAU,CAAEC,KAAMC,EAAeC,KACtCC,QAAQC,IAAI,mBAAoBP,EAClC,CAAE,MAAOQ,GAGP,GAFAd,EAAKO,aAAa,4BAA6B,KAC/CP,EAAKQ,UAAU,CAAEC,KAAMC,EAAeK,MAAOC,QAASF,EAAIE,UAC7C,SAATlC,EAAiB,CACGe,EAAOI,UAAU,YAAa,CAClDI,WAAY,CACV,iBAAkBS,EAAIG,KACtB,oBAAqBH,EAAIE,QACzB,uBAAwBF,EAAII,SAGlBC,KAChB,MACEnB,EAAKoB,gBAAgBN,GAEvBF,QAAQS,MAAM,YAAYP,EAAIG,iBAAiBnC,MAASgC,EAAIE,UAC9D,CACAhB,EAAKmB,YAEClC,EAASqC,WACfV,QAAQC,IAAI,cAAcjC,IAC5B,EAEA2C,GAAOC,MAAOV,IACZF,QAAQS,MAAM,SAAUP,GACxBrC,QAAQgD,KAAK"} \ No newline at end of file From f9604b24a2894b14eabcec3eba926dbd623ebbfa Mon Sep 17 00:00:00 2001 From: ddux Date: Thu, 11 Jun 2026 19:42:20 -0500 Subject: [PATCH 18/24] feat: progress --- .github/workflows/benchmark-processor.yml | 119 ++++ .../app/symbolicator/otelprocessor/README.md | 1 + .../app/symbolicator/otelprocessor/config.go | 2 + .../app/symbolicator/otelprocessor/factory.go | 7 + benchmarks/processor/.gitignore | 10 + benchmarks/processor/README.md | 93 +++ benchmarks/processor/config-honeycomb.yaml | 31 + benchmarks/processor/config-traceway.yaml | 33 ++ benchmarks/processor/corpusgen/go.mod | 3 + benchmarks/processor/corpusgen/main.go | 62 ++ benchmarks/processor/drain/Cargo.lock | 545 ++++++++++++++++++ benchmarks/processor/drain/Cargo.toml | 13 + benchmarks/processor/drain/src/main.rs | 85 +++ benchmarks/processor/loadgen/go.mod | 20 + benchmarks/processor/loadgen/go.sum | 76 +++ benchmarks/processor/loadgen/main.go | 174 ++++++ benchmarks/processor/manifest-honeycomb.yaml | 15 + benchmarks/processor/manifest-traceway.yaml | 18 + benchmarks/processor/rss-sampler.sh | 9 + benchmarks/processor/run-hetzner.sh | 80 +++ benchmarks/processor/run-local.sh | 129 +++++ 21 files changed, 1525 insertions(+) create mode 100644 .github/workflows/benchmark-processor.yml create mode 100644 benchmarks/processor/.gitignore create mode 100644 benchmarks/processor/README.md create mode 100644 benchmarks/processor/config-honeycomb.yaml create mode 100644 benchmarks/processor/config-traceway.yaml create mode 100644 benchmarks/processor/corpusgen/go.mod create mode 100644 benchmarks/processor/corpusgen/main.go create mode 100644 benchmarks/processor/drain/Cargo.lock create mode 100644 benchmarks/processor/drain/Cargo.toml create mode 100644 benchmarks/processor/drain/src/main.rs create mode 100644 benchmarks/processor/loadgen/go.mod create mode 100644 benchmarks/processor/loadgen/go.sum create mode 100644 benchmarks/processor/loadgen/main.go create mode 100644 benchmarks/processor/manifest-honeycomb.yaml create mode 100644 benchmarks/processor/manifest-traceway.yaml create mode 100755 benchmarks/processor/rss-sampler.sh create mode 100755 benchmarks/processor/run-hetzner.sh create mode 100755 benchmarks/processor/run-local.sh diff --git a/.github/workflows/benchmark-processor.yml b/.github/workflows/benchmark-processor.yml new file mode 100644 index 00000000..04e3a3a9 --- /dev/null +++ b/.github/workflows/benchmark-processor.yml @@ -0,0 +1,119 @@ +name: benchmark-processor + +on: + workflow_dispatch: + inputs: + scenarios: + description: 'Scenarios to run' + required: true + default: 'hot churn' + sut_type: + description: 'Hetzner server type for the collector under test' + required: true + default: 'ccx33' + connections: + description: 'Loadgen concurrency ramp' + required: true + default: '8,16,32,64,128,256' + step_duration: + description: 'Duration per concurrency step' + required: true + default: '60s' + churn_entries: + description: 'Corpus entries for the churn scenario' + required: true + default: '512' + pad_kb: + description: 'Bundle padding KB per corpus entry' + required: true + default: '256' + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: backend/go.mod + cache-dependency-path: backend/go.sum + - uses: dtolnay/rust-toolchain@stable + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Build oxc shim + run: bash scripts/build-oxc-shim.sh + - name: Build traceway collector (oxc) + working-directory: benchmarks/processor + run: | + GOBIN=$PWD/bin go install go.opentelemetry.io/collector/cmd/builder@v0.154.0 + ./bin/builder --config manifest-traceway.yaml --skip-compilation + cd build-traceway && CGO_ENABLED=1 go build -tags oxc -o otelcol-bench-traceway . + - name: Build honeycomb collector + working-directory: benchmarks/processor + run: | + GOBIN=$PWD/bin go install go.opentelemetry.io/collector/cmd/builder@v0.143.0 + CGO_ENABLED=1 ./bin/builder --config manifest-honeycomb.yaml + - name: Build drain + working-directory: benchmarks/processor/drain + run: cargo build --release + - name: Build loadgen and corpusgen + working-directory: benchmarks/processor + run: | + cd loadgen && go mod tidy && go build -o loadgen . && cd .. + cd corpusgen && go build -o corpusgen . + - name: Build node-app bundle + working-directory: testing/symbolication/node-app + run: npm install && npm run build + - name: Assemble artifacts + working-directory: benchmarks/processor + run: | + mkdir -p artifacts + cp build-traceway/otelcol-bench-traceway artifacts/ + cp build-honeycomb/otelcol-bench-honeycomb artifacts/ + cp "$(find ~/go/pkg/mod/github.com/honeycombio -name 'libsymbolic_cabi.so' -path '*linux_x86_64*' | head -1)" artifacts/ + cp drain/target/release/drain artifacts/ + cp loadgen/loadgen corpusgen/corpusgen artifacts/ + cp config-traceway.yaml config-honeycomb.yaml rss-sampler.sh artifacts/ + cp ../../testing/symbolication/node-app/dist/app.mjs ../../testing/symbolication/node-app/dist/app.mjs.map artifacts/ + - uses: actions/upload-artifact@v4 + with: + name: bench-artifacts + path: benchmarks/processor/artifacts/ + + bench: + needs: build + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + impl: [traceway-oxc, honeycomb] + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: bench-artifacts + path: benchmarks/processor/artifacts/ + - name: Install hcloud CLI + run: | + curl -sL https://github.com/hetznercloud/cli/releases/latest/download/hcloud-linux-amd64.tar.gz | tar xz hcloud + sudo mv hcloud /usr/local/bin/ + - name: Run benchmark + working-directory: benchmarks/processor + env: + HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }} + SCENARIOS: ${{ inputs.scenarios }} + SUT_TYPE: ${{ inputs.sut_type }} + CONNECTIONS: ${{ inputs.connections }} + STEP_DURATION: ${{ inputs.step_duration }} + CHURN_ENTRIES: ${{ inputs.churn_entries }} + PAD_KB: ${{ inputs.pad_kb }} + run: bash run-hetzner.sh ${{ matrix.impl }} + - uses: actions/upload-artifact@v4 + if: always() + with: + name: results-${{ matrix.impl }} + path: benchmarks/processor/results/ diff --git a/backend/app/symbolicator/otelprocessor/README.md b/backend/app/symbolicator/otelprocessor/README.md index ed1b6a52..d74e4ccf 100644 --- a/backend/app/symbolicator/otelprocessor/README.md +++ b/backend/app/symbolicator/otelprocessor/README.md @@ -74,6 +74,7 @@ For each span, span event, or log record carrying `exception.stacktrace`: | `build_uuid_attribute_key` | `app.debug.source_map_uuid` | Resource attribute used as a store key prefix | | `language_attribute_key` | `telemetry.sdk.language` | Attribute checked against `allowed_languages` | | `allowed_languages` | `[]` | When set, only records with a matching language are processed | +| `parser` | `""` | Bundle scope-analysis parser: `goja` (pure Go, the default) or `oxc` (requires a collector compiled with `-tags oxc` and the oxc shim, see `scripts/build-oxc-shim.sh`) | All attribute key names (`stack_trace_attribute_key`, `urls_attribute_key`, `symbolicator_failure_attribute_key`, and the rest) are remappable with the same configuration keys and defaults as Honeycomb's processor. diff --git a/backend/app/symbolicator/otelprocessor/config.go b/backend/app/symbolicator/otelprocessor/config.go index c894264d..b586083a 100644 --- a/backend/app/symbolicator/otelprocessor/config.go +++ b/backend/app/symbolicator/otelprocessor/config.go @@ -58,6 +58,8 @@ type Config struct { LanguageAttributeKey string `mapstructure:"language_attribute_key"` AllowedLanguages []string `mapstructure:"allowed_languages"` + + Parser string `mapstructure:"parser"` } func (c *Config) Validate() error { diff --git a/backend/app/symbolicator/otelprocessor/factory.go b/backend/app/symbolicator/otelprocessor/factory.go index bc7f9645..100ec040 100644 --- a/backend/app/symbolicator/otelprocessor/factory.go +++ b/backend/app/symbolicator/otelprocessor/factory.go @@ -8,6 +8,8 @@ import ( "go.opentelemetry.io/collector/consumer" "go.opentelemetry.io/collector/processor" "go.opentelemetry.io/collector/processor/processorhelper" + + "github.com/tracewayapp/traceway/backend/app/symbolicator/scopes" ) const processorVersion = "0.1.0" @@ -61,6 +63,11 @@ func createDefaultConfig() component.Config { } func newSymbolicator(cfg *Config, set processor.Settings) (*symbolicatorProcessor, error) { + if cfg.Parser != "" { + if err := scopes.SetParser(cfg.Parser); err != nil { + return nil, err + } + } store, err := newStore(cfg) if err != nil { return nil, err diff --git a/benchmarks/processor/.gitignore b/benchmarks/processor/.gitignore new file mode 100644 index 00000000..a0dfdfe1 --- /dev/null +++ b/benchmarks/processor/.gitignore @@ -0,0 +1,10 @@ +bin/ +build-traceway/ +build-honeycomb/ +corpus-*/ +results/ +artifacts/ +drain/target/ +loadgen/loadgen +corpusgen/corpusgen +*.log diff --git a/benchmarks/processor/README.md b/benchmarks/processor/README.md new file mode 100644 index 00000000..591fcb7d --- /dev/null +++ b/benchmarks/processor/README.md @@ -0,0 +1,93 @@ +# benchmarks/processor + +Head-to-head benchmark of the Traceway source map symbolicator processor (oxc build) +against Honeycomb's reference sourcemapprocessor, measuring sustained symbolication +throughput and resident memory while ramping load to the breaking point. + +## Topology + +``` +loadgen (Go) ----OTLP/HTTP----> collector under test ----OTLP/HTTP----> drain (Rust) + | (one symbolicator, | + | file store, no batch, | + | sync export) | + +---- ramps concurrency RSS sampled every 1s verifies the stacktrace + until saturation was actually symbolicated +``` + +The drain is a Rust server that gunzips each export request, scans for the +original-source marker (`../src/inventory.js`) and the minified marker (`.mjs:1:`), +and counts symbolicated vs unsymbolicated requests. It does no protobuf parsing, +so it sustains far more load than either collector can produce. + +The export path is synchronous (no sending queue, no retry, no batch processor), +so one accepted loadgen request equals one fully symbolicated export delivered to +the drain. Loadgen ok-rate times spans-per-request is the end-to-end +stacktraces/sec; the drain's symbolicated percentage is the correctness check. + +## Scenarios + +- `hot`: one bundle, always cache-warm. Pure resolve throughput. +- `churn`: 512 bundles (default) against the 128-entry resolver cache. + Honeycomb re-parses through Sentry symbolic on every eviction; Traceway + re-opens compiled `.tw` files from its disk cache. This is also where the + goja-vs-oxc parser choice matters, since the parser only runs on cache misses. + +Corpus entries are the real minified node-app bundle padded with `--pad-kb` +of valid JS (default 256 KB) so scope-analysis parse cost is realistic. The +sourcemap stays valid because frames sit on line 1 before the padding. + +## Memory methodology + +RSS is sampled once per second from outside the process (`rss.csv` per run). +Go heap numbers would be misleading here: Honeycomb's parsed maps live on the +C heap inside symbolic (invisible to Go), and Traceway's mmap'd `.tw` pages +are kernel-reclaimable (inflate RSS but are evictable). Compare the full RSS +timeline, not a single number. + +## Run locally + +``` +./run-local.sh +IMPLS="traceway-oxc traceway-goja honeycomb" SCENARIOS=churn CONNECTIONS=4,16,64 ./run-local.sh +``` + +Needs go, cargo, node, jq. Builds both collectors (the Traceway one with +`-tags oxc` after `scripts/build-oxc-shim.sh`), drain, loadgen, corpusgen, +then runs the matrix on localhost and prints a summary table. Results land +in `results/-/` as `loadgen.json`, `drain.json`, `rss.csv`, +`collector.log`. + +## Run on Hetzner + +``` +export HCLOUD_TOKEN=... +./run-hetzner.sh traceway-oxc +./run-hetzner.sh honeycomb +``` + +Provisions two dedicated-vCPU servers per invocation (SUT default ccx33, +loadgen+drain box default ccx23, same location), pushes prebuilt linux +artifacts from `artifacts/`, runs the scenarios, pulls results, and deletes +the servers on exit. The loadgen box hosts the drain so the SUT runs nothing +but the collector. + +## GitHub Action + +`benchmark-processor` (workflow_dispatch) builds all artifacts on the runner, +then runs the two implementations as parallel matrix entries, each on its own +Hetzner server pair. Needs the `HCLOUD_TOKEN` secret. Results are uploaded as +`results-traceway-oxc` and `results-honeycomb` artifacts. + +## Knobs + +| Env | Default | Meaning | +|-----|---------|---------| +| `IMPLS` | `traceway-oxc honeycomb` | run-local.sh only; `traceway-goja` selects the goja parser in the same oxc-built binary | +| `SCENARIOS` | `hot churn` | | +| `CHURN_ENTRIES` | `512` | corpus size for churn | +| `PAD_KB` | `256` | padding per bundle | +| `CONNECTIONS` | ramp | comma list of concurrency steps | +| `STEP_DURATION` | `30s` local, `60s` hetzner | time per step | +| `SPANS_PER_REQUEST` | `20` | exception spans per OTLP request | +| `SUT_TYPE` / `LDG_TYPE` | `ccx33` / `ccx23` | hetzner server types | diff --git a/benchmarks/processor/config-honeycomb.yaml b/benchmarks/processor/config-honeycomb.yaml new file mode 100644 index 00000000..2e876790 --- /dev/null +++ b/benchmarks/processor/config-honeycomb.yaml @@ -0,0 +1,31 @@ +receivers: + otlp: + protocols: + http: + endpoint: 0.0.0.0:4318 + +processors: + source_map_symbolicator: + source_map_store: file_store + local_source_maps: + path: ${env:STORE_PATH} + preserve_stack_trace: false + +exporters: + otlphttp/drain: + endpoint: ${env:DRAIN_ENDPOINT} + compression: none + sending_queue: + enabled: false + retry_on_failure: + enabled: false + +service: + telemetry: + metrics: + level: none + pipelines: + traces: + receivers: [otlp] + processors: [source_map_symbolicator] + exporters: [otlphttp/drain] diff --git a/benchmarks/processor/config-traceway.yaml b/benchmarks/processor/config-traceway.yaml new file mode 100644 index 00000000..444a5643 --- /dev/null +++ b/benchmarks/processor/config-traceway.yaml @@ -0,0 +1,33 @@ +receivers: + otlp: + protocols: + http: + endpoint: 0.0.0.0:4318 + +processors: + source_map_symbolicator: + source_map_store: file_store + local_source_maps: + path: ${env:STORE_PATH} + cache_dir: ${env:CACHE_DIR} + preserve_stack_trace: false + parser: ${env:SYMB_PARSER} + +exporters: + otlphttp/drain: + endpoint: ${env:DRAIN_ENDPOINT} + compression: none + sending_queue: + enabled: false + retry_on_failure: + enabled: false + +service: + telemetry: + metrics: + level: none + pipelines: + traces: + receivers: [otlp] + processors: [source_map_symbolicator] + exporters: [otlphttp/drain] diff --git a/benchmarks/processor/corpusgen/go.mod b/benchmarks/processor/corpusgen/go.mod new file mode 100644 index 00000000..1a6a0bb2 --- /dev/null +++ b/benchmarks/processor/corpusgen/go.mod @@ -0,0 +1,3 @@ +module github.com/tracewayapp/traceway/benchmarks/processor/corpusgen + +go 1.25.1 diff --git a/benchmarks/processor/corpusgen/main.go b/benchmarks/processor/corpusgen/main.go new file mode 100644 index 00000000..6b5170ff --- /dev/null +++ b/benchmarks/processor/corpusgen/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + "path/filepath" + "strings" +) + +type corpus struct { + Urls []string `json:"urls"` +} + +func main() { + bundle := flag.String("bundle", "../../testing/symbolication/node-app/dist/app.mjs", "") + mapFile := flag.String("map", "../../testing/symbolication/node-app/dist/app.mjs.map", "") + entries := flag.Int("entries", 1, "") + padKB := flag.Int("pad-kb", 256, "") + out := flag.String("out", "./corpus", "") + flag.Parse() + + bundleBytes, err := os.ReadFile(*bundle) + if err != nil { + panic(err) + } + mapBytes, err := os.ReadFile(*mapFile) + if err != nil { + panic(err) + } + firstLine := strings.SplitN(string(bundleBytes), "\n", 2)[0] + + var pad strings.Builder + chunk := "function __benchPad%d(a,b){var c=a*b+%d;for(var i=0;i<3;i++){c+=i*a-b}return c}\n" + i := 0 + for pad.Len() < *padKB*1024 { + pad.WriteString(fmt.Sprintf(chunk, i, i)) + i++ + } + + if err := os.MkdirAll(*out, 0o755); err != nil { + panic(err) + } + c := corpus{} + for n := 0; n < *entries; n++ { + name := fmt.Sprintf("app%d.mjs", n) + content := firstLine + "\n" + pad.String() + "//# sourceMappingURL=" + name + ".map\n" + if err := os.WriteFile(filepath.Join(*out, name), []byte(content), 0o644); err != nil { + panic(err) + } + if err := os.WriteFile(filepath.Join(*out, name+".map"), mapBytes, 0o644); err != nil { + panic(err) + } + c.Urls = append(c.Urls, name) + } + data, _ := json.MarshalIndent(c, "", " ") + if err := os.WriteFile(filepath.Join(*out, "corpus.json"), data, 0o644); err != nil { + panic(err) + } + fmt.Printf("wrote %d entries (%d KB padding each) to %s\n", *entries, *padKB, *out) +} diff --git a/benchmarks/processor/drain/Cargo.lock b/benchmarks/processor/drain/Cargo.lock new file mode 100644 index 00000000..ec871abf --- /dev/null +++ b/benchmarks/processor/drain/Cargo.lock @@ -0,0 +1,545 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "drain" +version = "0.1.0" +dependencies = [ + "axum", + "flate2", + "serde_json", + "tokio", +] + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/benchmarks/processor/drain/Cargo.toml b/benchmarks/processor/drain/Cargo.toml new file mode 100644 index 00000000..f6a4332a --- /dev/null +++ b/benchmarks/processor/drain/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "drain" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = "0.8" +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +flate2 = "1" +serde_json = "1" + +[profile.release] +lto = true diff --git a/benchmarks/processor/drain/src/main.rs b/benchmarks/processor/drain/src/main.rs new file mode 100644 index 00000000..8bc34721 --- /dev/null +++ b/benchmarks/processor/drain/src/main.rs @@ -0,0 +1,85 @@ +use axum::body::Bytes; +use axum::extract::State; +use axum::http::HeaderMap; +use axum::routing::{get, post}; +use axum::Router; +use flate2::read::GzDecoder; +use std::io::Read; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; + +const OK_MARKER: &[u8] = b"../src/inventory.js"; +const FAIL_MARKER: &[u8] = b".mjs:1:"; + +#[derive(Default)] +struct Counters { + requests: AtomicU64, + symbolicated: AtomicU64, + unsymbolicated: AtomicU64, + bytes: AtomicU64, +} + +fn contains(haystack: &[u8], needle: &[u8]) -> bool { + haystack.windows(needle.len()).any(|w| w == needle) +} + +async fn ingest(State(c): State>, headers: HeaderMap, body: Bytes) -> &'static str { + let decoded: Vec; + let data: &[u8] = if headers + .get("content-encoding") + .map(|v| v.as_bytes() == b"gzip") + .unwrap_or(false) + { + let mut d = GzDecoder::new(body.as_ref()); + let mut out = Vec::with_capacity(body.len() * 4); + if d.read_to_end(&mut out).is_err() { + c.unsymbolicated.fetch_add(1, Ordering::Relaxed); + return ""; + } + decoded = out; + &decoded + } else { + body.as_ref() + }; + c.requests.fetch_add(1, Ordering::Relaxed); + c.bytes.fetch_add(data.len() as u64, Ordering::Relaxed); + if contains(data, OK_MARKER) && !contains(data, FAIL_MARKER) { + c.symbolicated.fetch_add(1, Ordering::Relaxed); + } else { + c.unsymbolicated.fetch_add(1, Ordering::Relaxed); + } + "" +} + +async fn stats(State(c): State>) -> String { + serde_json::json!({ + "requests": c.requests.load(Ordering::Relaxed), + "symbolicated": c.symbolicated.load(Ordering::Relaxed), + "unsymbolicated": c.unsymbolicated.load(Ordering::Relaxed), + "bytes": c.bytes.load(Ordering::Relaxed), + }) + .to_string() +} + +async fn reset(State(c): State>) -> &'static str { + c.requests.store(0, Ordering::Relaxed); + c.symbolicated.store(0, Ordering::Relaxed); + c.unsymbolicated.store(0, Ordering::Relaxed); + c.bytes.store(0, Ordering::Relaxed); + "reset" +} + +#[tokio::main] +async fn main() { + let counters = Arc::new(Counters::default()); + let app = Router::new() + .route("/v1/traces", post(ingest)) + .route("/v1/logs", post(ingest)) + .route("/stats", get(stats)) + .route("/reset", post(reset)) + .with_state(counters); + let addr = std::env::var("DRAIN_ADDR").unwrap_or_else(|_| "0.0.0.0:9319".to_string()); + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + println!("drain listening on {}", addr); + axum::serve(listener, app).await.unwrap(); +} diff --git a/benchmarks/processor/loadgen/go.mod b/benchmarks/processor/loadgen/go.mod new file mode 100644 index 00000000..338ee46c --- /dev/null +++ b/benchmarks/processor/loadgen/go.mod @@ -0,0 +1,20 @@ +module github.com/tracewayapp/traceway/benchmarks/processor/loadgen + +go 1.25.1 + +require go.opentelemetry.io/collector/pdata v1.60.0 + +require ( + github.com/hashicorp/go-version v1.9.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.60.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/grpc v1.81.1 // indirect + google.golang.org/protobuf v1.36.11 // indirect +) diff --git a/benchmarks/processor/loadgen/go.sum b/benchmarks/processor/loadgen/go.sum new file mode 100644 index 00000000..3e353ad2 --- /dev/null +++ b/benchmarks/processor/loadgen/go.sum @@ -0,0 +1,76 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA= +github.com/hashicorp/go-version v1.9.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.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/collector/featuregate v1.60.0 h1:/HxHB8hq4N5Fhq5N0C8G6xbXTHxnGcWIryyJzmP7pdc= +go.opentelemetry.io/collector/featuregate v1.60.0/go.mod h1:4ga1QBMPEejXXmpyJS8lmaRpknJ3Lb9Bvk6e420bUFU= +go.opentelemetry.io/collector/internal/testutil v0.154.0 h1:iUYHOM8+wONW01A4jFnzauanOYGVBGchKWWtm51is6c= +go.opentelemetry.io/collector/internal/testutil v0.154.0/go.mod h1:Jkjs6rkqs973LqgZ0Fe3zrokQRKULYXPIf4HuqStiEE= +go.opentelemetry.io/collector/pdata v1.60.0 h1:YcGMHzeJucHen41AoR4mxHro8reUr9SVqt7P0KacKzQ= +go.opentelemetry.io/collector/pdata v1.60.0/go.mod h1:Ca8VgZX2wOr6wW4nihPWaCpkJVvzeo6Txa7BJ7/WO90= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/slim/otlp v1.10.0 h1:iR97Vs/ZDR+y9TfuP9b1XBtdPWeC+OMslIBmhcLU7jM= +go.opentelemetry.io/proto/slim/otlp v1.10.0/go.mod h1:lV9250stpjYLPNA5viFabIgP2QlUGRT1GdTgAf8SIUk= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0 h1:RUF5rO0hAlgiJt1fzQVzcVs3vZVNHIcMLgOgG4rWNcQ= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0/go.mod h1:I89cynRj8y+383o7tEQVg2SVA6SRgDVIouWPUVXjx0U= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.3.0 h1:CQvJSldHRUN6Z8jsUeYv8J0lXRvygALXIzsmAeCcZE0= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.3.0/go.mod h1:xSQ+mEfJe/GjK1LXEyVOoSI1N9JV9ZI923X5kup43W4= +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.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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/benchmarks/processor/loadgen/main.go b/benchmarks/processor/loadgen/main.go new file mode 100644 index 00000000..7965ec7f --- /dev/null +++ b/benchmarks/processor/loadgen/main.go @@ -0,0 +1,174 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "net/http" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" + "go.opentelemetry.io/collector/pdata/ptrace/ptraceotlp" +) + +func buildBody(url string, spans int) []byte { + td := ptrace.NewTraces() + rs := td.ResourceSpans().AppendEmpty() + rs.Resource().Attributes().PutStr("service.name", "processor-bench") + rs.Resource().Attributes().PutStr("telemetry.sdk.language", "nodejs") + ss := rs.ScopeSpans().AppendEmpty() + stack := fmt.Sprintf(`RangeError: cannot reserve 3 units for order ord_1042, only 2 in stock + at file:///bench/%s:1:435 + at p (file:///bench/%s:1:554) + at file:///bench/%s:1:1180 + at file:///bench/%s:1:1717`, url, url, url, url) + for i := 0; i < spans; i++ { + sp := ss.Spans().AppendEmpty() + sp.SetName("POST /orders/fulfill") + sp.SetKind(ptrace.SpanKindServer) + var tid pcommon.TraceID + var sid pcommon.SpanID + copy(tid[:], fmt.Sprintf("%016d", i)) + copy(sid[:], fmt.Sprintf("%08d", i)) + sp.SetTraceID(tid) + sp.SetSpanID(sid) + ev := sp.Events().AppendEmpty() + ev.SetName("exception") + ev.Attributes().PutStr("exception.type", "RangeError") + ev.Attributes().PutStr("exception.message", "bench") + ev.Attributes().PutStr("exception.stacktrace", stack) + } + req := ptraceotlp.NewExportRequestFromTraces(td) + data, err := req.MarshalProto() + if err != nil { + panic(err) + } + return data +} + +type stepResult struct { + Connections int `json:"connections"` + DurationSec float64 `json:"duration_sec"` + Sent int64 `json:"sent"` + Ok int64 `json:"ok"` + Rejected int64 `json:"rejected"` + Errors int64 `json:"errors"` + OkReqPerSec float64 `json:"ok_req_per_sec"` + StacksPerSec float64 `json:"stacks_per_sec"` + P50Ms float64 `json:"p50_ms"` + P99Ms float64 `json:"p99_ms"` +} + +func main() { + target := flag.String("target", "http://localhost:4318/v1/traces", "") + corpusFile := flag.String("corpus", "./corpus/corpus.json", "") + connSteps := flag.String("connections", "4,8,16,32,64,128", "") + stepDur := flag.Duration("step-duration", 30*time.Second, "") + spansPerReq := flag.Int("spans-per-request", 20, "") + outFile := flag.String("out", "loadgen-results.json", "") + flag.Parse() + + raw, err := os.ReadFile(*corpusFile) + if err != nil { + panic(err) + } + var c struct { + Urls []string `json:"urls"` + } + if err := json.Unmarshal(raw, &c); err != nil { + panic(err) + } + bodies := make([][]byte, len(c.Urls)) + for i, u := range c.Urls { + bodies[i] = buildBody(u, *spansPerReq) + } + fmt.Printf("prepared %d bodies, %d spans each, %d bytes first\n", len(bodies), *spansPerReq, len(bodies[0])) + + var results []stepResult + for _, s := range strings.Split(*connSteps, ",") { + conns, err := strconv.Atoi(strings.TrimSpace(s)) + if err != nil { + panic(err) + } + tr := &http.Transport{MaxIdleConns: conns * 2, MaxIdleConnsPerHost: conns * 2} + client := &http.Client{Transport: tr, Timeout: 30 * time.Second} + var sent, ok, rejected, errs int64 + var bodyIdx int64 + latCh := make(chan float64, 65536) + deadline := time.Now().Add(*stepDur) + var wg sync.WaitGroup + for w := 0; w < conns; w++ { + wg.Add(1) + go func() { + defer wg.Done() + for time.Now().Before(deadline) { + idx := atomic.AddInt64(&bodyIdx, 1) + body := bodies[idx%int64(len(bodies))] + atomic.AddInt64(&sent, 1) + t0 := time.Now() + resp, err := client.Post(*target, "application/x-protobuf", bytes.NewReader(body)) + lat := float64(time.Since(t0).Microseconds()) / 1000.0 + if err != nil { + atomic.AddInt64(&errs, 1) + continue + } + resp.Body.Close() + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + atomic.AddInt64(&ok, 1) + select { + case latCh <- lat: + default: + } + } else { + atomic.AddInt64(&rejected, 1) + } + } + }() + } + wg.Wait() + close(latCh) + var lats []float64 + for l := range latCh { + lats = append(lats, l) + } + sort.Float64s(lats) + pct := func(p float64) float64 { + if len(lats) == 0 { + return 0 + } + return lats[int(float64(len(lats)-1)*p)] + } + r := stepResult{ + Connections: conns, + DurationSec: stepDur.Seconds(), + Sent: sent, + Ok: ok, + Rejected: rejected, + Errors: errs, + OkReqPerSec: float64(ok) / stepDur.Seconds(), + StacksPerSec: float64(ok*int64(*spansPerReq)) / stepDur.Seconds(), + P50Ms: pct(0.50), + P99Ms: pct(0.99), + } + results = append(results, r) + line, _ := json.Marshal(r) + fmt.Println(string(line)) + tr.CloseIdleConnections() + } + data, _ := json.MarshalIndent(results, "", " ") + if err := os.MkdirAll(filepath.Dir(*outFile), 0o755); err != nil && filepath.Dir(*outFile) != "." { + panic(err) + } + if err := os.WriteFile(*outFile, data, 0o644); err != nil { + panic(err) + } +} diff --git a/benchmarks/processor/manifest-honeycomb.yaml b/benchmarks/processor/manifest-honeycomb.yaml new file mode 100644 index 00000000..6437f7ad --- /dev/null +++ b/benchmarks/processor/manifest-honeycomb.yaml @@ -0,0 +1,15 @@ +dist: + name: otelcol-bench-honeycomb + description: Bench collector with the Honeycomb symbolicator + output_path: ./build-honeycomb + cgo_enabled: true + +receivers: + - gomod: go.opentelemetry.io/collector/receiver/otlpreceiver v0.143.0 + +processors: + - gomod: go.opentelemetry.io/collector/processor/batchprocessor v0.143.0 + - gomod: github.com/honeycombio/opentelemetry-collector-symbolicator/sourcemapprocessor v1.0.3 + +exporters: + - gomod: go.opentelemetry.io/collector/exporter/otlphttpexporter v0.143.0 diff --git a/benchmarks/processor/manifest-traceway.yaml b/benchmarks/processor/manifest-traceway.yaml new file mode 100644 index 00000000..ef47cbb5 --- /dev/null +++ b/benchmarks/processor/manifest-traceway.yaml @@ -0,0 +1,18 @@ +dist: + name: otelcol-bench-traceway + description: Bench collector with the Traceway symbolicator + output_path: ./build-traceway + +receivers: + - gomod: go.opentelemetry.io/collector/receiver/otlpreceiver v0.154.0 + +processors: + - gomod: go.opentelemetry.io/collector/processor/batchprocessor v0.154.0 + - gomod: github.com/tracewayapp/traceway/backend v1.8.0 + import: github.com/tracewayapp/traceway/backend/app/symbolicator/otelprocessor + +exporters: + - gomod: go.opentelemetry.io/collector/exporter/otlphttpexporter v0.154.0 + +replaces: + - github.com/tracewayapp/traceway/backend => ../../../backend diff --git a/benchmarks/processor/rss-sampler.sh b/benchmarks/processor/rss-sampler.sh new file mode 100755 index 00000000..0b743f50 --- /dev/null +++ b/benchmarks/processor/rss-sampler.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +PID="$1" +OUT="$2" +echo "ts,rss_kb" > "$OUT" +while kill -0 "$PID" 2>/dev/null; do + RSS=$(ps -o rss= -p "$PID" 2>/dev/null | tr -d ' ') + [ -n "$RSS" ] && echo "$(date +%s),$RSS" >> "$OUT" + sleep 1 +done diff --git a/benchmarks/processor/run-hetzner.sh b/benchmarks/processor/run-hetzner.sh new file mode 100755 index 00000000..795e3f39 --- /dev/null +++ b/benchmarks/processor/run-hetzner.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +IMPL="${1:?usage: run-hetzner.sh }" +SCENARIOS="${SCENARIOS:-hot churn}" +CHURN_ENTRIES="${CHURN_ENTRIES:-512}" +PAD_KB="${PAD_KB:-256}" +CONNECTIONS="${CONNECTIONS:-8,16,32,64,128,256}" +STEP_DURATION="${STEP_DURATION:-60s}" +SPANS_PER_REQUEST="${SPANS_PER_REQUEST:-20}" +SUT_TYPE="${SUT_TYPE:-ccx33}" +LDG_TYPE="${LDG_TYPE:-ccx23}" +LOCATION="${LOCATION:-fsn1}" +RESULTS="${RESULTS:-./results}" +RUN_ID="${GITHUB_RUN_ID:-local}-$IMPL" + +command -v hcloud >/dev/null || { echo "hcloud CLI required" >&2; exit 1; } +[ -d artifacts ] || { echo "artifacts/ missing, build first (see workflow or run-local.sh build_all)" >&2; exit 1; } + +KEY_FILE=$(mktemp -u) +ssh-keygen -t ed25519 -N '' -f "$KEY_FILE" -q +SSH="ssh -i $KEY_FILE -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10" +SCP="scp -i $KEY_FILE -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + +cleanup() { + hcloud server delete "bench-sut-$RUN_ID" 2>/dev/null || true + hcloud server delete "bench-ldg-$RUN_ID" 2>/dev/null || true + hcloud ssh-key delete "bench-key-$RUN_ID" 2>/dev/null || true + rm -f "$KEY_FILE" "$KEY_FILE.pub" +} +trap cleanup EXIT + +hcloud ssh-key create --name "bench-key-$RUN_ID" --public-key-from-file "$KEY_FILE.pub" +hcloud server create --name "bench-sut-$RUN_ID" --type "$SUT_TYPE" --image ubuntu-24.04 --location "$LOCATION" --ssh-key "bench-key-$RUN_ID" +hcloud server create --name "bench-ldg-$RUN_ID" --type "$LDG_TYPE" --image ubuntu-24.04 --location "$LOCATION" --ssh-key "bench-key-$RUN_ID" +SUT_IP=$(hcloud server ip "bench-sut-$RUN_ID") +LDG_IP=$(hcloud server ip "bench-ldg-$RUN_ID") + +for ip in "$SUT_IP" "$LDG_IP"; do + for i in $(seq 1 60); do $SSH "root@$ip" true 2>/dev/null && break; sleep 5; done +done + +for ip in "$SUT_IP" "$LDG_IP"; do + $SCP -r artifacts "root@$ip:/opt/bench" + $SSH "root@$ip" "chmod +x /opt/bench/* 2>/dev/null || true" +done + +case "$IMPL" in + traceway-*) COL_BIN=otelcol-bench-traceway; COL_CFG=config-traceway.yaml ;; + honeycomb) COL_BIN=otelcol-bench-honeycomb; COL_CFG=config-honeycomb.yaml ;; +esac + +$SSH "root@$LDG_IP" "nohup env DRAIN_ADDR=0.0.0.0:9319 /opt/bench/drain > /opt/bench/drain.log 2>&1 & sleep 1; curl -sf http://127.0.0.1:9319/stats" + +for scenario in $SCENARIOS; do + entries=1 + [ "$scenario" = "churn" ] && entries="$CHURN_ENTRIES" + tag="$IMPL-$scenario" + outdir="$RESULTS/$tag" + mkdir -p "$outdir" + + $SSH "root@$SUT_IP" "cd /opt/bench && ./corpusgen --bundle app.mjs --map app.mjs.map --entries $entries --pad-kb $PAD_KB --out corpus-$scenario" + $SSH "root@$LDG_IP" "cd /opt/bench && ./corpusgen --bundle app.mjs --map app.mjs.map --entries $entries --pad-kb $PAD_KB --out corpus-$scenario" + + $SSH "root@$LDG_IP" "curl -sf -X POST http://127.0.0.1:9319/reset" + $SSH "root@$SUT_IP" "pkill -f $COL_BIN 2>/dev/null; rm -rf /opt/bench/twcache; cd /opt/bench && nohup env STORE_PATH=./corpus-$scenario CACHE_DIR=./twcache SYMB_PARSER=${IMPL#traceway-} DRAIN_ENDPOINT=http://$LDG_IP:9319 LD_LIBRARY_PATH=/opt/bench ./$COL_BIN --config $COL_CFG > collector.log 2>&1 & sleep 3; pgrep -f $COL_BIN" + $SSH "root@$SUT_IP" "cd /opt/bench && nohup bash rss-sampler.sh \$(pgrep -f $COL_BIN | head -1) rss.csv > /dev/null 2>&1 &" + + $SSH "root@$LDG_IP" "cd /opt/bench && ./loadgen --target http://$SUT_IP:4318/v1/traces --corpus corpus-$scenario/corpus.json --connections $CONNECTIONS --step-duration $STEP_DURATION --spans-per-request $SPANS_PER_REQUEST --out loadgen.json" + + $SSH "root@$LDG_IP" "curl -sf http://127.0.0.1:9319/stats" > "$outdir/drain.json" + $SCP "root@$LDG_IP:/opt/bench/loadgen.json" "$outdir/loadgen.json" + $SSH "root@$SUT_IP" "pkill -f $COL_BIN; sleep 2" || true + $SCP "root@$SUT_IP:/opt/bench/rss.csv" "$outdir/rss.csv" + $SCP "root@$SUT_IP:/opt/bench/collector.log" "$outdir/collector.log" + echo "== $tag ==" + cat "$outdir/drain.json"; echo +done diff --git a/benchmarks/processor/run-local.sh b/benchmarks/processor/run-local.sh new file mode 100755 index 00000000..c6245871 --- /dev/null +++ b/benchmarks/processor/run-local.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +IMPLS="${IMPLS:-traceway-oxc honeycomb}" +SCENARIOS="${SCENARIOS:-hot churn}" +CHURN_ENTRIES="${CHURN_ENTRIES:-512}" +PAD_KB="${PAD_KB:-256}" +CONNECTIONS="${CONNECTIONS:-4,8,16,32,64,128}" +STEP_DURATION="${STEP_DURATION:-30s}" +SPANS_PER_REQUEST="${SPANS_PER_REQUEST:-20}" +SKIP_BUILD="${SKIP_BUILD:-}" +RESULTS="${RESULTS:-./results}" + +OCB_154=./bin/ocb-0.154.0 +OCB_143=./bin/ocb-0.143.0 + +build_all() { + mkdir -p bin + [ -x "$OCB_154" ] || { GOBIN="$PWD/bin" go install go.opentelemetry.io/collector/cmd/builder@v0.154.0 && mv bin/builder "$OCB_154"; } + [ -x "$OCB_143" ] || { GOBIN="$PWD/bin" go install go.opentelemetry.io/collector/cmd/builder@v0.143.0 && mv bin/builder "$OCB_143"; } + + if echo "$IMPLS" | grep -q traceway; then + bash ../../scripts/build-oxc-shim.sh + "$OCB_154" --config manifest-traceway.yaml --skip-compilation + (cd build-traceway && CGO_ENABLED=1 go build -tags oxc -o otelcol-bench-traceway .) + fi + if echo "$IMPLS" | grep -q honeycomb; then + CGO_ENABLED=1 "$OCB_143" --config manifest-honeycomb.yaml + fi + + (cd drain && cargo build --release) + (cd loadgen && go mod tidy >/dev/null 2>&1; go build -o loadgen .) + (cd corpusgen && go build -o corpusgen .) + + if [ ! -f ../../testing/symbolication/node-app/dist/app.mjs ]; then + (cd ../../testing/symbolication/node-app && npm install && npm run build) + fi +} + +gen_corpus() { + local scenario="$1" entries="$2" + local dir="./corpus-$scenario" + if [ ! -f "$dir/corpus.json" ]; then + ./corpusgen/corpusgen --entries "$entries" --pad-kb "$PAD_KB" --out "$dir" + fi + echo "$dir" +} + +collector_bin() { + case "$1" in + traceway-*) echo "./build-traceway/otelcol-bench-traceway" ;; + honeycomb) echo "./build-honeycomb/otelcol-bench-honeycomb" ;; + esac +} + +collector_cfg() { + case "$1" in + traceway-*) echo "config-traceway.yaml" ;; + honeycomb) echo "config-honeycomb.yaml" ;; + esac +} + +run_one() { + local impl="$1" scenario="$2" + local entries=1 + [ "$scenario" = "churn" ] && entries="$CHURN_ENTRIES" + local store + store=$(gen_corpus "$scenario" "$entries") + local tag="$impl-$scenario" + local outdir="$RESULTS/$tag" + mkdir -p "$outdir" + + DRAIN_ADDR=127.0.0.1:9319 ./drain/target/release/drain & + local drain_pid=$! + sleep 1 + curl -sf -X POST http://127.0.0.1:9319/reset > /dev/null + + local cache_dir + cache_dir=$(mktemp -d) + STORE_PATH="$store" CACHE_DIR="$cache_dir" SYMB_PARSER="${impl#traceway-}" \ + DRAIN_ENDPOINT=http://127.0.0.1:9319 \ + "$(collector_bin "$impl")" --config "$(collector_cfg "$impl")" > "$outdir/collector.log" 2>&1 & + local col_pid=$! + + for i in $(seq 1 30); do + curl -sf -o /dev/null -X POST -H 'Content-Type: application/x-protobuf' --data-binary '' http://127.0.0.1:4318/v1/traces 2>/dev/null && break + kill -0 "$col_pid" 2>/dev/null || { echo "collector died, see $outdir/collector.log" >&2; kill "$drain_pid"; return 1; } + sleep 1 + done + + bash ./rss-sampler.sh "$col_pid" "$outdir/rss.csv" & + local rss_pid=$! + + ./loadgen/loadgen --target http://127.0.0.1:4318/v1/traces --corpus "$store/corpus.json" \ + --connections "$CONNECTIONS" --step-duration "$STEP_DURATION" \ + --spans-per-request "$SPANS_PER_REQUEST" --out "$outdir/loadgen.json" + + curl -sf http://127.0.0.1:9319/stats > "$outdir/drain.json" + kill "$col_pid" "$drain_pid" 2>/dev/null || true + wait "$rss_pid" 2>/dev/null || true + rm -rf "$cache_dir" + echo "== $tag ==" + cat "$outdir/drain.json"; echo +} + +summarize() { + echo + printf '%-28s %12s %12s %10s %14s\n' run max_stacks/s peak_rss_mb p99_ms drain_symb_pct + for d in "$RESULTS"/*/; do + [ -f "$d/loadgen.json" ] || continue + local name peak maxr p99 pct + name=$(basename "$d") + maxr=$(jq '[.[].stacks_per_sec] | max' "$d/loadgen.json") + p99=$(jq '[.[] | select(.stacks_per_sec == ([.[].stacks_per_sec] | max))] | .[0].p99_ms' "$d/loadgen.json" 2>/dev/null || jq '.[-1].p99_ms' "$d/loadgen.json") + peak=$(tail -n +2 "$d/rss.csv" | cut -d, -f2 | sort -n | tail -1) + pct=$(jq -r 'if .requests > 0 then (100 * .symbolicated / .requests | floor) else 0 end' "$d/drain.json") + printf '%-28s %12.0f %12d %10.1f %13s%%\n' "$name" "$maxr" "$((${peak:-0} / 1024))" "$p99" "$pct" + done +} + +[ -n "$SKIP_BUILD" ] || build_all +for impl in $IMPLS; do + for scenario in $SCENARIOS; do + run_one "$impl" "$scenario" + done +done +summarize From 91d00fa381e80a63f7e744504fd5f9619c3a8be7 Mon Sep 17 00:00:00 2001 From: ddux Date: Thu, 11 Jun 2026 20:23:12 -0500 Subject: [PATCH 19/24] benchmarks for symbolicator --- .github/workflows/benchmark-processor.yml | 53 ++++++- benchmarks/processor/README.md | 33 ++++- benchmarks/processor/config-honeycomb.yaml | 1 + benchmarks/processor/config-traceway.yaml | 5 +- benchmarks/processor/corpusgen/main.go | 159 +++++++++++++++++++-- benchmarks/processor/loadgen/main.go | 2 + benchmarks/processor/rss-sampler.sh | 6 +- benchmarks/processor/run-hetzner.sh | 94 ++++++++---- benchmarks/processor/run-local.sh | 95 ++++++------ benchmarks/processor/summarize.sh | 25 ++++ 10 files changed, 381 insertions(+), 92 deletions(-) create mode 100644 benchmarks/processor/summarize.sh diff --git a/.github/workflows/benchmark-processor.yml b/.github/workflows/benchmark-processor.yml index 04e3a3a9..f45f48a3 100644 --- a/.github/workflows/benchmark-processor.yml +++ b/.github/workflows/benchmark-processor.yml @@ -6,7 +6,31 @@ on: scenarios: description: 'Scenarios to run' required: true - default: 'hot churn' + default: 'hot churn oom' + oom_entries: + description: 'Corpus entries for the oom scenario' + required: true + default: '4096' + oom_pad_kb: + description: 'Bundle padding KB per oom corpus entry' + required: true + default: '1024' + oom_map_pad_kb: + description: 'sourcesContent padding KB per oom corpus map' + required: true + default: '1024' + oom_mappings_pad_kb: + description: 'VLQ mappings padding KB per oom corpus map' + required: true + default: '1024' + oom_duration: + description: 'Max duration of the oom load step' + required: true + default: '30m' + oom_sut_type: + description: 'Hetzner server type for the oom scenario' + required: true + default: 'ccx13' sut_type: description: 'Hetzner server type for the collector under test' required: true @@ -90,7 +114,7 @@ jobs: strategy: fail-fast: false matrix: - impl: [traceway-oxc, honeycomb] + impl: [honeycomb, traceway-oxc-mem, traceway-oxc-disk, traceway-goja-mem, traceway-goja-disk] steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 @@ -111,9 +135,34 @@ jobs: STEP_DURATION: ${{ inputs.step_duration }} CHURN_ENTRIES: ${{ inputs.churn_entries }} PAD_KB: ${{ inputs.pad_kb }} + OOM_ENTRIES: ${{ inputs.oom_entries }} + OOM_PAD_KB: ${{ inputs.oom_pad_kb }} + OOM_MAP_PAD_KB: ${{ inputs.oom_map_pad_kb }} + OOM_MAPPINGS_PAD_KB: ${{ inputs.oom_mappings_pad_kb }} + OOM_DURATION: ${{ inputs.oom_duration }} + OOM_SUT_TYPE: ${{ inputs.oom_sut_type }} run: bash run-hetzner.sh ${{ matrix.impl }} - uses: actions/upload-artifact@v4 if: always() with: name: results-${{ matrix.impl }} path: benchmarks/processor/results/ + + summary: + needs: bench + if: always() + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + pattern: results-* + merge-multiple: true + path: benchmarks/processor/results/ + - name: Summarize + working-directory: benchmarks/processor + run: bash summarize.sh ./results | tee -a "$GITHUB_STEP_SUMMARY" + - uses: actions/upload-artifact@v4 + with: + name: summary + path: benchmarks/processor/results/ diff --git a/benchmarks/processor/README.md b/benchmarks/processor/README.md index 591fcb7d..65491251 100644 --- a/benchmarks/processor/README.md +++ b/benchmarks/processor/README.md @@ -25,13 +25,36 @@ so one accepted loadgen request equals one fully symbolicated export delivered t the drain. Loadgen ok-rate times spans-per-request is the end-to-end stacktraces/sec; the drain's symbolicated percentage is the correctness check. +## Implementations + +| impl | binary | parser | cache | +|------|--------|--------|-------| +| `honeycomb` | otelcol-bench-honeycomb | symbolic (cgo) | RAM LRU, entry-count bound | +| `traceway-oxc-mem` | otelcol-bench-traceway | oxc | in-memory resolvers only | +| `traceway-oxc-disk` | otelcol-bench-traceway | oxc | mmap'd `.tw` disk tier | +| `traceway-goja-mem` | otelcol-bench-traceway | goja | in-memory resolvers only | +| `traceway-goja-disk` | otelcol-bench-traceway | goja | mmap'd `.tw` disk tier | + +One traceway binary (built with `-tags oxc`) serves all four variants; parser +and cache mode are runtime config (`parser`, `cache_dir`). + ## Scenarios -- `hot`: one bundle, always cache-warm. Pure resolve throughput. -- `churn`: 512 bundles (default) against the 128-entry resolver cache. - Honeycomb re-parses through Sentry symbolic on every eviction; Traceway - re-opens compiled `.tw` files from its disk cache. This is also where the - goja-vs-oxc parser choice matters, since the parser only runs on cache misses. +- `hot`: one bundle, always cache-warm. Pure resolve throughput ramp. +- `churn`: 512 bundles (default) against a 128-entry resolver cache. + Honeycomb re-parses through Sentry symbolic on every eviction; Traceway disk + variants re-open compiled `.tw` files. The goja-vs-oxc choice matters here, + since the parser only runs on cache misses. +- `oom`: 4096 bundles (default) with 1MB bundle padding, 1MB of + sourcesContent padding, AND 1MB of synthetic VLQ mappings per map (so + traceway's token table grows too and the corpus is realistic, not rigged), cache entry limit raised to corpus size, + fixed 32-connection load until the corpus is fully resident or the collector + dies. Honeycomb retains the raw map JSON plus the minified bundle on the C + heap per entry, so RSS grows with corpus bytes. Traceway discards bundles and + sourcesContent after compiling the compact `.tw` token table; the mem + variants keep resolvers on the Go heap, the disk variants only mmap handles. + The oom run defaults to a small SUT (ccx13, 8 GB) so the breaking point + arrives quickly. Corpus entries are the real minified node-app bundle padded with `--pad-kb` of valid JS (default 256 KB) so scope-analysis parse cost is realistic. The diff --git a/benchmarks/processor/config-honeycomb.yaml b/benchmarks/processor/config-honeycomb.yaml index 2e876790..d50020fe 100644 --- a/benchmarks/processor/config-honeycomb.yaml +++ b/benchmarks/processor/config-honeycomb.yaml @@ -9,6 +9,7 @@ processors: source_map_store: file_store local_source_maps: path: ${env:STORE_PATH} + source_map_cache_size: ${env:CACHE_SIZE:-128} preserve_stack_trace: false exporters: diff --git a/benchmarks/processor/config-traceway.yaml b/benchmarks/processor/config-traceway.yaml index 444a5643..7dba1700 100644 --- a/benchmarks/processor/config-traceway.yaml +++ b/benchmarks/processor/config-traceway.yaml @@ -9,9 +9,10 @@ processors: source_map_store: file_store local_source_maps: path: ${env:STORE_PATH} - cache_dir: ${env:CACHE_DIR} + cache_dir: ${env:CACHE_DIR:-} + source_map_cache_size: ${env:CACHE_SIZE:-128} preserve_stack_trace: false - parser: ${env:SYMB_PARSER} + parser: ${env:SYMB_PARSER:-goja} exporters: otlphttp/drain: diff --git a/benchmarks/processor/corpusgen/main.go b/benchmarks/processor/corpusgen/main.go index 6b5170ff..bef38264 100644 --- a/benchmarks/processor/corpusgen/main.go +++ b/benchmarks/processor/corpusgen/main.go @@ -13,11 +13,149 @@ type corpus struct { Urls []string `json:"urls"` } +const vlqChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + +var vlqIndex = func() [128]int8 { + var t [128]int8 + for i := range t { + t[i] = -1 + } + for i, c := range vlqChars { + t[c] = int8(i) + } + return t +}() + +func vlqEncode(b *strings.Builder, v int) { + u := v << 1 + if v < 0 { + u = (-v << 1) | 1 + } + for { + digit := u & 31 + u >>= 5 + if u > 0 { + digit |= 32 + } + b.WriteByte(vlqChars[digit]) + if u == 0 { + break + } + } +} + +func mappingsEndState(mappings string) (srcIdx, srcLine, srcCol, nameIdx int) { + vals := make([]int, 0, 5) + cur, shift := 0, 0 + flush := func() { + if len(vals) >= 4 { + srcIdx += vals[1] + srcLine += vals[2] + srcCol += vals[3] + } + if len(vals) >= 5 { + nameIdx += vals[4] + } + vals = vals[:0] + } + for _, c := range mappings { + if c == ';' || c == ',' { + flush() + continue + } + d := vlqIndex[c] + cur |= int(d&31) << shift + if d&32 != 0 { + shift += 5 + continue + } + v := cur >> 1 + if cur&1 != 0 { + v = -v + } + vals = append(vals, v) + cur, shift = 0, 0 + } + flush() + return +} + +func padMap(mapBytes []byte, kb, mappingsKB, seed int) []byte { + var m map[string]any + if err := json.Unmarshal(mapBytes, &m); err != nil { + panic(err) + } + sources, _ := m["sources"].([]any) + content, _ := m["sourcesContent"].([]any) + for len(content) < len(sources) { + content = append(content, nil) + } + padIdx := len(sources) + var pad strings.Builder + line := fmt.Sprintf("const benchPadValue%d = %d;\n", seed, seed) + for pad.Len() < kb*1024 { + pad.WriteString(line) + } + m["sources"] = append(sources, "__benchpad.js") + m["sourcesContent"] = append(content, pad.String()) + + if mappingsKB > 0 { + mappings, _ := m["mappings"].(string) + names, _ := m["names"].([]any) + nameBase := len(names) + for i := 0; i < 64; i++ { + names = append(names, fmt.Sprintf("__benchFn%d_%d", seed, i)) + } + m["names"] = names + + srcIdx, srcLine, srcCol, nameIdx := mappingsEndState(mappings) + var b strings.Builder + b.WriteString(mappings) + dSrc := padIdx - srcIdx + dLine := -srcLine + dCol := -srcCol + dName := nameBase - nameIdx + genLine := 0 + for b.Len() < len(mappings)+mappingsKB*1024 { + b.WriteByte(';') + genLine++ + col := 0 + for s := 0; s < 8; s++ { + if s > 0 { + b.WriteByte(',') + } + vlqEncode(&b, col) + vlqEncode(&b, dSrc) + vlqEncode(&b, dLine) + vlqEncode(&b, dCol) + vlqEncode(&b, dName) + dSrc, dCol = 0, 1 + dLine = 0 + dName = 1 + if s == 7 { + dLine, dCol = 1, -7 + dName = -7 + } + col = 10 + } + } + m["mappings"] = b.String() + } + + out, err := json.Marshal(m) + if err != nil { + panic(err) + } + return out +} + func main() { bundle := flag.String("bundle", "../../testing/symbolication/node-app/dist/app.mjs", "") mapFile := flag.String("map", "../../testing/symbolication/node-app/dist/app.mjs.map", "") entries := flag.Int("entries", 1, "") padKB := flag.Int("pad-kb", 256, "") + mapPadKB := flag.Int("map-pad-kb", 0, "") + mappingsPadKB := flag.Int("mappings-pad-kb", 0, "") out := flag.String("out", "./corpus", "") flag.Parse() @@ -31,25 +169,28 @@ func main() { } firstLine := strings.SplitN(string(bundleBytes), "\n", 2)[0] - var pad strings.Builder - chunk := "function __benchPad%d(a,b){var c=a*b+%d;for(var i=0;i<3;i++){c+=i*a-b}return c}\n" - i := 0 - for pad.Len() < *padKB*1024 { - pad.WriteString(fmt.Sprintf(chunk, i, i)) - i++ - } - if err := os.MkdirAll(*out, 0o755); err != nil { panic(err) } c := corpus{} for n := 0; n < *entries; n++ { + var pad strings.Builder + chunk := "function __benchPad%d_%d(a,b){var c=a*b+%d;for(var i=0;i<3;i++){c+=i*a-b}return c}\n" + i := 0 + for pad.Len() < *padKB*1024 { + pad.WriteString(fmt.Sprintf(chunk, n, i, i+n)) + i++ + } name := fmt.Sprintf("app%d.mjs", n) content := firstLine + "\n" + pad.String() + "//# sourceMappingURL=" + name + ".map\n" if err := os.WriteFile(filepath.Join(*out, name), []byte(content), 0o644); err != nil { panic(err) } - if err := os.WriteFile(filepath.Join(*out, name+".map"), mapBytes, 0o644); err != nil { + entryMap := mapBytes + if *mapPadKB > 0 || *mappingsPadKB > 0 { + entryMap = padMap(mapBytes, *mapPadKB, *mappingsPadKB, n) + } + if err := os.WriteFile(filepath.Join(*out, name+".map"), entryMap, 0o644); err != nil { panic(err) } c.Urls = append(c.Urls, name) diff --git a/benchmarks/processor/loadgen/main.go b/benchmarks/processor/loadgen/main.go index 7965ec7f..99fb45d2 100644 --- a/benchmarks/processor/loadgen/main.go +++ b/benchmarks/processor/loadgen/main.go @@ -5,6 +5,7 @@ import ( "encoding/json" "flag" "fmt" + "io" "net/http" "os" "path/filepath" @@ -121,6 +122,7 @@ func main() { atomic.AddInt64(&errs, 1) continue } + io.Copy(io.Discard, resp.Body) resp.Body.Close() if resp.StatusCode >= 200 && resp.StatusCode < 300 { atomic.AddInt64(&ok, 1) diff --git a/benchmarks/processor/rss-sampler.sh b/benchmarks/processor/rss-sampler.sh index 0b743f50..30f21ea1 100755 --- a/benchmarks/processor/rss-sampler.sh +++ b/benchmarks/processor/rss-sampler.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash PID="$1" OUT="$2" -echo "ts,rss_kb" > "$OUT" +echo "ts,rss_kb,cpu_pct" > "$OUT" while kill -0 "$PID" 2>/dev/null; do - RSS=$(ps -o rss= -p "$PID" 2>/dev/null | tr -d ' ') - [ -n "$RSS" ] && echo "$(date +%s),$RSS" >> "$OUT" + LINE=$(ps -o rss=,pcpu= -p "$PID" 2>/dev/null | awk '{print $1","$2}') + [ -n "$LINE" ] && echo "$(date +%s),$LINE" >> "$OUT" sleep 1 done diff --git a/benchmarks/processor/run-hetzner.sh b/benchmarks/processor/run-hetzner.sh index 795e3f39..b07eda49 100755 --- a/benchmarks/processor/run-hetzner.sh +++ b/benchmarks/processor/run-hetzner.sh @@ -3,14 +3,21 @@ set -euo pipefail cd "$(dirname "$0")" -IMPL="${1:?usage: run-hetzner.sh }" -SCENARIOS="${SCENARIOS:-hot churn}" +IMPL="${1:?usage: run-hetzner.sh }" +SCENARIOS="${SCENARIOS:-hot churn oom}" CHURN_ENTRIES="${CHURN_ENTRIES:-512}" PAD_KB="${PAD_KB:-256}" +OOM_ENTRIES="${OOM_ENTRIES:-4096}" +OOM_PAD_KB="${OOM_PAD_KB:-1024}" +OOM_MAP_PAD_KB="${OOM_MAP_PAD_KB:-1024}" +OOM_MAPPINGS_PAD_KB="${OOM_MAPPINGS_PAD_KB:-1024}" +OOM_CONNECTIONS="${OOM_CONNECTIONS:-32}" +OOM_DURATION="${OOM_DURATION:-30m}" CONNECTIONS="${CONNECTIONS:-8,16,32,64,128,256}" STEP_DURATION="${STEP_DURATION:-60s}" SPANS_PER_REQUEST="${SPANS_PER_REQUEST:-20}" SUT_TYPE="${SUT_TYPE:-ccx33}" +OOM_SUT_TYPE="${OOM_SUT_TYPE:-ccx13}" LDG_TYPE="${LDG_TYPE:-ccx23}" LOCATION="${LOCATION:-fsn1}" RESULTS="${RESULTS:-./results}" @@ -19,6 +26,23 @@ RUN_ID="${GITHUB_RUN_ID:-local}-$IMPL" command -v hcloud >/dev/null || { echo "hcloud CLI required" >&2; exit 1; } [ -d artifacts ] || { echo "artifacts/ missing, build first (see workflow or run-local.sh build_all)" >&2; exit 1; } +case "$IMPL" in + honeycomb) COL_BIN=otelcol-bench-honeycomb; COL_CFG=config-honeycomb.yaml; PARSER=goja; DISK= ;; + traceway-oxc-mem) COL_BIN=otelcol-bench-traceway; COL_CFG=config-traceway.yaml; PARSER=oxc; DISK= ;; + traceway-oxc-disk) COL_BIN=otelcol-bench-traceway; COL_CFG=config-traceway.yaml; PARSER=oxc; DISK=1 ;; + traceway-goja-mem) COL_BIN=otelcol-bench-traceway; COL_CFG=config-traceway.yaml; PARSER=goja; DISK= ;; + traceway-goja-disk) COL_BIN=otelcol-bench-traceway; COL_CFG=config-traceway.yaml; PARSER=goja; DISK=1 ;; + *) echo "unknown impl $IMPL" >&2; exit 1 ;; +esac + +scenario_params() { + case "$1" in + hot) echo "1 $PAD_KB 0:0 128 $CONNECTIONS $STEP_DURATION $SUT_TYPE" ;; + churn) echo "$CHURN_ENTRIES $PAD_KB 0:0 128 $CONNECTIONS $STEP_DURATION $SUT_TYPE" ;; + oom) echo "$OOM_ENTRIES $OOM_PAD_KB $OOM_MAP_PAD_KB:$OOM_MAPPINGS_PAD_KB $OOM_ENTRIES $OOM_CONNECTIONS $OOM_DURATION $OOM_SUT_TYPE" ;; + esac +} + KEY_FILE=$(mktemp -u) ssh-keygen -t ed25519 -N '' -f "$KEY_FILE" -q SSH="ssh -i $KEY_FILE -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10" @@ -32,49 +56,63 @@ cleanup() { } trap cleanup EXIT -hcloud ssh-key create --name "bench-key-$RUN_ID" --public-key-from-file "$KEY_FILE.pub" -hcloud server create --name "bench-sut-$RUN_ID" --type "$SUT_TYPE" --image ubuntu-24.04 --location "$LOCATION" --ssh-key "bench-key-$RUN_ID" -hcloud server create --name "bench-ldg-$RUN_ID" --type "$LDG_TYPE" --image ubuntu-24.04 --location "$LOCATION" --ssh-key "bench-key-$RUN_ID" -SUT_IP=$(hcloud server ip "bench-sut-$RUN_ID") -LDG_IP=$(hcloud server ip "bench-ldg-$RUN_ID") - -for ip in "$SUT_IP" "$LDG_IP"; do - for i in $(seq 1 60); do $SSH "root@$ip" true 2>/dev/null && break; sleep 5; done -done +wait_ssh() { + for i in $(seq 1 60); do $SSH "root@$1" true 2>/dev/null && return 0; sleep 5; done + return 1 +} -for ip in "$SUT_IP" "$LDG_IP"; do - $SCP -r artifacts "root@$ip:/opt/bench" +provision() { + local name="$1" type="$2" + hcloud server create --name "$name" --type "$type" --image ubuntu-24.04 --location "$LOCATION" --ssh-key "bench-key-$RUN_ID" > /dev/null + local ip + ip=$(hcloud server ip "$name") + wait_ssh "$ip" + $SCP -r artifacts "root@$ip:/opt/bench" > /dev/null $SSH "root@$ip" "chmod +x /opt/bench/* 2>/dev/null || true" -done - -case "$IMPL" in - traceway-*) COL_BIN=otelcol-bench-traceway; COL_CFG=config-traceway.yaml ;; - honeycomb) COL_BIN=otelcol-bench-honeycomb; COL_CFG=config-honeycomb.yaml ;; -esac + echo "$ip" +} +hcloud ssh-key create --name "bench-key-$RUN_ID" --public-key-from-file "$KEY_FILE.pub" > /dev/null +LDG_IP=$(provision "bench-ldg-$RUN_ID" "$LDG_TYPE") $SSH "root@$LDG_IP" "nohup env DRAIN_ADDR=0.0.0.0:9319 /opt/bench/drain > /opt/bench/drain.log 2>&1 & sleep 1; curl -sf http://127.0.0.1:9319/stats" +PREV_SUT_TYPE="" +SUT_IP="" for scenario in $SCENARIOS; do - entries=1 - [ "$scenario" = "churn" ] && entries="$CHURN_ENTRIES" + read -r entries pad mappad cachesize conns dur sut_type <<< "$(scenario_params "$scenario")" + if [ "$sut_type" != "$PREV_SUT_TYPE" ]; then + [ -n "$SUT_IP" ] && hcloud server delete "bench-sut-$RUN_ID" && sleep 3 + SUT_IP=$(provision "bench-sut-$RUN_ID" "$sut_type") + PREV_SUT_TYPE="$sut_type" + fi tag="$IMPL-$scenario" outdir="$RESULTS/$tag" mkdir -p "$outdir" - $SSH "root@$SUT_IP" "cd /opt/bench && ./corpusgen --bundle app.mjs --map app.mjs.map --entries $entries --pad-kb $PAD_KB --out corpus-$scenario" - $SSH "root@$LDG_IP" "cd /opt/bench && ./corpusgen --bundle app.mjs --map app.mjs.map --entries $entries --pad-kb $PAD_KB --out corpus-$scenario" + for ip in "$SUT_IP" "$LDG_IP"; do + $SSH "root@$ip" "cd /opt/bench && [ -f corpus-$scenario/corpus.json ] || ./corpusgen --bundle app.mjs --map app.mjs.map --entries $entries --pad-kb $pad --map-pad-kb ${mappad%%:*} --mappings-pad-kb ${mappad##*:} --out corpus-$scenario" + done + CACHE_DIR_REMOTE="" + [ -n "$DISK" ] && CACHE_DIR_REMOTE="/opt/bench/twcache" $SSH "root@$LDG_IP" "curl -sf -X POST http://127.0.0.1:9319/reset" - $SSH "root@$SUT_IP" "pkill -f $COL_BIN 2>/dev/null; rm -rf /opt/bench/twcache; cd /opt/bench && nohup env STORE_PATH=./corpus-$scenario CACHE_DIR=./twcache SYMB_PARSER=${IMPL#traceway-} DRAIN_ENDPOINT=http://$LDG_IP:9319 LD_LIBRARY_PATH=/opt/bench ./$COL_BIN --config $COL_CFG > collector.log 2>&1 & sleep 3; pgrep -f $COL_BIN" + $SSH "root@$SUT_IP" "pkill -f $COL_BIN 2>/dev/null; rm -rf /opt/bench/twcache; cd /opt/bench && nohup env STORE_PATH=./corpus-$scenario CACHE_DIR=$CACHE_DIR_REMOTE CACHE_SIZE=$cachesize SYMB_PARSER=$PARSER DRAIN_ENDPOINT=http://$LDG_IP:9319 LD_LIBRARY_PATH=/opt/bench ./$COL_BIN --config $COL_CFG > collector.log 2>&1 & sleep 3; pgrep -f $COL_BIN" + T0=$($SSH "root@$SUT_IP" "date +%s") $SSH "root@$SUT_IP" "cd /opt/bench && nohup bash rss-sampler.sh \$(pgrep -f $COL_BIN | head -1) rss.csv > /dev/null 2>&1 &" - $SSH "root@$LDG_IP" "cd /opt/bench && ./loadgen --target http://$SUT_IP:4318/v1/traces --corpus corpus-$scenario/corpus.json --connections $CONNECTIONS --step-duration $STEP_DURATION --spans-per-request $SPANS_PER_REQUEST --out loadgen.json" + $SSH "root@$LDG_IP" "cd /opt/bench && ./loadgen --target http://$SUT_IP:4318/v1/traces --corpus corpus-$scenario/corpus.json --connections $conns --step-duration $dur --spans-per-request $SPANS_PER_REQUEST --out loadgen.json" || true - $SSH "root@$LDG_IP" "curl -sf http://127.0.0.1:9319/stats" > "$outdir/drain.json" + if ! $SSH "root@$SUT_IP" "pgrep -f $COL_BIN > /dev/null"; then + TDEAD=$($SSH "root@$SUT_IP" "tail -1 /opt/bench/rss.csv | cut -d, -f1") + echo "$(( TDEAD - T0 ))" > "$outdir/died" + fi + $SSH "root@$LDG_IP" "curl -sf http://127.0.0.1:9319/stats" > "$outdir/drain.json" || echo '{}' > "$outdir/drain.json" $SCP "root@$LDG_IP:/opt/bench/loadgen.json" "$outdir/loadgen.json" $SSH "root@$SUT_IP" "pkill -f $COL_BIN; sleep 2" || true - $SCP "root@$SUT_IP:/opt/bench/rss.csv" "$outdir/rss.csv" - $SCP "root@$SUT_IP:/opt/bench/collector.log" "$outdir/collector.log" + $SCP "root@$SUT_IP:/opt/bench/rss.csv" "$outdir/rss.csv" || true + $SCP "root@$SUT_IP:/opt/bench/collector.log" "$outdir/collector.log" || true echo "== $tag ==" cat "$outdir/drain.json"; echo done + +bash ./summarize.sh "$RESULTS" diff --git a/benchmarks/processor/run-local.sh b/benchmarks/processor/run-local.sh index c6245871..f9e57723 100755 --- a/benchmarks/processor/run-local.sh +++ b/benchmarks/processor/run-local.sh @@ -3,10 +3,16 @@ set -euo pipefail cd "$(dirname "$0")" -IMPLS="${IMPLS:-traceway-oxc honeycomb}" -SCENARIOS="${SCENARIOS:-hot churn}" +IMPLS="${IMPLS:-honeycomb traceway-oxc-mem traceway-oxc-disk traceway-goja-mem traceway-goja-disk}" +SCENARIOS="${SCENARIOS:-hot churn oom}" CHURN_ENTRIES="${CHURN_ENTRIES:-512}" PAD_KB="${PAD_KB:-256}" +OOM_ENTRIES="${OOM_ENTRIES:-4096}" +OOM_PAD_KB="${OOM_PAD_KB:-1024}" +OOM_MAP_PAD_KB="${OOM_MAP_PAD_KB:-1024}" +OOM_MAPPINGS_PAD_KB="${OOM_MAPPINGS_PAD_KB:-1024}" +OOM_CONNECTIONS="${OOM_CONNECTIONS:-32}" +OOM_DURATION="${OOM_DURATION:-30m}" CONNECTIONS="${CONNECTIONS:-4,8,16,32,64,128}" STEP_DURATION="${STEP_DURATION:-30s}" SPANS_PER_REQUEST="${SPANS_PER_REQUEST:-20}" @@ -39,91 +45,94 @@ build_all() { fi } +scenario_params() { + case "$1" in + hot) echo "1 $PAD_KB 0:0 128 $CONNECTIONS $STEP_DURATION" ;; + churn) echo "$CHURN_ENTRIES $PAD_KB 0:0 128 $CONNECTIONS $STEP_DURATION" ;; + oom) echo "$OOM_ENTRIES $OOM_PAD_KB $OOM_MAP_PAD_KB:$OOM_MAPPINGS_PAD_KB $OOM_ENTRIES $OOM_CONNECTIONS $OOM_DURATION" ;; + esac +} + gen_corpus() { - local scenario="$1" entries="$2" + local scenario="$1" entries="$2" pad="$3" mappad="$4" local dir="./corpus-$scenario" if [ ! -f "$dir/corpus.json" ]; then - ./corpusgen/corpusgen --entries "$entries" --pad-kb "$PAD_KB" --out "$dir" + ./corpusgen/corpusgen --entries "$entries" --pad-kb "$pad" --map-pad-kb "${mappad%%:*}" --mappings-pad-kb "${mappad##*:}" --out "$dir" >&2 fi echo "$dir" } -collector_bin() { - case "$1" in - traceway-*) echo "./build-traceway/otelcol-bench-traceway" ;; - honeycomb) echo "./build-honeycomb/otelcol-bench-honeycomb" ;; - esac -} - -collector_cfg() { +impl_env() { case "$1" in - traceway-*) echo "config-traceway.yaml" ;; - honeycomb) echo "config-honeycomb.yaml" ;; + honeycomb) echo "BIN=./build-honeycomb/otelcol-bench-honeycomb CFG=config-honeycomb.yaml PARSER= DISK=" ;; + traceway-oxc-mem) echo "BIN=./build-traceway/otelcol-bench-traceway CFG=config-traceway.yaml PARSER=oxc DISK=" ;; + traceway-oxc-disk) echo "BIN=./build-traceway/otelcol-bench-traceway CFG=config-traceway.yaml PARSER=oxc DISK=1" ;; + traceway-goja-mem) echo "BIN=./build-traceway/otelcol-bench-traceway CFG=config-traceway.yaml PARSER=goja DISK=" ;; + traceway-goja-disk) echo "BIN=./build-traceway/otelcol-bench-traceway CFG=config-traceway.yaml PARSER=goja DISK=1" ;; + *) echo "unknown impl $1" >&2; return 1 ;; esac } run_one() { local impl="$1" scenario="$2" - local entries=1 - [ "$scenario" = "churn" ] && entries="$CHURN_ENTRIES" + read -r entries pad mappad cachesize conns dur <<< "$(scenario_params "$scenario")" local store - store=$(gen_corpus "$scenario" "$entries") + store=$(gen_corpus "$scenario" "$entries" "$pad" "$mappad") + local BIN CFG PARSER DISK + eval "$(impl_env "$impl")" local tag="$impl-$scenario" local outdir="$RESULTS/$tag" mkdir -p "$outdir" + pkill -f 'target/release/drain' 2>/dev/null || true + pkill -f otelcol-bench 2>/dev/null || true + for i in $(seq 1 10); do + lsof -iTCP:9319 -sTCP:LISTEN >/dev/null 2>&1 || lsof -iTCP:4318 -sTCP:LISTEN >/dev/null 2>&1 || break + sleep 1 + done + DRAIN_ADDR=127.0.0.1:9319 ./drain/target/release/drain & local drain_pid=$! sleep 1 curl -sf -X POST http://127.0.0.1:9319/reset > /dev/null - local cache_dir - cache_dir=$(mktemp -d) - STORE_PATH="$store" CACHE_DIR="$cache_dir" SYMB_PARSER="${impl#traceway-}" \ + local cache_dir="" + [ -n "$DISK" ] && cache_dir=$(mktemp -d) + STORE_PATH="$store" CACHE_DIR="$cache_dir" CACHE_SIZE="$cachesize" SYMB_PARSER="${PARSER:-goja}" \ DRAIN_ENDPOINT=http://127.0.0.1:9319 \ - "$(collector_bin "$impl")" --config "$(collector_cfg "$impl")" > "$outdir/collector.log" 2>&1 & + "$BIN" --config "$CFG" > "$outdir/collector.log" 2>&1 & local col_pid=$! for i in $(seq 1 30); do - curl -sf -o /dev/null -X POST -H 'Content-Type: application/x-protobuf' --data-binary '' http://127.0.0.1:4318/v1/traces 2>/dev/null && break - kill -0 "$col_pid" 2>/dev/null || { echo "collector died, see $outdir/collector.log" >&2; kill "$drain_pid"; return 1; } + curl -s -o /dev/null http://127.0.0.1:4318/ 2>/dev/null && break + kill -0 "$col_pid" 2>/dev/null || { echo "collector died at startup, see $outdir/collector.log" >&2; kill "$drain_pid"; return 1; } sleep 1 done bash ./rss-sampler.sh "$col_pid" "$outdir/rss.csv" & local rss_pid=$! + local t0 + t0=$(date +%s) ./loadgen/loadgen --target http://127.0.0.1:4318/v1/traces --corpus "$store/corpus.json" \ - --connections "$CONNECTIONS" --step-duration "$STEP_DURATION" \ - --spans-per-request "$SPANS_PER_REQUEST" --out "$outdir/loadgen.json" + --connections "$conns" --step-duration "$dur" \ + --spans-per-request "$SPANS_PER_REQUEST" --out "$outdir/loadgen.json" || true - curl -sf http://127.0.0.1:9319/stats > "$outdir/drain.json" + if ! kill -0 "$col_pid" 2>/dev/null; then + echo "$(( $(tail -1 "$outdir/rss.csv" | cut -d, -f1) - t0 ))" > "$outdir/died" + fi + curl -sf http://127.0.0.1:9319/stats > "$outdir/drain.json" || echo '{}' > "$outdir/drain.json" kill "$col_pid" "$drain_pid" 2>/dev/null || true wait "$rss_pid" 2>/dev/null || true - rm -rf "$cache_dir" + [ -n "$cache_dir" ] && rm -rf "$cache_dir" echo "== $tag ==" cat "$outdir/drain.json"; echo } -summarize() { - echo - printf '%-28s %12s %12s %10s %14s\n' run max_stacks/s peak_rss_mb p99_ms drain_symb_pct - for d in "$RESULTS"/*/; do - [ -f "$d/loadgen.json" ] || continue - local name peak maxr p99 pct - name=$(basename "$d") - maxr=$(jq '[.[].stacks_per_sec] | max' "$d/loadgen.json") - p99=$(jq '[.[] | select(.stacks_per_sec == ([.[].stacks_per_sec] | max))] | .[0].p99_ms' "$d/loadgen.json" 2>/dev/null || jq '.[-1].p99_ms' "$d/loadgen.json") - peak=$(tail -n +2 "$d/rss.csv" | cut -d, -f2 | sort -n | tail -1) - pct=$(jq -r 'if .requests > 0 then (100 * .symbolicated / .requests | floor) else 0 end' "$d/drain.json") - printf '%-28s %12.0f %12d %10.1f %13s%%\n' "$name" "$maxr" "$((${peak:-0} / 1024))" "$p99" "$pct" - done -} - [ -n "$SKIP_BUILD" ] || build_all for impl in $IMPLS; do for scenario in $SCENARIOS; do run_one "$impl" "$scenario" done done -summarize +bash ./summarize.sh "$RESULTS" diff --git a/benchmarks/processor/summarize.sh b/benchmarks/processor/summarize.sh new file mode 100644 index 00000000..f2b022aa --- /dev/null +++ b/benchmarks/processor/summarize.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +RESULTS="${1:-./results}" + +printf '| %-20s | %-6s | %12s | %10s | %12s | %8s | %7s | %s |\n' impl scenario max_stacks/s p99_ms_max peak_rss_mb avg_cpu% symb% outcome +printf '|----------------------|--------|--------------|------------|--------------|----------|---------|---------|\n' +for d in "$RESULTS"/*/; do + [ -f "$d/loadgen.json" ] || continue + name=$(basename "$d") + impl="${name%-*}" + scenario="${name##*-}" + maxr=$(jq '[.[].stacks_per_sec] | max | floor' "$d/loadgen.json") + p99=$(jq '([.[].stacks_per_sec] | max) as $m | [.[] | select(.stacks_per_sec == $m)][0].p99_ms' "$d/loadgen.json") + peak=$(tail -n +2 "$d/rss.csv" 2>/dev/null | cut -d, -f2 | sort -n | tail -1) + peak_mb=$(( ${peak:-0} / 1024 )) + cpu=$(tail -n +2 "$d/rss.csv" 2>/dev/null | cut -d, -f3 | awk '{s+=$1; n++} END {if (n>0) printf "%.0f", s/n; else print 0}') + pct=$(jq -r 'if .requests > 0 then (100 * .symbolicated / .requests | floor) else 0 end' "$d/drain.json" 2>/dev/null || echo 0) + outcome=survived + if [ -f "$d/died" ]; then + secs=$(cat "$d/died") + outcome="died@${secs}s" + fi + printf '| %-20s | %-6s | %12s | %10s | %12s | %8s | %7s | %s |\n' "$impl" "$scenario" "$maxr" "$p99" "$peak_mb" "$cpu" "$pct" "$outcome" +done From 7fb53faa2902900cad4adc096c23235ff204ef37 Mon Sep 17 00:00:00 2001 From: ddux Date: Thu, 11 Jun 2026 21:59:16 -0500 Subject: [PATCH 20/24] versions --- .../symbolication/node-app/package-lock.json | 404 +++++++++++++++--- testing/symbolication/node-app/package.json | 4 +- 2 files changed, 349 insertions(+), 59 deletions(-) diff --git a/testing/symbolication/node-app/package-lock.json b/testing/symbolication/node-app/package-lock.json index 045b8b11..285cc8f6 100644 --- a/testing/symbolication/node-app/package-lock.json +++ b/testing/symbolication/node-app/package-lock.json @@ -15,36 +15,13 @@ }, "devDependencies": { "@rollup/plugin-terser": "^0.4.4", - "@tracewayapp/bundler-plugin": "file:../../../../js-client/packages/bundler-plugin", - "@tracewayapp/sourcemap-upload": "file:../../../../js-client/packages/sourcemap-upload", + "@tracewayapp/bundler-plugin": "^1.2.0", + "@tracewayapp/sourcemap-upload": "^1.2.0", "rollup": "^4.0.0", "webpack": "^5.107.2", "webpack-cli": "^7.0.3" } }, - "../../../../js-client/packages/bundler-plugin": { - "name": "@tracewayapp/bundler-plugin", - "version": "1.1.0", - "dev": true, - "dependencies": { - "magic-string": "^0.30.0" - }, - "devDependencies": { - "rollup": "^4.0.0", - "webpack": "^5.90.0" - } - }, - "../../../../js-client/packages/sourcemap-upload": { - "name": "@tracewayapp/sourcemap-upload", - "version": "1.1.0", - "dev": true, - "dependencies": { - "glob": "^11.0.0" - }, - "bin": { - "traceway-sourcemaps": "dist/index.js" - } - }, "node_modules/@discoveryjs/json-ext": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-1.1.0.tgz", @@ -55,6 +32,16 @@ "node": ">=14.17.0" } }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -127,9 +114,9 @@ } }, "node_modules/@opentelemetry/context-async-hooks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.7.1.tgz", - "integrity": "sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.8.0.tgz", + "integrity": "sha512-/3FIraneMcng67SUJCxvyInk/oxzwsxyadufk0wwfOBLf5wqtAGX4MoQASwSbndBPeARzBryUM9Azr5kHIdWLw==", "license": "Apache-2.0", "engines": { "node": "^18.19.0 || >=20.6.0" @@ -172,6 +159,22 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", + "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.218.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.218.0.tgz", @@ -208,7 +211,7 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/resources": { + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", @@ -224,6 +227,37 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, + "node_modules/@opentelemetry/resources": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-logs": { "version": "0.218.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz", @@ -242,6 +276,22 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", + "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-metrics": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", @@ -258,6 +308,22 @@ "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", + "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-trace-base": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", @@ -275,15 +341,31 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-trace-node": { + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": { "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.7.1.tgz", - "integrity": "sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg==", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", + "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/context-async-hooks": "2.7.1", "@opentelemetry/core": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.8.0.tgz", + "integrity": "sha512-nZt9OGufioAc3AfoLTqA9bsAeaMJAictYDdI2VcNQ+PmT+3rfKjAZDZvgPfd8VPX0O5Bw1hdQF6kDK8VSpZiWg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "2.8.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -292,6 +374,38 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", + "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/semantic-conventions": { "version": "1.41.1", "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", @@ -675,12 +789,25 @@ ] }, "node_modules/@tracewayapp/bundler-plugin": { - "resolved": "../../../../js-client/packages/bundler-plugin", - "link": true + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@tracewayapp/bundler-plugin/-/bundler-plugin-1.2.0.tgz", + "integrity": "sha512-K3EjCozcZTHdPaJQIKGSBCdX+echAPJSdcQTBV6w7yxh0lM/OT77zW/59fx+P3yz49E1pfcOIVqFMPD3C2q0cQ==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.0" + } }, "node_modules/@tracewayapp/sourcemap-upload": { - "resolved": "../../../../js-client/packages/sourcemap-upload", - "link": true + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@tracewayapp/sourcemap-upload/-/sourcemap-upload-1.2.0.tgz", + "integrity": "sha512-nidakU2U7wvvP0CKdjvXlsPE/iPGmLNa3uJ9F7QiiAhlx67AbPQz3xOC7JtYQQAyK2Hj4VqI2RsGH9W87zEGuw==", + "dev": true, + "dependencies": { + "glob": "^11.0.0" + }, + "bin": { + "traceway-sourcemaps": "dist/index.js" + } }, "node_modules/@types/estree": { "version": "1.0.9", @@ -882,9 +1009,9 @@ "license": "Apache-2.0" }, "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "dev": true, "license": "MIT", "bin": { @@ -955,10 +1082,20 @@ "ajv": "^8.8.2" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/baseline-browser-mapping": { - "version": "2.10.35", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.35.tgz", - "integrity": "sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg==", + "version": "2.10.36", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.36.tgz", + "integrity": "sha512-lVq/Df7LXlO79MVaaUHztSwWiG9oXoWHlgvNS51v8Dpd4+G4/VIy6qYePTw31nAVls33nUtnfezYeLkYAak9dg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -968,6 +1105,19 @@ "node": ">=6.0.0" } }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/browserslist": { "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", @@ -1010,9 +1160,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001797", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz", - "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==", + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", "dev": true, "funding": [ { @@ -1078,16 +1228,16 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.371", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.371.tgz", - "integrity": "sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w==", + "version": "1.5.372", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz", + "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==", "dev": true, "license": "ISC" }, "node_modules/enhanced-resolve": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.23.0.tgz", - "integrity": "sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA==", + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.0.tgz", + "integrity": "sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1243,6 +1393,23 @@ "flat": "cli.js" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1268,6 +1435,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -1381,6 +1573,22 @@ "node": ">=0.10.0" } }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -1440,6 +1648,26 @@ "node": ">=8" } }, + "node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -1457,6 +1685,32 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -1513,6 +1767,13 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -1540,6 +1801,23 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1770,6 +2048,19 @@ "node": ">=8" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/smob": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.2.tgz", @@ -1963,13 +2254,12 @@ } }, "node_modules/watchpack": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", - "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", "dev": true, "license": "MIT", "dependencies": { - "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" }, "engines": { diff --git a/testing/symbolication/node-app/package.json b/testing/symbolication/node-app/package.json index 42bf4e9c..03b6321a 100644 --- a/testing/symbolication/node-app/package.json +++ b/testing/symbolication/node-app/package.json @@ -12,8 +12,8 @@ }, "devDependencies": { "@rollup/plugin-terser": "^0.4.4", - "@tracewayapp/bundler-plugin": "file:../../../../js-client/packages/bundler-plugin", - "@tracewayapp/sourcemap-upload": "file:../../../../js-client/packages/sourcemap-upload", + "@tracewayapp/bundler-plugin": "^1.2.0", + "@tracewayapp/sourcemap-upload": "^1.2.0", "rollup": "^4.0.0", "webpack": "^5.107.2", "webpack-cli": "^7.0.3" From b18c317774716f0b36ca59cabb1b8265d56a41ec Mon Sep 17 00:00:00 2001 From: ddux Date: Thu, 11 Jun 2026 23:19:45 -0500 Subject: [PATCH 21/24] update for limits --- .github/workflows/benchmark-processor.yml | 4 ++++ benchmarks/processor/run-hetzner.sh | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/benchmark-processor.yml b/.github/workflows/benchmark-processor.yml index f45f48a3..0f12c0c6 100644 --- a/.github/workflows/benchmark-processor.yml +++ b/.github/workflows/benchmark-processor.yml @@ -55,6 +55,9 @@ on: permissions: contents: read +concurrency: + group: hetzner-bench-processor + jobs: build: runs-on: ubuntu-latest @@ -113,6 +116,7 @@ jobs: runs-on: ubuntu-latest strategy: fail-fast: false + max-parallel: 1 matrix: impl: [honeycomb, traceway-oxc-mem, traceway-oxc-disk, traceway-goja-mem, traceway-goja-disk] steps: diff --git a/benchmarks/processor/run-hetzner.sh b/benchmarks/processor/run-hetzner.sh index b07eda49..09a81f44 100755 --- a/benchmarks/processor/run-hetzner.sh +++ b/benchmarks/processor/run-hetzner.sh @@ -18,7 +18,7 @@ STEP_DURATION="${STEP_DURATION:-60s}" SPANS_PER_REQUEST="${SPANS_PER_REQUEST:-20}" SUT_TYPE="${SUT_TYPE:-ccx33}" OOM_SUT_TYPE="${OOM_SUT_TYPE:-ccx13}" -LDG_TYPE="${LDG_TYPE:-ccx23}" +LDG_TYPE="${LDG_TYPE:-cpx41}" LOCATION="${LOCATION:-fsn1}" RESULTS="${RESULTS:-./results}" RUN_ID="${GITHUB_RUN_ID:-local}-$IMPL" @@ -63,10 +63,11 @@ wait_ssh() { provision() { local name="$1" type="$2" - hcloud server create --name "$name" --type "$type" --image ubuntu-24.04 --location "$LOCATION" --ssh-key "bench-key-$RUN_ID" > /dev/null + hcloud server create --name "$name" --type "$type" --image ubuntu-24.04 --location "$LOCATION" --ssh-key "bench-key-$RUN_ID" > /dev/null || exit 1 local ip ip=$(hcloud server ip "$name") - wait_ssh "$ip" + [ -n "$ip" ] || { echo "no ip for $name" >&2; exit 1; } + wait_ssh "$ip" || { echo "ssh unreachable on $name ($ip)" >&2; exit 1; } $SCP -r artifacts "root@$ip:/opt/bench" > /dev/null $SSH "root@$ip" "chmod +x /opt/bench/* 2>/dev/null || true" echo "$ip" From 2e5b31824ddf155c38c409a6b8c0b6bd4b2a4eea Mon Sep 17 00:00:00 2001 From: ddux Date: Thu, 11 Jun 2026 23:36:03 -0500 Subject: [PATCH 22/24] updated benchmark server sizes --- .github/workflows/benchmark-processor.yml | 5 +++++ benchmarks/processor/run-hetzner.sh | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/benchmark-processor.yml b/.github/workflows/benchmark-processor.yml index 0f12c0c6..a389a0b7 100644 --- a/.github/workflows/benchmark-processor.yml +++ b/.github/workflows/benchmark-processor.yml @@ -35,6 +35,10 @@ on: description: 'Hetzner server type for the collector under test' required: true default: 'ccx33' + ldg_type: + description: 'Hetzner server type for the load generator (shared vCPU)' + required: true + default: 'cpx42' connections: description: 'Loadgen concurrency ramp' required: true @@ -135,6 +139,7 @@ jobs: HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }} SCENARIOS: ${{ inputs.scenarios }} SUT_TYPE: ${{ inputs.sut_type }} + LDG_TYPE: ${{ inputs.ldg_type }} CONNECTIONS: ${{ inputs.connections }} STEP_DURATION: ${{ inputs.step_duration }} CHURN_ENTRIES: ${{ inputs.churn_entries }} diff --git a/benchmarks/processor/run-hetzner.sh b/benchmarks/processor/run-hetzner.sh index 09a81f44..358a3e9a 100755 --- a/benchmarks/processor/run-hetzner.sh +++ b/benchmarks/processor/run-hetzner.sh @@ -18,7 +18,7 @@ STEP_DURATION="${STEP_DURATION:-60s}" SPANS_PER_REQUEST="${SPANS_PER_REQUEST:-20}" SUT_TYPE="${SUT_TYPE:-ccx33}" OOM_SUT_TYPE="${OOM_SUT_TYPE:-ccx13}" -LDG_TYPE="${LDG_TYPE:-cpx41}" +LDG_TYPE="${LDG_TYPE:-cpx42}" LOCATION="${LOCATION:-fsn1}" RESULTS="${RESULTS:-./results}" RUN_ID="${GITHUB_RUN_ID:-local}-$IMPL" From c33c4beefde4632a26a9fbfbf00ceba7ed028548 Mon Sep 17 00:00:00 2001 From: ddux Date: Thu, 11 Jun 2026 23:48:09 -0500 Subject: [PATCH 23/24] Update run-hetzner.sh --- benchmarks/processor/run-hetzner.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/benchmarks/processor/run-hetzner.sh b/benchmarks/processor/run-hetzner.sh index 358a3e9a..8d0263df 100755 --- a/benchmarks/processor/run-hetzner.sh +++ b/benchmarks/processor/run-hetzner.sh @@ -97,19 +97,19 @@ for scenario in $SCENARIOS; do CACHE_DIR_REMOTE="" [ -n "$DISK" ] && CACHE_DIR_REMOTE="/opt/bench/twcache" $SSH "root@$LDG_IP" "curl -sf -X POST http://127.0.0.1:9319/reset" - $SSH "root@$SUT_IP" "pkill -f $COL_BIN 2>/dev/null; rm -rf /opt/bench/twcache; cd /opt/bench && nohup env STORE_PATH=./corpus-$scenario CACHE_DIR=$CACHE_DIR_REMOTE CACHE_SIZE=$cachesize SYMB_PARSER=$PARSER DRAIN_ENDPOINT=http://$LDG_IP:9319 LD_LIBRARY_PATH=/opt/bench ./$COL_BIN --config $COL_CFG > collector.log 2>&1 & sleep 3; pgrep -f $COL_BIN" + $SSH "root@$SUT_IP" "cd /opt/bench || exit 1; [ ! -f collector.pid ] || { kill \$(cat collector.pid) 2>/dev/null || true; sleep 1; }; rm -rf twcache; nohup env STORE_PATH=./corpus-$scenario CACHE_DIR=$CACHE_DIR_REMOTE CACHE_SIZE=$cachesize SYMB_PARSER=$PARSER DRAIN_ENDPOINT=http://$LDG_IP:9319 LD_LIBRARY_PATH=/opt/bench ./$COL_BIN --config $COL_CFG > collector.log 2>&1 & echo \$! > collector.pid; sleep 3; kill -0 \$!" T0=$($SSH "root@$SUT_IP" "date +%s") - $SSH "root@$SUT_IP" "cd /opt/bench && nohup bash rss-sampler.sh \$(pgrep -f $COL_BIN | head -1) rss.csv > /dev/null 2>&1 &" + $SSH "root@$SUT_IP" "cd /opt/bench && nohup bash rss-sampler.sh \$(cat collector.pid) rss.csv > /dev/null 2>&1 &" $SSH "root@$LDG_IP" "cd /opt/bench && ./loadgen --target http://$SUT_IP:4318/v1/traces --corpus corpus-$scenario/corpus.json --connections $conns --step-duration $dur --spans-per-request $SPANS_PER_REQUEST --out loadgen.json" || true - if ! $SSH "root@$SUT_IP" "pgrep -f $COL_BIN > /dev/null"; then + if ! $SSH "root@$SUT_IP" "kill -0 \$(cat /opt/bench/collector.pid) 2>/dev/null"; then TDEAD=$($SSH "root@$SUT_IP" "tail -1 /opt/bench/rss.csv | cut -d, -f1") echo "$(( TDEAD - T0 ))" > "$outdir/died" fi $SSH "root@$LDG_IP" "curl -sf http://127.0.0.1:9319/stats" > "$outdir/drain.json" || echo '{}' > "$outdir/drain.json" $SCP "root@$LDG_IP:/opt/bench/loadgen.json" "$outdir/loadgen.json" - $SSH "root@$SUT_IP" "pkill -f $COL_BIN; sleep 2" || true + $SSH "root@$SUT_IP" "kill \$(cat /opt/bench/collector.pid) 2>/dev/null; sleep 2" || true $SCP "root@$SUT_IP:/opt/bench/rss.csv" "$outdir/rss.csv" || true $SCP "root@$SUT_IP:/opt/bench/collector.log" "$outdir/collector.log" || true echo "== $tag ==" From 4f0f105b37ee13fd4a8879398bf8980cbcef0882 Mon Sep 17 00:00:00 2001 From: ddux Date: Fri, 12 Jun 2026 00:38:00 -0500 Subject: [PATCH 24/24] fixes --- .github/workflows/benchmark-processor.yml | 8 ++++++++ benchmarks/processor/run-hetzner.sh | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/benchmark-processor.yml b/.github/workflows/benchmark-processor.yml index a389a0b7..b8bd2945 100644 --- a/.github/workflows/benchmark-processor.yml +++ b/.github/workflows/benchmark-processor.yml @@ -151,6 +151,14 @@ jobs: OOM_DURATION: ${{ inputs.oom_duration }} OOM_SUT_TYPE: ${{ inputs.oom_sut_type }} run: bash run-hetzner.sh ${{ matrix.impl }} + - name: Cleanup hetzner + if: always() + env: + HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }} + run: | + hcloud server delete "bench-sut-${{ github.run_id }}-${{ matrix.impl }}" 2>/dev/null || true + hcloud server delete "bench-ldg-${{ github.run_id }}-${{ matrix.impl }}" 2>/dev/null || true + hcloud ssh-key delete "bench-key-${{ github.run_id }}-${{ matrix.impl }}" 2>/dev/null || true - uses: actions/upload-artifact@v4 if: always() with: diff --git a/benchmarks/processor/run-hetzner.sh b/benchmarks/processor/run-hetzner.sh index 8d0263df..e0cf8af0 100755 --- a/benchmarks/processor/run-hetzner.sh +++ b/benchmarks/processor/run-hetzner.sh @@ -99,7 +99,7 @@ for scenario in $SCENARIOS; do $SSH "root@$LDG_IP" "curl -sf -X POST http://127.0.0.1:9319/reset" $SSH "root@$SUT_IP" "cd /opt/bench || exit 1; [ ! -f collector.pid ] || { kill \$(cat collector.pid) 2>/dev/null || true; sleep 1; }; rm -rf twcache; nohup env STORE_PATH=./corpus-$scenario CACHE_DIR=$CACHE_DIR_REMOTE CACHE_SIZE=$cachesize SYMB_PARSER=$PARSER DRAIN_ENDPOINT=http://$LDG_IP:9319 LD_LIBRARY_PATH=/opt/bench ./$COL_BIN --config $COL_CFG > collector.log 2>&1 & echo \$! > collector.pid; sleep 3; kill -0 \$!" T0=$($SSH "root@$SUT_IP" "date +%s") - $SSH "root@$SUT_IP" "cd /opt/bench && nohup bash rss-sampler.sh \$(cat collector.pid) rss.csv > /dev/null 2>&1 &" + $SSH "root@$SUT_IP" "cd /opt/bench || exit 1; nohup bash rss-sampler.sh \$(cat collector.pid) rss.csv > /dev/null 2>&1 &" $SSH "root@$LDG_IP" "cd /opt/bench && ./loadgen --target http://$SUT_IP:4318/v1/traces --corpus corpus-$scenario/corpus.json --connections $conns --step-duration $dur --spans-per-request $SPANS_PER_REQUEST --out loadgen.json" || true