From 32fbed29aab9a4e42b31eca83c39e4dab546abf8 Mon Sep 17 00:00:00 2001 From: Andreas Gkizas Date: Tue, 11 Aug 2026 13:56:24 +0300 Subject: [PATCH 01/11] add vercel otelsoak Signed-off-by: Andreas Gkizas --- Makefile | 27 +++ .../elastic-components/manifest.yaml | 2 + ...026-08-11-otelsoak-vercel-drain-signals.md | 174 +++++++++++++++ ...11-otelsoak-vercel-drain-signals-design.md | 78 +++++++ internal/exporter/httpexporter/Makefile | 1 + internal/exporter/httpexporter/README.md | 47 ++++ internal/exporter/httpexporter/config.go | 58 +++++ internal/exporter/httpexporter/doc.go | 23 ++ internal/exporter/httpexporter/exporter.go | 114 ++++++++++ .../exporter/httpexporter/exporter_test.go | 114 ++++++++++ internal/exporter/httpexporter/factory.go | 79 +++++++ internal/exporter/httpexporter/go.mod | 92 ++++++++ internal/exporter/httpexporter/go.sum | 210 ++++++++++++++++++ .../internal/metadata/generated_status.go | 33 +++ internal/exporter/httpexporter/metadata.yaml | 9 + loadgen/cmd/otelbench/components.go | 3 + loadgen/cmd/otelbench/go.mod | 3 + loadgen/cmd/otelsoak/README.md | 48 ++++ .../cmd/otelsoak/config.vercel.example.yaml | 59 +++++ .../cmd/otelsoak/testdata/vercel/logs.jsonl | 2 + .../testdata/vercel/speed_insights.jsonl | 3 + versions.yaml | 1 + 22 files changed, 1180 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-11-otelsoak-vercel-drain-signals.md create mode 100644 docs/superpowers/specs/2026-08-11-otelsoak-vercel-drain-signals-design.md create mode 100644 internal/exporter/httpexporter/Makefile create mode 100644 internal/exporter/httpexporter/README.md create mode 100644 internal/exporter/httpexporter/config.go create mode 100644 internal/exporter/httpexporter/doc.go create mode 100644 internal/exporter/httpexporter/exporter.go create mode 100644 internal/exporter/httpexporter/exporter_test.go create mode 100644 internal/exporter/httpexporter/factory.go create mode 100644 internal/exporter/httpexporter/go.mod create mode 100644 internal/exporter/httpexporter/go.sum create mode 100644 internal/exporter/httpexporter/internal/metadata/generated_status.go create mode 100644 internal/exporter/httpexporter/metadata.yaml create mode 100644 loadgen/cmd/otelsoak/config.vercel.example.yaml create mode 100644 loadgen/cmd/otelsoak/testdata/vercel/logs.jsonl create mode 100644 loadgen/cmd/otelsoak/testdata/vercel/speed_insights.jsonl diff --git a/Makefile b/Makefile index ce37f3060..f6bd04fbe 100644 --- a/Makefile +++ b/Makefile @@ -121,12 +121,39 @@ builddocker: .PHONY: otelsoak-validate otelsoak-validate: genelasticcol ELASTIC_APM_SERVER_URL=http://localhost:8200 ELASTIC_APM_API_KEY=foobar ./loadgen/cmd/otelsoak/otelsoak validate --config ./loadgen/cmd/otelsoak/config.example.yaml + ELASTIC_SERVER_URL=http://localhost:8200 ELASTIC_APM_API_KEY=foobar ./loadgen/cmd/otelsoak/otelsoak validate --config ./loadgen/cmd/otelsoak/config.vercel.example.yaml # Run otelsoak .PHONY: otelsoak-run otelsoak-run: genelasticcol ./loadgen/cmd/otelsoak/otelsoak --config ./loadgen/cmd/otelsoak/config.example.yaml $(ARGS) +# Run otelsoak against a Vercel Managed Inputs drain endpoint (HTTP NDJSON via httpexporter). +# Optional: VERCEL_SIGNAL=logs|speed_insights|both (default logs). +# For both, cats logs.jsonl + speed_insights.jsonl into a temp file at run time. +VERCEL_SIGNAL ?= logs +VERCEL_TESTDATA := loadgen/cmd/otelsoak/testdata/vercel +.PHONY: otelsoak-run-vercel +otelsoak-run-vercel: genelasticcol + @case "$(VERCEL_SIGNAL)" in \ + logs|speed_insights) \ + ./loadgen/cmd/otelsoak/otelsoak --config ./loadgen/cmd/otelsoak/config.vercel.example.yaml $(ARGS); \ + ;; \ + both) \ + tmp=$$(mktemp); \ + cat $(VERCEL_TESTDATA)/logs.jsonl $(VERCEL_TESTDATA)/speed_insights.jsonl > $$tmp; \ + ./loadgen/cmd/otelsoak/otelsoak --config ./loadgen/cmd/otelsoak/config.vercel.example.yaml \ + --set receivers.loadgen.logs.jsonl_file=$$tmp $(ARGS); \ + status=$$?; \ + rm -f $$tmp; \ + exit $$status; \ + ;; \ + *) \ + echo "VERCEL_SIGNAL must be logs, speed_insights, or both (got: $(VERCEL_SIGNAL))" >&2; \ + exit 1; \ + ;; \ + esac + # Clones the upstream opentelemetry-collector repository in a temporal .release # directory. If the directory already exists, diff --git a/distributions/elastic-components/manifest.yaml b/distributions/elastic-components/manifest.yaml index 019e25c74..3f398cd7d 100644 --- a/distributions/elastic-components/manifest.yaml +++ b/distributions/elastic-components/manifest.yaml @@ -67,6 +67,7 @@ exporters: - gomod: go.opentelemetry.io/collector/exporter/debugexporter v0.157.0 - gomod: go.opentelemetry.io/collector/exporter/otlpexporter v0.157.0 - gomod: go.opentelemetry.io/collector/exporter/otlphttpexporter v0.157.0 + - gomod: github.com/elastic/opentelemetry-collector-components/internal/exporter/httpexporter v0.1.0 providers: - gomod: go.opentelemetry.io/collector/confmap/provider/envprovider v1.63.0 @@ -94,5 +95,6 @@ replaces: - github.com/elastic/opentelemetry-collector-components/receiver/integrationreceiver => ../receiver/integrationreceiver - github.com/elastic/opentelemetry-collector-components/receiver/entityanalyticsreceiver => ../receiver/entityanalyticsreceiver - github.com/elastic/opentelemetry-collector-components/receiver/akamaisiemreceiver => ../receiver/akamaisiemreceiver + - github.com/elastic/opentelemetry-collector-components/internal/exporter/httpexporter => ../internal/exporter/httpexporter - github.com/elastic/opentelemetry-collector-components/processor/elastictraceprocessor => ../processor/elastictraceprocessor - github.com/elastic/opentelemetry-collector-components/internal/elasticattr => ../internal/elasticattr diff --git a/docs/superpowers/plans/2026-08-11-otelsoak-vercel-drain-signals.md b/docs/superpowers/plans/2026-08-11-otelsoak-vercel-drain-signals.md new file mode 100644 index 000000000..5e0ec37b2 --- /dev/null +++ b/docs/superpowers/plans/2026-08-11-otelsoak-vercel-drain-signals.md @@ -0,0 +1,174 @@ +# otelsoak Vercel drain signals Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Let otelsoak select Vercel drain payload type via `VERCEL_SIGNAL` (`logs` | `speed_insights` | `both`) using env-selected fixture files on one HTTP pipeline. + +**Architecture:** Keep a single loadgen logs → httpexporter pipeline. Each fixture line is OTLP-JSONL whose log body is one drain NDJSON object. `jsonl_file` uses `${env:VERCEL_SIGNAL:-logs}` so the same config covers all three modes. + +**Tech Stack:** OTel Collector config YAML, loadgenreceiver jsonl fixtures, httpexporter (unchanged). + +## Global Constraints + +- Same Managed Input URL for all signals: `${env:ELASTIC_SERVER_URL}/inputs/vercel/_default_` +- No httpexporter API changes; Speed Insights rides log bodies +- Default `VERCEL_SIGNAL` is `logs` +- Spec: `docs/superpowers/specs/2026-08-11-otelsoak-vercel-drain-signals-design.md` + +## File map + +| File | Responsibility | +| --- | --- | +| `loadgen/cmd/otelsoak/testdata/vercel/speed_insights.jsonl` | Speed Insights drain samples | +| `loadgen/cmd/otelsoak/testdata/vercel/logs.jsonl` | Unchanged (existing) | +| `loadgen/cmd/otelsoak/config.vercel.example.yaml` | Env-selected `jsonl_file` + usage comment | +| `loadgen/cmd/otelsoak/README.md` | Document `VERCEL_SIGNAL` | +| `Makefile` | `otelsoak-run-vercel`: temp concat when `VERCEL_SIGNAL=both` | +| `internal/exporter/httpexporter/README.md` | Point at signal selection | + +Note: no checked-in `both.jsonl`; `both` is Makefile-only (cat + `--set`). + +--- + +### Task 1: Speed Insights + both fixtures + +**Files:** +- Create: `loadgen/cmd/otelsoak/testdata/vercel/speed_insights.jsonl` +- Create: `loadgen/cmd/otelsoak/testdata/vercel/both.jsonl` +- Keep: `loadgen/cmd/otelsoak/testdata/vercel/logs.jsonl` + +**Interfaces:** +- Consumes: existing `logs.jsonl` line shape (OTLP `resourceLogs` → `body.stringValue` = drain JSON string) +- Produces: `speed_insights.jsonl` and `both.jsonl` with same wrapping; SI bodies use `schema: vercel.speed_insights.v1` + +- [x] **Step 1: Create `speed_insights.jsonl`** + +Two OTLP-JSONL lines. Inner bodies (escaped inside `stringValue`) must be valid Speed Insights objects, e.g.: + +```json +{"schema":"vercel.speed_insights.v1","timestamp":"2026-08-11T10:00:00.000Z","projectId":"soak_si_project","ownerId":"team_soak","deviceId":1001,"metricType":"LCP","value":2.4,"origin":"https://soak.vercel.app","path":"/","route":"/","country":"US","region":"CA","city":"San Francisco","osName":"macOS","clientName":"Chrome","clientType":"browser","deviceType":"desktop","vercelEnvironment":"production","vercelUrl":"soak.vercel.app","deploymentId":"dpl_soak","sdkName":"@vercel/speed-insights","sdkVersion":"1.0.0"} +``` + +and a second line with `metricType":"CLS"` (and optionally a third with `INP`). Wrap each as: + +```json +{"resourceLogs":[{"resource":{},"scopeLogs":[{"scope":{},"logRecords":[{"body":{"stringValue":""},"timeUnixNano":"1786443432624000000"}]}]}]} +``` + +- [x] **Step 2: Create `both.jsonl`** + +Concatenate all lines from `logs.jsonl` then all lines from `speed_insights.jsonl` (or interleave 1:1). No other transforms. + +- [x] **Step 3: Sanity-check JSON** + +Run: + +```bash +python3 -c " +import json +from pathlib import Path +base = Path('loadgen/cmd/otelsoak/testdata/vercel') +for name in ('logs.jsonl','speed_insights.jsonl','both.jsonl'): + for i, line in enumerate(base.joinpath(name).read_text().splitlines(), 1): + if not line.strip(): + continue + outer = json.loads(line) + body = outer['resourceLogs'][0]['scopeLogs'][0]['logRecords'][0]['body']['stringValue'] + inner = json.loads(body) + print(f'{name}:{i} ok keys={sorted(inner)[:5]}...') +" +``` + +Expected: each line prints `ok` with no JSON decode errors. For SI files, inner must include `"schema": "vercel.speed_insights.v1"`. + +- [x] **Step 4: Commit** (only if user asked to commit) + +```bash +git add loadgen/cmd/otelsoak/testdata/vercel/speed_insights.jsonl loadgen/cmd/otelsoak/testdata/vercel/both.jsonl +git commit -m "$(cat <<'EOF' +Add Vercel speed-insights and both soak fixtures for otelsoak. + +EOF +)" +``` + +--- + +### Task 2: Config + docs for `VERCEL_SIGNAL` + +**Files:** +- Modify: `loadgen/cmd/otelsoak/config.vercel.example.yaml` +- Modify: `loadgen/cmd/otelsoak/README.md` +- Modify: `Makefile` (otelsoak-run-vercel comment) +- Modify: `internal/exporter/httpexporter/README.md` + +**Interfaces:** +- Consumes: fixture filenames from Task 1 (`logs` | `speed_insights` | `both`) +- Produces: config path `.../testdata/vercel/${env:VERCEL_SIGNAL:-logs}.jsonl` + +- [x] **Step 1: Update config header + `jsonl_file`** + +In `config.vercel.example.yaml`: + +- Document `VERCEL_SIGNAL=logs|speed_insights|both` in the usage comment block +- Set: + +```yaml + jsonl_file: loadgen/cmd/otelsoak/testdata/vercel/${env:VERCEL_SIGNAL:-logs}.jsonl +``` + +Leave endpoint and pipeline unchanged. + +- [x] **Step 2: Update README Vercel section** + +Document: + +```bash +VERCEL_SIGNAL=speed_insights \ +ELASTIC_SERVER_URL=... ELASTIC_APM_API_KEY=... \ +./loadgen/cmd/otelsoak/otelsoak --config ./loadgen/cmd/otelsoak/config.vercel.example.yaml +``` + +List allowed values and that unset defaults to `logs`. Link fixtures directory. + +- [x] **Step 3: Touch Makefile + httpexporter README** + +- Makefile comment above `otelsoak-run-vercel`: mention `VERCEL_SIGNAL`. +- httpexporter README: say fixtures are selected via `VERCEL_SIGNAL` / point at vercel testdata dir. + +- [x] **Step 4: Validate config expands** + +Run (after `make genelasticcol` if binary missing): + +```bash +VERCEL_SIGNAL=speed_insights ELASTIC_SERVER_URL=http://localhost:8200 ELASTIC_APM_API_KEY=foobar \ + ./loadgen/cmd/otelsoak/otelsoak validate --config ./loadgen/cmd/otelsoak/config.vercel.example.yaml +``` + +Expected: exit 0, no “file not found” / config error. + +Repeat with `VERCEL_SIGNAL=both` and with `VERCEL_SIGNAL` unset (defaults to logs). + +- [x] **Step 5: Commit** (only if user asked to commit) + +```bash +git add loadgen/cmd/otelsoak/config.vercel.example.yaml loadgen/cmd/otelsoak/README.md Makefile internal/exporter/httpexporter/README.md +git commit -m "$(cat <<'EOF' +Select Vercel soak fixtures with VERCEL_SIGNAL env. + +EOF +)" +``` + +--- + +## Spec coverage check + +| Spec requirement | Task | +| --- | --- | +| `speed_insights.jsonl` fixture | Task 1 | +| `both.jsonl` interleaved/combined | Task 1 | +| Env-selected `jsonl_file` with default `logs` | Task 2 | +| Same endpoint | Task 2 (unchanged) | +| README / Makefile docs | Task 2 | +| No httpexporter API change | N/A (explicit non-change) | diff --git a/docs/superpowers/specs/2026-08-11-otelsoak-vercel-drain-signals-design.md b/docs/superpowers/specs/2026-08-11-otelsoak-vercel-drain-signals-design.md new file mode 100644 index 000000000..d06cebc84 --- /dev/null +++ b/docs/superpowers/specs/2026-08-11-otelsoak-vercel-drain-signals-design.md @@ -0,0 +1,78 @@ +# otelsoak Vercel drain signals (logs + speed-insights) + +## Goal + +Make otelsoak Vercel Managed Input soak traffic selectable via env so operators can replay **logs**, **speed-insights**, or **both** against the same drain endpoint—covering [hosted-otel-collector#3428](https://github.com/elastic/hosted-otel-collector/issues/3428) fixture needs without changing `httpexporter`. + +## Context + +- Vercel drains (logs and Speed Insights) are raw HTTPS NDJSON, not OTLP. +- `httpexporter` POSTs **log record bodies** joined by newlines to a full URL. +- loadgen for OTLP uses separate logs/metrics/traces pipelines because those are real OTel signals. Vercel drain types are **payload variants on one HTTP path**, so they stay on a single loadgen **logs** → `http` pipeline. +- Speed Insights schema: https://vercel.com/docs/drains/reference/speed-insights (`schema: vercel.speed_insights.v1`). + +## Decisions + +| Topic | Choice | +| --- | --- | +| Selection UX | Env var `VERCEL_SIGNAL` | +| Values | `logs` \| `speed_insights` \| `both` | +| Default | `logs` (current behavior) | +| Endpoint | Same for all: `${ELASTIC_SERVER_URL}/inputs/vercel/_default_` | +| `both` semantics | `make otelsoak-run-vercel` concatenates `logs.jsonl` + `speed_insights.jsonl` into a temp file | +| Implementation | Env-expanded `jsonl_file` for `logs`/`speed_insights`; Makefile temp concat for `both` | + +## Design + +### Fixtures + +Directory: `loadgen/cmd/otelsoak/testdata/vercel/` + +| File | Contents | +| --- | --- | +| `logs.jsonl` | Existing Vercel log drain samples (OTLP-JSONL; each body is one drain NDJSON line) | +| `speed_insights.jsonl` | Same OTLP-JSONL wrapping; bodies are Speed Insights NDJSON objects (`vercel.speed_insights.v1`) with a few metric types (e.g. LCP, CLS, INP) | + +There is no checked-in `both.jsonl`. `VERCEL_SIGNAL=both` is handled by `make otelsoak-run-vercel`, which cats the two fixtures into a temp path and passes it via `--set`. + +Each loadgen line remains one OTLP log record whose `body.stringValue` is the exact drain NDJSON object string that `httpexporter` will POST. + +### Config + +`loadgen/cmd/otelsoak/config.vercel.example.yaml`: + +- `receivers.loadgen.logs.jsonl_file: loadgen/cmd/otelsoak/testdata/vercel/${env:VERCEL_SIGNAL:-logs}.jsonl` +- Unchanged: concurrency, ratelimit, `http` exporter endpoint/headers/TLS, single logs pipeline + +Invalid `VERCEL_SIGNAL` values fail when loadgen cannot open the file; document allowed values in README/Makefile. + +### Usage + +```bash +make genelasticcol +VERCEL_SIGNAL=logs|speed_insights|both \ +ELASTIC_SERVER_URL=https://.ingest...qa.elastic.cloud \ +ELASTIC_APM_API_KEY= \ +./loadgen/cmd/otelsoak/otelsoak --config ./loadgen/cmd/otelsoak/config.vercel.example.yaml +``` + +`make otelsoak-run-vercel` continues to work; callers pass `VERCEL_SIGNAL` in the environment. + +### Docs + +- Update `loadgen/cmd/otelsoak/README.md` Vercel section for `VERCEL_SIGNAL` and new fixtures. +- Brief note in Makefile `otelsoak-run-vercel` comment and `internal/exporter/httpexporter/README.md` sample pointer if it names only `logs.jsonl`. + +## Out of scope + +- `httpexporter` API/signal changes (no metrics exporter path). +- Separate Managed Input URLs per drain type. +- Parallel pipelines or independent rates for logs vs speed-insights. +- Encoding/parsing work inside hosted-otel-collector (issue 3428 server-side); this repo only supplies soak fixtures and config. + +## Success criteria + +1. `VERCEL_SIGNAL=logs` (or unset) behaves as today’s vercel soak. +2. `VERCEL_SIGNAL=speed_insights` POSTs Speed Insights NDJSON to the same endpoint. +3. `make otelsoak-run-vercel VERCEL_SIGNAL=both` POSTs both payload kinds from a temp concat of the two fixtures. +4. Docs show the three values and the env-selected path pattern. diff --git a/internal/exporter/httpexporter/Makefile b/internal/exporter/httpexporter/Makefile new file mode 100644 index 000000000..ded7a3609 --- /dev/null +++ b/internal/exporter/httpexporter/Makefile @@ -0,0 +1 @@ +include ../../Makefile.Common diff --git a/internal/exporter/httpexporter/README.md b/internal/exporter/httpexporter/README.md new file mode 100644 index 000000000..58257f0b1 --- /dev/null +++ b/internal/exporter/httpexporter/README.md @@ -0,0 +1,47 @@ +# HTTP body exporter + + +| Status | | +| ------------- |-----------| +| Stability | [development]: logs | +| Distributions | [] | + +[development]: https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/component-stability.md#development + + +The HTTP body exporter POSTs log record bodies as the raw HTTP request body. +Bodies are joined with newlines (NDJSON-friendly). It does **not** send OTLP. + +This is intended for load generation against Managed Input drain endpoints +(for example Vercel) when used with [`loadgenreceiver`](../../receiver/loadgenreceiver) both in otelsoak / otelbench. + +## Configuration + +```yaml +exporters: + http: + endpoint: https://example.ingest.us-central1.gcp.qa.elastic.cloud/inputs/vercel/_default_ + headers: + - name: Authorization + value: "ApiKey ${env:ELASTIC_APM_API_KEY}" + tls: + insecure_skip_verify: true + timeout: 60s +``` + +| Field | Description | +| --- | --- | +| `endpoint` | Full URL to POST (required). | +| `headers` | Extra HTTP headers (e.g. `Authorization`). | +| `tls` | Standard collector TLS client settings. | +| `timeout` | HTTP client timeout (default `30s`). | +| `retry_on_failure` | Standard exporter retry settings. | +| `sending_queue` | Standard exporter queue settings. | + +`Content-Type` defaults to `application/json` when not set in `headers`. + +## Sample otelsoak pipeline (Vercel drain) + +Put each drain NDJSON line in a loadgen OTLP-JSONL log body (see +`loadgen/cmd/otelsoak/testdata/vercel/`). Select logs, speed-insights, or both +with `VERCEL_SIGNAL` in [`config.vercel.example.yaml`](../../loadgen/cmd/otelsoak/config.vercel.example.yaml). diff --git a/internal/exporter/httpexporter/config.go b/internal/exporter/httpexporter/config.go new file mode 100644 index 000000000..84d07079d --- /dev/null +++ b/internal/exporter/httpexporter/config.go @@ -0,0 +1,58 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package httpexporter // import "github.com/elastic/opentelemetry-collector-components/internal/exporter/httpexporter" + +import ( + "errors" + "fmt" + "net/url" + + "go.opentelemetry.io/collector/config/confighttp" + "go.opentelemetry.io/collector/config/configoptional" + "go.opentelemetry.io/collector/config/configretry" + "go.opentelemetry.io/collector/exporter/exporterhelper" +) + +// Config configures the HTTP body exporter. +type Config struct { + // ClientConfig holds standard HTTP client settings (endpoint, headers, TLS, timeout). + confighttp.ClientConfig `mapstructure:",squash"` + + // RetryConfig defines retry configuration for failed exports. + RetryConfig configretry.BackOffConfig `mapstructure:"retry_on_failure"` + + // QueueConfig defines optional sending queue settings. + QueueConfig configoptional.Optional[exporterhelper.QueueBatchConfig] `mapstructure:"sending_queue"` +} + +func (cfg *Config) Validate() error { + if cfg.Endpoint == "" { + return errors.New("endpoint is required") + } + u, err := url.Parse(cfg.Endpoint) + if err != nil { + return fmt.Errorf("endpoint must be a valid URL: %w", err) + } + if u.Scheme != "http" && u.Scheme != "https" { + return fmt.Errorf("endpoint scheme must be http or https, got %q", u.Scheme) + } + if u.Host == "" { + return errors.New("endpoint must include a host") + } + return nil +} diff --git a/internal/exporter/httpexporter/doc.go b/internal/exporter/httpexporter/doc.go new file mode 100644 index 000000000..20f89bf58 --- /dev/null +++ b/internal/exporter/httpexporter/doc.go @@ -0,0 +1,23 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//go:generate mdatagen metadata.yaml + +// Package httpexporter posts log record bodies as an HTTP request body. +// It is intended for load generation against raw HTTP ingest endpoints +// such as Managed Input drains (e.g. Vercel NDJSON), not OTLP. +package httpexporter // import "github.com/elastic/opentelemetry-collector-components/internal/exporter/httpexporter" diff --git a/internal/exporter/httpexporter/exporter.go b/internal/exporter/httpexporter/exporter.go new file mode 100644 index 000000000..5889639c8 --- /dev/null +++ b/internal/exporter/httpexporter/exporter.go @@ -0,0 +1,114 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package httpexporter // import "github.com/elastic/opentelemetry-collector-components/internal/exporter/httpexporter" + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "strings" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/exporter" + "go.opentelemetry.io/collector/pdata/plog" + "go.uber.org/zap" +) + +const defaultContentType = "application/json" + +type httpExporter struct { + config *Config + logger *zap.Logger + settings component.TelemetrySettings + httpClient *http.Client +} + +func newExporter(cfg *Config, set exporter.Settings) (*httpExporter, error) { + if err := cfg.Validate(); err != nil { + return nil, err + } + return &httpExporter{ + config: cfg, + logger: set.Logger, + settings: set.TelemetrySettings, + }, nil +} + +func (e *httpExporter) start(ctx context.Context, host component.Host) error { + client, err := e.config.ToClient(ctx, host.GetExtensions(), e.settings) + if err != nil { + return fmt.Errorf("failed to create HTTP client: %w", err) + } + e.httpClient = client + return nil +} + +func (e *httpExporter) pushLogs(ctx context.Context, ld plog.Logs) error { + body := encodeLogBodies(ld) + if len(body) == 0 { + return nil + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, e.config.Endpoint, bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + if req.Header.Get("Content-Type") == "" { + req.Header.Set("Content-Type", defaultContentType) + } + + resp, err := e.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to POST to %s: %w", e.config.Endpoint, err) + } + defer resp.Body.Close() + + // Drain body so connections can be reused. + _, _ = io.Copy(io.Discard, resp.Body) + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("POST %s returned status %d", e.config.Endpoint, resp.StatusCode) + } + return nil +} + +// encodeLogBodies joins each log record body as a line (NDJSON-friendly). +func encodeLogBodies(ld plog.Logs) []byte { + var b strings.Builder + rls := ld.ResourceLogs() + for i := 0; i < rls.Len(); i++ { + sls := rls.At(i).ScopeLogs() + for j := 0; j < sls.Len(); j++ { + records := sls.At(j).LogRecords() + for k := 0; k < records.Len(); k++ { + line := records.At(k).Body().AsString() + if line == "" { + continue + } + if b.Len() > 0 { + b.WriteByte('\n') + } + b.WriteString(line) + } + } + } + return []byte(b.String()) +} diff --git a/internal/exporter/httpexporter/exporter_test.go b/internal/exporter/httpexporter/exporter_test.go new file mode 100644 index 000000000..2d36217b7 --- /dev/null +++ b/internal/exporter/httpexporter/exporter_test.go @@ -0,0 +1,114 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package httpexporter + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/config/confighttp" + "go.opentelemetry.io/collector/config/configopaque" + "go.opentelemetry.io/collector/exporter/exportertest" + "go.opentelemetry.io/collector/pdata/plog" + + "github.com/elastic/opentelemetry-collector-components/internal/exporter/httpexporter/internal/metadata" +) + +func TestPushLogsPostsNDJSONBodies(t *testing.T) { + var gotMethod, gotContentType, gotAuth, gotBody string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotMethod = r.Method + gotContentType = r.Header.Get("Content-Type") + gotAuth = r.Header.Get("Authorization") + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + gotBody = string(body) + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(srv.Close) + + cfg := createDefaultConfig().(*Config) + cfg.Endpoint = srv.URL + "/inputs/vercel/_default_" + cfg.Headers.Set("Authorization", configopaque.String("ApiKey test-key")) + + set := exportertest.NewNopSettings(metadata.Type) + exp, err := newExporter(cfg, set) + require.NoError(t, err) + require.NoError(t, exp.start(t.Context(), componenttest.NewNopHost())) + + logs := plog.NewLogs() + records := logs.ResourceLogs().AppendEmpty().ScopeLogs().AppendEmpty().LogRecords() + records.AppendEmpty().Body().SetStr(`{"id":"1","projectId":"p1"}`) + records.AppendEmpty().Body().SetStr(`{"id":"2","projectId":"p1"}`) + + require.NoError(t, exp.pushLogs(t.Context(), logs)) + + assert.Equal(t, http.MethodPost, gotMethod) + assert.Equal(t, "application/json", gotContentType) + assert.Equal(t, "ApiKey test-key", gotAuth) + assert.Equal(t, "{\"id\":\"1\",\"projectId\":\"p1\"}\n{\"id\":\"2\",\"projectId\":\"p1\"}", gotBody) +} + +func TestPushLogsNon2xxReturnsError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + })) + t.Cleanup(srv.Close) + + cfg := createDefaultConfig().(*Config) + cfg.Endpoint = srv.URL + + set := exportertest.NewNopSettings(metadata.Type) + exp, err := newExporter(cfg, set) + require.NoError(t, err) + require.NoError(t, exp.start(t.Context(), componenttest.NewNopHost())) + + logs := plog.NewLogs() + logs.ResourceLogs().AppendEmpty().ScopeLogs().AppendEmpty().LogRecords().AppendEmpty().Body().SetStr(`{}`) + + err = exp.pushLogs(t.Context(), logs) + require.Error(t, err) + assert.Contains(t, err.Error(), "401") +} + +func TestConfigValidate(t *testing.T) { + cfg := &Config{ClientConfig: confighttp.NewDefaultClientConfig()} + assert.Error(t, cfg.Validate()) + + cfg.Endpoint = "ftp://example.com" + assert.Error(t, cfg.Validate()) + + cfg.Endpoint = "https://example.com/inputs/vercel/_default_" + assert.NoError(t, cfg.Validate()) +} + +func TestFactoryCreateLogs(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig().(*Config) + cfg.Endpoint = "https://example.com/inputs/vercel/_default_" + + exp, err := factory.CreateLogs(t.Context(), exportertest.NewNopSettings(factory.Type()), cfg) + require.NoError(t, err) + require.NotNil(t, exp) + require.NoError(t, exp.Shutdown(t.Context())) +} diff --git a/internal/exporter/httpexporter/factory.go b/internal/exporter/httpexporter/factory.go new file mode 100644 index 000000000..d55d33c45 --- /dev/null +++ b/internal/exporter/httpexporter/factory.go @@ -0,0 +1,79 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package httpexporter // import "github.com/elastic/opentelemetry-collector-components/internal/exporter/httpexporter" + +import ( + "context" + "time" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/confighttp" + "go.opentelemetry.io/collector/config/configoptional" + "go.opentelemetry.io/collector/config/configretry" + "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/exporter" + "go.opentelemetry.io/collector/exporter/exporterhelper" + + "github.com/elastic/opentelemetry-collector-components/internal/exporter/httpexporter/internal/metadata" +) + +// NewFactory creates a factory for the HTTP body exporter. +func NewFactory() exporter.Factory { + return exporter.NewFactory( + metadata.Type, + createDefaultConfig, + exporter.WithLogs(createLogsExporter, metadata.LogsStability), + ) +} + +func createDefaultConfig() component.Config { + clientConfig := confighttp.NewDefaultClientConfig() + clientConfig.Timeout = 30 * time.Second + // Drain endpoints expect raw NDJSON; do not gzip by default. + clientConfig.Compression = "" + + return &Config{ + ClientConfig: clientConfig, + RetryConfig: configretry.NewDefaultBackOffConfig(), + QueueConfig: configoptional.Some(exporterhelper.NewDefaultQueueConfig()), + } +} + +func createLogsExporter( + ctx context.Context, + set exporter.Settings, + cfg component.Config, +) (exporter.Logs, error) { + oCfg := cfg.(*Config) + exp, err := newExporter(oCfg, set) + if err != nil { + return nil, err + } + + return exporterhelper.NewLogs( + ctx, + set, + cfg, + exp.pushLogs, + exporterhelper.WithStart(exp.start), + exporterhelper.WithCapabilities(consumer.Capabilities{MutatesData: false}), + exporterhelper.WithTimeout(exporterhelper.TimeoutConfig{Timeout: 0}), + exporterhelper.WithRetry(oCfg.RetryConfig), + exporterhelper.WithQueue(oCfg.QueueConfig), + ) +} diff --git a/internal/exporter/httpexporter/go.mod b/internal/exporter/httpexporter/go.mod new file mode 100644 index 000000000..792082aa7 --- /dev/null +++ b/internal/exporter/httpexporter/go.mod @@ -0,0 +1,92 @@ +module github.com/elastic/opentelemetry-collector-components/internal/exporter/httpexporter + +go 1.25.0 + +require ( + github.com/stretchr/testify v1.11.1 + go.opentelemetry.io/collector/component v1.62.0 + go.opentelemetry.io/collector/component/componenttest v0.156.0 + go.opentelemetry.io/collector/config/confighttp v0.156.0 + go.opentelemetry.io/collector/config/configopaque v1.62.0 + go.opentelemetry.io/collector/config/configoptional v1.62.0 + go.opentelemetry.io/collector/config/configretry v1.62.0 + go.opentelemetry.io/collector/consumer v1.62.0 + go.opentelemetry.io/collector/exporter v1.62.0 + go.opentelemetry.io/collector/exporter/exporterhelper v0.156.0 + go.opentelemetry.io/collector/exporter/exportertest v0.156.0 + go.opentelemetry.io/collector/pdata v1.62.0 + go.uber.org/zap v1.28.0 +) + +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cenkalti/backoff/v7 v7.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/foxboron/go-tpm-keyfiles v0.0.0-20251226215517-609e4778396f // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/go-tpm v0.9.8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-version v1.9.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.7 // indirect + github.com/knadh/koanf/maps v0.1.2 // indirect + github.com/knadh/koanf/providers/confmap v1.0.0 // indirect + github.com/knadh/koanf/v2 v2.3.5 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/pierrec/lz4/v4 v4.1.27 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rs/cors v1.11.1 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/collector/client v1.62.0 // indirect + go.opentelemetry.io/collector/config/configauth v1.62.0 // indirect + go.opentelemetry.io/collector/config/configcompression v1.62.0 // indirect + go.opentelemetry.io/collector/config/configmiddleware v1.62.0 // indirect + go.opentelemetry.io/collector/config/confignet v1.62.0 // indirect + go.opentelemetry.io/collector/config/configtls v1.62.0 // indirect + go.opentelemetry.io/collector/confmap v1.62.0 // indirect + go.opentelemetry.io/collector/confmap/xconfmap v0.156.0 // indirect + go.opentelemetry.io/collector/consumer/consumererror v0.156.0 // indirect + go.opentelemetry.io/collector/consumer/consumertest v0.156.0 // indirect + go.opentelemetry.io/collector/consumer/xconsumer v0.156.0 // indirect + go.opentelemetry.io/collector/exporter/xexporter v0.156.0 // indirect + go.opentelemetry.io/collector/extension v1.62.0 // indirect + go.opentelemetry.io/collector/extension/extensionauth v1.62.0 // indirect + go.opentelemetry.io/collector/extension/extensionmiddleware v0.156.0 // indirect + go.opentelemetry.io/collector/extension/xextension v0.156.0 // indirect + go.opentelemetry.io/collector/featuregate v1.62.0 // indirect + go.opentelemetry.io/collector/internal/componentalias v0.156.0 // indirect + go.opentelemetry.io/collector/pdata/pprofile v0.156.0 // indirect + go.opentelemetry.io/collector/pdata/xpdata v0.156.0 // indirect + go.opentelemetry.io/collector/pipeline v1.62.0 // indirect + go.opentelemetry.io/collector/pipeline/xpipeline v0.156.0 // indirect + go.opentelemetry.io/collector/receiver v1.62.0 // indirect + go.opentelemetry.io/collector/receiver/receivertest v0.156.0 // indirect + go.opentelemetry.io/collector/receiver/xreceiver v0.156.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/sdk v1.44.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.38.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/grpc v1.82.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/internal/exporter/httpexporter/go.sum b/internal/exporter/httpexporter/go.sum new file mode 100644 index 000000000..283253230 --- /dev/null +++ b/internal/exporter/httpexporter/go.sum @@ -0,0 +1,210 @@ +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v7 v7.0.0 h1:ZP+QAaaOnVUHo+ufFpZ835hbT3x2fy+h2lecVEosZ6A= +github.com/cenkalti/backoff/v7 v7.0.0/go.mod h1:qcKBGwsu4hpxHtQ8tWYsQ+ifzx2+sS+Xx/3jfe30lI8= +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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/foxboron/go-tpm-keyfiles v0.0.0-20251226215517-609e4778396f h1:RJ+BDPLSHQO7cSjKBqjPJSbi1qfk9WcsjQDtZiw3dZw= +github.com/foxboron/go-tpm-keyfiles v0.0.0-20251226215517-609e4778396f/go.mod h1:VHbbch/X4roIY22jL1s3qRbZhCiRIgUAF/PdSUcx2io= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +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/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= +github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/go-tpm-tools v0.4.7 h1:J3ycC8umYxM9A4eF73EofRZu4BxY0jjQnUnkhIBbvws= +github.com/google/go-tpm-tools v0.4.7/go.mod h1:gSyXTZHe3fgbzb6WEGd90QucmsnT1SRdlye82gH8QjQ= +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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +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/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw= +github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= +github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5zwp8Aebir+/EaRE= +github.com/knadh/koanf/providers/confmap v1.0.0/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A= +github.com/knadh/koanf/v2 v2.3.5 h1:2dXJUYaKGm4SGYeoAtBviq9+02JZo/pxQ2ssOd60rJg= +github.com/knadh/koanf/v2 v2.3.5/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +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/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk= +github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +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/client v1.62.0 h1:Vud5nn4gX2TzMnHpNhuUhvAi4GGcO0RsaId0dftHAjM= +go.opentelemetry.io/collector/client v1.62.0/go.mod h1:iao8KfxMeND0zdp+PcHGPY9r1BDgS+OppY7RKLlUeUU= +go.opentelemetry.io/collector/component v1.62.0 h1:F1MHUlUEjSJgwcumsCbbH2rRmTK4dC8m/ipp9v4vFh0= +go.opentelemetry.io/collector/component v1.62.0/go.mod h1:NqdVWse4diWnlqh5WurI2KncJuBXe1zzYtxuC9Mmew0= +go.opentelemetry.io/collector/component/componenttest v0.156.0 h1:IV7xYP57kkKoBk7o9dYvToeotZ369A6/V+QIlLgnsEc= +go.opentelemetry.io/collector/component/componenttest v0.156.0/go.mod h1:YL7ByaKwuSuB+eBtm56awLXFlKJ7KI6jfrsjZd0uv8Y= +go.opentelemetry.io/collector/config/configauth v1.62.0 h1:fWKSqjVBI9FawaDT/U3ExexSvae8J1umeX48yoqPXa8= +go.opentelemetry.io/collector/config/configauth v1.62.0/go.mod h1:+iVvJAENMpZ3A3/YambobaGb58UvtiVWOjQkVoPSzHE= +go.opentelemetry.io/collector/config/configcompression v1.62.0 h1:Mebc3WPbIdDiEPsLgd2zOQ7m5rBlOHfNeGchv9zw2hU= +go.opentelemetry.io/collector/config/configcompression v1.62.0/go.mod h1:SEcE2uFLHHPc/Vi8WCkW5MhOMUwaT321HBdZ3P8x8D0= +go.opentelemetry.io/collector/config/confighttp v0.156.0 h1:fIXLu8IwsF+oleh93jR8j7V3H4dpFXO8+DtMqtOv738= +go.opentelemetry.io/collector/config/confighttp v0.156.0/go.mod h1:cTbAATe9Yq3tAkF61A4os3LLaCqezQ3ZFhyB7i2/WSs= +go.opentelemetry.io/collector/config/configmiddleware v1.62.0 h1:R1gIInUuC3JPnD2EyKlLvQraLZT3qIioOcrFgRKpDDA= +go.opentelemetry.io/collector/config/configmiddleware v1.62.0/go.mod h1:G8EcGOVHFYNIo2fjukZsVykCldDHuOIyvzr2Ga1gvFw= +go.opentelemetry.io/collector/config/confignet v1.62.0 h1:tFK4VJMaYUAhLQOzBmOteq2b0ccEq5q1ToDw2QqZT7A= +go.opentelemetry.io/collector/config/confignet v1.62.0/go.mod h1:Op+r1B/DtzXgIuKEL7/JkTqtJdL9veu2uEXvSxH3lks= +go.opentelemetry.io/collector/config/configopaque v1.62.0 h1:E64BPiumLcJO501g6XETf/vX6r+AK1ytqBc5UEcmkmI= +go.opentelemetry.io/collector/config/configopaque v1.62.0/go.mod h1:z4FPFfKiO83yJz/DqzjlGofUYF9u1A5U/s9NLaa6L1w= +go.opentelemetry.io/collector/config/configoptional v1.62.0 h1:ekpmgw4FMhjqtmK+W8TC/92BCaXeql/g8iDgx0jmF9k= +go.opentelemetry.io/collector/config/configoptional v1.62.0/go.mod h1:7csNTdQCovjYC2HVzYU/lpHSmNxNgaQ3Vlq4037BeHI= +go.opentelemetry.io/collector/config/configretry v1.62.0 h1:OuttS/NoH8DIlmAH9ErbFoj3Pw9OUJtc53vWKlOni7g= +go.opentelemetry.io/collector/config/configretry v1.62.0/go.mod h1:W6bJYhzZ3FQ2Tg0K5SWprF3l7MotMqD1uQbgYm00SU8= +go.opentelemetry.io/collector/config/configtls v1.62.0 h1:C4WywYuIhIHMkAcWmK19gHxub9KjHdxUREv281bKrvU= +go.opentelemetry.io/collector/config/configtls v1.62.0/go.mod h1:2r+Hlr7RXBs9u03HSd4eYJCLi6hukRQv7o36WrgzNkY= +go.opentelemetry.io/collector/confmap v1.62.0 h1:JF1hNjXeZGDKKyK0QBa9yAtGUado+zj4hLHM0BCag40= +go.opentelemetry.io/collector/confmap v1.62.0/go.mod h1:4rRpkbOkE/LvUSmrMX+jCr94i8P4JtYf93TBvfR5LUA= +go.opentelemetry.io/collector/confmap/xconfmap v0.156.0 h1:klJDLtd4+xeCttXAL0teEdnR8w1veNEOBvaP1YzAWm4= +go.opentelemetry.io/collector/confmap/xconfmap v0.156.0/go.mod h1:SGEOhF001IBHO1CMw7lUjzpvRu3eH4T+aayeGSC6alo= +go.opentelemetry.io/collector/consumer v1.62.0 h1:nJzGs8soiciZvGhiA4OYwPRRCrTsXnNHrmzi/jaT3ck= +go.opentelemetry.io/collector/consumer v1.62.0/go.mod h1:uNbRHJ9LqgHxcWdLTvRTO4K3SSGZop1qlHKfV5lUvGg= +go.opentelemetry.io/collector/consumer/consumererror v0.156.0 h1:cbP/TPvhmWYmu9OQWYfMJQWhUjy9QJW7nwI4ndDMKcA= +go.opentelemetry.io/collector/consumer/consumererror v0.156.0/go.mod h1:vCs2p3dVyx1cSiZPi8zxr6FvspEPhJ0vw5QqqEj6EaY= +go.opentelemetry.io/collector/consumer/consumertest v0.156.0 h1:hQcocbgZHL/ebRjO7VzXmHv0sYLzg6dl8vGn3BNxukg= +go.opentelemetry.io/collector/consumer/consumertest v0.156.0/go.mod h1:R/OttdDWuo4Hz80AFBop6VA79Rd/Pk9HROUWySSwiGc= +go.opentelemetry.io/collector/consumer/xconsumer v0.156.0 h1:XRkLqtyWnc1CVzAFdMDfmozKhrrqe/WW0ldzNALce7U= +go.opentelemetry.io/collector/consumer/xconsumer v0.156.0/go.mod h1:noYZwt6zId25ebyGRJfWSs4TfFV8RkUJeNyCoE0YaEU= +go.opentelemetry.io/collector/exporter v1.62.0 h1:EjtTH/BuhVhoF7Yq7pWJkfWtGEYueV76OBaZOIIs510= +go.opentelemetry.io/collector/exporter v1.62.0/go.mod h1:7wZ/xNhiidMk9RRGWVd1cEENReVZFyoLIDT09wSiZHI= +go.opentelemetry.io/collector/exporter/exporterhelper v0.156.0 h1:ky+cQEYiCXC2qJ/1vZljUaRsKe6fp7eTZMjxZPBftOs= +go.opentelemetry.io/collector/exporter/exporterhelper v0.156.0/go.mod h1:uTpZ/H1BCIivLPS4q0FDoPsfs0BR3KUYxbUkkoT+BqE= +go.opentelemetry.io/collector/exporter/exportertest v0.156.0 h1:jnPTqaF58YCKeU8T8FjkcWMjI08viY0q5jm0tsY6w2o= +go.opentelemetry.io/collector/exporter/exportertest v0.156.0/go.mod h1:q7KPayeka+yCIEty6ysVe8l7XQCx+q6GwDTh3twmLD8= +go.opentelemetry.io/collector/exporter/xexporter v0.156.0 h1:RCgT47Fy3rFi8ytvT2wazKdsBIxkgxHUEgc0z5IksYU= +go.opentelemetry.io/collector/exporter/xexporter v0.156.0/go.mod h1:1KnwVOzi9dhfGJQ5I62J6Z8ywL1siUzLVyMvBajz9Q0= +go.opentelemetry.io/collector/extension v1.62.0 h1:otGURB9mCfpmRrBr+aI2NS/RjwZr2TZ4Crbqi1N3D7w= +go.opentelemetry.io/collector/extension v1.62.0/go.mod h1:EmaC0bqQ6cc4cEkiR29r04UZWQLVT7KLJTfzfycLEEQ= +go.opentelemetry.io/collector/extension/extensionauth v1.62.0 h1:2yhRG9OFxUSCrc+0GqgON+WKVciV65s+rrnOoWLR4V4= +go.opentelemetry.io/collector/extension/extensionauth v1.62.0/go.mod h1:bJV7oxY/JWRDXrZDbjuv9DjU0NNNs6r+YQcYkWVzf7o= +go.opentelemetry.io/collector/extension/extensionauth/extensionauthtest v0.156.0 h1:bIDTqJGRZ3r0ArC+cH+sr8LUOij1pEf3teBK1+UEvJQ= +go.opentelemetry.io/collector/extension/extensionauth/extensionauthtest v0.156.0/go.mod h1:ezdHmVHezn0T1s0lMZfYssYIms9qp25B7x4ad1vVOnY= +go.opentelemetry.io/collector/extension/extensionmiddleware v0.156.0 h1:cS4SVO/OJA+YeFblSNnjDl3ZzZyo0B2qQP3NQ56UsSY= +go.opentelemetry.io/collector/extension/extensionmiddleware v0.156.0/go.mod h1:wucOUbf33iZEtOSLtUi7UsULqmlIeMsCp0kIRtlevdw= +go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest v0.156.0 h1:+0nhgaInmoYU9iHKqxD9wzRCTIghuDi+zbiNIWOe2ME= +go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest v0.156.0/go.mod h1:YLJft5vQ5o03yETsG6qoKjoAaCGsrJVxCmh36RVPAKo= +go.opentelemetry.io/collector/extension/extensiontest v0.156.0 h1:PwjcAv345HLUeMJUQAz++lg7HnZ3aNMNqFBHc8+OEeY= +go.opentelemetry.io/collector/extension/extensiontest v0.156.0/go.mod h1:31dxT9F85G50+/jYRsI5t6uUeSvVK08IyDZXEvBooF8= +go.opentelemetry.io/collector/extension/xextension v0.156.0 h1:DKjVhlLEvFpEd1C/FSJt9jYmWkDAhFe7ypbUZcAg//U= +go.opentelemetry.io/collector/extension/xextension v0.156.0/go.mod h1:dq8AbQJvnIlInXTZBPmlk7mQuqrN/K35V3RnomyOazk= +go.opentelemetry.io/collector/featuregate v1.62.0 h1:pYY7RlulSCTOS9mFWxasMLwYJCfNXHtnOkZlv3jg/V4= +go.opentelemetry.io/collector/featuregate v1.62.0/go.mod h1:4ga1QBMPEejXXmpyJS8lmaRpknJ3Lb9Bvk6e420bUFU= +go.opentelemetry.io/collector/internal/componentalias v0.156.0 h1:Ku9pTxb4imQME35PoR0mzXv+v3jLtbGxRT0PiH4j034= +go.opentelemetry.io/collector/internal/componentalias v0.156.0/go.mod h1:1YJUCQ6Her24ZhJnYgKSuov7AaFB1jEPawvEAjrp1ms= +go.opentelemetry.io/collector/internal/testutil v0.156.0 h1:Nu02vhHA2UQ3Yjyjisk3N24HHxwvw7PQiTz9O1PuiUY= +go.opentelemetry.io/collector/internal/testutil v0.156.0/go.mod h1:Jkjs6rkqs973LqgZ0Fe3zrokQRKULYXPIf4HuqStiEE= +go.opentelemetry.io/collector/pdata v1.62.0 h1:xGdwl2Cs5Rq5nKs0nYvAxm3Qq20HcySVAmUElATS8Es= +go.opentelemetry.io/collector/pdata v1.62.0/go.mod h1:WFy5R6XGpz2Q4MaekeEm+qc4GY5V3+BhQIwGPkp+fj0= +go.opentelemetry.io/collector/pdata/pprofile v0.156.0 h1:TnQzA2d5iMGH5//mGLqPjwdYqsFD/A7o2WgDdppxdVM= +go.opentelemetry.io/collector/pdata/pprofile v0.156.0/go.mod h1:3dtjs/mliblJJCCTXUE0AkpBNfBEybPruj3ml6WCOoI= +go.opentelemetry.io/collector/pdata/testdata v0.156.0 h1:0+0YZYap+zHwx4c3TrgvWGbODlErrFXpUsT+RsyGmoQ= +go.opentelemetry.io/collector/pdata/testdata v0.156.0/go.mod h1:7amnd10hSandpk/VHGBJ9vMR59PnKh2ngwbtFLKezi4= +go.opentelemetry.io/collector/pdata/xpdata v0.156.0 h1:p5eRg+/kJduIzXUDyCM1tMiYomV5Yz0JzG30t7iwi4w= +go.opentelemetry.io/collector/pdata/xpdata v0.156.0/go.mod h1:cs5rPBIE1du6CSJIUIqDYRRGzfuV4kyURKEMQHnu+zQ= +go.opentelemetry.io/collector/pipeline v1.62.0 h1:+fFaLegFsMPhBl6oHauS09qOoKWgtufjM3g9i/wXZ44= +go.opentelemetry.io/collector/pipeline v1.62.0/go.mod h1:RD90NG3Jbk965Xaqym3JyHkuol4uZJjQVUkD9ddXJIs= +go.opentelemetry.io/collector/pipeline/xpipeline v0.156.0 h1:j62f0ILpqzwzSJQ8cJygJCnthSHyqN47uomk14IXmaA= +go.opentelemetry.io/collector/pipeline/xpipeline v0.156.0/go.mod h1:ymWYILTf6bO5qrEKD1EyIl7g30AaENPZfS3OFCb5IRA= +go.opentelemetry.io/collector/receiver v1.62.0 h1:hBjVSZTLrY5IXgcI8SQyDE2D/15vivQrIiaIvi8Yri0= +go.opentelemetry.io/collector/receiver v1.62.0/go.mod h1:Sao2WTwFxmX563Q/CIEXzU6cql+rCQ1NCwG2IALtBrg= +go.opentelemetry.io/collector/receiver/receivertest v0.156.0 h1:7Z+8tXDZv11Qfaf/DmWxaCpUAdjWrwRtd9xttMjNZko= +go.opentelemetry.io/collector/receiver/receivertest v0.156.0/go.mod h1:qRWqCgqOSglqCaMqlmAiryXtWOktPbHjm8VQggbUgq8= +go.opentelemetry.io/collector/receiver/xreceiver v0.156.0 h1:f8YN4oLLoXa1pNyrSDu316JOEUkG4bhtYQMuU08Xyf0= +go.opentelemetry.io/collector/receiver/xreceiver v0.156.0/go.mod h1:ywkZIgtGTiLm0KBbhL1lRrxu5iytUeAhsstd0IyuG+w= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +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/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +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= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +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-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= +google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +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/internal/exporter/httpexporter/internal/metadata/generated_status.go b/internal/exporter/httpexporter/internal/metadata/generated_status.go new file mode 100644 index 000000000..95655d6a7 --- /dev/null +++ b/internal/exporter/httpexporter/internal/metadata/generated_status.go @@ -0,0 +1,33 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/collector/component" +) + +var ( + Type = component.MustNewType("http") + ScopeName = "github.com/elastic/opentelemetry-collector-components/internal/exporter/httpexporter" +) + +const ( + LogsStability = component.StabilityLevelDevelopment +) diff --git a/internal/exporter/httpexporter/metadata.yaml b/internal/exporter/httpexporter/metadata.yaml new file mode 100644 index 000000000..e62f59f13 --- /dev/null +++ b/internal/exporter/httpexporter/metadata.yaml @@ -0,0 +1,9 @@ +type: http +scope_name: github.com/elastic/opentelemetry-collector-components/internal/exporter/httpexporter +github_project: elastic/opentelemetry-collector-components + +status: + class: exporter + stability: + development: [logs] + distributions: [] diff --git a/loadgen/cmd/otelbench/components.go b/loadgen/cmd/otelbench/components.go index cf271f4a7..4b72fdc79 100644 --- a/loadgen/cmd/otelbench/components.go +++ b/loadgen/cmd/otelbench/components.go @@ -38,6 +38,7 @@ import ( "go.opentelemetry.io/collector/service/telemetry/otelconftelemetry" "github.com/elastic/metricsgenreceiver/metricsgenreceiver" + "github.com/elastic/opentelemetry-collector-components/internal/exporter/httpexporter" "github.com/elastic/opentelemetry-collector-components/processor/ratelimitprocessor" "github.com/elastic/opentelemetry-collector-components/receiver/loadgenreceiver" ) @@ -50,6 +51,7 @@ var defaultBenchmarkExporters = map[string]bool{ var nonBenchmarkExporters = map[string]struct{}{ "debug": {}, "nop": {}, + "http": {}, // raw HTTP body exporter; not part of the default OTLP bench matrix } func components(logsDone, metricsDone, tracesDone, profilesDone chan loadgenreceiver.Stats) (otelcol.Factories, error) { @@ -82,6 +84,7 @@ func components(logsDone, metricsDone, tracesDone, profilesDone chan loadgenrece otlpexporter.NewFactory(), otlphttpexporter.NewFactory(), prometheusremotewriteexporter.NewFactory(), + httpexporter.NewFactory(), debugexporter.NewFactory(), nopexporter.NewFactory(), ) diff --git a/loadgen/cmd/otelbench/go.mod b/loadgen/cmd/otelbench/go.mod index 6ea550b13..bfa0b787a 100644 --- a/loadgen/cmd/otelbench/go.mod +++ b/loadgen/cmd/otelbench/go.mod @@ -5,6 +5,7 @@ go 1.25.0 require ( github.com/elastic/go-elasticsearch/v8 v8.19.4 github.com/elastic/metricsgenreceiver/metricsgenreceiver v0.0.0-20260619100109-5151cd4d2928 + github.com/elastic/opentelemetry-collector-components/internal/exporter/httpexporter v0.0.0-00010101000000-000000000000 github.com/elastic/opentelemetry-collector-components/processor/ratelimitprocessor v0.0.0-00010101000000-000000000000 github.com/elastic/opentelemetry-collector-components/receiver/loadgenreceiver v0.0.0-00010101000000-000000000000 github.com/open-telemetry/opentelemetry-collector-contrib/exporter/elasticsearchexporter v0.156.0 @@ -267,3 +268,5 @@ replace ( // https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/33137 TODO: Remove this once the issue is fixed. github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter => ../../../internal/exporter/prometheusremotewriteexporter ) + +replace github.com/elastic/opentelemetry-collector-components/internal/exporter/httpexporter => ../../../internal/exporter/httpexporter diff --git a/loadgen/cmd/otelsoak/README.md b/loadgen/cmd/otelsoak/README.md index 34ff8279c..5a4c96c79 100644 --- a/loadgen/cmd/otelsoak/README.md +++ b/loadgen/cmd/otelsoak/README.md @@ -25,3 +25,51 @@ ELASTIC_APM_SERVER_URL=http://localhost:8200 ELASTIC_APM_API_KEY=some_api_key ./ ``` It is recommended to create your own `config.yaml` from `config.example.yaml` to fit your needs. + +## Vercel drain (raw HTTP NDJSON) + +OTLP exporters used in the OTLP otelsoak scenarios cannot target Managed Input +drains. The Vercel drain scenario sends NDJSON over HTTP instead. + +Use the `http` exporter +([internal/exporter/httpexporter](../../../internal/exporter/httpexporter)) with +loadgen log bodies that contain drain NDJSON lines. See +[`config.vercel.example.yaml`](./config.vercel.example.yaml) and +[`testdata/vercel/`](./testdata/vercel/). + +`VERCEL_SIGNAL` selects which fixture to replay (default `logs`): + +| Value | Fixture | +| --- | --- | +| `logs` | [`testdata/vercel/logs.jsonl`](./testdata/vercel/logs.jsonl) | +| `speed_insights` | [`testdata/vercel/speed_insights.jsonl`](./testdata/vercel/speed_insights.jsonl) | +| `both` | Concat of the two files at run time (`make otelsoak-run-vercel` only) | + +Logs and speed-insights use the same Managed Input URL; only the NDJSON body shape changes. +Leave `VERCEL_SIGNAL` unset for the default; an empty value (`VERCEL_SIGNAL=`) is not a default and will fail. + +How to run: + +```bash +make genelasticcol +VERCEL_SIGNAL=speed_insights \ +ELASTIC_SERVER_URL=https://example.ingest.us-central1.gcp.qa.elastic.cloud \ +ELASTIC_APM_API_KEY=some_api_key \ +make otelsoak-run-vercel +``` + +For both signals (temp file = `logs.jsonl` + `speed_insights.jsonl`): + +```bash +VERCEL_SIGNAL=both \ +ELASTIC_SERVER_URL=https://example.ingest.us-central1.gcp.qa.elastic.cloud \ +ELASTIC_APM_API_KEY=some_api_key \ +make otelsoak-run-vercel +``` + +Validate the config without sending traffic: + +```bash +VERCEL_SIGNAL=speed_insights ELASTIC_SERVER_URL=http://localhost:8200 ELASTIC_APM_API_KEY=foobar \ + ./loadgen/cmd/otelsoak/otelsoak validate --config ./loadgen/cmd/otelsoak/config.vercel.example.yaml +``` diff --git a/loadgen/cmd/otelsoak/config.vercel.example.yaml b/loadgen/cmd/otelsoak/config.vercel.example.yaml new file mode 100644 index 000000000..80225c8c9 --- /dev/null +++ b/loadgen/cmd/otelsoak/config.vercel.example.yaml @@ -0,0 +1,59 @@ +# otelsoak example: POST Vercel drain NDJSON to a Managed Inputs endpoint. +# +# VERCEL_SIGNAL selects the fixture file (default: logs): +# logs | speed_insights +# For both (logs.jsonl + speed_insights.jsonl concatenated at run time), use: +# make otelsoak-run-vercel VERCEL_SIGNAL=both +# +# Example: +# make genelasticcol +# VERCEL_SIGNAL=speed_insights \ +# ELASTIC_SERVER_URL=https://.ingest...qa.elastic.cloud \ +# ELASTIC_APM_API_KEY= \ +# ./loadgen/cmd/otelsoak/otelsoak --config ./loadgen/cmd/otelsoak/config.vercel.example.yaml +# +# Each loadgen log body is one Vercel drain JSON line (logs or speed-insights). +# The http exporter joins bodies with newlines and POSTs them as +# application/json (not OTLP) to the same Managed Input URL. + +receivers: + loadgen: + concurrency: 4 + logs: + # Path is relative to the process working directory (repo root when using make). + jsonl_file: loadgen/cmd/otelsoak/testdata/vercel/${env:VERCEL_SIGNAL:-logs}.jsonl + +processors: + ratelimit: + strategy: requests + rate: 100 + burst: 100 + throttle_behavior: delay + +exporters: + http: + endpoint: "${env:ELASTIC_SERVER_URL}/inputs/vercel/_default_" + headers: + - name: Authorization + value: "ApiKey ${env:ELASTIC_APM_API_KEY}" + tls: + insecure_skip_verify: true + timeout: 60s + sending_queue: + enabled: false + debug: + +service: + pipelines: + logs: + receivers: [loadgen] + processors: [ratelimit] + exporters: [http, debug] + telemetry: + metrics: + readers: + - pull: + exporter: + prometheus: + host: '127.0.0.1' + port: 8888 diff --git a/loadgen/cmd/otelsoak/testdata/vercel/logs.jsonl b/loadgen/cmd/otelsoak/testdata/vercel/logs.jsonl new file mode 100644 index 000000000..b451152ee --- /dev/null +++ b/loadgen/cmd/otelsoak/testdata/vercel/logs.jsonl @@ -0,0 +1,2 @@ +{"resourceLogs":[{"resource":{},"scopeLogs":[{"scope":{},"logRecords":[{"body":{"stringValue":"{\"id\":\"soak-vercel-1\",\"deploymentId\":\"dpl_soak\",\"source\":\"build\",\"host\":\"soak.vercel.app\",\"timestamp\":1786443432624,\"projectId\":\"soak_logs_project\",\"level\":\"info\",\"message\":\"Build completed successfully\",\"buildId\":\"bld_soak\",\"type\":\"stdout\",\"projectName\":\"soak-app\"}"},"timeUnixNano":"1786443432624000000"}]}]}]} +{"resourceLogs":[{"resource":{},"scopeLogs":[{"scope":{},"logRecords":[{"body":{"stringValue":"{\"id\":\"soak-vercel-2\",\"deploymentId\":\"dpl_soak\",\"source\":\"lambda\",\"host\":\"soak.vercel.app\",\"timestamp\":1786443432624,\"projectId\":\"soak_logs_project\",\"level\":\"info\",\"message\":\"API request processed\",\"entrypoint\":\"api/index.js\",\"requestId\":\"req-soak-1\",\"statusCode\":200,\"path\":\"/api/users\",\"executionRegion\":\"sfo1\",\"environment\":\"production\"}"},"timeUnixNano":"1786443432624000000"}]}]}]} diff --git a/loadgen/cmd/otelsoak/testdata/vercel/speed_insights.jsonl b/loadgen/cmd/otelsoak/testdata/vercel/speed_insights.jsonl new file mode 100644 index 000000000..5693a9708 --- /dev/null +++ b/loadgen/cmd/otelsoak/testdata/vercel/speed_insights.jsonl @@ -0,0 +1,3 @@ +{"resourceLogs":[{"resource":{},"scopeLogs":[{"scope":{},"logRecords":[{"body":{"stringValue":"{\"schema\":\"vercel.speed_insights.v1\",\"timestamp\":\"2026-08-11T10:00:00.000Z\",\"projectId\":\"soak_si_project\",\"ownerId\":\"team_soak\",\"deviceId\":1001,\"metricType\":\"LCP\",\"value\":2.4,\"origin\":\"https://soak.vercel.app\",\"path\":\"/\",\"route\":\"/\",\"country\":\"US\",\"region\":\"CA\",\"city\":\"San Francisco\",\"osName\":\"macOS\",\"osVersion\":\"14.0\",\"clientName\":\"Chrome\",\"clientType\":\"browser\",\"clientVersion\":\"126.0.0.0\",\"deviceType\":\"desktop\",\"deviceBrand\":\"Apple\",\"connectionSpeed\":\"4g\",\"vercelEnvironment\":\"production\",\"vercelUrl\":\"soak.vercel.app\",\"deploymentId\":\"dpl_soak\",\"sdkName\":\"@vercel/speed-insights\",\"sdkVersion\":\"1.0.0\"}"},"timeUnixNano":"1786443432624000000"}]}]}]} +{"resourceLogs":[{"resource":{},"scopeLogs":[{"scope":{},"logRecords":[{"body":{"stringValue":"{\"schema\":\"vercel.speed_insights.v1\",\"timestamp\":\"2026-08-11T10:00:05.000Z\",\"projectId\":\"soak_si_project\",\"ownerId\":\"team_soak\",\"deviceId\":1002,\"metricType\":\"CLS\",\"value\":0.08,\"origin\":\"https://soak.vercel.app\",\"path\":\"/dashboard\",\"route\":\"/dashboard\",\"country\":\"US\",\"region\":\"CA\",\"city\":\"San Francisco\",\"osName\":\"macOS\",\"clientName\":\"Chrome\",\"clientType\":\"browser\",\"deviceType\":\"desktop\",\"vercelEnvironment\":\"production\",\"vercelUrl\":\"soak.vercel.app\",\"deploymentId\":\"dpl_soak\",\"sdkName\":\"@vercel/speed-insights\",\"sdkVersion\":\"1.0.0\"}"},"timeUnixNano":"1786443432624000000"}]}]}]} +{"resourceLogs":[{"resource":{},"scopeLogs":[{"scope":{},"logRecords":[{"body":{"stringValue":"{\"schema\":\"vercel.speed_insights.v1\",\"timestamp\":\"2026-08-11T10:00:10.000Z\",\"projectId\":\"soak_si_project\",\"ownerId\":\"team_soak\",\"deviceId\":1003,\"metricType\":\"INP\",\"value\":180,\"origin\":\"https://soak.vercel.app\",\"path\":\"/api/users\",\"route\":\"/api/users\",\"country\":\"DE\",\"region\":\"BE\",\"city\":\"Berlin\",\"osName\":\"Android\",\"clientName\":\"Chrome\",\"clientType\":\"browser\",\"deviceType\":\"mobile\",\"vercelEnvironment\":\"production\",\"vercelUrl\":\"soak.vercel.app\",\"deploymentId\":\"dpl_soak\",\"sdkName\":\"@vercel/speed-insights\",\"sdkVersion\":\"1.0.0\"}"},"timeUnixNano":"1786443432624000000"}]}]}]} diff --git a/versions.yaml b/versions.yaml index 94d6bfa54..2338d760b 100644 --- a/versions.yaml +++ b/versions.yaml @@ -35,5 +35,6 @@ excluded-modules: - github.com/elastic/opentelemetry-collector-components/loadgen - github.com/elastic/opentelemetry-collector-components/receiver/prometheusremotewritev1receiver/correctnesstests - github.com/elastic/opentelemetry-collector-components/receiver/akamaisiemreceiver + - github.com/elastic/opentelemetry-collector-components/internal/exporter/httpexporter - github.com/elastic/opentelemetry-collector-components/receiver/entityanalyticsreceiver - github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter From 373902c4151202b15ed0ab9d915397cc490988cf Mon Sep 17 00:00:00 2001 From: Andreas Gkizas Date: Tue, 11 Aug 2026 14:09:14 +0300 Subject: [PATCH 02/11] add chlog Signed-off-by: Andreas Gkizas --- .../.chloggen/vercel_drain_otelsoak.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 loadgen/cmd/otelbench/.chloggen/vercel_drain_otelsoak.yaml diff --git a/loadgen/cmd/otelbench/.chloggen/vercel_drain_otelsoak.yaml b/loadgen/cmd/otelbench/.chloggen/vercel_drain_otelsoak.yaml new file mode 100644 index 000000000..586406d73 --- /dev/null +++ b/loadgen/cmd/otelbench/.chloggen/vercel_drain_otelsoak.yaml @@ -0,0 +1,18 @@ +# One of 'breaking', 'deprecation', 'enhancement', 'bug_fix' +change_type: enhancement + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Registers a new httpexporter for raw HTTP Managed Input drain benchmarks (e.g. Vercel) + +# It is mandatory to specify the component. Do not change this. +component: otelbench + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [1340] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: | + Adds the `http` exporter factory so otelbench/otelsoak can POST NDJSON bodies + to Managed Input endpoints. The exporter is excluded from the default OTLP benchmark matrix. From 71299c8764061d98ed96d002d9f58f64f9250b64 Mon Sep 17 00:00:00 2001 From: Andreas Gkizas Date: Tue, 11 Aug 2026 15:02:43 +0300 Subject: [PATCH 03/11] add processor to change times Signed-off-by: Andreas Gkizas --- Makefile | 14 ++++++++++++-- loadgen/cmd/otelsoak/README.md | 6 ++++++ loadgen/cmd/otelsoak/config.vercel.example.yaml | 17 ++++++++++++++++- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index ebf2698c3..f9d5b614e 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,8 @@ INTEGRATION_MODS := $(shell $(FIND_INTEGRATION_TEST_MODS) | xargs $(TO_MOD_DIR) GROUP ?= all FOR_GROUP_TARGET=for-$(GROUP)-target -GOTOOLCHAIN ?= go1.25.7+auto +# Modules such as processor/elasticapmprocessor require go >= 1.26. +GOTOOLCHAIN ?= go1.26.5+auto .DEFAULT_GOAL := all @@ -92,7 +93,16 @@ remove-toolchain: # Build a collector based on the Elastic components (generate Elastic collector) .PHONY: genelasticcol genelasticcol: - GOTOOLCHAIN=${GOTOOLCHAIN} GOOS=${TARGET_GOOS} GOARCH=${TARGET_GOARCH} $(BUILDER) --config ./distributions/elastic-components/manifest.yaml + @set +e; \ + GOTOOLCHAIN=${GOTOOLCHAIN} GOOS=${TARGET_GOOS} GOARCH=${TARGET_GOARCH} $(BUILDER) \ + --config ./distributions/elastic-components/manifest.yaml --skip-compilation=true; \ + status=$$?; \ + set -e; \ + if [ ! -f ./_build/go.mod ]; then exit $${status:-1}; fi; \ + cd ./_build && GOTOOLCHAIN=${GOTOOLCHAIN} $(GOCMD) mod tidy; \ + cd ..; \ + GOTOOLCHAIN=${GOTOOLCHAIN} GOOS=${TARGET_GOOS} GOARCH=${TARGET_GOARCH} $(BUILDER) \ + --config ./distributions/elastic-components/manifest.yaml --skip-get-modules=true # Validate that the Elastic components collector can run with the example configuration. .PHONY: elasticcol-validate diff --git a/loadgen/cmd/otelsoak/README.md b/loadgen/cmd/otelsoak/README.md index 5a4c96c79..bb1930d80 100644 --- a/loadgen/cmd/otelsoak/README.md +++ b/loadgen/cmd/otelsoak/README.md @@ -48,6 +48,12 @@ loadgen log bodies that contain drain NDJSON lines. See Logs and speed-insights use the same Managed Input URL; only the NDJSON body shape changes. Leave `VERCEL_SIGNAL` unset for the default; an empty value (`VERCEL_SIGNAL=`) is not a default and will fail. +`config.vercel.example.yaml` runs `transform/vercel_refresh` before the `http` exporter +so drain JSON fields keep advancing (loadgen only refreshes the OTLP log timestamp): + +- Speed Insights: body `timestamp` (ISO) and `deviceId` +- Vercel logs: body unix-millis `timestamp` (including nested `proxy.timestamp`) and `id` + How to run: ```bash diff --git a/loadgen/cmd/otelsoak/config.vercel.example.yaml b/loadgen/cmd/otelsoak/config.vercel.example.yaml index 80225c8c9..dab66d9ef 100644 --- a/loadgen/cmd/otelsoak/config.vercel.example.yaml +++ b/loadgen/cmd/otelsoak/config.vercel.example.yaml @@ -24,6 +24,21 @@ receivers: jsonl_file: loadgen/cmd/otelsoak/testdata/vercel/${env:VERCEL_SIGNAL:-logs}.jsonl processors: + # vercelencodingextension reads event time (and log id / SI deviceId) from the + # drain JSON body, not the OTLP log timestamp loadgen rewrites. Refresh body + # fields each send so documents keep advancing under concurrency. + transform/vercel_refresh: + error_mode: ignore + log_statements: + - context: log + statements: + # Speed Insights: ISO timestamp + unique deviceId + - replace_pattern(body, "\"timestamp\":\"[^\"]+\"", Concat(["\"timestamp\":\"", FormatTime(Now(), "%Y-%m-%dT%H:%M:%S.%f%j"), "\""], "")) where IsString(body) and IsMatch(body, "vercel\\.speed_insights\\.v1") + - replace_pattern(body, "\"deviceId\":\\d+", Concat(["\"deviceId\":", String(UnixNano(Now()))], "")) where IsString(body) and IsMatch(body, "vercel\\.speed_insights\\.v1") + # Vercel logs: unix-millis timestamp(s) + unique id (also used as log.record.uid) + - replace_pattern(body, "\"timestamp\":\\d+", Concat(["\"timestamp\":", String(UnixMilli(Now()))], "")) where IsString(body) and IsMatch(body, "vercel\\.speed_insights\\.v1") == false + - replace_pattern(body, "\"id\":\"[^\"]+\"", Concat(["\"id\":\"", UUID(), "\""], "")) where IsString(body) and IsMatch(body, "vercel\\.speed_insights\\.v1") == false + ratelimit: strategy: requests rate: 100 @@ -47,7 +62,7 @@ service: pipelines: logs: receivers: [loadgen] - processors: [ratelimit] + processors: [transform/vercel_refresh, ratelimit] exporters: [http, debug] telemetry: metrics: From bacc61132bff71c8143b42d488ff495809e4d450 Mon Sep 17 00:00:00 2001 From: Andreas Gkizas Date: Tue, 11 Aug 2026 15:11:16 +0300 Subject: [PATCH 04/11] remove doc Signed-off-by: Andreas Gkizas --- ...026-08-11-otelsoak-vercel-drain-signals.md | 174 ------------------ ...11-otelsoak-vercel-drain-signals-design.md | 78 -------- 2 files changed, 252 deletions(-) delete mode 100644 docs/superpowers/plans/2026-08-11-otelsoak-vercel-drain-signals.md delete mode 100644 docs/superpowers/specs/2026-08-11-otelsoak-vercel-drain-signals-design.md diff --git a/docs/superpowers/plans/2026-08-11-otelsoak-vercel-drain-signals.md b/docs/superpowers/plans/2026-08-11-otelsoak-vercel-drain-signals.md deleted file mode 100644 index 5e0ec37b2..000000000 --- a/docs/superpowers/plans/2026-08-11-otelsoak-vercel-drain-signals.md +++ /dev/null @@ -1,174 +0,0 @@ -# otelsoak Vercel drain signals Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Let otelsoak select Vercel drain payload type via `VERCEL_SIGNAL` (`logs` | `speed_insights` | `both`) using env-selected fixture files on one HTTP pipeline. - -**Architecture:** Keep a single loadgen logs → httpexporter pipeline. Each fixture line is OTLP-JSONL whose log body is one drain NDJSON object. `jsonl_file` uses `${env:VERCEL_SIGNAL:-logs}` so the same config covers all three modes. - -**Tech Stack:** OTel Collector config YAML, loadgenreceiver jsonl fixtures, httpexporter (unchanged). - -## Global Constraints - -- Same Managed Input URL for all signals: `${env:ELASTIC_SERVER_URL}/inputs/vercel/_default_` -- No httpexporter API changes; Speed Insights rides log bodies -- Default `VERCEL_SIGNAL` is `logs` -- Spec: `docs/superpowers/specs/2026-08-11-otelsoak-vercel-drain-signals-design.md` - -## File map - -| File | Responsibility | -| --- | --- | -| `loadgen/cmd/otelsoak/testdata/vercel/speed_insights.jsonl` | Speed Insights drain samples | -| `loadgen/cmd/otelsoak/testdata/vercel/logs.jsonl` | Unchanged (existing) | -| `loadgen/cmd/otelsoak/config.vercel.example.yaml` | Env-selected `jsonl_file` + usage comment | -| `loadgen/cmd/otelsoak/README.md` | Document `VERCEL_SIGNAL` | -| `Makefile` | `otelsoak-run-vercel`: temp concat when `VERCEL_SIGNAL=both` | -| `internal/exporter/httpexporter/README.md` | Point at signal selection | - -Note: no checked-in `both.jsonl`; `both` is Makefile-only (cat + `--set`). - ---- - -### Task 1: Speed Insights + both fixtures - -**Files:** -- Create: `loadgen/cmd/otelsoak/testdata/vercel/speed_insights.jsonl` -- Create: `loadgen/cmd/otelsoak/testdata/vercel/both.jsonl` -- Keep: `loadgen/cmd/otelsoak/testdata/vercel/logs.jsonl` - -**Interfaces:** -- Consumes: existing `logs.jsonl` line shape (OTLP `resourceLogs` → `body.stringValue` = drain JSON string) -- Produces: `speed_insights.jsonl` and `both.jsonl` with same wrapping; SI bodies use `schema: vercel.speed_insights.v1` - -- [x] **Step 1: Create `speed_insights.jsonl`** - -Two OTLP-JSONL lines. Inner bodies (escaped inside `stringValue`) must be valid Speed Insights objects, e.g.: - -```json -{"schema":"vercel.speed_insights.v1","timestamp":"2026-08-11T10:00:00.000Z","projectId":"soak_si_project","ownerId":"team_soak","deviceId":1001,"metricType":"LCP","value":2.4,"origin":"https://soak.vercel.app","path":"/","route":"/","country":"US","region":"CA","city":"San Francisco","osName":"macOS","clientName":"Chrome","clientType":"browser","deviceType":"desktop","vercelEnvironment":"production","vercelUrl":"soak.vercel.app","deploymentId":"dpl_soak","sdkName":"@vercel/speed-insights","sdkVersion":"1.0.0"} -``` - -and a second line with `metricType":"CLS"` (and optionally a third with `INP`). Wrap each as: - -```json -{"resourceLogs":[{"resource":{},"scopeLogs":[{"scope":{},"logRecords":[{"body":{"stringValue":""},"timeUnixNano":"1786443432624000000"}]}]}]} -``` - -- [x] **Step 2: Create `both.jsonl`** - -Concatenate all lines from `logs.jsonl` then all lines from `speed_insights.jsonl` (or interleave 1:1). No other transforms. - -- [x] **Step 3: Sanity-check JSON** - -Run: - -```bash -python3 -c " -import json -from pathlib import Path -base = Path('loadgen/cmd/otelsoak/testdata/vercel') -for name in ('logs.jsonl','speed_insights.jsonl','both.jsonl'): - for i, line in enumerate(base.joinpath(name).read_text().splitlines(), 1): - if not line.strip(): - continue - outer = json.loads(line) - body = outer['resourceLogs'][0]['scopeLogs'][0]['logRecords'][0]['body']['stringValue'] - inner = json.loads(body) - print(f'{name}:{i} ok keys={sorted(inner)[:5]}...') -" -``` - -Expected: each line prints `ok` with no JSON decode errors. For SI files, inner must include `"schema": "vercel.speed_insights.v1"`. - -- [x] **Step 4: Commit** (only if user asked to commit) - -```bash -git add loadgen/cmd/otelsoak/testdata/vercel/speed_insights.jsonl loadgen/cmd/otelsoak/testdata/vercel/both.jsonl -git commit -m "$(cat <<'EOF' -Add Vercel speed-insights and both soak fixtures for otelsoak. - -EOF -)" -``` - ---- - -### Task 2: Config + docs for `VERCEL_SIGNAL` - -**Files:** -- Modify: `loadgen/cmd/otelsoak/config.vercel.example.yaml` -- Modify: `loadgen/cmd/otelsoak/README.md` -- Modify: `Makefile` (otelsoak-run-vercel comment) -- Modify: `internal/exporter/httpexporter/README.md` - -**Interfaces:** -- Consumes: fixture filenames from Task 1 (`logs` | `speed_insights` | `both`) -- Produces: config path `.../testdata/vercel/${env:VERCEL_SIGNAL:-logs}.jsonl` - -- [x] **Step 1: Update config header + `jsonl_file`** - -In `config.vercel.example.yaml`: - -- Document `VERCEL_SIGNAL=logs|speed_insights|both` in the usage comment block -- Set: - -```yaml - jsonl_file: loadgen/cmd/otelsoak/testdata/vercel/${env:VERCEL_SIGNAL:-logs}.jsonl -``` - -Leave endpoint and pipeline unchanged. - -- [x] **Step 2: Update README Vercel section** - -Document: - -```bash -VERCEL_SIGNAL=speed_insights \ -ELASTIC_SERVER_URL=... ELASTIC_APM_API_KEY=... \ -./loadgen/cmd/otelsoak/otelsoak --config ./loadgen/cmd/otelsoak/config.vercel.example.yaml -``` - -List allowed values and that unset defaults to `logs`. Link fixtures directory. - -- [x] **Step 3: Touch Makefile + httpexporter README** - -- Makefile comment above `otelsoak-run-vercel`: mention `VERCEL_SIGNAL`. -- httpexporter README: say fixtures are selected via `VERCEL_SIGNAL` / point at vercel testdata dir. - -- [x] **Step 4: Validate config expands** - -Run (after `make genelasticcol` if binary missing): - -```bash -VERCEL_SIGNAL=speed_insights ELASTIC_SERVER_URL=http://localhost:8200 ELASTIC_APM_API_KEY=foobar \ - ./loadgen/cmd/otelsoak/otelsoak validate --config ./loadgen/cmd/otelsoak/config.vercel.example.yaml -``` - -Expected: exit 0, no “file not found” / config error. - -Repeat with `VERCEL_SIGNAL=both` and with `VERCEL_SIGNAL` unset (defaults to logs). - -- [x] **Step 5: Commit** (only if user asked to commit) - -```bash -git add loadgen/cmd/otelsoak/config.vercel.example.yaml loadgen/cmd/otelsoak/README.md Makefile internal/exporter/httpexporter/README.md -git commit -m "$(cat <<'EOF' -Select Vercel soak fixtures with VERCEL_SIGNAL env. - -EOF -)" -``` - ---- - -## Spec coverage check - -| Spec requirement | Task | -| --- | --- | -| `speed_insights.jsonl` fixture | Task 1 | -| `both.jsonl` interleaved/combined | Task 1 | -| Env-selected `jsonl_file` with default `logs` | Task 2 | -| Same endpoint | Task 2 (unchanged) | -| README / Makefile docs | Task 2 | -| No httpexporter API change | N/A (explicit non-change) | diff --git a/docs/superpowers/specs/2026-08-11-otelsoak-vercel-drain-signals-design.md b/docs/superpowers/specs/2026-08-11-otelsoak-vercel-drain-signals-design.md deleted file mode 100644 index d06cebc84..000000000 --- a/docs/superpowers/specs/2026-08-11-otelsoak-vercel-drain-signals-design.md +++ /dev/null @@ -1,78 +0,0 @@ -# otelsoak Vercel drain signals (logs + speed-insights) - -## Goal - -Make otelsoak Vercel Managed Input soak traffic selectable via env so operators can replay **logs**, **speed-insights**, or **both** against the same drain endpoint—covering [hosted-otel-collector#3428](https://github.com/elastic/hosted-otel-collector/issues/3428) fixture needs without changing `httpexporter`. - -## Context - -- Vercel drains (logs and Speed Insights) are raw HTTPS NDJSON, not OTLP. -- `httpexporter` POSTs **log record bodies** joined by newlines to a full URL. -- loadgen for OTLP uses separate logs/metrics/traces pipelines because those are real OTel signals. Vercel drain types are **payload variants on one HTTP path**, so they stay on a single loadgen **logs** → `http` pipeline. -- Speed Insights schema: https://vercel.com/docs/drains/reference/speed-insights (`schema: vercel.speed_insights.v1`). - -## Decisions - -| Topic | Choice | -| --- | --- | -| Selection UX | Env var `VERCEL_SIGNAL` | -| Values | `logs` \| `speed_insights` \| `both` | -| Default | `logs` (current behavior) | -| Endpoint | Same for all: `${ELASTIC_SERVER_URL}/inputs/vercel/_default_` | -| `both` semantics | `make otelsoak-run-vercel` concatenates `logs.jsonl` + `speed_insights.jsonl` into a temp file | -| Implementation | Env-expanded `jsonl_file` for `logs`/`speed_insights`; Makefile temp concat for `both` | - -## Design - -### Fixtures - -Directory: `loadgen/cmd/otelsoak/testdata/vercel/` - -| File | Contents | -| --- | --- | -| `logs.jsonl` | Existing Vercel log drain samples (OTLP-JSONL; each body is one drain NDJSON line) | -| `speed_insights.jsonl` | Same OTLP-JSONL wrapping; bodies are Speed Insights NDJSON objects (`vercel.speed_insights.v1`) with a few metric types (e.g. LCP, CLS, INP) | - -There is no checked-in `both.jsonl`. `VERCEL_SIGNAL=both` is handled by `make otelsoak-run-vercel`, which cats the two fixtures into a temp path and passes it via `--set`. - -Each loadgen line remains one OTLP log record whose `body.stringValue` is the exact drain NDJSON object string that `httpexporter` will POST. - -### Config - -`loadgen/cmd/otelsoak/config.vercel.example.yaml`: - -- `receivers.loadgen.logs.jsonl_file: loadgen/cmd/otelsoak/testdata/vercel/${env:VERCEL_SIGNAL:-logs}.jsonl` -- Unchanged: concurrency, ratelimit, `http` exporter endpoint/headers/TLS, single logs pipeline - -Invalid `VERCEL_SIGNAL` values fail when loadgen cannot open the file; document allowed values in README/Makefile. - -### Usage - -```bash -make genelasticcol -VERCEL_SIGNAL=logs|speed_insights|both \ -ELASTIC_SERVER_URL=https://.ingest...qa.elastic.cloud \ -ELASTIC_APM_API_KEY= \ -./loadgen/cmd/otelsoak/otelsoak --config ./loadgen/cmd/otelsoak/config.vercel.example.yaml -``` - -`make otelsoak-run-vercel` continues to work; callers pass `VERCEL_SIGNAL` in the environment. - -### Docs - -- Update `loadgen/cmd/otelsoak/README.md` Vercel section for `VERCEL_SIGNAL` and new fixtures. -- Brief note in Makefile `otelsoak-run-vercel` comment and `internal/exporter/httpexporter/README.md` sample pointer if it names only `logs.jsonl`. - -## Out of scope - -- `httpexporter` API/signal changes (no metrics exporter path). -- Separate Managed Input URLs per drain type. -- Parallel pipelines or independent rates for logs vs speed-insights. -- Encoding/parsing work inside hosted-otel-collector (issue 3428 server-side); this repo only supplies soak fixtures and config. - -## Success criteria - -1. `VERCEL_SIGNAL=logs` (or unset) behaves as today’s vercel soak. -2. `VERCEL_SIGNAL=speed_insights` POSTs Speed Insights NDJSON to the same endpoint. -3. `make otelsoak-run-vercel VERCEL_SIGNAL=both` POSTs both payload kinds from a temp concat of the two fixtures. -4. Docs show the three values and the env-selected path pattern. From 5b56899634de908db105902412eb37f1b3aadfd3 Mon Sep 17 00:00:00 2001 From: Andreas Gkizas Date: Tue, 11 Aug 2026 15:15:54 +0300 Subject: [PATCH 05/11] update pr number Signed-off-by: Andreas Gkizas --- loadgen/cmd/otelbench/.chloggen/vercel_drain_otelsoak.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/loadgen/cmd/otelbench/.chloggen/vercel_drain_otelsoak.yaml b/loadgen/cmd/otelbench/.chloggen/vercel_drain_otelsoak.yaml index 586406d73..4c5e54a8b 100644 --- a/loadgen/cmd/otelbench/.chloggen/vercel_drain_otelsoak.yaml +++ b/loadgen/cmd/otelbench/.chloggen/vercel_drain_otelsoak.yaml @@ -8,7 +8,7 @@ note: Registers a new httpexporter for raw HTTP Managed Input drain benchmarks ( component: otelbench # Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. -issues: [1340] +issues: [1347] # (Optional) One or more lines of additional information to render under the primary note. # These lines will be padded with 2 spaces and then inserted directly into the document. From 8bd0d38e92f1fc23e95a1c53ebaa9aa95546bda7 Mon Sep 17 00:00:00 2001 From: Andreas Gkizas Date: Tue, 11 Aug 2026 15:38:03 +0300 Subject: [PATCH 06/11] fix make common for http Signed-off-by: Andreas Gkizas --- internal/exporter/httpexporter/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/exporter/httpexporter/Makefile b/internal/exporter/httpexporter/Makefile index ded7a3609..bdd863a20 100644 --- a/internal/exporter/httpexporter/Makefile +++ b/internal/exporter/httpexporter/Makefile @@ -1 +1 @@ -include ../../Makefile.Common +include ../../../Makefile.Common From 87cc4e230544ec9a119aef934d0acebb298355a1 Mon Sep 17 00:00:00 2001 From: Andreas Gkizas Date: Tue, 11 Aug 2026 16:37:59 +0300 Subject: [PATCH 07/11] fix lint and govlun Signed-off-by: Andreas Gkizas --- internal/exporter/httpexporter/exporter.go | 11 +++++++---- internal/exporter/httpexporter/go.mod | 2 +- internal/exporter/httpexporter/go.sum | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/internal/exporter/httpexporter/exporter.go b/internal/exporter/httpexporter/exporter.go index 5889639c8..7e0cf607f 100644 --- a/internal/exporter/httpexporter/exporter.go +++ b/internal/exporter/httpexporter/exporter.go @@ -79,10 +79,13 @@ func (e *httpExporter) pushLogs(ctx context.Context, ld plog.Logs) error { if err != nil { return fmt.Errorf("failed to POST to %s: %w", e.config.Endpoint, err) } - defer resp.Body.Close() - - // Drain body so connections can be reused. - _, _ = io.Copy(io.Discard, resp.Body) + defer func() { + // Drain body so connections can be reused. + _, _ = io.Copy(io.Discard, resp.Body) + if err := resp.Body.Close(); err != nil { + e.logger.Warn("failed to close response body", zap.Error(err)) + } + }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("POST %s returned status %d", e.config.Endpoint, resp.StatusCode) diff --git a/internal/exporter/httpexporter/go.mod b/internal/exporter/httpexporter/go.mod index 792082aa7..71ee48737 100644 --- a/internal/exporter/httpexporter/go.mod +++ b/internal/exporter/httpexporter/go.mod @@ -86,7 +86,7 @@ require ( golang.org/x/sys v0.46.0 // indirect golang.org/x/text v0.38.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect - google.golang.org/grpc v1.82.0 // indirect + google.golang.org/grpc v1.82.1 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/internal/exporter/httpexporter/go.sum b/internal/exporter/httpexporter/go.sum index 283253230..a95d379a8 100644 --- a/internal/exporter/httpexporter/go.sum +++ b/internal/exporter/httpexporter/go.sum @@ -199,8 +199,8 @@ 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-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= -google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From db420a93e71dabcd32c8eabfa24822a9b52f76ed Mon Sep 17 00:00:00 2001 From: Andreas Gkizas Date: Wed, 12 Aug 2026 14:43:02 +0300 Subject: [PATCH 08/11] add files inside build Signed-off-by: Andreas Gkizas --- Makefile | 22 +---- internal/exporter/httpexporter/README.md | 7 +- internal/exporter/httpexporter/exporter.go | 13 ++- .../exporter/httpexporter/exporter_test.go | 60 ++++++++----- loadgen/cmd/otelsoak/README.md | 19 ++-- .../cmd/otelsoak/config.vercel.example.yaml | 11 ++- receiver/loadgenreceiver/README.md | 20 +++++ receiver/loadgenreceiver/config.go | 11 +++ receiver/loadgenreceiver/logs.go | 12 ++- receiver/loadgenreceiver/presets.go | 68 ++++++++++++++ receiver/loadgenreceiver/presets_test.go | 89 +++++++++++++++++++ .../testdata/vercel/logs.jsonl | 0 .../testdata/vercel/speed_insights.jsonl | 0 13 files changed, 267 insertions(+), 65 deletions(-) create mode 100644 receiver/loadgenreceiver/presets.go create mode 100644 receiver/loadgenreceiver/presets_test.go rename {loadgen/cmd/otelsoak => receiver/loadgenreceiver}/testdata/vercel/logs.jsonl (100%) rename {loadgen/cmd/otelsoak => receiver/loadgenreceiver}/testdata/vercel/speed_insights.jsonl (100%) diff --git a/Makefile b/Makefile index f9d5b614e..b6614b581 100644 --- a/Makefile +++ b/Makefile @@ -138,29 +138,9 @@ otelsoak-run: genelasticcol # Run otelsoak against a Vercel Managed Inputs drain endpoint (HTTP NDJSON via httpexporter). # Optional: VERCEL_SIGNAL=logs|speed_insights|both (default logs). -# For both, cats logs.jsonl + speed_insights.jsonl into a temp file at run time. -VERCEL_SIGNAL ?= logs -VERCEL_TESTDATA := loadgen/cmd/otelsoak/testdata/vercel .PHONY: otelsoak-run-vercel otelsoak-run-vercel: genelasticcol - @case "$(VERCEL_SIGNAL)" in \ - logs|speed_insights) \ - ./loadgen/cmd/otelsoak/otelsoak --config ./loadgen/cmd/otelsoak/config.vercel.example.yaml $(ARGS); \ - ;; \ - both) \ - tmp=$$(mktemp); \ - cat $(VERCEL_TESTDATA)/logs.jsonl $(VERCEL_TESTDATA)/speed_insights.jsonl > $$tmp; \ - ./loadgen/cmd/otelsoak/otelsoak --config ./loadgen/cmd/otelsoak/config.vercel.example.yaml \ - --set receivers.loadgen.logs.jsonl_file=$$tmp $(ARGS); \ - status=$$?; \ - rm -f $$tmp; \ - exit $$status; \ - ;; \ - *) \ - echo "VERCEL_SIGNAL must be logs, speed_insights, or both (got: $(VERCEL_SIGNAL))" >&2; \ - exit 1; \ - ;; \ - esac + ./loadgen/cmd/otelsoak/otelsoak --config ./loadgen/cmd/otelsoak/config.vercel.example.yaml $(ARGS) # Clones the upstream opentelemetry-collector repository in a temporal .release diff --git a/internal/exporter/httpexporter/README.md b/internal/exporter/httpexporter/README.md index 58257f0b1..5731e1c9a 100644 --- a/internal/exporter/httpexporter/README.md +++ b/internal/exporter/httpexporter/README.md @@ -42,6 +42,7 @@ exporters: ## Sample otelsoak pipeline (Vercel drain) -Put each drain NDJSON line in a loadgen OTLP-JSONL log body (see -`loadgen/cmd/otelsoak/testdata/vercel/`). Select logs, speed-insights, or both -with `VERCEL_SIGNAL` in [`config.vercel.example.yaml`](../../loadgen/cmd/otelsoak/config.vercel.example.yaml). +Use loadgenreceiver embedded Vercel presets (`vercel_logs`, +`vercel_speed_insights`, `vercel_both`) via `VERCEL_SIGNAL` in +[`config.vercel.example.yaml`](../../loadgen/cmd/otelsoak/config.vercel.example.yaml). +Fixtures live under [`receiver/loadgenreceiver/testdata/vercel/`](../../receiver/loadgenreceiver/testdata/vercel/). diff --git a/internal/exporter/httpexporter/exporter.go b/internal/exporter/httpexporter/exporter.go index 7e0cf607f..683e42c4c 100644 --- a/internal/exporter/httpexporter/exporter.go +++ b/internal/exporter/httpexporter/exporter.go @@ -26,6 +26,7 @@ import ( "strings" "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/consumer/consumererror" "go.opentelemetry.io/collector/exporter" "go.opentelemetry.io/collector/pdata/plog" "go.uber.org/zap" @@ -87,10 +88,16 @@ func (e *httpExporter) pushLogs(ctx context.Context, ld plog.Logs) error { } }() - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return fmt.Errorf("POST %s returned status %d", e.config.Endpoint, resp.StatusCode) + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + return nil } - return nil + + err = fmt.Errorf("POST %s returned status %d", e.config.Endpoint, resp.StatusCode) + // 5xx and 429 are typically transient; other 4xx (auth, bad request) are not. + if resp.StatusCode >= 500 || resp.StatusCode == http.StatusTooManyRequests { + return err + } + return consumererror.NewPermanent(err) } // encodeLogBodies joins each log record body as a line (NDJSON-friendly). diff --git a/internal/exporter/httpexporter/exporter_test.go b/internal/exporter/httpexporter/exporter_test.go index 2d36217b7..3ae06b4e8 100644 --- a/internal/exporter/httpexporter/exporter_test.go +++ b/internal/exporter/httpexporter/exporter_test.go @@ -18,6 +18,7 @@ package httpexporter import ( + "fmt" "io" "net/http" "net/http/httptest" @@ -28,6 +29,7 @@ import ( "go.opentelemetry.io/collector/component/componenttest" "go.opentelemetry.io/collector/config/confighttp" "go.opentelemetry.io/collector/config/configopaque" + "go.opentelemetry.io/collector/consumer/consumererror" "go.opentelemetry.io/collector/exporter/exportertest" "go.opentelemetry.io/collector/pdata/plog" @@ -69,26 +71,44 @@ func TestPushLogsPostsNDJSONBodies(t *testing.T) { assert.Equal(t, "{\"id\":\"1\",\"projectId\":\"p1\"}\n{\"id\":\"2\",\"projectId\":\"p1\"}", gotBody) } -func TestPushLogsNon2xxReturnsError(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusUnauthorized) - })) - t.Cleanup(srv.Close) - - cfg := createDefaultConfig().(*Config) - cfg.Endpoint = srv.URL - - set := exportertest.NewNopSettings(metadata.Type) - exp, err := newExporter(cfg, set) - require.NoError(t, err) - require.NoError(t, exp.start(t.Context(), componenttest.NewNopHost())) - - logs := plog.NewLogs() - logs.ResourceLogs().AppendEmpty().ScopeLogs().AppendEmpty().LogRecords().AppendEmpty().Body().SetStr(`{}`) - - err = exp.pushLogs(t.Context(), logs) - require.Error(t, err) - assert.Contains(t, err.Error(), "401") +func TestPushLogsStatusCodeErrors(t *testing.T) { + tests := []struct { + name string + status int + permanent bool + }{ + {name: "unauthorized is permanent", status: http.StatusUnauthorized, permanent: true}, + {name: "forbidden is permanent", status: http.StatusForbidden, permanent: true}, + {name: "bad request is permanent", status: http.StatusBadRequest, permanent: true}, + {name: "too many requests is retryable", status: http.StatusTooManyRequests, permanent: false}, + {name: "server error is retryable", status: http.StatusInternalServerError, permanent: false}, + {name: "bad gateway is retryable", status: http.StatusBadGateway, permanent: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(tt.status) + })) + t.Cleanup(srv.Close) + + cfg := createDefaultConfig().(*Config) + cfg.Endpoint = srv.URL + + set := exportertest.NewNopSettings(metadata.Type) + exp, err := newExporter(cfg, set) + require.NoError(t, err) + require.NoError(t, exp.start(t.Context(), componenttest.NewNopHost())) + + logs := plog.NewLogs() + logs.ResourceLogs().AppendEmpty().ScopeLogs().AppendEmpty().LogRecords().AppendEmpty().Body().SetStr(`{}`) + + err = exp.pushLogs(t.Context(), logs) + require.Error(t, err) + assert.Contains(t, err.Error(), fmt.Sprintf("%d", tt.status)) + assert.Equal(t, tt.permanent, consumererror.IsPermanent(err)) + }) + } } func TestConfigValidate(t *testing.T) { diff --git a/loadgen/cmd/otelsoak/README.md b/loadgen/cmd/otelsoak/README.md index bb1930d80..d4afed231 100644 --- a/loadgen/cmd/otelsoak/README.md +++ b/loadgen/cmd/otelsoak/README.md @@ -34,16 +34,15 @@ drains. The Vercel drain scenario sends NDJSON over HTTP instead. Use the `http` exporter ([internal/exporter/httpexporter](../../../internal/exporter/httpexporter)) with loadgen log bodies that contain drain NDJSON lines. See -[`config.vercel.example.yaml`](./config.vercel.example.yaml) and -[`testdata/vercel/`](./testdata/vercel/). +[`config.vercel.example.yaml`](./config.vercel.example.yaml). -`VERCEL_SIGNAL` selects which fixture to replay (default `logs`): +`VERCEL_SIGNAL` selects an embedded loadgenreceiver logs preset (default `logs`): -| Value | Fixture | -| --- | --- | -| `logs` | [`testdata/vercel/logs.jsonl`](./testdata/vercel/logs.jsonl) | -| `speed_insights` | [`testdata/vercel/speed_insights.jsonl`](./testdata/vercel/speed_insights.jsonl) | -| `both` | Concat of the two files at run time (`make otelsoak-run-vercel` only) | +| Value | loadgen `logs.preset` | Embedded fixture | +| --- | --- | --- | +| `logs` | `vercel_logs` | [`receiver/loadgenreceiver/testdata/vercel/logs.jsonl`](../../../receiver/loadgenreceiver/testdata/vercel/logs.jsonl) | +| `speed_insights` | `vercel_speed_insights` | [`receiver/loadgenreceiver/testdata/vercel/speed_insights.jsonl`](../../../receiver/loadgenreceiver/testdata/vercel/speed_insights.jsonl) | +| `both` | `vercel_both` | concat of the two embeds | Logs and speed-insights use the same Managed Input URL; only the NDJSON body shape changes. Leave `VERCEL_SIGNAL` unset for the default; an empty value (`VERCEL_SIGNAL=`) is not a default and will fail. @@ -64,7 +63,7 @@ ELASTIC_APM_API_KEY=some_api_key \ make otelsoak-run-vercel ``` -For both signals (temp file = `logs.jsonl` + `speed_insights.jsonl`): +For both signals: ```bash VERCEL_SIGNAL=both \ @@ -76,6 +75,6 @@ make otelsoak-run-vercel Validate the config without sending traffic: ```bash -VERCEL_SIGNAL=speed_insights ELASTIC_SERVER_URL=http://localhost:8200 ELASTIC_APM_API_KEY=foobar \ +VERCEL_SIGNAL=both ELASTIC_SERVER_URL=http://localhost:8200 ELASTIC_APM_API_KEY=foobar \ ./loadgen/cmd/otelsoak/otelsoak validate --config ./loadgen/cmd/otelsoak/config.vercel.example.yaml ``` diff --git a/loadgen/cmd/otelsoak/config.vercel.example.yaml b/loadgen/cmd/otelsoak/config.vercel.example.yaml index dab66d9ef..01aff657a 100644 --- a/loadgen/cmd/otelsoak/config.vercel.example.yaml +++ b/loadgen/cmd/otelsoak/config.vercel.example.yaml @@ -1,9 +1,9 @@ # otelsoak example: POST Vercel drain NDJSON to a Managed Inputs endpoint. # -# VERCEL_SIGNAL selects the fixture file (default: logs): -# logs | speed_insights -# For both (logs.jsonl + speed_insights.jsonl concatenated at run time), use: -# make otelsoak-run-vercel VERCEL_SIGNAL=both +# VERCEL_SIGNAL selects the embedded loadgen logs preset (default: logs): +# logs | speed_insights | both +# These map to loadgenreceiver presets vercel_logs / vercel_speed_insights / +# vercel_both (fixtures embedded in the receiver). # # Example: # make genelasticcol @@ -20,8 +20,7 @@ receivers: loadgen: concurrency: 4 logs: - # Path is relative to the process working directory (repo root when using make). - jsonl_file: loadgen/cmd/otelsoak/testdata/vercel/${env:VERCEL_SIGNAL:-logs}.jsonl + preset: vercel_${env:VERCEL_SIGNAL:-logs} processors: # vercelencodingextension reads event time (and log id / SI deviceId) from the diff --git a/receiver/loadgenreceiver/README.md b/receiver/loadgenreceiver/README.md index 4af7058ea..49f3bf10f 100644 --- a/receiver/loadgenreceiver/README.md +++ b/receiver/loadgenreceiver/README.md @@ -29,6 +29,26 @@ The receiver only rewrites timestamps to Now, and does not modify any other fiel See [./config.go](./config.go) for configurations. +### Logs presets + +In addition to the default OpenTelemetry Demo embed and optional `jsonl_file`, +logs can select an embedded preset via `logs.preset` (mutually exclusive with +`jsonl_file`): + +| Preset | Description | +| --- | --- | +| `vercel_logs` | Vercel log-drain NDJSON samples | +| `vercel_speed_insights` | Vercel Speed Insights drain samples | +| `vercel_both` | Concatenation of the two Vercel presets | + +```yaml +receivers: + loadgen: + logs: + preset: vercel_both +``` + + ## Sample configuration ```yaml diff --git a/receiver/loadgenreceiver/config.go b/receiver/loadgenreceiver/config.go index a947037bb..0bece85e9 100644 --- a/receiver/loadgenreceiver/config.go +++ b/receiver/loadgenreceiver/config.go @@ -96,6 +96,11 @@ type LogsConfig struct { JsonlFile `mapstructure:",squash"` SignalConfig `mapstructure:",squash"` + + // Preset selects embedded JSONL samples for logs. Mutually exclusive with jsonl_file. + // Supported values: vercel_logs, vercel_speed_insights, vercel_both. + // Empty uses the default OpenTelemetry Demo logs embed. + Preset string `mapstructure:"preset"` } type TracesConfig struct { @@ -169,6 +174,12 @@ func (cfg *Config) Validate() error { if err != nil { return fmt.Errorf("logs::%w", err) } + if cfg.Logs.Preset != "" && cfg.Logs.Path != "" { + return fmt.Errorf("logs::preset and logs::jsonl_file are mutually exclusive") + } + if _, err := logsPresetData(cfg.Logs.Preset); err != nil { + return fmt.Errorf("logs::%w", err) + } err = validateSignal(cfg.Metrics.SignalConfig, cfg.Metrics.JsonlFile) if err != nil { diff --git a/receiver/loadgenreceiver/logs.go b/receiver/loadgenreceiver/logs.go index 20631783c..1e42bb2e1 100644 --- a/receiver/loadgenreceiver/logs.go +++ b/receiver/loadgenreceiver/logs.go @@ -64,9 +64,10 @@ func createLogsReceiver( genConfig := config.(*Config) parser := plog.JSONUnmarshaler{} - var sampleLogs io.Reader = bytes.NewReader(demoLogs) + var sampleLogs io.Reader - if genConfig.Logs.Path != "" { + switch { + case genConfig.Logs.Path != "": var rc io.ReadCloser rc, err = openJSONLFile(genConfig.Logs.JsonlFile) if err != nil { @@ -78,6 +79,13 @@ func createLogsReceiver( } }() sampleLogs = rc + default: + var data []byte + data, err = logsPresetData(genConfig.Logs.Preset) + if err != nil { + return nil, err + } + sampleLogs = bytes.NewReader(data) } maxBufferSize := genConfig.Logs.MaxBufferSize diff --git a/receiver/loadgenreceiver/presets.go b/receiver/loadgenreceiver/presets.go new file mode 100644 index 000000000..59a6b7906 --- /dev/null +++ b/receiver/loadgenreceiver/presets.go @@ -0,0 +1,68 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package loadgenreceiver // import "github.com/elastic/opentelemetry-collector-components/receiver/loadgenreceiver" + +import ( + "bytes" + _ "embed" + "fmt" +) + +const ( + presetVercelLogs = "vercel_logs" + presetVercelSpeedInsights = "vercel_speed_insights" + presetVercelBoth = "vercel_both" +) + +//go:embed testdata/vercel/logs.jsonl +var vercelLogs []byte + +//go:embed testdata/vercel/speed_insights.jsonl +var vercelSpeedInsights []byte + +// logsPresetData returns embedded JSONL sample bytes for the given logs preset. +// An empty preset uses the default OpenTelemetry Demo logs. +func logsPresetData(preset string) ([]byte, error) { + switch preset { + case "": + return demoLogs, nil + case presetVercelLogs: + return vercelLogs, nil + case presetVercelSpeedInsights: + return vercelSpeedInsights, nil + case presetVercelBoth: + return concatJSONL(vercelLogs, vercelSpeedInsights), nil + default: + return nil, fmt.Errorf("unknown logs preset %q (supported: %q, %q, %q)", + preset, presetVercelLogs, presetVercelSpeedInsights, presetVercelBoth) + } +} + +func concatJSONL(parts ...[]byte) []byte { + var b bytes.Buffer + for _, part := range parts { + if len(part) == 0 { + continue + } + if b.Len() > 0 && !bytes.HasSuffix(b.Bytes(), []byte("\n")) { + b.WriteByte('\n') + } + b.Write(part) + } + return b.Bytes() +} diff --git a/receiver/loadgenreceiver/presets_test.go b/receiver/loadgenreceiver/presets_test.go new file mode 100644 index 000000000..ab9238bf6 --- /dev/null +++ b/receiver/loadgenreceiver/presets_test.go @@ -0,0 +1,89 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package loadgenreceiver + +import ( + "bytes" + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/consumer/consumertest" + "go.opentelemetry.io/collector/receiver" + "go.uber.org/zap" +) + +func TestLogsPresetData(t *testing.T) { + demo, err := logsPresetData("") + require.NoError(t, err) + assert.Equal(t, demoLogs, demo) + + logs, err := logsPresetData(presetVercelLogs) + require.NoError(t, err) + assert.Equal(t, vercelLogs, logs) + assert.Equal(t, 2, bytes.Count(logs, []byte("\n"))) + + si, err := logsPresetData(presetVercelSpeedInsights) + require.NoError(t, err) + assert.Equal(t, vercelSpeedInsights, si) + assert.Equal(t, 3, bytes.Count(si, []byte("\n"))) + + both, err := logsPresetData(presetVercelBoth) + require.NoError(t, err) + assert.Equal(t, 5, bytes.Count(both, []byte("\n"))) + assert.Contains(t, string(both), "soak-vercel-1") + assert.Contains(t, string(both), "vercel.speed_insights.v1") + + _, err = logsPresetData("unknown") + require.Error(t, err) +} + +func TestLogsPresetMutuallyExclusiveWithJSONLFile(t *testing.T) { + cfg := createDefaultReceiverConfig(nil, nil, nil, nil).(*Config) + cfg.Logs.Preset = presetVercelLogs + cfg.Logs.Path = "testdata/logs.jsonl" + require.EqualError(t, cfg.Validate(), "logs::preset and logs::jsonl_file are mutually exclusive") +} + +func TestLogsGenerator_VercelPreset(t *testing.T) { + doneCh := make(chan Stats) + sink := &consumertest.LogsSink{} + cfg := createDefaultReceiverConfig(doneCh, nil, nil, nil).(*Config) + cfg.Logs.Preset = presetVercelBoth + cfg.Logs.MaxReplay = 1 + cfg.Concurrency = 1 + + r, err := createLogsReceiver(context.Background(), receiver.Settings{ + ID: component.ID{}, + TelemetrySettings: component.TelemetrySettings{ + Logger: zap.NewNop(), + }, + }, cfg, sink) + require.NoError(t, err) + require.NoError(t, r.Start(context.Background(), componenttest.NewNopHost())) + t.Cleanup(func() { + assert.NoError(t, r.Shutdown(context.Background())) + }) + + stats := <-doneCh + assert.Equal(t, 5, stats.Requests) + assert.Equal(t, 5, len(sink.AllLogs())) +} diff --git a/loadgen/cmd/otelsoak/testdata/vercel/logs.jsonl b/receiver/loadgenreceiver/testdata/vercel/logs.jsonl similarity index 100% rename from loadgen/cmd/otelsoak/testdata/vercel/logs.jsonl rename to receiver/loadgenreceiver/testdata/vercel/logs.jsonl diff --git a/loadgen/cmd/otelsoak/testdata/vercel/speed_insights.jsonl b/receiver/loadgenreceiver/testdata/vercel/speed_insights.jsonl similarity index 100% rename from loadgen/cmd/otelsoak/testdata/vercel/speed_insights.jsonl rename to receiver/loadgenreceiver/testdata/vercel/speed_insights.jsonl From 80404da42308542fbd07837a8d8a3e22202d7d4e Mon Sep 17 00:00:00 2001 From: Andreas Gkizas Date: Wed, 12 Aug 2026 14:46:47 +0300 Subject: [PATCH 09/11] gotidy Signed-off-by: Andreas Gkizas --- internal/exporter/httpexporter/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/exporter/httpexporter/go.mod b/internal/exporter/httpexporter/go.mod index 71ee48737..ecf7019be 100644 --- a/internal/exporter/httpexporter/go.mod +++ b/internal/exporter/httpexporter/go.mod @@ -11,6 +11,7 @@ require ( go.opentelemetry.io/collector/config/configoptional v1.62.0 go.opentelemetry.io/collector/config/configretry v1.62.0 go.opentelemetry.io/collector/consumer v1.62.0 + go.opentelemetry.io/collector/consumer/consumererror v0.156.0 go.opentelemetry.io/collector/exporter v1.62.0 go.opentelemetry.io/collector/exporter/exporterhelper v0.156.0 go.opentelemetry.io/collector/exporter/exportertest v0.156.0 @@ -56,7 +57,6 @@ require ( go.opentelemetry.io/collector/config/configtls v1.62.0 // indirect go.opentelemetry.io/collector/confmap v1.62.0 // indirect go.opentelemetry.io/collector/confmap/xconfmap v0.156.0 // indirect - go.opentelemetry.io/collector/consumer/consumererror v0.156.0 // indirect go.opentelemetry.io/collector/consumer/consumertest v0.156.0 // indirect go.opentelemetry.io/collector/consumer/xconsumer v0.156.0 // indirect go.opentelemetry.io/collector/exporter/xexporter v0.156.0 // indirect From 77ae6c8fa2c54f78901289826a31013818889317 Mon Sep 17 00:00:00 2001 From: Andreas Gkizas Date: Wed, 12 Aug 2026 18:56:48 +0300 Subject: [PATCH 10/11] fix bytes changes Signed-off-by: Andreas Gkizas --- internal/exporter/httpexporter/exporter.go | 21 ++++++++----------- .../exporter/httpexporter/exporter_test.go | 2 +- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/internal/exporter/httpexporter/exporter.go b/internal/exporter/httpexporter/exporter.go index 683e42c4c..394867e88 100644 --- a/internal/exporter/httpexporter/exporter.go +++ b/internal/exporter/httpexporter/exporter.go @@ -23,7 +23,6 @@ import ( "fmt" "io" "net/http" - "strings" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/consumer/consumererror" @@ -62,12 +61,12 @@ func (e *httpExporter) start(ctx context.Context, host component.Host) error { } func (e *httpExporter) pushLogs(ctx context.Context, ld plog.Logs) error { - body := encodeLogBodies(ld) - if len(body) == 0 { + buf, empty := encodeLogBodies(ld) + if empty { return nil } - req, err := http.NewRequestWithContext(ctx, http.MethodPost, e.config.Endpoint, bytes.NewReader(body)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, e.config.Endpoint, bytes.NewReader(buf.Bytes())) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -100,9 +99,9 @@ func (e *httpExporter) pushLogs(ctx context.Context, ld plog.Logs) error { return consumererror.NewPermanent(err) } -// encodeLogBodies joins each log record body as a line (NDJSON-friendly). -func encodeLogBodies(ld plog.Logs) []byte { - var b strings.Builder +// encodeLogBodies joins each log record body as NDJSON (one JSON text per line, +// each terminated by '\n', including after the last record). +func encodeLogBodies(ld plog.Logs) (buf bytes.Buffer, empty bool) { rls := ld.ResourceLogs() for i := 0; i < rls.Len(); i++ { sls := rls.At(i).ScopeLogs() @@ -113,12 +112,10 @@ func encodeLogBodies(ld plog.Logs) []byte { if line == "" { continue } - if b.Len() > 0 { - b.WriteByte('\n') - } - b.WriteString(line) + buf.WriteString(line) + buf.WriteByte('\n') } } } - return []byte(b.String()) + return buf, buf.Len() == 0 } diff --git a/internal/exporter/httpexporter/exporter_test.go b/internal/exporter/httpexporter/exporter_test.go index 3ae06b4e8..4aef835f3 100644 --- a/internal/exporter/httpexporter/exporter_test.go +++ b/internal/exporter/httpexporter/exporter_test.go @@ -68,7 +68,7 @@ func TestPushLogsPostsNDJSONBodies(t *testing.T) { assert.Equal(t, http.MethodPost, gotMethod) assert.Equal(t, "application/json", gotContentType) assert.Equal(t, "ApiKey test-key", gotAuth) - assert.Equal(t, "{\"id\":\"1\",\"projectId\":\"p1\"}\n{\"id\":\"2\",\"projectId\":\"p1\"}", gotBody) + assert.Equal(t, "{\"id\":\"1\",\"projectId\":\"p1\"}\n{\"id\":\"2\",\"projectId\":\"p1\"}\n", gotBody) } func TestPushLogsStatusCodeErrors(t *testing.T) { From ddccaee1902c7de9fdf00a123df71d307006f93c Mon Sep 17 00:00:00 2001 From: Andreas Gkizas Date: Thu, 13 Aug 2026 18:03:01 +0300 Subject: [PATCH 11/11] revert make changes Signed-off-by: Andreas Gkizas --- Makefile | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index b6614b581..314528671 100644 --- a/Makefile +++ b/Makefile @@ -14,8 +14,7 @@ INTEGRATION_MODS := $(shell $(FIND_INTEGRATION_TEST_MODS) | xargs $(TO_MOD_DIR) GROUP ?= all FOR_GROUP_TARGET=for-$(GROUP)-target -# Modules such as processor/elasticapmprocessor require go >= 1.26. -GOTOOLCHAIN ?= go1.26.5+auto +GOTOOLCHAIN ?= go1.25.7+auto .DEFAULT_GOAL := all @@ -93,16 +92,7 @@ remove-toolchain: # Build a collector based on the Elastic components (generate Elastic collector) .PHONY: genelasticcol genelasticcol: - @set +e; \ - GOTOOLCHAIN=${GOTOOLCHAIN} GOOS=${TARGET_GOOS} GOARCH=${TARGET_GOARCH} $(BUILDER) \ - --config ./distributions/elastic-components/manifest.yaml --skip-compilation=true; \ - status=$$?; \ - set -e; \ - if [ ! -f ./_build/go.mod ]; then exit $${status:-1}; fi; \ - cd ./_build && GOTOOLCHAIN=${GOTOOLCHAIN} $(GOCMD) mod tidy; \ - cd ..; \ - GOTOOLCHAIN=${GOTOOLCHAIN} GOOS=${TARGET_GOOS} GOARCH=${TARGET_GOARCH} $(BUILDER) \ - --config ./distributions/elastic-components/manifest.yaml --skip-get-modules=true + GOTOOLCHAIN=${GOTOOLCHAIN} GOOS=${TARGET_GOOS} GOARCH=${TARGET_GOARCH} $(BUILDER) --config ./distributions/elastic-components/manifest.yaml # Validate that the Elastic components collector can run with the example configuration. .PHONY: elasticcol-validate