diff --git a/Makefile b/Makefile index ae1aacc..8690c6f 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ MODULE_DEPS=$(sort $(shell go list -deps -tags=darwin,linux,windows -f "{{with . all: test fmt: tools/go.mod - @go run -modfile=tools/go.mod github.com/elastic/go-licenser -license=Elasticv2 . + @go run -modfile=tools/go.mod github.com/elastic/go-licenser -license=Elasticv2 -exclude internal/telemetrygen . @go run -modfile=tools/go.mod golang.org/x/tools/cmd/goimports -local github.com/elastic/ -w . lint: tools/go.mod diff --git a/README.md b/README.md index e53c5b3..b5c0825 100644 --- a/README.md +++ b/README.md @@ -35,3 +35,7 @@ Note that the managed service will use API key based auth and one soaktest can t ```sh ./apmsoak run --scenario=fairness --api-keys="project_1:key-to-project_1,project_2:key-to-project_2" ``` + +## internal/telemetrygen + +This package is from https://github.com/open-telemetry/opentelemetry-collector-contrib but slightly modified so that it can be used within apm-perf. diff --git a/cmd/apmbench/bench.go b/cmd/apmbench/bench.go index af0423c..e1755e5 100644 --- a/cmd/apmbench/bench.go +++ b/cmd/apmbench/bench.go @@ -6,27 +6,20 @@ package main import ( "context" - "crypto/tls" "fmt" "net/url" "strings" "testing" "time" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" - sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.elastic.co/apm/v2" + "go.elastic.co/apm/v2/transport" "go.uber.org/zap" "golang.org/x/time/rate" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" "github.com/elastic/apm-perf/internal/loadgen" loadgencfg "github.com/elastic/apm-perf/internal/loadgen/config" "github.com/elastic/apm-perf/internal/loadgen/eventhandler" - - "go.elastic.co/apm/v2" - "go.elastic.co/apm/v2/transport" ) func Benchmark1000Transactions(b *testing.B, l *rate.Limiter) { @@ -50,27 +43,6 @@ func Benchmark1000Transactions(b *testing.B, l *rate.Limiter) { }) } -func BenchmarkOTLPTraces(b *testing.B, l *rate.Limiter) { - b.RunParallel(func(pb *testing.PB) { - exporter := newOTLPExporter(b) - tracerProvider := sdktrace.NewTracerProvider( - sdktrace.WithSampler(sdktrace.AlwaysSample()), - sdktrace.WithBatcher(exporter, sdktrace.WithBlocking()), - ) - tracer := tracerProvider.Tracer("tracer") - for pb.Next() { - if err := l.Wait(context.Background()); err != nil { - // panicing ensures that the error is reported - // see: https://github.com/golang/go/issues/32066 - panic(err) - } - _, span := tracer.Start(context.Background(), "name") - span.End() - } - tracerProvider.ForceFlush(context.Background()) - }) -} - func BenchmarkAgentAll(b *testing.B, l *rate.Limiter) { benchmarkAgent(b, l, `apm-*.ndjson`) } @@ -147,56 +119,6 @@ func newTracer(tb testing.TB) *apm.Tracer { return tracer } -func newOTLPExporter(tb testing.TB) *otlptrace.Exporter { - serverURL := loadgencfg.Config.ServerURL - secretToken := loadgencfg.Config.SecretToken - apiKey := loadgencfg.Config.APIKey - endpoint := serverURL.Host - if serverURL.Port() == "" { - switch serverURL.Scheme { - case "http": - endpoint += ":80" - case "https": - endpoint += ":443" - } - } - - headers := make(map[string]string) - for k, v := range loadgencfg.Config.Headers { - headers[k] = v - } - if secretToken != "" || apiKey != "" { - if apiKey != "" { - // higher priority to APIKey auth - headers["Authorization"] = "ApiKey " + apiKey - } else { - headers["Authorization"] = "Bearer " + secretToken - } - } - - opts := []otlptracegrpc.Option{ - otlptracegrpc.WithEndpoint(endpoint), - otlptracegrpc.WithDialOption(grpc.WithBlock()), - otlptracegrpc.WithHeaders(headers), - } - if serverURL.Scheme == "http" { - opts = append(opts, otlptracegrpc.WithInsecure()) - } else { - tlsCredentials := credentials.NewTLS(&tls.Config{ - InsecureSkipVerify: true, - }) - opts = append(opts, otlptracegrpc.WithTLSCredentials(tlsCredentials)) - } - exporter, err := otlptracegrpc.New(context.Background(), opts...) - if err != nil { - // panicing ensures that the error is reported - // see: https://github.com/golang/go/issues/32066 - panic(err) - } - tb.Cleanup(func() { exporter.Shutdown(context.Background()) }) - return exporter -} - func newEventHandler(tb testing.TB, p string, l *rate.Limiter) *eventhandler.Handler { protocol := "apm/http" if strings.HasPrefix(p, "otlp-") { diff --git a/cmd/apmbench/bench_otlp.go b/cmd/apmbench/bench_otlp.go new file mode 100644 index 0000000..6325442 --- /dev/null +++ b/cmd/apmbench/bench_otlp.go @@ -0,0 +1,108 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package main + +import ( + "runtime" + "testing" + "time" + + "go.uber.org/zap" + "golang.org/x/time/rate" + + loadgencfg "github.com/elastic/apm-perf/internal/loadgen/config" + "github.com/elastic/apm-perf/internal/telemetrygen/common" + "github.com/elastic/apm-perf/internal/telemetrygen/logs" + "github.com/elastic/apm-perf/internal/telemetrygen/metrics" + "github.com/elastic/apm-perf/internal/telemetrygen/traces" +) + +func commonConfigWithHTTPPath(httpPath string) common.Config { + insecure := !loadgencfg.Config.Secure + + serverURL := loadgencfg.Config.ServerURL + endpoint := serverURL.Host + if serverURL.Port() == "" { + switch serverURL.Scheme { + case "http": + endpoint += ":80" + insecure = true + case "https": + endpoint += ":443" + } + } + + secretToken := loadgencfg.Config.SecretToken + apiKey := loadgencfg.Config.APIKey + headers := make(map[string]string) + for k, v := range loadgencfg.Config.Headers { + headers[k] = v + } + if secretToken != "" || apiKey != "" { + if apiKey != "" { + // higher priority to APIKey auth + headers["Authorization"] = "ApiKey " + apiKey + } else { + headers["Authorization"] = "Bearer " + secretToken + } + } + + return common.Config{ + WorkerCount: runtime.GOMAXPROCS(0), + Rate: 0, + TotalDuration: 0, + ReportingInterval: 0, + SkipSettingGRPCLogger: true, + CustomEndpoint: endpoint, + Insecure: insecure, + UseHTTP: false, + HTTPPath: httpPath, + Headers: headers, + ResourceAttributes: nil, + TelemetryAttributes: nil, + CaFile: "", + ClientAuth: common.ClientAuth{}, + Logger: zap.NewNop(), + } +} + +func BenchmarkOTLPLogs(b *testing.B, l *rate.Limiter) { + config := logs.Config{ + Config: commonConfigWithHTTPPath("/v1/logs"), + NumLogs: b.N, + Body: "test", + } + if err := logs.Start(&config); err != nil { + b.Fatal(err) + } +} + +func BenchmarkOTLPTraces(b *testing.B, l *rate.Limiter) { + config := traces.Config{ + Config: commonConfigWithHTTPPath("/v1/traces"), + NumTraces: b.N, + NumChildSpans: 1, + PropagateContext: false, + ServiceName: "foo", + StatusCode: "0", + Batch: true, + LoadSize: 0, + SpanDuration: 123 * time.Microsecond, + } + if err := traces.Start(&config); err != nil { + b.Fatal(err) + } +} + +func BenchmarkOTLPMetrics(b *testing.B, l *rate.Limiter) { + config := metrics.Config{ + Config: commonConfigWithHTTPPath("/v1/metrics"), + NumMetrics: b.N, + MetricType: "Sum", + } + if err := metrics.Start(&config); err != nil { + b.Fatal(err) + } +} diff --git a/cmd/apmbench/main.go b/cmd/apmbench/main.go index 2befeb3..19a0831 100644 --- a/cmd/apmbench/main.go +++ b/cmd/apmbench/main.go @@ -45,13 +45,15 @@ func main() { extraMetrics, resetStoreFunc, Benchmark1000Transactions, - BenchmarkOTLPTraces, BenchmarkAgentAll, BenchmarkAgentGo, BenchmarkAgentNodeJS, BenchmarkAgentPython, BenchmarkAgentRuby, Benchmark10000AggregationGroups, + BenchmarkOTLPTraces, + BenchmarkOTLPLogs, + BenchmarkOTLPMetrics, ); err != nil { logger.Fatal("failed to run benchmarks", zap.Error(err)) } diff --git a/go.mod b/go.mod index e2f4a6f..02354e0 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/elastic/apm-perf go 1.21 require ( + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/klauspost/compress v1.17.7 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 @@ -12,9 +13,18 @@ require ( go.elastic.co/apm/v2 v2.5.0 go.elastic.co/ecszap v1.0.2 go.elastic.co/fastjson v1.3.0 + go.opentelemetry.io/collector/pdata v1.4.0 + go.opentelemetry.io/collector/semconv v0.97.0 + go.opentelemetry.io/otel v1.24.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.24.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.24.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 go.opentelemetry.io/otel/sdk v1.24.0 + go.opentelemetry.io/otel/sdk/metric v1.24.0 + go.opentelemetry.io/otel/trace v1.24.0 + go.uber.org/goleak v1.3.0 go.uber.org/zap v1.27.0 golang.org/x/sync v0.6.0 golang.org/x/time v0.5.0 @@ -30,18 +40,20 @@ require ( github.com/elastic/go-windows v1.0.1 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect - go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 // indirect go.opentelemetry.io/proto/otlp v1.1.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/net v0.20.0 // indirect @@ -50,6 +62,6 @@ require ( golang.org/x/tools v0.14.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect - google.golang.org/protobuf v1.32.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect howett.net/plist v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index abd7f2f..afa6b9f 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= @@ -5,6 +7,9 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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= @@ -21,19 +26,35 @@ github.com/elastic/go-sysinfo v1.11.1 h1:g9mwl05njS4r69TisC+vwHWTSKywZFYYUu3so3T github.com/elastic/go-sysinfo v1.11.1/go.mod h1:6KQb31j0QeWBDF88jIdWSxE8cwoOB9tO4Y4osN7Q70E= github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0= github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -41,8 +62,13 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4= github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak= +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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -50,28 +76,39 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -88,6 +125,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.elastic.co/apm/v2 v2.5.0 h1:UYqdu/bjcubcP9BIy5+os2ExRzw03yOQFG+sRGGhVlQ= go.elastic.co/apm/v2 v2.5.0/go.mod h1:+CiBUdrrAGnGCL9TNx7tQz3BrfYV23L8Ljvotoc87so= @@ -95,52 +134,87 @@ go.elastic.co/ecszap v1.0.2 h1:iW5OGx8IiokiUzx/shD4AJCPFMC9uUtr7ycaiEIU++I= go.elastic.co/ecszap v1.0.2/go.mod h1:dJkSlK3BTiwG/qXhCwe50Mz/jwu854vSip8sIeQhNZg= go.elastic.co/fastjson v1.3.0 h1:hJO3OsYIhiqiT4Fgu0ZxAECnKASbwgiS+LMW5oCopKs= go.elastic.co/fastjson v1.3.0/go.mod h1:K9vDh7O0ODsVKV2B5e2XYLY277QZaCbB3tS1SnARvko= +go.opentelemetry.io/collector/pdata v1.4.0 h1:cA6Pr7Z2V7mE+i7FmYpavX7nefzd6H4CICgW0T9aJX0= +go.opentelemetry.io/collector/pdata v1.4.0/go.mod h1:0Ttp4wQinhV5oJTd9MjyvUegmZBO9O0nrlh/+EDLw+Q= +go.opentelemetry.io/collector/semconv v0.97.0 h1:iF3nTfThbiOwz7o5Pocn0dDnDoffd18ijDuf6Mwzi1s= +go.opentelemetry.io/collector/semconv v0.97.0/go.mod h1:8ElcRZ8Cdw5JnvhTOQOdYizkJaQ10Z2fS+R6djOnj6A= go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.24.0 h1:f2jriWfOdldanBwS9jNBdeOKAQN7b4ugAMaNu1/1k9g= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.24.0/go.mod h1:B+bcQI1yTY+N0vqMpoZbEN7+XU4tNM0DmUiOwebFJWI= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.24.0 h1:mM8nKi6/iFQ0iqst80wDHU2ge198Ye/TfN0WBS5U24Y= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.24.0/go.mod h1:0PrIIzDteLSmNyxqcGYRL4mDIo8OTuBAOI/Bn1URxac= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= +go.opentelemetry.io/otel/sdk/metric v1.24.0 h1:yyMQrPzF+k88/DbH7o4FMAs80puqd+9osbiBrJrz/w8= +go.opentelemetry.io/otel/sdk/metric v1.24.0/go.mod h1:I6Y5FjH6rvEnTTAYQz3Mmv2kl6Ek5IIrmwTLqMrrOE0= go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 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.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 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.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -151,8 +225,14 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= @@ -160,25 +240,40 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1:Lj5rbfG876hIAYFjqiJnPHfhXbv+nzTWfm04Fg/XSVU= google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM= google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= diff --git a/internal/telemetrygen/APACHE-LICENSE-2.0.txt b/internal/telemetrygen/APACHE-LICENSE-2.0.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/internal/telemetrygen/APACHE-LICENSE-2.0.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. diff --git a/internal/telemetrygen/README.md b/internal/telemetrygen/README.md new file mode 100644 index 0000000..6020c05 --- /dev/null +++ b/internal/telemetrygen/README.md @@ -0,0 +1,5 @@ +# Copy of Telemetry generator for OpenTelemetry + +This directory contains files that are from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/cmd/telemetrygen which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. The full text of license is stored in `APACHE-LICENSE-2.0.txt`. + +The files contain modifications and are now relicensed under Elastic License 2.0. Each file contains a description of the modifications made to that file. diff --git a/internal/telemetrygen/common/config.go b/internal/telemetrygen/common/config.go new file mode 100644 index 0000000..0dcbe3b --- /dev/null +++ b/internal/telemetrygen/common/config.go @@ -0,0 +1,157 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/common/config.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// The original implementation was modified. Config struct is changed to accept a logger. + +package common + +import ( + "fmt" + "strings" + "time" + + "github.com/spf13/pflag" + "go.opentelemetry.io/otel/attribute" + "go.uber.org/zap" +) + +var ( + errFormatOTLPAttributes = fmt.Errorf("value should be of the format key=\"value\"") + errDoubleQuotesOTLPAttributes = fmt.Errorf("value should be a string wrapped in double quotes") +) + +const ( + defaultGRPCEndpoint = "localhost:4317" + defaultHTTPEndpoint = "localhost:4318" +) + +type KeyValue map[string]string + +var _ pflag.Value = (*KeyValue)(nil) + +func (v *KeyValue) String() string { + return "" +} + +func (v *KeyValue) Set(s string) error { + kv := strings.SplitN(s, "=", 2) + if len(kv) != 2 { + return errFormatOTLPAttributes + } + val := kv[1] + if len(val) < 2 || !strings.HasPrefix(val, "\"") || !strings.HasSuffix(val, "\"") { + return errDoubleQuotesOTLPAttributes + } + + (*v)[kv[0]] = val[1 : len(val)-1] + return nil +} + +func (v *KeyValue) Type() string { + return "map[string]string" +} + +type Config struct { + WorkerCount int + Rate int64 + TotalDuration time.Duration + ReportingInterval time.Duration + SkipSettingGRPCLogger bool + + // OTLP config + CustomEndpoint string + Insecure bool + UseHTTP bool + HTTPPath string + Headers KeyValue + ResourceAttributes KeyValue + TelemetryAttributes KeyValue + + // OTLP TLS configuration + CaFile string + + // OTLP mTLS configuration + ClientAuth ClientAuth + + Logger *zap.Logger +} + +type ClientAuth struct { + Enabled bool + ClientCertFile string + ClientKeyFile string +} + +// Endpoint returns the appropriate endpoint URL based on the selected communication mode (gRPC or HTTP) +// or custom endpoint provided in the configuration. +func (c *Config) Endpoint() string { + if c.CustomEndpoint != "" { + return c.CustomEndpoint + } + if c.UseHTTP { + return defaultHTTPEndpoint + } + return defaultGRPCEndpoint +} + +func (c *Config) GetAttributes() []attribute.KeyValue { + var attributes []attribute.KeyValue + + if len(c.ResourceAttributes) > 0 { + for k, v := range c.ResourceAttributes { + attributes = append(attributes, attribute.String(k, v)) + } + } + return attributes +} + +func (c *Config) GetTelemetryAttributes() []attribute.KeyValue { + var attributes []attribute.KeyValue + + if len(c.TelemetryAttributes) > 0 { + for k, v := range c.TelemetryAttributes { + attributes = append(attributes, attribute.String(k, v)) + } + } + return attributes +} + +// CommonFlags registers common config flags. +func (c *Config) CommonFlags(fs *pflag.FlagSet) { + fs.IntVar(&c.WorkerCount, "workers", 1, "Number of workers (goroutines) to run") + fs.Int64Var(&c.Rate, "rate", 0, "Approximately how many metrics per second each worker should generate. Zero means no throttling.") + fs.DurationVar(&c.TotalDuration, "duration", 0, "For how long to run the test") + fs.DurationVar(&c.ReportingInterval, "interval", 1*time.Second, "Reporting interval") + + fs.StringVar(&c.CustomEndpoint, "otlp-endpoint", "", "Destination endpoint for exporting logs, metrics and traces") + fs.BoolVar(&c.Insecure, "otlp-insecure", false, "Whether to enable client transport security for the exporter's grpc or http connection") + fs.BoolVar(&c.UseHTTP, "otlp-http", false, "Whether to use HTTP exporter rather than a gRPC one") + + // custom headers + c.Headers = make(map[string]string) + fs.Var(&c.Headers, "otlp-header", "Custom header to be passed along with each OTLP request. The value is expected in the format key=\"value\"."+ + "Note you may need to escape the quotes when using the tool from a cli."+ + "Flag may be repeated to set multiple headers (e.g -otlp-header key1=value1 -otlp-header key2=value2)") + + // custom resource attributes + c.ResourceAttributes = make(map[string]string) + fs.Var(&c.ResourceAttributes, "otlp-attributes", "Custom resource attributes to use. The value is expected in the format key=\"value\"."+ + "Note you may need to escape the quotes when using the tool from a cli."+ + "Flag may be repeated to set multiple attributes (e.g --otlp-attributes key1=\"value1\" --otlp-attributes key2=\"value2\")") + + c.TelemetryAttributes = make(map[string]string) + fs.Var(&c.TelemetryAttributes, "telemetry-attributes", "Custom telemetry attributes to use. The value is expected in the format \"key=\\\"value\\\"\". "+ + "Flag may be repeated to set multiple attributes (e.g --telemetry-attributes \"key1=\\\"value1\\\"\" --telemetry-attributes \"key2=\\\"value2\\\"\")") + + // TLS CA configuration + fs.StringVar(&c.CaFile, "ca-cert", "", "Trusted Certificate Authority to verify server certificate") + + // mTLS configuration + fs.BoolVar(&c.ClientAuth.Enabled, "mtls", false, "Whether to require client authentication for mTLS") + fs.StringVar(&c.ClientAuth.ClientCertFile, "client-cert", "", "Client certificate file") + fs.StringVar(&c.ClientAuth.ClientKeyFile, "client-key", "", "Client private key file") +} diff --git a/internal/telemetrygen/common/config_test.go b/internal/telemetrygen/common/config_test.go new file mode 100644 index 0000000..cdd54d3 --- /dev/null +++ b/internal/telemetrygen/common/config_test.go @@ -0,0 +1,108 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/common/config_test.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This file does not contain functional modifications. + +package common + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestKeyValueSet(t *testing.T) { + tests := []struct { + flag string + expected KeyValue + err error + }{ + { + flag: "key=\"value\"", + expected: KeyValue(map[string]string{"key": "value"}), + }, + { + flag: "key=\"\"", + expected: KeyValue(map[string]string{"key": ""}), + }, + { + flag: "key=\"", + err: errDoubleQuotesOTLPAttributes, + }, + { + flag: "key=value", + err: errDoubleQuotesOTLPAttributes, + }, + { + flag: "key", + err: errFormatOTLPAttributes, + }, + } + + for _, tt := range tests { + t.Run(tt.flag, func(t *testing.T) { + kv := KeyValue(make(map[string]string)) + err := kv.Set(tt.flag) + if err != nil || tt.err != nil { + assert.Equal(t, err, tt.err) + } else { + assert.Equal(t, tt.expected, kv) + } + }) + } +} + +func TestEndpoint(t *testing.T) { + tests := []struct { + name string + endpoint string + http bool + expected string + }{ + { + "default-no-http", + "", + false, + defaultGRPCEndpoint, + }, + { + "default-with-http", + "", + true, + defaultHTTPEndpoint, + }, + { + "custom-endpoint-no-http", + "collector:4317", + false, + "collector:4317", + }, + { + "custom-endpoint-with-http", + "collector:4317", + true, + "collector:4317", + }, + { + "wrong-custom-endpoint-with-http", + defaultGRPCEndpoint, + true, + defaultGRPCEndpoint, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cfg := &Config{ + CustomEndpoint: tc.endpoint, + UseHTTP: tc.http, + } + + assert.Equal(t, tc.expected, cfg.Endpoint()) + }) + } +} diff --git a/internal/telemetrygen/common/doc.go b/internal/telemetrygen/common/doc.go new file mode 100644 index 0000000..672283b --- /dev/null +++ b/internal/telemetrygen/common/doc.go @@ -0,0 +1,6 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// Package common is from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/common/ +package common diff --git a/internal/telemetrygen/common/log.go b/internal/telemetrygen/common/log.go new file mode 100644 index 0000000..308027b --- /dev/null +++ b/internal/telemetrygen/common/log.go @@ -0,0 +1,31 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/common/log.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This file does not contain functional modifications. + +package common + +import ( + "fmt" + + grpcZap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap" + "go.uber.org/zap" +) + +// CreateLogger creates a logger for use by telemetrygen +func CreateLogger(skipSettingGRPCLogger bool) (*zap.Logger, error) { + logger, err := zap.NewDevelopment() + if err != nil { + return nil, fmt.Errorf("failed to obtain logger: %w", err) + } + if !skipSettingGRPCLogger { + grpcZap.ReplaceGrpcLoggerV2WithVerbosity(logger.WithOptions( + zap.AddCallerSkip(3), + ), 1) // set to warn verbosity to avoid copious logging from grpc framework + } + return logger, err +} diff --git a/internal/telemetrygen/common/package_test.go b/internal/telemetrygen/common/package_test.go new file mode 100644 index 0000000..9ae4b1a --- /dev/null +++ b/internal/telemetrygen/common/package_test.go @@ -0,0 +1,20 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/common/package_test.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This file does not contain functional modifications. + +package common + +import ( + "testing" + + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} diff --git a/internal/telemetrygen/common/tls_utils.go b/internal/telemetrygen/common/tls_utils.go new file mode 100644 index 0000000..a278dab --- /dev/null +++ b/internal/telemetrygen/common/tls_utils.go @@ -0,0 +1,83 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/common/tls_utils.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This file does not contain functional modifications. + +package common + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "os" + + "google.golang.org/grpc/credentials" +) + +// caPool loads CA certificate from a file and returns a CertPool. +// The certPool is used to set RootCAs in certificate verification. +func caPool(caFile string) (*x509.CertPool, error) { + pool := x509.NewCertPool() + if caFile != "" { + data, err := os.ReadFile(caFile) + if err != nil { + return nil, err + } + if !pool.AppendCertsFromPEM(data) { + return nil, errors.New("failed to add CA certificate to root CA pool") + } + } + return pool, nil +} + +func GetTLSCredentialsForGRPCExporter(caFile string, cAuth ClientAuth) (credentials.TransportCredentials, error) { + + pool, err := caPool(caFile) + if err != nil { + return nil, err + } + + creds := credentials.NewTLS(&tls.Config{ + RootCAs: pool, + }) + + // Configuration for mTLS + if cAuth.Enabled { + keypair, err := tls.LoadX509KeyPair(cAuth.ClientCertFile, cAuth.ClientKeyFile) + if err != nil { + return nil, err + } + creds = credentials.NewTLS(&tls.Config{ + RootCAs: pool, + Certificates: []tls.Certificate{keypair}, + }) + } + + return creds, nil +} + +func GetTLSCredentialsForHTTPExporter(caFile string, cAuth ClientAuth) (*tls.Config, error) { + pool, err := caPool(caFile) + if err != nil { + return nil, err + } + + tlsCfg := tls.Config{ + RootCAs: pool, + } + + // Configuration for mTLS + if cAuth.Enabled { + keypair, err := tls.LoadX509KeyPair(cAuth.ClientCertFile, cAuth.ClientKeyFile) + if err != nil { + return nil, err + } + tlsCfg.ClientAuth = tls.RequireAndVerifyClientCert + tlsCfg.Certificates = []tls.Certificate{keypair} + } + return &tlsCfg, nil +} diff --git a/internal/telemetrygen/logs/config.go b/internal/telemetrygen/logs/config.go new file mode 100644 index 0000000..899d6e4 --- /dev/null +++ b/internal/telemetrygen/logs/config.go @@ -0,0 +1,33 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/logs/config.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This file does not contain functional modifications. + +package logs + +import ( + "github.com/spf13/pflag" + + "github.com/elastic/apm-perf/internal/telemetrygen/common" +) + +// Config describes the test scenario. +type Config struct { + common.Config + NumLogs int + Body string +} + +// Flags registers config flags. +func (c *Config) Flags(fs *pflag.FlagSet) { + c.CommonFlags(fs) + + fs.StringVar(&c.HTTPPath, "otlp-http-url-path", "/v1/logs", "Which URL path to write to") + + fs.IntVar(&c.NumLogs, "logs", 1, "Number of logs to generate in each worker (ignored if duration is provided)") + fs.StringVar(&c.Body, "body", "the message", "Body of the log") +} diff --git a/internal/telemetrygen/logs/doc.go b/internal/telemetrygen/logs/doc.go new file mode 100644 index 0000000..4a4e01d --- /dev/null +++ b/internal/telemetrygen/logs/doc.go @@ -0,0 +1,6 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// Package logs is from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/logs +package logs diff --git a/internal/telemetrygen/logs/exporter.go b/internal/telemetrygen/logs/exporter.go new file mode 100644 index 0000000..b86a2d2 --- /dev/null +++ b/internal/telemetrygen/logs/exporter.go @@ -0,0 +1,123 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/logs/exporter.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This file does not contain functional modifications. + +package logs + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/pdata/plog/plogotlp" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/elastic/apm-perf/internal/telemetrygen/common" +) + +type exporter interface { + export(plog.Logs) error +} + +func newExporter(ctx context.Context, cfg *Config) (exporter, error) { + + // Exporter with HTTP + if cfg.UseHTTP { + if cfg.Insecure { + return &httpClientExporter{ + client: http.DefaultClient, + cfg: cfg, + }, nil + } + creds, err := common.GetTLSCredentialsForHTTPExporter(cfg.CaFile, cfg.ClientAuth) + if err != nil { + return nil, fmt.Errorf("failed to get TLS credentials: %w", err) + } + return &httpClientExporter{ + client: &http.Client{Transport: &http.Transport{TLSClientConfig: creds}}, + cfg: cfg, + }, nil + } + + // Exporter with GRPC + var err error + var clientConn *grpc.ClientConn + if cfg.Insecure { + clientConn, err = grpc.DialContext(ctx, cfg.Endpoint(), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, err + } + } else { + creds, err := common.GetTLSCredentialsForGRPCExporter(cfg.CaFile, cfg.ClientAuth) + if err != nil { + return nil, fmt.Errorf("failed to get TLS credentials: %w", err) + } + clientConn, err = grpc.DialContext(ctx, cfg.Endpoint(), grpc.WithTransportCredentials(creds)) + if err != nil { + return nil, err + } + } + return &gRPCClientExporter{client: plogotlp.NewGRPCClient(clientConn)}, nil +} + +type gRPCClientExporter struct { + client plogotlp.GRPCClient +} + +func (e *gRPCClientExporter) export(logs plog.Logs) error { + req := plogotlp.NewExportRequestFromLogs(logs) + if _, err := e.client.Export(context.Background(), req); err != nil { + return err + } + return nil +} + +type httpClientExporter struct { + client *http.Client + cfg *Config +} + +func (e *httpClientExporter) export(logs plog.Logs) error { + scheme := "https" + if e.cfg.Insecure { + scheme = "http" + } + path := e.cfg.HTTPPath + url := fmt.Sprintf("%s://%s%s", scheme, e.cfg.Endpoint(), path) + + req := plogotlp.NewExportRequestFromLogs(logs) + body, err := req.MarshalProto() + if err != nil { + return fmt.Errorf("failed to marshal logs to protobuf: %w", err) + } + + httpReq, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("failed to create logs HTTP request: %w", err) + } + for k, v := range e.cfg.Headers { + httpReq.Header.Set(k, v) + } + httpReq.Header.Set("Content-Type", "application/x-protobuf") + resp, err := e.client.Do(httpReq) + if err != nil { + return fmt.Errorf("failed to execute logs HTTP request: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + var respData bytes.Buffer + _, _ = io.Copy(&respData, resp.Body) + return fmt.Errorf("log request failed with status %s (%s)", resp.Status, respData.String()) + } + + return nil +} diff --git a/internal/telemetrygen/logs/logs.go b/internal/telemetrygen/logs/logs.go new file mode 100644 index 0000000..182c1f5 --- /dev/null +++ b/internal/telemetrygen/logs/logs.go @@ -0,0 +1,95 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/logs/logs.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This original implementation is modified: +// - Start function now only creates a logger when it is not already configured in cfg + +package logs + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "time" + + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" + "go.uber.org/zap" + "golang.org/x/time/rate" + + "github.com/elastic/apm-perf/internal/telemetrygen/common" +) + +// Start starts the log telemetry generator +func Start(cfg *Config) error { + logger := cfg.Logger + if logger == nil { + newLogger, err := common.CreateLogger(cfg.SkipSettingGRPCLogger) + if err != nil { + return err + } + logger = newLogger + } + + e, err := newExporter(context.Background(), cfg) + if err != nil { + return err + } + + if err = Run(cfg, e, logger); err != nil { + logger.Error("failed to stop the exporter", zap.Error(err)) + return err + } + + return nil +} + +// Run executes the test scenario. +func Run(c *Config, exp exporter, logger *zap.Logger) error { + if c.TotalDuration > 0 { + c.NumLogs = 0 + } else if c.NumLogs <= 0 { + return fmt.Errorf("either `logs` or `duration` must be greater than 0") + } + + limit := rate.Limit(c.Rate) + if c.Rate == 0 { + limit = rate.Inf + logger.Info("generation of logs isn't being throttled") + } else { + logger.Info("generation of logs is limited", zap.Float64("per-second", float64(limit))) + } + + wg := sync.WaitGroup{} + res := resource.NewWithAttributes(semconv.SchemaURL, c.GetAttributes()...) + + running := &atomic.Bool{} + running.Store(true) + + for i := 0; i < c.WorkerCount; i++ { + wg.Add(1) + w := worker{ + numLogs: c.NumLogs, + limitPerSecond: limit, + body: c.Body, + totalDuration: c.TotalDuration, + running: running, + wg: &wg, + logger: logger.With(zap.Int("worker", i)), + index: i, + } + + go w.simulateLogs(res, exp, c.GetTelemetryAttributes()) + } + if c.TotalDuration > 0 { + time.Sleep(c.TotalDuration) + running.Store(false) + } + wg.Wait() + return nil +} diff --git a/internal/telemetrygen/logs/package_test.go b/internal/telemetrygen/logs/package_test.go new file mode 100644 index 0000000..02c17e8 --- /dev/null +++ b/internal/telemetrygen/logs/package_test.go @@ -0,0 +1,20 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/logs/package_test.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This file does not contain functional modifications. + +package logs + +import ( + "testing" + + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} diff --git a/internal/telemetrygen/logs/worker.go b/internal/telemetrygen/logs/worker.go new file mode 100644 index 0000000..f360a3f --- /dev/null +++ b/internal/telemetrygen/logs/worker.go @@ -0,0 +1,77 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/logs/worker.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This file does not contain functional modifications. + +package logs + +import ( + "context" + "sync" + "sync/atomic" + "time" + + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/resource" + "go.uber.org/zap" + "golang.org/x/time/rate" +) + +type worker struct { + running *atomic.Bool // pointer to shared flag that indicates it's time to stop the test + numLogs int // how many logs the worker has to generate (only when duration==0) + body string // the body of the log + totalDuration time.Duration // how long to run the test for (overrides `numLogs`) + limitPerSecond rate.Limit // how many logs per second to generate + wg *sync.WaitGroup // notify when done + logger *zap.Logger // logger + index int // worker index +} + +func (w worker) simulateLogs(res *resource.Resource, exporter exporter, telemetryAttributes []attribute.KeyValue) { + limiter := rate.NewLimiter(w.limitPerSecond, 1) + var i int64 + + for w.running.Load() { + logs := plog.NewLogs() + nRes := logs.ResourceLogs().AppendEmpty().Resource() + attrs := res.Attributes() + for _, attr := range attrs { + nRes.Attributes().PutStr(string(attr.Key), attr.Value.AsString()) + } + log := logs.ResourceLogs().At(0).ScopeLogs().AppendEmpty().LogRecords().AppendEmpty() + log.Body().SetStr(w.body) + log.SetTimestamp(pcommon.NewTimestampFromTime(time.Now())) + log.SetDroppedAttributesCount(1) + log.SetSeverityNumber(plog.SeverityNumberInfo) + log.SetSeverityText("Info") + log.Attributes() + lattrs := log.Attributes() + lattrs.PutStr("app", "server") + + for i, key := range telemetryAttributes { + lattrs.PutStr(key.Value.AsString(), telemetryAttributes[i].Value.AsString()) + } + + if err := exporter.export(logs); err != nil { + w.logger.Fatal("exporter failed", zap.Error(err)) + } + if err := limiter.Wait(context.Background()); err != nil { + w.logger.Fatal("limiter wait failed, retry", zap.Error(err)) + } + + i++ + if w.numLogs != 0 && i >= int64(w.numLogs) { + break + } + } + + w.logger.Info("logs generated", zap.Int64("logs", i)) + w.wg.Done() +} diff --git a/internal/telemetrygen/logs/worker_test.go b/internal/telemetrygen/logs/worker_test.go new file mode 100644 index 0000000..4f73136 --- /dev/null +++ b/internal/telemetrygen/logs/worker_test.go @@ -0,0 +1,213 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/logs/worker_test.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This file does not contain functional modifications. + +package logs + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/pdata/plog" + "go.uber.org/zap" + + "github.com/elastic/apm-perf/internal/telemetrygen/common" +) + +const ( + telemetryAttrKeyOne = "k1" + telemetryAttrKeyTwo = "k2" + telemetryAttrValueOne = "v1" + telemetryAttrValueTwo = "v2" +) + +type mockExporter struct { + logs []plog.Logs +} + +func (m *mockExporter) export(logs plog.Logs) error { + m.logs = append(m.logs, logs) + return nil +} + +func TestFixedNumberOfLogs(t *testing.T) { + cfg := &Config{ + Config: common.Config{ + WorkerCount: 1, + }, + NumLogs: 5, + } + + exp := &mockExporter{} + + // test + logger, _ := zap.NewDevelopment() + require.NoError(t, Run(cfg, exp, logger)) + + time.Sleep(1 * time.Second) + + // verify + require.Len(t, exp.logs, 5) +} + +func TestRateOfLogs(t *testing.T) { + cfg := &Config{ + Config: common.Config{ + Rate: 10, + TotalDuration: time.Second / 2, + WorkerCount: 1, + }, + } + exp := &mockExporter{} + + // test + require.NoError(t, Run(cfg, exp, zap.NewNop())) + + // verify + // the minimum acceptable number of logs for the rate of 10/sec for half a second + assert.True(t, len(exp.logs) >= 5, "there should have been 5 or more logs, had %d", len(exp.logs)) + // the maximum acceptable number of logs for the rate of 10/sec for half a second + assert.True(t, len(exp.logs) <= 20, "there should have been less than 20 logs, had %d", len(exp.logs)) +} + +func TestUnthrottled(t *testing.T) { + cfg := &Config{ + Config: common.Config{ + TotalDuration: 1 * time.Second, + WorkerCount: 1, + }, + } + exp := &mockExporter{} + + // test + logger, _ := zap.NewDevelopment() + require.NoError(t, Run(cfg, exp, logger)) + + assert.True(t, len(exp.logs) > 100, "there should have been more than 100 logs, had %d", len(exp.logs)) +} + +func TestCustomBody(t *testing.T) { + cfg := &Config{ + Body: "custom body", + NumLogs: 1, + Config: common.Config{ + WorkerCount: 1, + }, + } + exp := &mockExporter{} + + // test + logger, _ := zap.NewDevelopment() + require.NoError(t, Run(cfg, exp, logger)) + + assert.Equal(t, "custom body", exp.logs[0].ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().At(0).Body().AsString()) +} + +func TestLogsWithNoTelemetryAttributes(t *testing.T) { + cfg := configWithNoAttributes(2, "custom body") + + exp := &mockExporter{} + + // test + logger, _ := zap.NewDevelopment() + require.NoError(t, Run(cfg, exp, logger)) + + time.Sleep(1 * time.Second) + + // verify + require.Len(t, exp.logs, 2) + for _, log := range exp.logs { + rlogs := log.ResourceLogs() + for i := 0; i < rlogs.Len(); i++ { + attrs := rlogs.At(i).ScopeLogs().At(0).LogRecords().At(0).Attributes() + assert.Equal(t, 1, attrs.Len(), "shouldn't have more than 1 attribute") + } + } +} + +func TestLogsWithOneTelemetryAttributes(t *testing.T) { + qty := 1 + cfg := configWithOneAttribute(qty, "custom body") + + exp := &mockExporter{} + + // test + logger, _ := zap.NewDevelopment() + require.NoError(t, Run(cfg, exp, logger)) + + time.Sleep(1 * time.Second) + + // verify + require.Len(t, exp.logs, qty) + for _, log := range exp.logs { + rlogs := log.ResourceLogs() + for i := 0; i < rlogs.Len(); i++ { + attrs := rlogs.At(i).ScopeLogs().At(0).LogRecords().At(0).Attributes() + assert.Equal(t, 2, attrs.Len(), "shouldn't have less than 2 attributes") + } + } +} + +func TestLogsWithMultipleTelemetryAttributes(t *testing.T) { + qty := 1 + cfg := configWithMultipleAttributes(qty, "custom body") + + exp := &mockExporter{} + + // test + logger, _ := zap.NewDevelopment() + require.NoError(t, Run(cfg, exp, logger)) + + time.Sleep(1 * time.Second) + + // verify + require.Len(t, exp.logs, qty) + for _, log := range exp.logs { + rlogs := log.ResourceLogs() + for i := 0; i < rlogs.Len(); i++ { + attrs := rlogs.At(i).ScopeLogs().At(0).LogRecords().At(0).Attributes() + assert.Equal(t, 3, attrs.Len(), "shouldn't have less than 3 attributes") + } + } +} + +func configWithNoAttributes(qty int, body string) *Config { + return &Config{ + Body: body, + NumLogs: qty, + Config: common.Config{ + WorkerCount: 1, + TelemetryAttributes: nil, + }, + } +} + +func configWithOneAttribute(qty int, body string) *Config { + return &Config{ + Body: body, + NumLogs: qty, + Config: common.Config{ + WorkerCount: 1, + TelemetryAttributes: common.KeyValue{telemetryAttrKeyOne: telemetryAttrValueOne}, + }, + } +} + +func configWithMultipleAttributes(qty int, body string) *Config { + kvs := common.KeyValue{telemetryAttrKeyOne: telemetryAttrValueOne, telemetryAttrKeyTwo: telemetryAttrValueTwo} + return &Config{ + Body: body, + NumLogs: qty, + Config: common.Config{ + WorkerCount: 1, + TelemetryAttributes: kvs, + }, + } +} diff --git a/internal/telemetrygen/metrics/config.go b/internal/telemetrygen/metrics/config.go new file mode 100644 index 0000000..2edc36c --- /dev/null +++ b/internal/telemetrygen/metrics/config.go @@ -0,0 +1,36 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/metrics/config.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This file does not contain functional modifications. + +package metrics + +import ( + "github.com/spf13/pflag" + + "github.com/elastic/apm-perf/internal/telemetrygen/common" +) + +// Config describes the test scenario. +type Config struct { + common.Config + NumMetrics int + MetricType metricType +} + +// Flags registers config flags. +func (c *Config) Flags(fs *pflag.FlagSet) { + // Use Gauge as default metric type. + c.MetricType = metricTypeGauge + + c.CommonFlags(fs) + + fs.StringVar(&c.HTTPPath, "otlp-http-url-path", "/v1/metrics", "Which URL path to write to") + + fs.Var(&c.MetricType, "metric-type", "Metric type enum. must be one of 'Gauge' or 'Sum'") + fs.IntVar(&c.NumMetrics, "metrics", 1, "Number of metrics to generate in each worker (ignored if duration is provided)") +} diff --git a/internal/telemetrygen/metrics/doc.go b/internal/telemetrygen/metrics/doc.go new file mode 100644 index 0000000..68d3ca5 --- /dev/null +++ b/internal/telemetrygen/metrics/doc.go @@ -0,0 +1,6 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// Package metrics is from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/metrics +package metrics diff --git a/internal/telemetrygen/metrics/exporter.go b/internal/telemetrygen/metrics/exporter.go new file mode 100644 index 0000000..0592010 --- /dev/null +++ b/internal/telemetrygen/metrics/exporter.go @@ -0,0 +1,72 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/metrics/exporter.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This file does not contain functional modifications. + +package metrics + +import ( + "fmt" + + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" + "google.golang.org/grpc" + + "github.com/elastic/apm-perf/internal/telemetrygen/common" +) + +// grpcExporterOptions creates the configuration options for a gRPC-based OTLP metric exporter. +// It configures the exporter with the provided endpoint, connection security settings, and headers. +func grpcExporterOptions(cfg *Config) ([]otlpmetricgrpc.Option, error) { + grpcExpOpt := []otlpmetricgrpc.Option{ + otlpmetricgrpc.WithEndpoint(cfg.Endpoint()), + otlpmetricgrpc.WithDialOption( + grpc.WithBlock(), + ), + } + + if cfg.Insecure { + grpcExpOpt = append(grpcExpOpt, otlpmetricgrpc.WithInsecure()) + } else { + credentials, err := common.GetTLSCredentialsForGRPCExporter(cfg.CaFile, cfg.ClientAuth) + if err != nil { + return nil, fmt.Errorf("failed to get TLS credentials: %w", err) + } + grpcExpOpt = append(grpcExpOpt, otlpmetricgrpc.WithTLSCredentials(credentials)) + } + + if len(cfg.Headers) > 0 { + grpcExpOpt = append(grpcExpOpt, otlpmetricgrpc.WithHeaders(cfg.Headers)) + } + + return grpcExpOpt, nil +} + +// httpExporterOptions creates the configuration options for an HTTP-based OTLP metric exporter. +// It configures the exporter with the provided endpoint, URL path, connection security settings, and headers. +func httpExporterOptions(cfg *Config) ([]otlpmetrichttp.Option, error) { + httpExpOpt := []otlpmetrichttp.Option{ + otlpmetrichttp.WithEndpoint(cfg.Endpoint()), + otlpmetrichttp.WithURLPath(cfg.HTTPPath), + } + + if cfg.Insecure { + httpExpOpt = append(httpExpOpt, otlpmetrichttp.WithInsecure()) + } else { + tlsCfg, err := common.GetTLSCredentialsForHTTPExporter(cfg.CaFile, cfg.ClientAuth) + if err != nil { + return nil, fmt.Errorf("failed to get TLS credentials: %w", err) + } + httpExpOpt = append(httpExpOpt, otlpmetrichttp.WithTLSClientConfig(tlsCfg)) + } + + if len(cfg.Headers) > 0 { + httpExpOpt = append(httpExpOpt, otlpmetrichttp.WithHeaders(cfg.Headers)) + } + + return httpExpOpt, nil +} diff --git a/internal/telemetrygen/metrics/metrics.go b/internal/telemetrygen/metrics/metrics.go new file mode 100644 index 0000000..e5f6b73 --- /dev/null +++ b/internal/telemetrygen/metrics/metrics.go @@ -0,0 +1,124 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/metrics/metrics.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This original implementation is modified: +// - Start function now only creates a logger when it is not already configured in cfg + +package metrics + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "time" + + semconv "go.opentelemetry.io/collector/semconv/v1.13.0" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + "go.uber.org/zap" + "golang.org/x/time/rate" + + "github.com/elastic/apm-perf/internal/telemetrygen/common" +) + +// Start starts the metric telemetry generator +func Start(cfg *Config) error { + logger := cfg.Logger + if logger == nil { + newLogger, err := common.CreateLogger(cfg.SkipSettingGRPCLogger) + if err != nil { + return err + } + logger = newLogger + } + logger.Info("starting the metrics generator with configuration", zap.Any("config", cfg)) + + expFunc := func() (sdkmetric.Exporter, error) { + var exp sdkmetric.Exporter + if cfg.UseHTTP { + var exporterOpts []otlpmetrichttp.Option + + logger.Info("starting HTTP exporter") + exporterOpts, err := httpExporterOptions(cfg) + if err != nil { + return nil, err + } + exp, err = otlpmetrichttp.New(context.Background(), exporterOpts...) + if err != nil { + return nil, fmt.Errorf("failed to obtain OTLP HTTP exporter: %w", err) + } + } else { + var exporterOpts []otlpmetricgrpc.Option + + logger.Info("starting gRPC exporter") + exporterOpts, err := grpcExporterOptions(cfg) + if err != nil { + return nil, err + } + exp, err = otlpmetricgrpc.New(context.Background(), exporterOpts...) + if err != nil { + return nil, fmt.Errorf("failed to obtain OTLP gRPC exporter: %w", err) + } + } + return exp, nil + } + + if err := Run(cfg, expFunc, logger); err != nil { + logger.Error("failed to stop the exporter", zap.Error(err)) + return err + } + + return nil +} + +// Run executes the test scenario. +func Run(c *Config, exp func() (sdkmetric.Exporter, error), logger *zap.Logger) error { + if c.TotalDuration > 0 { + c.NumMetrics = 0 + } else if c.NumMetrics <= 0 { + return fmt.Errorf("either `metrics` or `duration` must be greater than 0") + } + + limit := rate.Limit(c.Rate) + if c.Rate == 0 { + limit = rate.Inf + logger.Info("generation of metrics isn't being throttled") + } else { + logger.Info("generation of metrics is limited", zap.Float64("per-second", float64(limit))) + } + + wg := sync.WaitGroup{} + res := resource.NewWithAttributes(semconv.SchemaURL, c.GetAttributes()...) + + running := &atomic.Bool{} + running.Store(true) + + for i := 0; i < c.WorkerCount; i++ { + wg.Add(1) + w := worker{ + numMetrics: c.NumMetrics, + metricType: c.MetricType, + limitPerSecond: limit, + totalDuration: c.TotalDuration, + running: running, + wg: &wg, + logger: logger.With(zap.Int("worker", i)), + index: i, + } + + go w.simulateMetrics(res, exp, c.GetTelemetryAttributes()) + } + if c.TotalDuration > 0 { + time.Sleep(c.TotalDuration) + running.Store(false) + } + wg.Wait() + return nil +} diff --git a/internal/telemetrygen/metrics/metrics_types.go b/internal/telemetrygen/metrics/metrics_types.go new file mode 100644 index 0000000..a517733 --- /dev/null +++ b/internal/telemetrygen/metrics/metrics_types.go @@ -0,0 +1,42 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/metrics/metrics_types.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This file does not contain functional modifications. + +package metrics + +import ( + "errors" +) + +type metricType string + +const ( + metricTypeGauge = "Gauge" + metricTypeSum = "Sum" +) + +// String is used both by fmt.Print and by Cobra in help text +func (e *metricType) String() string { + return string(*e) +} + +// Set must have pointer receiver so it doesn't change the value of a copy +func (e *metricType) Set(v string) error { + switch v { + case "Gauge", "Sum": + *e = metricType(v) + return nil + default: + return errors.New(`must be one of "Gauge" or "Sum"`) + } +} + +// Type is only used in help text +func (e *metricType) Type() string { + return "metricType" +} diff --git a/internal/telemetrygen/metrics/package_test.go b/internal/telemetrygen/metrics/package_test.go new file mode 100644 index 0000000..1541ca8 --- /dev/null +++ b/internal/telemetrygen/metrics/package_test.go @@ -0,0 +1,20 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/metrics/package_test.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This file does not contain functional modifications. + +package metrics + +import ( + "testing" + + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} diff --git a/internal/telemetrygen/metrics/worker.go b/internal/telemetrygen/metrics/worker.go new file mode 100644 index 0000000..1fe608a --- /dev/null +++ b/internal/telemetrygen/metrics/worker.go @@ -0,0 +1,111 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/metrics/worker.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This file does not contain functional modifications. + +package metrics + +import ( + "context" + "sync" + "sync/atomic" + "time" + + "go.opentelemetry.io/otel/attribute" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + "go.opentelemetry.io/otel/sdk/resource" + "go.uber.org/zap" + "golang.org/x/time/rate" +) + +type worker struct { + running *atomic.Bool // pointer to shared flag that indicates it's time to stop the test + metricType metricType // type of metric to generate + numMetrics int // how many metrics the worker has to generate (only when duration==0) + totalDuration time.Duration // how long to run the test for (overrides `numMetrics`) + limitPerSecond rate.Limit // how many metrics per second to generate + wg *sync.WaitGroup // notify when done + logger *zap.Logger // logger + index int // worker index +} + +func (w worker) simulateMetrics(res *resource.Resource, exporterFunc func() (sdkmetric.Exporter, error), signalAttrs []attribute.KeyValue) { + limiter := rate.NewLimiter(w.limitPerSecond, 1) + + exporter, err := exporterFunc() + if err != nil { + w.logger.Error("failed to create the exporter", zap.Error(err)) + return + } + + defer func() { + w.logger.Info("stopping the exporter") + if tempError := exporter.Shutdown(context.Background()); tempError != nil { + w.logger.Error("failed to stop the exporter", zap.Error(tempError)) + } + }() + + var i int64 + for w.running.Load() { + var metrics []metricdata.Metrics + + switch w.metricType { + case metricTypeGauge: + metrics = append(metrics, metricdata.Metrics{ + Name: "gen", + Data: metricdata.Gauge[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Time: time.Now(), + Value: i, + Attributes: attribute.NewSet(signalAttrs...), + }, + }, + }, + }) + case metricTypeSum: + metrics = append(metrics, metricdata.Metrics{ + Name: "gen", + Data: metricdata.Sum[int64]{ + IsMonotonic: true, + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.DataPoint[int64]{ + { + StartTime: time.Now().Add(-1 * time.Second), + Time: time.Now(), + Value: i, + Attributes: attribute.NewSet(signalAttrs...), + }, + }, + }, + }) + default: + w.logger.Fatal("unknown metric type") + } + + rm := metricdata.ResourceMetrics{ + Resource: res, + ScopeMetrics: []metricdata.ScopeMetrics{{Metrics: metrics}}, + } + + if err := exporter.Export(context.Background(), &rm); err != nil { + w.logger.Fatal("exporter failed", zap.Error(err)) + } + if err := limiter.Wait(context.Background()); err != nil { + w.logger.Fatal("limiter wait failed, retry", zap.Error(err)) + } + + i++ + if w.numMetrics != 0 && i >= int64(w.numMetrics) { + break + } + } + + w.logger.Info("metrics generated", zap.Int64("metrics", i)) + w.wg.Done() +} diff --git a/internal/telemetrygen/metrics/worker_test.go b/internal/telemetrygen/metrics/worker_test.go new file mode 100644 index 0000000..d2c90c2 --- /dev/null +++ b/internal/telemetrygen/metrics/worker_test.go @@ -0,0 +1,337 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/metrics/worker_test.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This file does not contain functional modifications. + +package metrics + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/attribute" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + "go.uber.org/zap" + + "github.com/elastic/apm-perf/internal/telemetrygen/common" +) + +const ( + telemetryAttrKeyOne = "key1" + telemetryAttrKeyTwo = "key2" + telemetryAttrValueOne = "value1" + telemetryAttrValueTwo = "value2" +) + +type mockExporter struct { + rms []*metricdata.ResourceMetrics +} + +func (m *mockExporter) Temporality(_ sdkmetric.InstrumentKind) metricdata.Temporality { + return metricdata.DeltaTemporality +} + +func (m *mockExporter) Aggregation(_ sdkmetric.InstrumentKind) sdkmetric.Aggregation { + return sdkmetric.AggregationDefault{} +} + +func (m *mockExporter) Export(_ context.Context, metrics *metricdata.ResourceMetrics) error { + m.rms = append(m.rms, metrics) + return nil +} + +func (m *mockExporter) ForceFlush(_ context.Context) error { + return nil +} + +func (m *mockExporter) Shutdown(_ context.Context) error { + return nil +} + +func TestFixedNumberOfMetrics(t *testing.T) { + // arrange + cfg := &Config{ + Config: common.Config{ + WorkerCount: 1, + }, + NumMetrics: 5, + MetricType: metricTypeSum, + } + m := &mockExporter{} + expFunc := func() (sdkmetric.Exporter, error) { + return m, nil + } + + // act + logger, _ := zap.NewDevelopment() + require.NoError(t, Run(cfg, expFunc, logger)) + time.Sleep(1 * time.Second) + + // assert + require.Len(t, m.rms, 5) +} + +func TestRateOfMetrics(t *testing.T) { + // arrange + cfg := &Config{ + Config: common.Config{ + Rate: 10, + TotalDuration: time.Second / 2, + WorkerCount: 1, + }, + MetricType: metricTypeSum, + } + m := &mockExporter{} + expFunc := func() (sdkmetric.Exporter, error) { + return m, nil + } + + // act + require.NoError(t, Run(cfg, expFunc, zap.NewNop())) + + // assert + // the minimum acceptable number of metrics for the rate of 10/sec for half a second + assert.True(t, len(m.rms) >= 6, "there should have been more than 6 metrics, had %d", len(m.rms)) + // the maximum acceptable number of metrics for the rate of 10/sec for half a second + assert.True(t, len(m.rms) <= 20, "there should have been less than 20 metrics, had %d", len(m.rms)) +} + +func TestUnthrottled(t *testing.T) { + // arrange + cfg := &Config{ + Config: common.Config{ + TotalDuration: 1 * time.Second, + WorkerCount: 1, + }, + MetricType: metricTypeSum, + } + m := &mockExporter{} + expFunc := func() (sdkmetric.Exporter, error) { + return m, nil + } + + // act + logger, _ := zap.NewDevelopment() + require.NoError(t, Run(cfg, expFunc, logger)) + + // assert + assert.True(t, len(m.rms) > 100, "there should have been more than 100 metrics, had %d", len(m.rms)) +} + +func TestSumNoTelemetryAttrs(t *testing.T) { + // arrange + qty := 2 + cfg := configWithNoAttributes(metricTypeSum, qty) + m := &mockExporter{} + expFunc := func() (sdkmetric.Exporter, error) { + return m, nil + } + + // act + logger, _ := zap.NewDevelopment() + require.NoError(t, Run(cfg, expFunc, logger)) + + time.Sleep(1 * time.Second) + + // asserts + require.Len(t, m.rms, qty) + + rms := m.rms + for i := 0; i < qty; i++ { + ms := rms[i].ScopeMetrics[0].Metrics[0] + // @note update when telemetrygen allow other metric types + attr := ms.Data.(metricdata.Sum[int64]).DataPoints[0].Attributes + assert.Equal(t, attr.Len(), 0, "it shouldn't have attrs here") + } +} + +func TestGaugeNoTelemetryAttrs(t *testing.T) { + // arrange + qty := 2 + cfg := configWithNoAttributes(metricTypeGauge, qty) + m := &mockExporter{} + expFunc := func() (sdkmetric.Exporter, error) { + return m, nil + } + + // act + logger, _ := zap.NewDevelopment() + require.NoError(t, Run(cfg, expFunc, logger)) + + time.Sleep(1 * time.Second) + + // asserts + require.Len(t, m.rms, qty) + + rms := m.rms + for i := 0; i < qty; i++ { + ms := rms[i].ScopeMetrics[0].Metrics[0] + // @note update when telemetrygen allow other metric types + attr := ms.Data.(metricdata.Gauge[int64]).DataPoints[0].Attributes + assert.Equal(t, attr.Len(), 0, "it shouldn't have attrs here") + } +} + +func TestSumSingleTelemetryAttr(t *testing.T) { + // arrange + qty := 2 + cfg := configWithOneAttribute(metricTypeSum, qty) + m := &mockExporter{} + expFunc := func() (sdkmetric.Exporter, error) { + return m, nil + } + + // act + logger, _ := zap.NewDevelopment() + require.NoError(t, Run(cfg, expFunc, logger)) + + time.Sleep(1 * time.Second) + + // asserts + require.Len(t, m.rms, qty) + + rms := m.rms + for i := 0; i < qty; i++ { + ms := rms[i].ScopeMetrics[0].Metrics[0] + // @note update when telemetrygen allow other metric types + attr := ms.Data.(metricdata.Sum[int64]).DataPoints[0].Attributes + assert.Equal(t, attr.Len(), 1, "it must have a single attribute here") + actualValue, _ := attr.Value(telemetryAttrKeyOne) + assert.Equal(t, actualValue.AsString(), telemetryAttrValueOne, "it should be "+telemetryAttrValueOne) + } +} + +func TestGaugeSingleTelemetryAttr(t *testing.T) { + // arrange + qty := 2 + cfg := configWithOneAttribute(metricTypeGauge, qty) + m := &mockExporter{} + expFunc := func() (sdkmetric.Exporter, error) { + return m, nil + } + + // act + logger, _ := zap.NewDevelopment() + require.NoError(t, Run(cfg, expFunc, logger)) + + time.Sleep(1 * time.Second) + + // asserts + require.Len(t, m.rms, qty) + + rms := m.rms + for i := 0; i < qty; i++ { + ms := rms[i].ScopeMetrics[0].Metrics[0] + // @note update when telemetrygen allow other metric types + attr := ms.Data.(metricdata.Gauge[int64]).DataPoints[0].Attributes + assert.Equal(t, attr.Len(), 1, "it must have a single attribute here") + actualValue, _ := attr.Value(telemetryAttrKeyOne) + assert.Equal(t, actualValue.AsString(), telemetryAttrValueOne, "it should be "+telemetryAttrValueOne) + } +} + +func TestSumMultipleTelemetryAttr(t *testing.T) { + // arrange + qty := 2 + cfg := configWithMultipleAttributes(metricTypeSum, qty) + m := &mockExporter{} + expFunc := func() (sdkmetric.Exporter, error) { + return m, nil + } + + // act + logger, _ := zap.NewDevelopment() + require.NoError(t, Run(cfg, expFunc, logger)) + + time.Sleep(1 * time.Second) + + // asserts + require.Len(t, m.rms, qty) + + rms := m.rms + var actualValue attribute.Value + for i := 0; i < qty; i++ { + ms := rms[i].ScopeMetrics[0].Metrics[0] + // @note update when telemetrygen allow other metric types + attr := ms.Data.(metricdata.Sum[int64]).DataPoints[0].Attributes + assert.Equal(t, 2, attr.Len(), "it must have multiple attributes here") + actualValue, _ = attr.Value(telemetryAttrKeyOne) + assert.Equal(t, telemetryAttrValueOne, actualValue.AsString(), "it should be "+telemetryAttrValueOne) + actualValue, _ = attr.Value(telemetryAttrKeyTwo) + assert.Equal(t, telemetryAttrValueTwo, actualValue.AsString(), "it should be "+telemetryAttrValueTwo) + } +} + +func TestGaugeMultipleTelemetryAttr(t *testing.T) { + // arrange + qty := 2 + cfg := configWithMultipleAttributes(metricTypeGauge, qty) + m := &mockExporter{} + expFunc := func() (sdkmetric.Exporter, error) { + return m, nil + } + + // act + logger, _ := zap.NewDevelopment() + require.NoError(t, Run(cfg, expFunc, logger)) + + time.Sleep(1 * time.Second) + + // asserts + require.Len(t, m.rms, qty) + + rms := m.rms + var actualValue attribute.Value + for i := 0; i < qty; i++ { + ms := rms[i].ScopeMetrics[0].Metrics[0] + // @note update when telemetrygen allow other metric types + attr := ms.Data.(metricdata.Gauge[int64]).DataPoints[0].Attributes + assert.Equal(t, 2, attr.Len(), "it must have multiple attributes here") + actualValue, _ = attr.Value(telemetryAttrKeyOne) + assert.Equal(t, telemetryAttrValueOne, actualValue.AsString(), "it should be "+telemetryAttrValueOne) + actualValue, _ = attr.Value(telemetryAttrKeyTwo) + assert.Equal(t, telemetryAttrValueTwo, actualValue.AsString(), "it should be "+telemetryAttrValueTwo) + } +} + +func configWithNoAttributes(metric metricType, qty int) *Config { + return &Config{ + Config: common.Config{ + WorkerCount: 1, + TelemetryAttributes: nil, + }, + NumMetrics: qty, + MetricType: metric, + } +} + +func configWithOneAttribute(metric metricType, qty int) *Config { + return &Config{ + Config: common.Config{ + WorkerCount: 1, + TelemetryAttributes: common.KeyValue{telemetryAttrKeyOne: telemetryAttrValueOne}, + }, + NumMetrics: qty, + MetricType: metric, + } +} + +func configWithMultipleAttributes(metric metricType, qty int) *Config { + kvs := common.KeyValue{telemetryAttrKeyOne: telemetryAttrValueOne, telemetryAttrKeyTwo: telemetryAttrValueTwo} + return &Config{ + Config: common.Config{ + WorkerCount: 1, + TelemetryAttributes: kvs, + }, + NumMetrics: qty, + MetricType: metric, + } +} diff --git a/internal/telemetrygen/traces/config.go b/internal/telemetrygen/traces/config.go new file mode 100644 index 0000000..4984a1e --- /dev/null +++ b/internal/telemetrygen/traces/config.go @@ -0,0 +1,48 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/traces/config.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This file does not contain functional modifications. + +package traces + +import ( + "time" + + "github.com/spf13/pflag" + + "github.com/elastic/apm-perf/internal/telemetrygen/common" +) + +// Config describes the test scenario. +type Config struct { + common.Config + NumTraces int + NumChildSpans int + PropagateContext bool + ServiceName string + StatusCode string + Batch bool + LoadSize int + + SpanDuration time.Duration +} + +// Flags registers config flags. +func (c *Config) Flags(fs *pflag.FlagSet) { + c.CommonFlags(fs) + + fs.StringVar(&c.HTTPPath, "otlp-http-url-path", "/v1/traces", "Which URL path to write to") + + fs.IntVar(&c.NumTraces, "traces", 1, "Number of traces to generate in each worker (ignored if duration is provided)") + fs.IntVar(&c.NumChildSpans, "child-spans", 1, "Number of child spans to generate for each trace") + fs.BoolVar(&c.PropagateContext, "marshal", false, "Whether to marshal trace context via HTTP headers") + fs.StringVar(&c.ServiceName, "service", "telemetrygen", "Service name to use") + fs.StringVar(&c.StatusCode, "status-code", "0", "Status code to use for the spans, one of (Unset, Error, Ok) or the equivalent integer (0,1,2)") + fs.BoolVar(&c.Batch, "batch", true, "Whether to batch traces") + fs.IntVar(&c.LoadSize, "size", 0, "Desired minimum size in MB of string data for each trace generated. This can be used to test traces with large payloads, i.e. when testing the OTLP receiver endpoint max receive size.") + fs.DurationVar(&c.SpanDuration, "span-duration", 123*time.Microsecond, "The duration of each generated span.") +} diff --git a/internal/telemetrygen/traces/doc.go b/internal/telemetrygen/traces/doc.go new file mode 100644 index 0000000..bdeaedb --- /dev/null +++ b/internal/telemetrygen/traces/doc.go @@ -0,0 +1,6 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// Package traces is from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/traces +package traces diff --git a/internal/telemetrygen/traces/exporter.go b/internal/telemetrygen/traces/exporter.go new file mode 100644 index 0000000..6f3907b --- /dev/null +++ b/internal/telemetrygen/traces/exporter.go @@ -0,0 +1,72 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/traces/exporter.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This file does not contain functional modifications. + +package traces + +import ( + "fmt" + + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "google.golang.org/grpc" + + "github.com/elastic/apm-perf/internal/telemetrygen/common" +) + +// grpcExporterOptions creates the configuration options for a gRPC-based OTLP trace exporter. +// It configures the exporter with the provided endpoint, connection security settings, and headers. +func grpcExporterOptions(cfg *Config) ([]otlptracegrpc.Option, error) { + grpcExpOpt := []otlptracegrpc.Option{ + otlptracegrpc.WithEndpoint(cfg.Endpoint()), + otlptracegrpc.WithDialOption( + grpc.WithBlock(), + ), + } + + if cfg.Insecure { + grpcExpOpt = append(grpcExpOpt, otlptracegrpc.WithInsecure()) + } else { + credentials, err := common.GetTLSCredentialsForGRPCExporter(cfg.CaFile, cfg.ClientAuth) + if err != nil { + return nil, fmt.Errorf("failed to get TLS credentials: %w", err) + } + grpcExpOpt = append(grpcExpOpt, otlptracegrpc.WithTLSCredentials(credentials)) + } + + if len(cfg.Headers) > 0 { + grpcExpOpt = append(grpcExpOpt, otlptracegrpc.WithHeaders(cfg.Headers)) + } + + return grpcExpOpt, nil +} + +// httpExporterOptions creates the configuration options for an HTTP-based OTLP trace exporter. +// It configures the exporter with the provided endpoint, URL path, connection security settings, and headers. +func httpExporterOptions(cfg *Config) ([]otlptracehttp.Option, error) { + httpExpOpt := []otlptracehttp.Option{ + otlptracehttp.WithEndpoint(cfg.Endpoint()), + otlptracehttp.WithURLPath(cfg.HTTPPath), + } + + if cfg.Insecure { + httpExpOpt = append(httpExpOpt, otlptracehttp.WithInsecure()) + } else { + tlsCfg, err := common.GetTLSCredentialsForHTTPExporter(cfg.CaFile, cfg.ClientAuth) + if err != nil { + return nil, fmt.Errorf("failed to get TLS credentials: %w", err) + } + httpExpOpt = append(httpExpOpt, otlptracehttp.WithTLSClientConfig(tlsCfg)) + } + + if len(cfg.Headers) > 0 { + httpExpOpt = append(httpExpOpt, otlptracehttp.WithHeaders(cfg.Headers)) + } + + return httpExpOpt, nil +} diff --git a/internal/telemetrygen/traces/package_test.go b/internal/telemetrygen/traces/package_test.go new file mode 100644 index 0000000..2e1f9da --- /dev/null +++ b/internal/telemetrygen/traces/package_test.go @@ -0,0 +1,20 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/traces/package_test.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This file does not contain functional modifications. + +package traces + +import ( + "testing" + + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} diff --git a/internal/telemetrygen/traces/traces.go b/internal/telemetrygen/traces/traces.go new file mode 100644 index 0000000..dad0a63 --- /dev/null +++ b/internal/telemetrygen/traces/traces.go @@ -0,0 +1,176 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/traces/traces.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This original implementation is modified: +// - Start function now only creates a logger when it is not already configured in cfg +// - Use the correct error in batch span processor error logging. +// - Use WithBlocking instead of WithBatchTimeout in BatchSpanProcessor + +package traces + +import ( + "context" + "fmt" + "math" + "strings" + "sync" + "sync/atomic" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" + "go.uber.org/zap" + "golang.org/x/time/rate" + + "github.com/elastic/apm-perf/internal/telemetrygen/common" +) + +func Start(cfg *Config) error { + logger := cfg.Logger + if logger == nil { + newLogger, err := common.CreateLogger(cfg.SkipSettingGRPCLogger) + if err != nil { + return err + } + logger = newLogger + } + + var exp *otlptrace.Exporter + if cfg.UseHTTP { + var exporterOpts []otlptracehttp.Option + + logger.Info("starting HTTP exporter") + exporterOpts, err := httpExporterOptions(cfg) + if err != nil { + return err + } + exp, err = otlptracehttp.New(context.Background(), exporterOpts...) + if err != nil { + return fmt.Errorf("failed to obtain OTLP HTTP exporter: %w", err) + } + } else { + var exporterOpts []otlptracegrpc.Option + + logger.Info("starting gRPC exporter") + exporterOpts, err := grpcExporterOptions(cfg) + if err != nil { + return err + } + exp, err = otlptracegrpc.New(context.Background(), exporterOpts...) + if err != nil { + return fmt.Errorf("failed to obtain OTLP gRPC exporter: %w", err) + } + } + + defer func() { + logger.Info("stopping the exporter") + if tempError := exp.Shutdown(context.Background()); tempError != nil { + logger.Error("failed to stop the exporter", zap.Error(tempError)) + } + }() + + var ssp sdktrace.SpanProcessor + if cfg.Batch { + ssp = sdktrace.NewBatchSpanProcessor(exp, sdktrace.WithBlocking()) + defer func() { + logger.Info("stop the batch span processor") + if tempError := ssp.Shutdown(context.Background()); tempError != nil { + logger.Error("failed to stop the batch span processor", zap.Error(tempError)) + } + }() + } + + var attributes []attribute.KeyValue + // may be overridden by `--otlp-attributes service.name="foo"` + attributes = append(attributes, semconv.ServiceNameKey.String(cfg.ServiceName)) + attributes = append(attributes, cfg.GetAttributes()...) + + tracerProvider := sdktrace.NewTracerProvider( + sdktrace.WithResource(resource.NewWithAttributes(semconv.SchemaURL, attributes...)), + ) + + if cfg.Batch { + tracerProvider.RegisterSpanProcessor(ssp) + } + otel.SetTracerProvider(tracerProvider) + + if err := Run(cfg, logger); err != nil { + logger.Error("failed to execute the test scenario.", zap.Error(err)) + return err + } + + return nil +} + +// Run executes the test scenario. +func Run(c *Config, logger *zap.Logger) error { + if c.TotalDuration > 0 { + c.NumTraces = 0 + } else if c.NumTraces <= 0 { + return fmt.Errorf("either `traces` or `duration` must be greater than 0") + } + + limit := rate.Limit(c.Rate) + if c.Rate == 0 { + limit = rate.Inf + logger.Info("generation of traces isn't being throttled") + } else { + logger.Info("generation of traces is limited", zap.Float64("per-second", float64(limit))) + } + + var statusCode codes.Code + + switch strings.ToLower(c.StatusCode) { + case "0", "unset", "": + statusCode = codes.Unset + case "1", "error": + statusCode = codes.Error + case "2", "ok": + statusCode = codes.Ok + default: + return fmt.Errorf("expected `status-code` to be one of (Unset, Error, Ok) or (0, 1, 2), got %q instead", c.StatusCode) + } + + wg := sync.WaitGroup{} + + running := &atomic.Bool{} + running.Store(true) + + telemetryAttributes := c.GetTelemetryAttributes() + + for i := 0; i < c.WorkerCount; i++ { + wg.Add(1) + w := worker{ + numTraces: c.NumTraces, + numChildSpans: int(math.Max(1, float64(c.NumChildSpans))), + propagateContext: c.PropagateContext, + statusCode: statusCode, + limitPerSecond: limit, + totalDuration: c.TotalDuration, + running: running, + wg: &wg, + logger: logger.With(zap.Int("worker", i)), + loadSize: c.LoadSize, + spanDuration: c.SpanDuration, + } + + go w.simulateTraces(telemetryAttributes) + } + if c.TotalDuration > 0 { + time.Sleep(c.TotalDuration) + running.Store(false) + } + wg.Wait() + return nil +} diff --git a/internal/telemetrygen/traces/worker.go b/internal/telemetrygen/traces/worker.go new file mode 100644 index 0000000..20ee378 --- /dev/null +++ b/internal/telemetrygen/traces/worker.go @@ -0,0 +1,116 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/traces/worker.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This file does not contain functional modifications. + +package traces + +import ( + "context" + "fmt" + "strconv" + "sync" + "sync/atomic" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/propagation" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + "golang.org/x/time/rate" +) + +type worker struct { + running *atomic.Bool // pointer to shared flag that indicates it's time to stop the test + numTraces int // how many traces the worker has to generate (only when duration==0) + numChildSpans int // how many child spans the worker has to generate per trace + propagateContext bool // whether the worker needs to propagate the trace context via HTTP headers + statusCode codes.Code // the status code set for the child and parent spans + totalDuration time.Duration // how long to run the test for (overrides `numTraces`) + limitPerSecond rate.Limit // how many spans per second to generate + wg *sync.WaitGroup // notify when done + loadSize int // desired minimum size in MB of string data for each generated trace + spanDuration time.Duration // duration of generated spans + logger *zap.Logger +} + +const ( + fakeIP string = "1.2.3.4" + + charactersPerMB = 1024 * 1024 // One character takes up one byte of space, so this number comes from the number of bytes in a megabyte +) + +func (w worker) simulateTraces(telemetryAttributes []attribute.KeyValue) { + tracer := otel.Tracer("telemetrygen") + limiter := rate.NewLimiter(w.limitPerSecond, 1) + var i int + + for w.running.Load() { + spanStart := time.Now() + spanEnd := spanStart.Add(w.spanDuration) + + ctx, sp := tracer.Start(context.Background(), "lets-go", trace.WithAttributes( + semconv.NetPeerIPKey.String(fakeIP), + semconv.PeerServiceKey.String("telemetrygen-server"), + ), + trace.WithSpanKind(trace.SpanKindClient), + trace.WithTimestamp(spanStart), + ) + sp.SetAttributes(telemetryAttributes...) + for j := 0; j < w.loadSize; j++ { + sp.SetAttributes(attribute.String(fmt.Sprintf("load-%v", j), string(make([]byte, charactersPerMB)))) + } + + childCtx := ctx + if w.propagateContext { + header := propagation.HeaderCarrier{} + // simulates going remote + otel.GetTextMapPropagator().Inject(childCtx, header) + + // simulates getting a request from a client + childCtx = otel.GetTextMapPropagator().Extract(childCtx, header) + } + var endTimestamp trace.SpanEventOption + + for j := 0; j < w.numChildSpans; j++ { + _, child := tracer.Start(childCtx, "okey-dokey-"+strconv.Itoa(j), trace.WithAttributes( + semconv.NetPeerIPKey.String(fakeIP), + semconv.PeerServiceKey.String("telemetrygen-client"), + ), + trace.WithSpanKind(trace.SpanKindServer), + trace.WithTimestamp(spanStart), + ) + child.SetAttributes(telemetryAttributes...) + + if err := limiter.Wait(context.Background()); err != nil { + w.logger.Fatal("limiter waited failed, retry", zap.Error(err)) + } + + endTimestamp = trace.WithTimestamp(spanEnd) + child.SetStatus(w.statusCode, "") + child.End(endTimestamp) + + // Reset the start and end for next span + spanStart = spanEnd + spanEnd = spanStart.Add(w.spanDuration) + } + sp.SetStatus(w.statusCode, "") + sp.End(endTimestamp) + + i++ + if w.numTraces != 0 { + if i >= w.numTraces { + break + } + } + } + w.logger.Info("traces generated", zap.Int("traces", i)) + w.wg.Done() +} diff --git a/internal/telemetrygen/traces/worker_test.go b/internal/telemetrygen/traces/worker_test.go new file mode 100644 index 0000000..e3dd9d6 --- /dev/null +++ b/internal/telemetrygen/traces/worker_test.go @@ -0,0 +1,371 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// This file is forked from https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/790e18f1733e71debc7608aed98ace654ac76a60/cmd/telemetrygen/internal/traces/worker_test.go, +// which is licensed under Apache-2 and Copyright The OpenTelemetry Authors. +// +// This file does not contain functional modifications. + +package traces + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + + "github.com/elastic/apm-perf/internal/telemetrygen/common" +) + +const ( + telemetryAttrKeyOne = "k1" + telemetryAttrKeyTwo = "k2" + telemetryAttrValueOne = "v1" + telemetryAttrValueTwo = "v2" +) + +func TestFixedNumberOfTraces(t *testing.T) { + // prepare + syncer := &mockSyncer{} + + tracerProvider := sdktrace.NewTracerProvider() + sp := sdktrace.NewSimpleSpanProcessor(syncer) + tracerProvider.RegisterSpanProcessor(sp) + otel.SetTracerProvider(tracerProvider) + + cfg := &Config{ + Config: common.Config{ + WorkerCount: 1, + }, + NumTraces: 1, + } + + // test + require.NoError(t, Run(cfg, zap.NewNop())) + + // verify + assert.Len(t, syncer.spans, 2) // each trace has two spans +} + +func TestNumberOfSpans(t *testing.T) { + // prepare + syncer := &mockSyncer{} + + tracerProvider := sdktrace.NewTracerProvider() + sp := sdktrace.NewSimpleSpanProcessor(syncer) + tracerProvider.RegisterSpanProcessor(sp) + otel.SetTracerProvider(tracerProvider) + + cfg := &Config{ + Config: common.Config{ + WorkerCount: 1, + }, + NumTraces: 1, + NumChildSpans: 5, + } + expectedNumSpans := cfg.NumChildSpans + 1 // each trace has 1 + NumChildSpans spans + + // test + require.NoError(t, Run(cfg, zap.NewNop())) + + // verify + assert.Len(t, syncer.spans, expectedNumSpans) +} + +func TestRateOfSpans(t *testing.T) { + // prepare + syncer := &mockSyncer{} + + tracerProvider := sdktrace.NewTracerProvider() + sp := sdktrace.NewSimpleSpanProcessor(syncer) + tracerProvider.RegisterSpanProcessor(sp) + otel.SetTracerProvider(tracerProvider) + + cfg := &Config{ + Config: common.Config{ + Rate: 10, + TotalDuration: time.Second / 2, + WorkerCount: 1, + }, + } + + // sanity check + require.Len(t, syncer.spans, 0) + + // test + require.NoError(t, Run(cfg, zap.NewNop())) + + // verify + // the minimum acceptable number of spans for the rate of 10/sec for half a second + assert.True(t, len(syncer.spans) >= 6, "there should have been more than 6 spans, had %d", len(syncer.spans)) + // the maximum acceptable number of spans for the rate of 10/sec for half a second + assert.True(t, len(syncer.spans) <= 20, "there should have been less than 20 spans, had %d", len(syncer.spans)) +} + +func TestSpanDuration(t *testing.T) { + // prepare + syncer := &mockSyncer{} + + tracerProvider := sdktrace.NewTracerProvider() + sp := sdktrace.NewSimpleSpanProcessor(syncer) + tracerProvider.RegisterSpanProcessor(sp) + otel.SetTracerProvider(tracerProvider) + + targetDuration := 1 * time.Second + cfg := &Config{ + Config: common.Config{ + Rate: 10, + TotalDuration: time.Second / 2, + WorkerCount: 1, + }, + SpanDuration: targetDuration, + } + + // sanity check + require.Len(t, syncer.spans, 0) + + // test + require.NoError(t, Run(cfg, zap.NewNop())) + + for _, span := range syncer.spans { + startTime, endTime := span.StartTime(), span.EndTime() + spanDuration := endTime.Sub(startTime) + assert.Equal(t, targetDuration, spanDuration) + } +} + +func TestUnthrottled(t *testing.T) { + // prepare + syncer := &mockSyncer{} + + tracerProvider := sdktrace.NewTracerProvider() + sp := sdktrace.NewSimpleSpanProcessor(syncer) + tracerProvider.RegisterSpanProcessor(sp) + otel.SetTracerProvider(tracerProvider) + + cfg := &Config{ + Config: common.Config{ + TotalDuration: 50 * time.Millisecond, + WorkerCount: 1, + }, + } + + // sanity check + require.Len(t, syncer.spans, 0) + + // test + require.NoError(t, Run(cfg, zap.NewNop())) + + // verify + // the minimum acceptable number of spans -- the real number should be > 10k, but CI env might be slower + assert.True(t, len(syncer.spans) > 100, "there should have been more than 100 spans, had %d", len(syncer.spans)) +} + +func TestSpanKind(t *testing.T) { + // prepare + syncer := &mockSyncer{} + + tracerProvider := sdktrace.NewTracerProvider() + sp := sdktrace.NewSimpleSpanProcessor(syncer) + tracerProvider.RegisterSpanProcessor(sp) + otel.SetTracerProvider(tracerProvider) + + cfg := &Config{ + Config: common.Config{ + WorkerCount: 1, + }, + NumTraces: 1, + } + + // test + require.NoError(t, Run(cfg, zap.NewNop())) + + // verify that the default Span Kind is being overridden + for _, span := range syncer.spans { + assert.NotEqual(t, span.SpanKind(), trace.SpanKindInternal) + } +} + +func TestSpanStatuses(t *testing.T) { + tests := []struct { + inputStatus string + spanStatus codes.Code + validInput bool + }{ + {inputStatus: `Unset`, spanStatus: codes.Unset, validInput: true}, + {inputStatus: `Error`, spanStatus: codes.Error, validInput: true}, + {inputStatus: `Ok`, spanStatus: codes.Ok, validInput: true}, + {inputStatus: `unset`, spanStatus: codes.Unset, validInput: true}, + {inputStatus: `error`, spanStatus: codes.Error, validInput: true}, + {inputStatus: `ok`, spanStatus: codes.Ok, validInput: true}, + {inputStatus: `UNSET`, spanStatus: codes.Unset, validInput: true}, + {inputStatus: `ERROR`, spanStatus: codes.Error, validInput: true}, + {inputStatus: `OK`, spanStatus: codes.Ok, validInput: true}, + {inputStatus: `0`, spanStatus: codes.Unset, validInput: true}, + {inputStatus: `1`, spanStatus: codes.Error, validInput: true}, + {inputStatus: `2`, spanStatus: codes.Ok, validInput: true}, + {inputStatus: `Foo`, spanStatus: codes.Unset, validInput: false}, + {inputStatus: `-1`, spanStatus: codes.Unset, validInput: false}, + {inputStatus: `3`, spanStatus: codes.Unset, validInput: false}, + {inputStatus: `Err`, spanStatus: codes.Unset, validInput: false}, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("inputStatus=%s", tt.inputStatus), func(t *testing.T) { + syncer := &mockSyncer{} + + tracerProvider := sdktrace.NewTracerProvider() + sp := sdktrace.NewSimpleSpanProcessor(syncer) + tracerProvider.RegisterSpanProcessor(sp) + otel.SetTracerProvider(tracerProvider) + + cfg := &Config{ + Config: common.Config{ + WorkerCount: 1, + }, + NumTraces: 1, + StatusCode: tt.inputStatus, + } + + // test the program given input, including erroneous inputs + if tt.validInput { + require.NoError(t, Run(cfg, zap.NewNop())) + // verify that the default the span status is set as expected + for _, span := range syncer.spans { + assert.Equal(t, span.Status().Code, tt.spanStatus, fmt.Sprintf("span status: %v and expected status %v", span.Status().Code, tt.spanStatus)) + } + } else { + require.Error(t, Run(cfg, zap.NewNop())) + } + }) + } +} + +func TestSpansWithNoAttrs(t *testing.T) { + // prepare + syncer := &mockSyncer{} + + tracerProvider := sdktrace.NewTracerProvider() + sp := sdktrace.NewSimpleSpanProcessor(syncer) + tracerProvider.RegisterSpanProcessor(sp) + otel.SetTracerProvider(tracerProvider) + + cfg := configWithNoAttributes(2, "") + + // test + require.NoError(t, Run(cfg, zap.NewNop())) + + // verify + assert.Len(t, syncer.spans, 4) // each trace has two spans + for _, span := range syncer.spans { + attributes := span.Attributes() + assert.Equal(t, 2, len(attributes), "it shouldn't have more than 2 fixed attributes") + } +} + +func TestSpansWithOneAttrs(t *testing.T) { + // prepare + syncer := &mockSyncer{} + + tracerProvider := sdktrace.NewTracerProvider() + sp := sdktrace.NewSimpleSpanProcessor(syncer) + tracerProvider.RegisterSpanProcessor(sp) + otel.SetTracerProvider(tracerProvider) + + cfg := configWithOneAttribute(2, "") + + // test + require.NoError(t, Run(cfg, zap.NewNop())) + + // verify + assert.Len(t, syncer.spans, 4) // each trace has two spans + for _, span := range syncer.spans { + attributes := span.Attributes() + assert.Equal(t, 3, len(attributes), "it should have more than 3 attributes") + } +} + +func TestSpansWithMultipleAttrs(t *testing.T) { + // prepare + syncer := &mockSyncer{} + + tracerProvider := sdktrace.NewTracerProvider() + sp := sdktrace.NewSimpleSpanProcessor(syncer) + tracerProvider.RegisterSpanProcessor(sp) + otel.SetTracerProvider(tracerProvider) + + cfg := configWithMultipleAttributes(2, "") + + // test + require.NoError(t, Run(cfg, zap.NewNop())) + + // verify + assert.Len(t, syncer.spans, 4) // each trace has two spans + for _, span := range syncer.spans { + attributes := span.Attributes() + assert.Equal(t, 4, len(attributes), "it should have more than 4 attributes") + } +} + +var _ sdktrace.SpanExporter = (*mockSyncer)(nil) + +type mockSyncer struct { + spans []sdktrace.ReadOnlySpan +} + +func (m *mockSyncer) ExportSpans(_ context.Context, spanData []sdktrace.ReadOnlySpan) error { + m.spans = append(m.spans, spanData...) + return nil +} + +func (m *mockSyncer) Shutdown(context.Context) error { + panic("implement me") +} + +func (m *mockSyncer) Reset() { + m.spans = []sdktrace.ReadOnlySpan{} +} + +func configWithNoAttributes(qty int, statusCode string) *Config { + return &Config{ + Config: common.Config{ + WorkerCount: 1, + TelemetryAttributes: nil, + }, + NumTraces: qty, + StatusCode: statusCode, + } +} + +func configWithOneAttribute(qty int, statusCode string) *Config { + return &Config{ + Config: common.Config{ + WorkerCount: 1, + TelemetryAttributes: common.KeyValue{telemetryAttrKeyOne: telemetryAttrValueOne}, + }, + NumTraces: qty, + StatusCode: statusCode, + } +} + +func configWithMultipleAttributes(qty int, statusCode string) *Config { + kvs := common.KeyValue{telemetryAttrKeyOne: telemetryAttrValueOne, telemetryAttrKeyTwo: telemetryAttrValueTwo} + return &Config{ + Config: common.Config{ + WorkerCount: 1, + TelemetryAttributes: kvs, + }, + NumTraces: qty, + StatusCode: statusCode, + } + +}