diff --git a/Makefile b/Makefile index e40215243..314528671 100644 --- a/Makefile +++ b/Makefile @@ -119,12 +119,19 @@ 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). +.PHONY: otelsoak-run-vercel +otelsoak-run-vercel: genelasticcol + ./loadgen/cmd/otelsoak/otelsoak --config ./loadgen/cmd/otelsoak/config.vercel.example.yaml $(ARGS) + # 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 46566008f..f6ce8bcb1 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/internal/exporter/httpexporter/Makefile b/internal/exporter/httpexporter/Makefile new file mode 100644 index 000000000..bdd863a20 --- /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..5731e1c9a --- /dev/null +++ b/internal/exporter/httpexporter/README.md @@ -0,0 +1,48 @@ +# 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) + +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/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..394867e88 --- /dev/null +++ b/internal/exporter/httpexporter/exporter.go @@ -0,0 +1,121 @@ +// 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" + + "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" +) + +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 { + buf, empty := encodeLogBodies(ld) + if empty { + return nil + } + + 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) + } + + 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 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 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 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() + 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 + } + buf.WriteString(line) + buf.WriteByte('\n') + } + } + } + return buf, buf.Len() == 0 +} diff --git a/internal/exporter/httpexporter/exporter_test.go b/internal/exporter/httpexporter/exporter_test.go new file mode 100644 index 000000000..4aef835f3 --- /dev/null +++ b/internal/exporter/httpexporter/exporter_test.go @@ -0,0 +1,134 @@ +// 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 ( + "fmt" + "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/consumer/consumererror" + "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\"}\n", gotBody) +} + +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) { + 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..ecf7019be --- /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/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 + 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/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.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 new file mode 100644 index 000000000..a95d379a8 --- /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.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= +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/.chloggen/vercel_drain_otelsoak.yaml b/loadgen/cmd/otelbench/.chloggen/vercel_drain_otelsoak.yaml new file mode 100644 index 000000000..4c5e54a8b --- /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: [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. +# 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. 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 9d0afa46c..079461d78 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..d4afed231 100644 --- a/loadgen/cmd/otelsoak/README.md +++ b/loadgen/cmd/otelsoak/README.md @@ -25,3 +25,56 @@ 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). + +`VERCEL_SIGNAL` selects an embedded loadgenreceiver logs preset (default `logs`): + +| 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. + +`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 +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: + +```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=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 new file mode 100644 index 000000000..01aff657a --- /dev/null +++ b/loadgen/cmd/otelsoak/config.vercel.example.yaml @@ -0,0 +1,73 @@ +# otelsoak example: POST Vercel drain NDJSON to a Managed Inputs endpoint. +# +# 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 +# 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: + preset: vercel_${env:VERCEL_SIGNAL:-logs} + +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 + 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: [transform/vercel_refresh, ratelimit] + exporters: [http, debug] + telemetry: + metrics: + readers: + - pull: + exporter: + prometheus: + host: '127.0.0.1' + port: 8888 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/receiver/loadgenreceiver/testdata/vercel/logs.jsonl b/receiver/loadgenreceiver/testdata/vercel/logs.jsonl new file mode 100644 index 000000000..b451152ee --- /dev/null +++ b/receiver/loadgenreceiver/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/receiver/loadgenreceiver/testdata/vercel/speed_insights.jsonl b/receiver/loadgenreceiver/testdata/vercel/speed_insights.jsonl new file mode 100644 index 000000000..5693a9708 --- /dev/null +++ b/receiver/loadgenreceiver/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 95d8e3913..a81387a35 100644 --- a/versions.yaml +++ b/versions.yaml @@ -38,5 +38,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