Repository: jaegertracing/jaeger Branch: main Commit: 733146d827b4 Files: 1166 Total size: 4.9 MB Directory structure: gitextract_8w1ucnwf/ ├── .codecov.yml ├── .fossa.yml ├── .github/ │ ├── CODEOWNERS │ ├── actions/ │ │ ├── block-pr-from-main-branch/ │ │ │ └── action.yml │ │ ├── setup-branch/ │ │ │ └── action.yml │ │ ├── setup-go-tip/ │ │ │ └── action.yml │ │ ├── setup-node.js/ │ │ │ └── action.yml │ │ ├── upload-codecov/ │ │ │ └── action.yml │ │ └── verify-metrics-snapshot/ │ │ └── action.yaml │ ├── scripts/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── ci-summary-report-publish.js │ │ ├── ci-summary-report-publish.test.js │ │ ├── list-open-prs-by-author.js │ │ ├── package.json │ │ ├── pr-quota-manager.js │ │ ├── pr-quota-manager.test.js │ │ └── waiting-for-author.js │ └── workflows/ │ ├── README.md │ ├── ci-build-binaries.yml │ ├── ci-deploy-demo.yml │ ├── ci-docker-all-in-one.yml │ ├── ci-docker-build.yml │ ├── ci-docker-hotrod.yml │ ├── ci-e2e-all.yml │ ├── ci-e2e-badger.yaml │ ├── ci-e2e-cassandra.yml │ ├── ci-e2e-clickhouse.yml │ ├── ci-e2e-elasticsearch.yml │ ├── ci-e2e-grpc.yml │ ├── ci-e2e-kafka.yml │ ├── ci-e2e-memory.yaml │ ├── ci-e2e-opensearch.yml │ ├── ci-e2e-query.yml │ ├── ci-e2e-spm.yml │ ├── ci-e2e-tailsampling.yml │ ├── ci-lint-checks.yaml │ ├── ci-orchestrator-stage1.yml │ ├── ci-orchestrator-stage2.yml │ ├── ci-orchestrator-stage3.yml │ ├── ci-orchestrator.yml │ ├── ci-release.yml │ ├── ci-summary-report-publish.yml │ ├── ci-summary-report.yml │ ├── ci-unit-tests-go-tip.yml │ ├── ci-unit-tests.yml │ ├── codeql.yml │ ├── dco_merge_group.yml │ ├── dependency-review.yml │ ├── fossa.yml │ ├── label-check.yml │ ├── pr-quota-manager.yml │ ├── scorecard.yml │ ├── stale.yml │ └── waiting-for-author.yml ├── .gitignore ├── .gitmodules ├── .golangci.yml ├── .mockery.header.txt ├── .mockery.yaml ├── ADOPTERS.md ├── AGENTS.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTING_GUIDELINES.md ├── DCO ├── GOVERNANCE.md ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── NOTICE ├── README.md ├── RELEASE.md ├── SECURITY-INSIGHTS.yml ├── SECURITY.md ├── THREAT-MODEL.md ├── _To_People_of_Russia.md ├── cmd/ │ ├── anonymizer/ │ │ ├── Dockerfile │ │ ├── app/ │ │ │ ├── anonymizer/ │ │ │ │ ├── anonymizer.go │ │ │ │ ├── anonymizer_test.go │ │ │ │ └── package_test.go │ │ │ ├── flags.go │ │ │ ├── flags_test.go │ │ │ ├── query/ │ │ │ │ ├── package_test.go │ │ │ │ ├── query.go │ │ │ │ └── query_test.go │ │ │ ├── uiconv/ │ │ │ │ ├── extractor.go │ │ │ │ ├── extractor_test.go │ │ │ │ ├── fixtures/ │ │ │ │ │ ├── trace_empty.json │ │ │ │ │ ├── trace_invalid_json.json │ │ │ │ │ ├── trace_scan_error.json │ │ │ │ │ ├── trace_success.json │ │ │ │ │ └── trace_wrong_format.json │ │ │ │ ├── module.go │ │ │ │ ├── module_test.go │ │ │ │ ├── package_test.go │ │ │ │ ├── reader.go │ │ │ │ └── reader_test.go │ │ │ └── writer/ │ │ │ ├── package_test.go │ │ │ ├── writer.go │ │ │ └── writer_test.go │ │ └── main.go │ ├── es-index-cleaner/ │ │ ├── .gitignore │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── app/ │ │ │ ├── cutoff_time.go │ │ │ ├── cutoff_time_test.go │ │ │ ├── flags.go │ │ │ ├── flags_test.go │ │ │ ├── index_filter.go │ │ │ ├── index_filter_test.go │ │ │ └── package_test.go │ │ └── main.go │ ├── es-rollover/ │ │ ├── Dockerfile │ │ ├── app/ │ │ │ ├── actions.go │ │ │ ├── actions_test.go │ │ │ ├── flags.go │ │ │ ├── flags_test.go │ │ │ ├── index_options.go │ │ │ ├── index_options_test.go │ │ │ ├── init/ │ │ │ │ ├── action.go │ │ │ │ ├── action_test.go │ │ │ │ ├── flags.go │ │ │ │ ├── flags_test.go │ │ │ │ └── package_test.go │ │ │ ├── lookback/ │ │ │ │ ├── action.go │ │ │ │ ├── action_test.go │ │ │ │ ├── flags.go │ │ │ │ ├── flags_test.go │ │ │ │ ├── package_test.go │ │ │ │ ├── time_reference.go │ │ │ │ └── time_reference_test.go │ │ │ ├── package_test.go │ │ │ └── rollover/ │ │ │ ├── action.go │ │ │ ├── action_test.go │ │ │ ├── flags.go │ │ │ ├── flags_test.go │ │ │ └── package_test.go │ │ └── main.go │ ├── esmapping-generator/ │ │ └── main.go │ ├── internal/ │ │ ├── docs/ │ │ │ ├── .gitignore │ │ │ ├── command.go │ │ │ └── command_test.go │ │ ├── featuregate/ │ │ │ ├── command.go │ │ │ ├── command_test.go │ │ │ └── package_test.go │ │ ├── flags/ │ │ │ ├── admin.go │ │ │ ├── admin_test.go │ │ │ ├── doc.go │ │ │ ├── flags.go │ │ │ ├── flags_test.go │ │ │ ├── healthhost.go │ │ │ ├── healthhost_test.go │ │ │ ├── package_test.go │ │ │ ├── service.go │ │ │ └── service_test.go │ │ ├── printconfig/ │ │ │ ├── command.go │ │ │ └── command_test.go │ │ ├── status/ │ │ │ ├── command.go │ │ │ └── command_test.go │ │ └── storageconfig/ │ │ ├── config.go │ │ ├── config_test.go │ │ ├── factory.go │ │ ├── factory_test.go │ │ └── package_test.go │ ├── remote-storage/ │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── app/ │ │ │ ├── config.go │ │ │ ├── config_test.go │ │ │ ├── package_test.go │ │ │ ├── server.go │ │ │ └── server_test.go │ │ ├── config-badger.yaml │ │ ├── config.yaml │ │ └── main.go │ └── tracegen/ │ ├── Dockerfile │ ├── README.md │ ├── docker-compose.yml │ └── main.go ├── doc.go ├── docker-compose/ │ ├── cassandra/ │ │ ├── v4/ │ │ │ └── docker-compose.yaml │ │ └── v5/ │ │ └── docker-compose.yaml │ ├── clickhouse/ │ │ └── docker-compose.yml │ ├── elasticsearch/ │ │ ├── v6/ │ │ │ └── docker-compose.yml │ │ ├── v7/ │ │ │ └── docker-compose.yml │ │ ├── v8/ │ │ │ └── docker-compose.yml │ │ └── v9/ │ │ └── docker-compose.yml │ ├── kafka/ │ │ ├── README.md │ │ ├── docker-compose.yml │ │ ├── jaeger-ingester-remote-storage.yaml │ │ └── v3/ │ │ └── docker-compose.yml │ ├── monitor/ │ │ ├── .gitignore │ │ ├── Makefile │ │ ├── README.md │ │ ├── datasource.yml │ │ ├── docker-compose-elasticsearch.yml │ │ ├── docker-compose-opensearch.yml │ │ ├── docker-compose.yml │ │ ├── jaeger-ui.json │ │ ├── otel-collector-config-connector.yml │ │ └── prometheus.yml │ ├── opensearch/ │ │ ├── v1/ │ │ │ └── docker-compose.yml │ │ ├── v2/ │ │ │ └── docker-compose.yml │ │ └── v3/ │ │ └── docker-compose.yml │ ├── scylladb/ │ │ ├── README.md │ │ └── docker-compose.yml │ └── tail-sampling/ │ ├── Makefile │ ├── README.md │ ├── docker-compose.yml │ ├── jaeger-v2-config.yml │ └── otel-collector-config-connector.yml ├── docs/ │ ├── SCARF.md │ ├── adr/ │ │ ├── 001-cassandra-find-traces-duration.md │ │ ├── 002-mcp-server.md │ │ ├── 003-lazy-storage-factory-initialization.md │ │ ├── 004-migrating-coverage-gating-to-github-actions.md │ │ ├── 005-badger-storage-record-layouts.md │ │ ├── 006-internal-tracing-via-otelcol-telemetry-factory.md │ │ └── README.md │ ├── release/ │ │ └── remove-v1-checklist.md │ └── security/ │ ├── architecture.md │ ├── assurance-case.md │ ├── self-assessment.md │ ├── threat-model.md │ └── verifying-releases.md ├── empty_test.go ├── examples/ │ ├── grafana-integration/ │ │ ├── README.md │ │ ├── docker-compose.yaml │ │ ├── grafana/ │ │ │ ├── dashboard.yml │ │ │ ├── datasources.yaml │ │ │ └── hotrod_metrics_logs.json │ │ └── prometheus/ │ │ └── prometheus.yml │ ├── hotrod/ │ │ ├── .gitignore │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── cmd/ │ │ │ ├── all.go │ │ │ ├── customer.go │ │ │ ├── driver.go │ │ │ ├── empty_test.go │ │ │ ├── flags.go │ │ │ ├── frontend.go │ │ │ ├── root.go │ │ │ └── route.go │ │ ├── docker-compose.yml │ │ ├── kubernetes/ │ │ │ └── README.md │ │ ├── main.go │ │ ├── pkg/ │ │ │ ├── delay/ │ │ │ │ ├── delay.go │ │ │ │ └── empty_test.go │ │ │ ├── httperr/ │ │ │ │ ├── empty_test.go │ │ │ │ └── httperr.go │ │ │ ├── log/ │ │ │ │ ├── empty_test.go │ │ │ │ ├── factory.go │ │ │ │ ├── logger.go │ │ │ │ └── spanlogger.go │ │ │ ├── pool/ │ │ │ │ ├── empty_test.go │ │ │ │ └── pool.go │ │ │ └── tracing/ │ │ │ ├── baggage.go │ │ │ ├── empty_test.go │ │ │ ├── http.go │ │ │ ├── init.go │ │ │ ├── mutex.go │ │ │ ├── mux.go │ │ │ └── rpcmetrics/ │ │ │ ├── README.md │ │ │ ├── endpoints.go │ │ │ ├── endpoints_test.go │ │ │ ├── metrics.go │ │ │ ├── metrics_test.go │ │ │ ├── normalizer.go │ │ │ ├── normalizer_test.go │ │ │ ├── observer.go │ │ │ ├── observer_test.go │ │ │ └── package_test.go │ │ └── services/ │ │ ├── config/ │ │ │ ├── config.go │ │ │ └── empty_test.go │ │ ├── customer/ │ │ │ ├── client.go │ │ │ ├── database.go │ │ │ ├── empty_test.go │ │ │ ├── interface.go │ │ │ └── server.go │ │ ├── driver/ │ │ │ ├── client.go │ │ │ ├── driver.pb.go │ │ │ ├── driver.proto │ │ │ ├── empty_test.go │ │ │ ├── interface.go │ │ │ ├── redis.go │ │ │ └── server.go │ │ ├── frontend/ │ │ │ ├── best_eta.go │ │ │ ├── empty_test.go │ │ │ ├── server.go │ │ │ └── web_assets/ │ │ │ └── index.html │ │ └── route/ │ │ ├── client.go │ │ ├── empty_test.go │ │ ├── interface.go │ │ ├── server.go │ │ └── stats.go │ ├── oci/ │ │ ├── README.md │ │ ├── config.yaml │ │ ├── deploy-all.sh │ │ ├── ingress.yaml │ │ ├── jaeger-values.yaml │ │ ├── load-generator/ │ │ │ ├── generate_traces.py │ │ │ └── load-generator.yaml │ │ ├── monitoring-values.yaml │ │ ├── prometheus-svc.yaml │ │ ├── tls-cert/ │ │ │ └── issuer.yaml │ │ └── ui-config.json │ ├── opentracing-tutorial/ │ │ └── README.md │ ├── otel-demo/ │ │ ├── README.md │ │ ├── cleanup.sh │ │ ├── deploy-all.sh │ │ ├── generate_traces.py │ │ ├── ingress/ │ │ │ ├── README.md │ │ │ ├── clusterissuer-letsencrypt-prod.yaml │ │ │ ├── ingress-jaeger.yaml │ │ │ ├── ingress-opensearch.yaml │ │ │ └── ingress-otel-demo.yaml │ │ ├── jaeger-config.yaml │ │ ├── jaeger-query-service.yaml │ │ ├── jaeger-values.yaml │ │ ├── load-generator.yaml │ │ ├── opensearch-dashboard-values.yaml │ │ ├── opensearch-values.yaml │ │ ├── otel-demo-values.yaml │ │ └── start-port-forward.sh │ ├── reverse-proxy/ │ │ ├── README.md │ │ ├── docker-compose.yml │ │ └── httpd.conf │ └── service-performance-monitoring/ │ └── README.md ├── go.mod ├── go.sum ├── internal/ │ ├── auth/ │ │ ├── apikey/ │ │ │ ├── apikey-context.go │ │ │ ├── apikey-context_test.go │ │ │ └── package_test.go │ │ ├── bearertoken/ │ │ │ ├── context.go │ │ │ ├── context_test.go │ │ │ ├── grpc.go │ │ │ ├── grpc_test.go │ │ │ ├── http.go │ │ │ ├── http_test.go │ │ │ └── package_test.go │ │ ├── package_test.go │ │ ├── tokenloader.go │ │ ├── tokenloader_test.go │ │ ├── transport.go │ │ └── transport_test.go │ ├── cache/ │ │ ├── cache.go │ │ ├── lru.go │ │ └── lru_test.go │ ├── config/ │ │ ├── config.go │ │ ├── config_test.go │ │ ├── package_test.go │ │ ├── promcfg/ │ │ │ ├── config.go │ │ │ └── config_test.go │ │ ├── string_slice.go │ │ ├── string_slice_test.go │ │ └── tlscfg/ │ │ ├── flags.go │ │ ├── flags_test.go │ │ ├── options.go │ │ ├── options_test.go │ │ ├── package_test.go │ │ └── testdata/ │ │ ├── README.md │ │ ├── bad-CA-cert.txt │ │ ├── example-CA-cert.pem │ │ ├── example-client-cert.pem │ │ ├── example-client-key.pem │ │ ├── example-server-cert.pem │ │ ├── example-server-key.pem │ │ ├── gen-certs.sh │ │ └── wrong-CA-cert.pem │ ├── converter/ │ │ ├── doc.go │ │ ├── empty_test.go │ │ └── thrift/ │ │ ├── doc.go │ │ ├── empty_test.go │ │ └── jaeger/ │ │ ├── doc.go │ │ ├── fixtures/ │ │ │ ├── domain_01.json │ │ │ ├── domain_02.json │ │ │ ├── domain_03.json │ │ │ ├── thrift_batch_01.json │ │ │ └── thrift_batch_02.json │ │ ├── package_test.go │ │ ├── sampling_from_domain.go │ │ ├── sampling_from_domain_test.go │ │ ├── sampling_to_domain.go │ │ ├── sampling_to_domain_test.go │ │ ├── to_domain.go │ │ └── to_domain_test.go │ ├── distributedlock/ │ │ ├── empty_test.go │ │ ├── interface.go │ │ └── mocks/ │ │ └── mocks.go │ ├── fswatcher/ │ │ ├── fswatcher.go │ │ └── fswatcher_test.go │ ├── gogocodec/ │ │ ├── codec.go │ │ └── codec_test.go │ ├── grpctest/ │ │ ├── reflection.go │ │ └── reflection_test.go │ ├── gzipfs/ │ │ ├── gzip.go │ │ ├── gzip_test.go │ │ └── testdata/ │ │ └── foobar │ ├── hostname/ │ │ ├── hostname.go │ │ └── hostname_test.go │ ├── httpfs/ │ │ ├── prefixed.go │ │ ├── prefixed_test.go │ │ └── test_assets/ │ │ └── somefile.txt │ ├── jaegerclientenv2otel/ │ │ ├── envvars.go │ │ └── envvars_test.go │ ├── jiter/ │ │ ├── iter.go │ │ ├── iter_test.go │ │ └── package_test.go │ ├── jptrace/ │ │ ├── aggregator.go │ │ ├── aggregator_test.go │ │ ├── attributes.go │ │ ├── attributes_test.go │ │ ├── package_test.go │ │ ├── sanitizer/ │ │ │ ├── emptyservicename.go │ │ │ ├── emptyservicename_test.go │ │ │ ├── emptyspanname.go │ │ │ ├── emptyspanname_test.go │ │ │ ├── negative_duration_santizer.go │ │ │ ├── negative_duration_santizer_test.go │ │ │ ├── package_test.go │ │ │ ├── readonly_test.go │ │ │ ├── sanitizer.go │ │ │ ├── sanitizer_test.go │ │ │ ├── utf8.go │ │ │ └── utf8_test.go │ │ ├── spaniter.go │ │ ├── spaniter_test.go │ │ ├── spankind.go │ │ ├── spankind_test.go │ │ ├── spanmap.go │ │ ├── spanmap_test.go │ │ ├── statuscode.go │ │ ├── statuscode_test.go │ │ ├── traces.go │ │ ├── traces_test.go │ │ ├── valuetype.go │ │ ├── valuetype_test.go │ │ ├── warning.go │ │ └── warning_test.go │ ├── jtracer/ │ │ ├── jtracer.go │ │ └── jtracer_test.go │ ├── leaderelection/ │ │ ├── leader_election.go │ │ ├── leader_election_test.go │ │ └── mocks/ │ │ └── mocks.go │ ├── metrics/ │ │ ├── benchmark/ │ │ │ └── benchmark_test.go │ │ ├── counter.go │ │ ├── factory.go │ │ ├── gauge.go │ │ ├── histogram.go │ │ ├── metrics.go │ │ ├── metrics_test.go │ │ ├── metricsbuilder/ │ │ │ ├── builder.go │ │ │ └── builder_test.go │ │ ├── otelmetrics/ │ │ │ ├── counter.go │ │ │ ├── factory.go │ │ │ ├── factory_test.go │ │ │ ├── gauge.go │ │ │ ├── histogram.go │ │ │ └── timer.go │ │ ├── package.go │ │ ├── prometheus/ │ │ │ ├── cache.go │ │ │ ├── factory.go │ │ │ └── factory_test.go │ │ ├── stopwatch.go │ │ └── timer.go │ ├── metricstest/ │ │ ├── keys.go │ │ ├── local.go │ │ ├── local_test.go │ │ ├── metricstest.go │ │ ├── metricstest_test.go │ │ └── package_test.go │ ├── proto/ │ │ ├── api_v3/ │ │ │ └── query_service.pb.go │ │ └── metrics/ │ │ ├── README.md │ │ ├── openmetrics.proto │ │ └── otelspankind.proto │ ├── proto-gen/ │ │ ├── .gitignore │ │ ├── api_v2/ │ │ │ └── metrics/ │ │ │ ├── openmetrics.pb.go │ │ │ └── otelspankind.pb.go │ │ ├── patch.sed │ │ ├── storage/ │ │ │ └── v2/ │ │ │ ├── dependency_storage.pb.go │ │ │ └── trace_storage.pb.go │ │ ├── storage_v1/ │ │ │ ├── mocks/ │ │ │ │ └── mocks.go │ │ │ └── storage.pb.go │ │ └── zipkin/ │ │ └── zipkin.pb.go │ ├── recoveryhandler/ │ │ ├── zap.go │ │ └── zap_test.go │ ├── safeexpvar/ │ │ ├── safeexpvar.go │ │ └── safeexpvar_test.go │ ├── sampling/ │ │ ├── grpc/ │ │ │ ├── grpc_handler.go │ │ │ └── grpc_handler_test.go │ │ ├── http/ │ │ │ ├── cfgmgr.go │ │ │ ├── cfgmgr_test.go │ │ │ ├── handler.go │ │ │ ├── handler_test.go │ │ │ └── package_test.go │ │ └── samplingstrategy/ │ │ ├── adaptive/ │ │ │ ├── README.md │ │ │ ├── aggregator.go │ │ │ ├── aggregator_test.go │ │ │ ├── cache.go │ │ │ ├── cache_test.go │ │ │ ├── calculationstrategy/ │ │ │ │ ├── interface.go │ │ │ │ ├── package_test.go │ │ │ │ ├── percentage_increase_capped_calculator.go │ │ │ │ └── percentage_increase_capped_calculator_test.go │ │ │ ├── floatutils.go │ │ │ ├── floatutils_test.go │ │ │ ├── options.go │ │ │ ├── options_test.go │ │ │ ├── package_test.go │ │ │ ├── post_aggregator.go │ │ │ ├── post_aggregator_test.go │ │ │ ├── provider.go │ │ │ ├── provider_test.go │ │ │ ├── weightvectorcache.go │ │ │ └── weightvectorcache_test.go │ │ ├── aggregator.go │ │ ├── empty_test.go │ │ ├── factory.go │ │ ├── file/ │ │ │ ├── constants.go │ │ │ ├── fixtures/ │ │ │ │ ├── TestServiceNoPerOperationStrategiesDeprecatedBehavior_ServiceA.json │ │ │ │ ├── TestServiceNoPerOperationStrategiesDeprecatedBehavior_ServiceB.json │ │ │ │ ├── TestServiceNoPerOperationStrategies_ServiceA.json │ │ │ │ ├── TestServiceNoPerOperationStrategies_ServiceB.json │ │ │ │ ├── bad_strategies.json │ │ │ │ ├── missing-service-types.json │ │ │ │ ├── operation_strategies.json │ │ │ │ ├── service_no_per_operation.json │ │ │ │ └── strategies.json │ │ │ ├── options.go │ │ │ ├── package_test.go │ │ │ ├── provider.go │ │ │ ├── provider_test.go │ │ │ └── strategy.go │ │ └── provider.go │ ├── storage/ │ │ ├── cassandra/ │ │ │ ├── config/ │ │ │ │ ├── config.go │ │ │ │ ├── config_test.go │ │ │ │ └── package_test.go │ │ │ ├── empty_test.go │ │ │ ├── gocql/ │ │ │ │ ├── empty_test.go │ │ │ │ ├── gocql.go │ │ │ │ └── testutils/ │ │ │ │ ├── udt.go │ │ │ │ └── udt_test.go │ │ │ ├── metrics/ │ │ │ │ ├── table.go │ │ │ │ └── table_test.go │ │ │ ├── mocks/ │ │ │ │ └── mocks.go │ │ │ └── session.go │ │ ├── distributedlock/ │ │ │ └── cassandra/ │ │ │ ├── lock.go │ │ │ └── lock_test.go │ │ ├── elasticsearch/ │ │ │ ├── client/ │ │ │ │ ├── basic_auth.go │ │ │ │ ├── basic_auth_test.go │ │ │ │ ├── client.go │ │ │ │ ├── cluster_client.go │ │ │ │ ├── cluster_client_test.go │ │ │ │ ├── ilm_client.go │ │ │ │ ├── ilm_client_test.go │ │ │ │ ├── index_client.go │ │ │ │ ├── index_client_test.go │ │ │ │ ├── interfaces.go │ │ │ │ ├── mocks/ │ │ │ │ │ └── mocks.go │ │ │ │ └── package_test.go │ │ │ ├── client.go │ │ │ ├── config/ │ │ │ │ ├── auth_helper.go │ │ │ │ ├── auth_helper_test.go │ │ │ │ ├── config.go │ │ │ │ └── config_test.go │ │ │ ├── dbmodel/ │ │ │ │ ├── dot_replacer.go │ │ │ │ ├── dot_replacer_test.go │ │ │ │ ├── model.go │ │ │ │ └── package_test.go │ │ │ ├── empty_test.go │ │ │ ├── errors.go │ │ │ ├── errors_test.go │ │ │ ├── filter/ │ │ │ │ ├── alias.go │ │ │ │ ├── alias_test.go │ │ │ │ ├── date.go │ │ │ │ ├── date_test.go │ │ │ │ └── package_test.go │ │ │ ├── mocks/ │ │ │ │ └── mocks.go │ │ │ ├── query/ │ │ │ │ ├── range_query.go │ │ │ │ └── range_query_test.go │ │ │ ├── textTemplate.go │ │ │ ├── textTemplate_test.go │ │ │ └── wrapper/ │ │ │ ├── empty_test.go │ │ │ ├── wrapper.go │ │ │ └── wrapper_nolint.go │ │ ├── integration/ │ │ │ ├── badgerstore_test.go │ │ │ ├── cassandra_test.go │ │ │ ├── dates.go │ │ │ ├── dates_test.go │ │ │ ├── elasticsearch_test.go │ │ │ ├── es_index_cleaner_test.go │ │ │ ├── es_index_rollover_test.go │ │ │ ├── fixtures/ │ │ │ │ ├── grpc_plugin_conf.yaml │ │ │ │ ├── queries.json │ │ │ │ ├── queries_es.json │ │ │ │ └── traces/ │ │ │ │ ├── default.json │ │ │ │ ├── dur_trace.json │ │ │ │ ├── example_trace.json │ │ │ │ ├── log_tags_trace.json │ │ │ │ ├── max_dur_trace.json │ │ │ │ ├── multi_index_trace.json │ │ │ │ ├── multi_spot_tags_trace.json │ │ │ │ ├── multiple1_trace.json │ │ │ │ ├── multiple2_trace.json │ │ │ │ ├── multiple3_trace.json │ │ │ │ ├── multispottag_dur_trace.json │ │ │ │ ├── multispottag_maxdur_trace.json │ │ │ │ ├── multispottag_opname_dur_trace.json │ │ │ │ ├── multispottag_opname_maxdur_trace.json │ │ │ │ ├── multispottag_opname_trace.json │ │ │ │ ├── opname_dur_trace.json │ │ │ │ ├── opname_maxdur_trace.json │ │ │ │ ├── opname_trace.json │ │ │ │ ├── process_tags_trace.json │ │ │ │ ├── scope_name_version_trace.json │ │ │ │ ├── span_tags_trace.json │ │ │ │ ├── tags_dur_trace.json │ │ │ │ ├── tags_escaped_operator_trace_1.json │ │ │ │ ├── tags_escaped_operator_trace_2.json │ │ │ │ ├── tags_maxdur_trace.json │ │ │ │ ├── tags_opname_dur_trace.json │ │ │ │ ├── tags_opname_maxdur_trace.json │ │ │ │ ├── tags_opname_trace.json │ │ │ │ ├── tags_wildcard_regex_1.json │ │ │ │ └── tags_wildcard_regex_2.json │ │ │ ├── grpc_test.go │ │ │ ├── integration.go │ │ │ ├── memstore_test.go │ │ │ ├── package_test.go │ │ │ ├── remote_memory_storage.go │ │ │ ├── trace_compare.go │ │ │ └── trace_compare_test.go │ │ ├── metricstore/ │ │ │ ├── disabled/ │ │ │ │ ├── factory.go │ │ │ │ ├── factory_test.go │ │ │ │ ├── package_test.go │ │ │ │ ├── reader.go │ │ │ │ └── reader_test.go │ │ │ ├── elasticsearch/ │ │ │ │ ├── README.md │ │ │ │ ├── factory.go │ │ │ │ ├── factory_test.go │ │ │ │ ├── package_test.go │ │ │ │ ├── processor.go │ │ │ │ ├── processor_test.go │ │ │ │ ├── query_builder.go │ │ │ │ ├── query_builder_test.go │ │ │ │ ├── query_logger.go │ │ │ │ ├── query_logger_test.go │ │ │ │ ├── reader.go │ │ │ │ ├── reader_test.go │ │ │ │ ├── testdata/ │ │ │ │ │ ├── output_call_rate.json │ │ │ │ │ ├── output_call_rate_operation.json │ │ │ │ │ ├── output_empty.json │ │ │ │ │ ├── output_error_es.json │ │ │ │ │ ├── output_error_latencies.json │ │ │ │ │ ├── output_errors_rate.json │ │ │ │ │ ├── output_errors_rate_operation.json │ │ │ │ │ ├── output_latencies.json │ │ │ │ │ ├── output_latencies_50.json │ │ │ │ │ ├── output_latencies_75.json │ │ │ │ │ ├── output_latencies_95.json │ │ │ │ │ ├── output_latencies_operation.json │ │ │ │ │ └── output_valid_es.json │ │ │ │ ├── to_domain.go │ │ │ │ └── to_domain_test.go │ │ │ ├── factory.go │ │ │ ├── factory_config.go │ │ │ ├── factory_config_test.go │ │ │ ├── factory_test.go │ │ │ ├── package_test.go │ │ │ └── prometheus/ │ │ │ ├── factory.go │ │ │ ├── factory_test.go │ │ │ ├── metricstore/ │ │ │ │ ├── dbmodel/ │ │ │ │ │ ├── to_domain.go │ │ │ │ │ └── to_domain_test.go │ │ │ │ ├── reader.go │ │ │ │ ├── reader_test.go │ │ │ │ └── testdata/ │ │ │ │ ├── empty_response.json │ │ │ │ ├── service_datapoint_response.json │ │ │ │ ├── service_span_name_datapoint_response.json │ │ │ │ └── warning_response.json │ │ │ ├── options.go │ │ │ └── options_test.go │ │ ├── v1/ │ │ │ ├── api/ │ │ │ │ ├── README.md │ │ │ │ ├── dependencystore/ │ │ │ │ │ ├── empty_test.go │ │ │ │ │ ├── interface.go │ │ │ │ │ └── mocks/ │ │ │ │ │ └── mocks.go │ │ │ │ ├── doc.go │ │ │ │ ├── empty_test.go │ │ │ │ ├── metricstore/ │ │ │ │ │ ├── empty_test.go │ │ │ │ │ ├── interface.go │ │ │ │ │ ├── metricstoremetrics/ │ │ │ │ │ │ ├── decorator.go │ │ │ │ │ │ └── decorator_test.go │ │ │ │ │ └── mocks/ │ │ │ │ │ └── mocks.go │ │ │ │ ├── samplingstore/ │ │ │ │ │ ├── empty_test.go │ │ │ │ │ ├── interface.go │ │ │ │ │ ├── mocks/ │ │ │ │ │ │ └── mocks.go │ │ │ │ │ └── model/ │ │ │ │ │ ├── empty_test.go │ │ │ │ │ └── sampling.go │ │ │ │ └── spanstore/ │ │ │ │ ├── interface.go │ │ │ │ ├── interface_test.go │ │ │ │ ├── mocks/ │ │ │ │ │ └── mocks.go │ │ │ │ └── spanstoremetrics/ │ │ │ │ ├── package_test.go │ │ │ │ ├── read_metrics.go │ │ │ │ ├── read_metrics_test.go │ │ │ │ ├── write_metrics.go │ │ │ │ └── write_metrics_test.go │ │ │ ├── badger/ │ │ │ │ ├── README.md │ │ │ │ ├── config.go │ │ │ │ ├── config_test.go │ │ │ │ ├── dependencystore/ │ │ │ │ │ ├── package_test.go │ │ │ │ │ ├── storage.go │ │ │ │ │ ├── storage_internal_test.go │ │ │ │ │ └── storage_test.go │ │ │ │ ├── docs/ │ │ │ │ │ ├── storage-file-non-root-permission.md │ │ │ │ │ └── upgrade-v1-to-v3.md │ │ │ │ ├── factory.go │ │ │ │ ├── factory_test.go │ │ │ │ ├── lock.go │ │ │ │ ├── lock_test.go │ │ │ │ ├── options.go │ │ │ │ ├── options_test.go │ │ │ │ ├── package_test.go │ │ │ │ ├── samplingstore/ │ │ │ │ │ ├── storage.go │ │ │ │ │ └── storage_test.go │ │ │ │ ├── spanstore/ │ │ │ │ │ ├── cache.go │ │ │ │ │ ├── cache_test.go │ │ │ │ │ ├── package_test.go │ │ │ │ │ ├── read_write_test.go │ │ │ │ │ ├── reader.go │ │ │ │ │ ├── rw_internal_test.go │ │ │ │ │ └── writer.go │ │ │ │ ├── stats.go │ │ │ │ ├── stats_linux.go │ │ │ │ ├── stats_linux_test.go │ │ │ │ └── stats_test.go │ │ │ ├── blackhole/ │ │ │ │ ├── blackhole.go │ │ │ │ ├── blackhole_test.go │ │ │ │ ├── factory.go │ │ │ │ ├── factory_test.go │ │ │ │ └── package_test.go │ │ │ ├── cassandra/ │ │ │ │ ├── Dockerfile │ │ │ │ ├── dependencystore/ │ │ │ │ │ ├── bootstrap.go │ │ │ │ │ ├── bootstrap_test.go │ │ │ │ │ ├── model.go │ │ │ │ │ ├── model_test.go │ │ │ │ │ ├── package_test.go │ │ │ │ │ ├── storage.go │ │ │ │ │ └── storage_test.go │ │ │ │ ├── factory.go │ │ │ │ ├── factory_test.go │ │ │ │ ├── helper.go │ │ │ │ ├── options.go │ │ │ │ ├── options_test.go │ │ │ │ ├── package_test.go │ │ │ │ ├── samplingstore/ │ │ │ │ │ ├── storage.go │ │ │ │ │ └── storage_test.go │ │ │ │ ├── savetracetest/ │ │ │ │ │ └── main.go │ │ │ │ ├── schema/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── create.sh │ │ │ │ │ ├── create.test.sh │ │ │ │ │ ├── docker.sh │ │ │ │ │ ├── migration/ │ │ │ │ │ │ ├── V002toV003.sh │ │ │ │ │ │ ├── v001tov002part1.sh │ │ │ │ │ │ └── v001tov002part2.sh │ │ │ │ │ ├── package_test.go │ │ │ │ │ ├── schema.go │ │ │ │ │ ├── schema_test.go │ │ │ │ │ ├── v001.cql.tmpl │ │ │ │ │ ├── v002.cql.tmpl │ │ │ │ │ ├── v003.cql.tmpl │ │ │ │ │ ├── v004-go-tmpl.cql.tmpl │ │ │ │ │ └── v004.cql.tmpl │ │ │ │ └── spanstore/ │ │ │ │ ├── dbmodel/ │ │ │ │ │ ├── converter.go │ │ │ │ │ ├── converter_test.go │ │ │ │ │ ├── cql_udt.go │ │ │ │ │ ├── cql_udt_test.go │ │ │ │ │ ├── ids.go │ │ │ │ │ ├── ids_test.go │ │ │ │ │ ├── index_filter.go │ │ │ │ │ ├── index_filter_test.go │ │ │ │ │ ├── model.go │ │ │ │ │ ├── model_test.go │ │ │ │ │ ├── operation.go │ │ │ │ │ ├── package_test.go │ │ │ │ │ ├── tag_filter.go │ │ │ │ │ ├── tag_filter_drop_all.go │ │ │ │ │ ├── tag_filter_drop_all_test.go │ │ │ │ │ ├── tag_filter_exact_match.go │ │ │ │ │ ├── tag_filter_exact_match_test.go │ │ │ │ │ ├── tag_filter_test.go │ │ │ │ │ ├── unique_ids.go │ │ │ │ │ ├── unique_ids_test.go │ │ │ │ │ ├── unique_tags.go │ │ │ │ │ └── unique_tags_test.go │ │ │ │ ├── matchers_test.go │ │ │ │ ├── mocks/ │ │ │ │ │ └── mocks.go │ │ │ │ ├── operation_names.go │ │ │ │ ├── operation_names_test.go │ │ │ │ ├── package_test.go │ │ │ │ ├── reader.go │ │ │ │ ├── reader_test.go │ │ │ │ ├── service_names.go │ │ │ │ ├── service_names_test.go │ │ │ │ ├── writer.go │ │ │ │ ├── writer_options.go │ │ │ │ ├── writer_options_test.go │ │ │ │ └── writer_test.go │ │ │ ├── configurable.go │ │ │ ├── elasticsearch/ │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── dependencystore/ │ │ │ │ │ ├── package_test.go │ │ │ │ │ ├── storagev1.go │ │ │ │ │ └── storagev1_test.go │ │ │ │ ├── factory.go │ │ │ │ ├── factory_test.go │ │ │ │ ├── fixtures/ │ │ │ │ │ ├── tags_01.txt │ │ │ │ │ └── tags_02.txt │ │ │ │ ├── mappings/ │ │ │ │ │ ├── command.go │ │ │ │ │ ├── command_test.go │ │ │ │ │ ├── fixtures/ │ │ │ │ │ │ ├── jaeger-dependencies-6.json │ │ │ │ │ │ ├── jaeger-dependencies-7.json │ │ │ │ │ │ ├── jaeger-dependencies-8.json │ │ │ │ │ │ ├── jaeger-sampling-6.json │ │ │ │ │ │ ├── jaeger-sampling-7.json │ │ │ │ │ │ ├── jaeger-sampling-8.json │ │ │ │ │ │ ├── jaeger-service-6.json │ │ │ │ │ │ ├── jaeger-service-7.json │ │ │ │ │ │ ├── jaeger-service-8.json │ │ │ │ │ │ ├── jaeger-span-6.json │ │ │ │ │ │ ├── jaeger-span-7.json │ │ │ │ │ │ └── jaeger-span-8.json │ │ │ │ │ ├── flags.go │ │ │ │ │ ├── flags_test.go │ │ │ │ │ ├── jaeger-dependencies-6.json │ │ │ │ │ ├── jaeger-dependencies-7.json │ │ │ │ │ ├── jaeger-dependencies-8.json │ │ │ │ │ ├── jaeger-sampling-6.json │ │ │ │ │ ├── jaeger-sampling-7.json │ │ │ │ │ ├── jaeger-sampling-8.json │ │ │ │ │ ├── jaeger-service-6.json │ │ │ │ │ ├── jaeger-service-7.json │ │ │ │ │ ├── jaeger-service-8.json │ │ │ │ │ ├── jaeger-span-6.json │ │ │ │ │ ├── jaeger-span-7.json │ │ │ │ │ ├── jaeger-span-8.json │ │ │ │ │ ├── mapping.go │ │ │ │ │ └── mapping_test.go │ │ │ │ ├── options.go │ │ │ │ ├── options_test.go │ │ │ │ ├── package_test.go │ │ │ │ ├── samplingstore/ │ │ │ │ │ ├── dbmodel/ │ │ │ │ │ │ ├── converter.go │ │ │ │ │ │ ├── converter_test.go │ │ │ │ │ │ └── model.go │ │ │ │ │ ├── storage.go │ │ │ │ │ └── storage_test.go │ │ │ │ └── spanstore/ │ │ │ │ ├── core_span_reader.go │ │ │ │ ├── fixtures/ │ │ │ │ │ ├── domain_01.json │ │ │ │ │ ├── es_01.json │ │ │ │ │ ├── query_01.json │ │ │ │ │ ├── query_02.json │ │ │ │ │ └── query_03.json │ │ │ │ ├── from_domain.go │ │ │ │ ├── from_domain_test.go │ │ │ │ ├── index_utils.go │ │ │ │ ├── json_span_compare_test.go │ │ │ │ ├── mocks/ │ │ │ │ │ └── mocks.go │ │ │ │ ├── package_test.go │ │ │ │ ├── reader.go │ │ │ │ ├── reader_test.go │ │ │ │ ├── readerv1.go │ │ │ │ ├── readerv1_test.go │ │ │ │ ├── service_operation.go │ │ │ │ ├── service_operation_test.go │ │ │ │ ├── to_domain.go │ │ │ │ ├── to_domain_test.go │ │ │ │ ├── writer.go │ │ │ │ ├── writer_test.go │ │ │ │ ├── writerv1.go │ │ │ │ └── writerv1_test.go │ │ │ ├── factory.go │ │ │ ├── mocks/ │ │ │ │ └── mocks.go │ │ │ └── package_test.go │ │ └── v2/ │ │ ├── api/ │ │ │ ├── depstore/ │ │ │ │ ├── factory.go │ │ │ │ ├── mocks/ │ │ │ │ │ └── mocks.go │ │ │ │ ├── package_test.go │ │ │ │ ├── reader.go │ │ │ │ └── writer.go │ │ │ ├── package_test.go │ │ │ └── tracestore/ │ │ │ ├── empty_test.go │ │ │ ├── factory.go │ │ │ ├── mocks/ │ │ │ │ └── mocks.go │ │ │ ├── reader.go │ │ │ ├── reader_test.go │ │ │ ├── tracestoremetrics/ │ │ │ │ ├── package_test.go │ │ │ │ ├── reader_metrics.go │ │ │ │ └── reader_metrics_test.go │ │ │ └── writer.go │ │ ├── badger/ │ │ │ ├── factory.go │ │ │ ├── factory_test.go │ │ │ └── package_test.go │ │ ├── cassandra/ │ │ │ ├── factory.go │ │ │ ├── factory_test.go │ │ │ ├── package_test.go │ │ │ └── tracestore/ │ │ │ ├── fixtures/ │ │ │ │ ├── .gitignore │ │ │ │ ├── cas_01.json │ │ │ │ └── otel_traces_01.json │ │ │ ├── from_dbmodel.go │ │ │ ├── from_dbmodel_test.go │ │ │ ├── package_test.go │ │ │ ├── reader.go │ │ │ ├── reader_test.go │ │ │ ├── to_dbmodel.go │ │ │ └── to_dbmodel_test.go │ │ ├── clickhouse/ │ │ │ ├── README.md │ │ │ ├── clickhousetest/ │ │ │ │ ├── package_test.go │ │ │ │ └── server.go │ │ │ ├── config.go │ │ │ ├── config_test.go │ │ │ ├── depstore/ │ │ │ │ ├── package_test.go │ │ │ │ ├── reader.go │ │ │ │ └── reader_test.go │ │ │ ├── factory.go │ │ │ ├── factory_test.go │ │ │ ├── package_test.go │ │ │ ├── sql/ │ │ │ │ ├── create_attribute_metadata_mv.sql │ │ │ │ ├── create_attribute_metadata_table.sql │ │ │ │ ├── create_event_attribute_metadata_mv.sql │ │ │ │ ├── create_link_attribute_metadata_mv.sql │ │ │ │ ├── create_operations_mv.sql │ │ │ │ ├── create_operations_table.sql │ │ │ │ ├── create_services_mv.sql │ │ │ │ ├── create_services_table.sql │ │ │ │ ├── create_spans_table.sql │ │ │ │ ├── create_trace_id_timestamps_mv.sql │ │ │ │ ├── create_trace_id_timestamps_table.sql │ │ │ │ ├── package_test.go │ │ │ │ └── queries.go │ │ │ └── tracestore/ │ │ │ ├── assert_test.go │ │ │ ├── attribute_metadata.go │ │ │ ├── attribute_metadata_test.go │ │ │ ├── dbmodel/ │ │ │ │ ├── attribute_metadata.go │ │ │ │ ├── dbmodel_test.go │ │ │ │ ├── from.go │ │ │ │ ├── from_test.go │ │ │ │ ├── operation.go │ │ │ │ ├── package_test.go │ │ │ │ ├── service.go │ │ │ │ ├── spanrow.go │ │ │ │ ├── to.go │ │ │ │ └── to_test.go │ │ │ ├── driver_test.go │ │ │ ├── package_test.go │ │ │ ├── query_builder.go │ │ │ ├── query_builder_test.go │ │ │ ├── reader.go │ │ │ ├── reader_test.go │ │ │ ├── snapshots/ │ │ │ │ ├── TestFindTraceIDs_1.sql │ │ │ │ ├── TestFindTraceIDs_2.sql │ │ │ │ ├── TestFindTraces_Success/ │ │ │ │ │ ├── multiple_spans_1.sql │ │ │ │ │ └── single_span_1.sql │ │ │ │ ├── TestFindTraces_WithFilters_1.sql │ │ │ │ ├── TestFindTraces_WithFilters_2.sql │ │ │ │ ├── TestGetOperations/ │ │ │ │ │ ├── successfully_returns_operations_by_kind_1.sql │ │ │ │ │ └── successfully_returns_operations_for_all_kinds_1.sql │ │ │ │ ├── TestGetServices/ │ │ │ │ │ └── successfully_returns_services_1.sql │ │ │ │ ├── TestGetTraces_Success/ │ │ │ │ │ ├── multiple_spans_1.sql │ │ │ │ │ ├── single_span_1.sql │ │ │ │ │ └── with_time_range_1.sql │ │ │ │ └── TestWriter_Success_1.sql │ │ │ ├── spans_test.go │ │ │ ├── writer.go │ │ │ └── writer_test.go │ │ ├── elasticsearch/ │ │ │ ├── depstore/ │ │ │ │ ├── dbmodel/ │ │ │ │ │ ├── converter.go │ │ │ │ │ ├── converter_test.go │ │ │ │ │ └── model.go │ │ │ │ ├── mocks/ │ │ │ │ │ └── mocks.go │ │ │ │ ├── storage.go │ │ │ │ ├── storage_test.go │ │ │ │ ├── storagev2.go │ │ │ │ └── storagev2_test.go │ │ │ ├── factory.go │ │ │ ├── factory_test.go │ │ │ ├── package_test.go │ │ │ └── tracestore/ │ │ │ ├── fixtures/ │ │ │ │ ├── .gitignore │ │ │ │ ├── es_01.json │ │ │ │ ├── es_01_string_tags.json │ │ │ │ └── otel_traces_01.json │ │ │ ├── from_dbmodel.go │ │ │ ├── from_dbmodel_test.go │ │ │ ├── ids.go │ │ │ ├── package_test.go │ │ │ ├── reader.go │ │ │ ├── reader_test.go │ │ │ ├── to_dbmodel.go │ │ │ ├── to_dbmodel_test.go │ │ │ ├── writer.go │ │ │ └── writer_test.go │ │ ├── grpc/ │ │ │ ├── README.md │ │ │ ├── config.go │ │ │ ├── config_test.go │ │ │ ├── depreader.go │ │ │ ├── depreader_test.go │ │ │ ├── factory.go │ │ │ ├── factory_test.go │ │ │ ├── handler.go │ │ │ ├── handler_test.go │ │ │ ├── package_test.go │ │ │ ├── tracereader.go │ │ │ ├── tracereader_test.go │ │ │ ├── tracewriter.go │ │ │ └── tracewriter_test.go │ │ ├── memory/ │ │ │ ├── config.go │ │ │ ├── config_test.go │ │ │ ├── factory.go │ │ │ ├── factory_test.go │ │ │ ├── fixtures/ │ │ │ │ ├── db_traces_01.json │ │ │ │ ├── db_traces_02.json │ │ │ │ └── otel_traces_01.json │ │ │ ├── lock.go │ │ │ ├── lock_test.go │ │ │ ├── memory.go │ │ │ ├── memory_test.go │ │ │ ├── package_test.go │ │ │ ├── sampling.go │ │ │ ├── sampling_test.go │ │ │ └── tenant.go │ │ └── v1adapter/ │ │ ├── README.md │ │ ├── depreader.go │ │ ├── depreader_test.go │ │ ├── factory.go │ │ ├── factory_test.go │ │ ├── otelids.go │ │ ├── otelids_test.go │ │ ├── package_test.go │ │ ├── spanreader.go │ │ ├── spanreader_test.go │ │ ├── spanwriter.go │ │ ├── spanwriter_test.go │ │ ├── tracereader.go │ │ ├── tracereader_test.go │ │ ├── tracewriter.go │ │ ├── tracewriter_test.go │ │ ├── translator.go │ │ └── translator_test.go │ ├── telemetry/ │ │ ├── otelsemconv/ │ │ │ ├── empty_test.go │ │ │ ├── semconv.go │ │ │ └── semconv_test.go │ │ ├── settings.go │ │ └── settings_test.go │ ├── tenancy/ │ │ ├── context.go │ │ ├── context_test.go │ │ ├── flags.go │ │ ├── flags_test.go │ │ ├── grpc.go │ │ ├── grpc_test.go │ │ ├── http.go │ │ ├── http_test.go │ │ ├── manage_test.go │ │ ├── manager.go │ │ └── package_test.go │ ├── testutils/ │ │ ├── leakcheck.go │ │ ├── leakcheck_test.go │ │ ├── logger.go │ │ └── logger_test.go │ ├── tools/ │ │ ├── empty.go │ │ ├── go.mod │ │ ├── go.sum │ │ └── tools.go │ ├── tracegen/ │ │ ├── config.go │ │ ├── config_test.go │ │ ├── package_test.go │ │ ├── worker.go │ │ └── worker_test.go │ ├── uimodel/ │ │ ├── converter/ │ │ │ └── v1/ │ │ │ └── json/ │ │ │ ├── doc.go │ │ │ ├── fixtures/ │ │ │ │ ├── domain_01.json │ │ │ │ ├── domain_es_01.json │ │ │ │ ├── es_01.json │ │ │ │ └── ui_01.json │ │ │ ├── from_domain.go │ │ │ ├── from_domain_test.go │ │ │ ├── json_span_compare_test.go │ │ │ ├── package_test.go │ │ │ ├── process_hashtable.go │ │ │ ├── process_hashtable_test.go │ │ │ ├── sampling.go │ │ │ └── sampling_test.go │ │ ├── doc.go │ │ ├── empty_test.go │ │ └── model.go │ └── version/ │ ├── build.go │ ├── build_test.go │ ├── command.go │ ├── command_test.go │ ├── handler.go │ ├── handler_test.go │ └── package_test.go ├── monitoring/ │ └── jaeger-mixin/ │ ├── README.md │ ├── alerts.libsonnet │ ├── dashboard-for-grafana.json │ ├── dashboards.libsonnet │ ├── jsonnetfile.json │ ├── mixin.libsonnet │ ├── monitoring-setup.example.jsonnet │ ├── prometheus_alerts.yml │ └── prometheus_alerts_v2.yml ├── ports/ │ ├── ports.go │ └── ports_test.go ├── renovate.json └── scripts/ ├── build/ │ ├── build-all-in-one-image.sh │ ├── build-hotrod-image.sh │ ├── build-upload-a-docker-image.sh │ ├── build-upload-docker-images.sh │ ├── clean-binaries.sh │ ├── docker/ │ │ ├── base/ │ │ │ └── Dockerfile │ │ └── debug/ │ │ ├── Dockerfile │ │ ├── go.mod │ │ ├── go.sum │ │ └── tools.go │ ├── package-deploy.sh │ ├── rebuild-ui.sh │ └── upload-docker-readme.sh ├── e2e/ │ ├── adaptive-sampling-integration-test.sh │ ├── cassandra.sh │ ├── clickhouse.sh │ ├── compare_metrics.py │ ├── elasticsearch.sh │ ├── filter_coverage.py │ ├── kafka.sh │ ├── metrics_summary.py │ ├── metrics_summary.sh │ └── spm.sh ├── lint/ │ ├── check-go-version.sh │ ├── check-goleak-files.sh │ ├── check-jaeger-idl-version.sh │ ├── check-semconv-version.sh │ ├── check-test-files.sh │ ├── dco_check.py │ ├── import-order-cleanup.py │ ├── replace_license_headers.py │ ├── update-semconv-version.sh │ └── updateLicense.py ├── makefiles/ │ ├── BuildBinaries.mk │ ├── BuildInfo.mk │ ├── Docker.mk │ ├── IntegrationTests.mk │ ├── Protobuf.mk │ ├── Tools.mk │ └── Windows.mk ├── release/ │ ├── draft.py │ ├── formatter.py │ ├── notes.py │ ├── prepare.sh │ ├── rotate-managers.py │ ├── start.sh │ └── update-changelog.py └── utils/ ├── compare_metrics.py ├── compute-tags.sh ├── compute-tags.test.sh ├── compute-version.sh ├── docker-login.sh ├── find-official-remote.sh ├── generate-help-output.sh ├── ids-to-base64.py ├── metrics-md.py ├── platforms-to-gh-matrix.sh └── run-tests.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .codecov.yml ================================================ codecov: notify: require_ci_to_pass: yes after_n_builds: 18 strict_yaml_branch: main # only use the latest copy on the main branch ignore: - "**/*.pb.go" - "**/mocks/*" - "proto-gen/*/*" - "thrift-gen/*/*" - "**/main.go" - "examples/hotrod" - "internal/storage/integration" - "cmd/jaeger/internal/integration" - "internal/tools" coverage: precision: 2 round: down range: "95...100" status: project: default: enabled: yes target: 95% patch: default: enabled: yes target: 95% ================================================ FILE: .fossa.yml ================================================ # FOSSA Configuration File # https://github.com/fossas/fossa-cli/blob/master/docs/references/files/fossa-yml.md # # This configuration excludes development-only tooling from license scanning. # The excluded paths contain GPL-3.0 licensed tools (golangci-lint and its plugins) # that are used only during development and are NOT compiled into or distributed # with the final Jaeger binaries. # # These tools are in separate Go modules specifically to isolate them from # production dependencies. Using GPL-licensed development tools to build # Apache 2.0 licensed software does not create any license compliance issues. version: 3 project: id: git+github.com/jaegertracing/jaeger name: jaeger paths: exclude: # Development tooling (golangci-lint and plugins) - GPL-3.0 licensed # These are NOT part of the distributed binaries - ./internal/tools # Debug build utilities - only used for debugging, not distributed - ./scripts/build/docker/debug # Git submodules - scanned separately in their own repositories - ./idl - ./jaeger-ui ================================================ FILE: .github/CODEOWNERS ================================================ * @jaegertracing/jaeger-maintainers ================================================ FILE: .github/actions/block-pr-from-main-branch/action.yml ================================================ name: 'block-pr-not-on-main' description: 'Blocks PRs from main branch of forked repository' runs: using: "composite" steps: - name: Ensure PR is not on main branch shell: bash run: | echo "Repo: ${{ github.repository }}" echo "Head Repo: ${{ github.event.pull_request.head.repo.full_name }}" echo "Forked: ${{ github.event.pull_request.head.repo.fork }}" echo "Branch: ${{ github.event.pull_request.head.ref }}" if [ "${{ github.event.pull_request.head.repo.fork }}" == "true" ] && [ "${{ github.event.pull_request.head.ref }}" == 'main' ]; then echo "Error 🛑: PRs from the main branch of forked repositories are not allowed." echo " Please create a named branch and resubmit the PR." echo " See https://github.com/jaegertracing/jaeger/blob/main/CONTRIBUTING_GUIDELINES.md#branches" exit 1 fi ================================================ FILE: .github/actions/setup-branch/action.yml ================================================ name: 'Setup BRANCH' description: 'Make BRANCH var accessible to job' runs: using: "composite" steps: - name: Setup BRANCH shell: bash run: | echo GITHUB_EVENT_NAME=${GITHUB_EVENT_NAME} echo GITHUB_HEAD_REF=${GITHUB_HEAD_REF} echo GITHUB_REF=${GITHUB_REF} case ${GITHUB_EVENT_NAME} in pull_request) BRANCH=${GITHUB_HEAD_REF} if [[ $BRANCH == 'main' ]]; then BRANCH=main_from_fork fi ;; *) BRANCH=${GITHUB_REF##*/} ;; esac echo "we are on branch=${BRANCH}" echo "BRANCH=${BRANCH}" >> ${GITHUB_ENV} ================================================ FILE: .github/actions/setup-go-tip/action.yml ================================================ # Inspired by https://github.com/actions/setup-go/issues/21#issuecomment-997208686 name: 'Install Go Tip' description: 'Install Go Tip toolchain' inputs: gh_token: description: 'The GitHub Token' required: true runs: using: "composite" steps: - name: Download pre-built Go tip from grafana/gotip repo id: download shell: bash run: | set -euo pipefail gh release download ubuntu-latest --repo grafana/gotip --pattern 'go.zip' echo "::group::unzip" unzip go.zip -d $HOME/sdk echo "::endgroup::" export GOROOT="$HOME/sdk/gotip" echo "GOROOT=$GOROOT" >> $GITHUB_ENV echo "success=true" >> $GITHUB_OUTPUT env: GH_TOKEN: ${{ inputs.gh_token }} # If download failed, we will try to build tip from source. # This requires Go toolchain, so install it first. # However, gotip is picky about the version of Go used to build it, # sometimes it requires the latest version, so determine it dynamically. - name: Determine latest Go version if: steps.download.outputs.success == 'false' id: get_go_version shell: bash run: | LATEST_GO_VERSION=$(curl -s "https://go.dev/VERSION?m=text" | grep go1 | sed 's/^go//g') echo "LATEST_GO_VERSION=$LATEST_GO_VERSION" >> $GITHUB_OUTPUT - name: Install Go toolchain if: steps.download.outputs.success == 'false' uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{ steps.get_go_version.outputs.LATEST_GO_VERSION }} - name: Build Go Tip from source if: steps.download.outputs.success == 'false' shell: bash run: | echo Build Go Tip from source set -euo pipefail go install golang.org/dl/gotip@latest gotip download export GOROOT="$(gotip env GOROOT)" echo "GOROOT=$GOROOT" >> $GITHUB_ENV # for some reason even though we put gotip at the front of PATH later, # the go binary installed in previous step still takes precedence. So remove it. rm -f $(which go) - name: Setup Go environment shell: bash run: | echo Setup Go environment set -euo pipefail $GOROOT/bin/go version GOPATH="$HOME/gotip" PATH="$GOROOT/bin:$GOPATH/bin:$PATH" echo "GOPATH=$GOPATH" >> $GITHUB_ENV echo "PATH=$PATH" >> $GITHUB_ENV - name: Check Go Version shell: bash run: | echo Check Go Version set -euo pipefail echo "GOPATH=$GOPATH" echo "GOROOT=$GOROOT" echo "which go:" which -a go echo "Active Go version:" go version ================================================ FILE: .github/actions/setup-node.js/action.yml ================================================ name: 'Setup Node.js' description: 'Setup Node.js version as required by jaeger-ui repo. Must be called after checkout with submodules.' runs: using: "composite" steps: - name: Get Node.js version from jaeger-ui shell: bash run: | echo "JAEGER_UI_NODE_JS_VERSION=$(cat jaeger-ui/.nvmrc)" >> ${GITHUB_ENV} - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: ${{ env.JAEGER_UI_NODE_JS_VERSION }} cache: 'npm' cache-dependency-path: jaeger-ui/package-lock.json ================================================ FILE: .github/actions/upload-codecov/action.yml ================================================ # Copyright (c) 2023 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # Codecov upload often fails on rate limits if used without a token. # See https://github.com/codecov/codecov-action/issues/837 # This action embeds a token directly. # We cannot define it as "secret" as we need it accessible from forks. name: 'Upload coverage to codecov' description: 'Uploads coverage to codecov with retries and saves raw profiles as artifacts' inputs: files: description: 'Coverage files to upload (comma-separated)' required: true flag: description: 'Codecov flag for this upload; also used as the artifact name suffix (coverage-)' required: true runs: using: 'composite' steps: # Upload raw profiles first so they are available to the fan-in workflow # even if the Codecov upload below fails (e.g. rate-limit). - name: Stage coverage files for artifact upload shell: bash run: | set -x mkdir -p /tmp/coverage-staging IFS=',' read -ra FILES <<< "${{ inputs.files }}" echo "Staging ${#FILES[@]} coverage file(s): ${{ inputs.files }}" for f in "${FILES[@]}"; do f=$(echo "$f" | xargs) # trim whitespace if [ -f "$f" ]; then echo "Staging: $f" cp "$f" /tmp/coverage-staging/ else echo "Warning: coverage file not found, skipping: $f" fi done echo "Staged files:" ls -la /tmp/coverage-staging/ - name: Upload coverage profiles as artifact uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: coverage-${{ inputs.flag }} path: /tmp/coverage-staging/ retention-days: 7 - name: Retry upload uses: Wandalen/wretry.action@e68c23e6309f2871ca8ae4763e7629b9c258e1ea # v3.8.0 with: attempt_limit: 6 # sleep 10 seconds between retries attempt_delay: 10000 action: codecov/codecov-action@7afa10ed9b269c561c2336fd862446844e0cbf71 # v4.2.0 with: | files: ${{ inputs.files }} flags: ${{ inputs.flag }} verbose: true fail_ci_if_error: false token: f457b710-93af-4191-8678-bcf51281f98c ================================================ FILE: .github/actions/verify-metrics-snapshot/action.yaml ================================================ # Copyright (c) 2023 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 name: 'Verify Metric Snapshot and Upload Metrics' description: 'Upload or cache the metrics data after verification' inputs: snapshot: description: 'Path to the metric file' required: true artifact_key: description: 'Artifact key used for uploading and fetching artifacts' required: true runs: using: 'composite' steps: - name: Upload current metrics snapshot uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ inputs.artifact_key }} path: ./.metrics/${{ inputs.snapshot }}.txt retention-days: 7 # The github cache restore successfully restores when cache saved has same key and same path. # Hence to restore release metric with name relese_{metric_name} , the name must be changed to the same. - name: Change file name before caching if: github.ref_name == 'main' shell: bash run: | mv ./.metrics/${{ inputs.snapshot }}.txt ./.metrics/baseline_${{ inputs.snapshot }}.txt - name: Cache metrics snapshot on main branch for longer retention if: github.ref_name == 'main' uses: actions/cache/save@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: ./.metrics/baseline_${{ inputs.snapshot }}.txt key: ${{ inputs.artifact_key }}_${{ github.run_id }} # Use restore keys to match prefix and fetch the latest cache # Here , restore keys is an ordered list of prefixes that need to be matched - name: Download the cached tagged metrics id: download-release-snapshot if: github.ref_name != 'main' uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: ./.metrics/baseline_${{ inputs.snapshot }}.txt key: ${{ inputs.artifact_key }} restore-keys: | ${{ inputs.artifact_key }} # Create an empty stub so the diff artifact is always uploaded on PRs. # The fan-in uses 1-to-1 presence of diff artifacts to detect infra failures # (a missing diff_* artifact means this action never ran for that snapshot). # The stub is overwritten by the compare step when a baseline exists. - name: Create diff file stub if: github.ref_name != 'main' shell: bash run: touch ./.metrics/diff_${{ inputs.snapshot }}.txt - name: Calculate diff between the snapshots id: compare-snapshots if: ${{ (github.ref_name != 'main') && (steps.download-release-snapshot.outputs.cache-matched-key != '') }} continue-on-error: true shell: bash run: | python3 -m pip install prometheus-client if python3 ./scripts/e2e/compare_metrics.py --file1 ./.metrics/${{ inputs.snapshot }}.txt --file2 ./.metrics/baseline_${{ inputs.snapshot }}.txt --output ./.metrics/diff_${{ inputs.snapshot }}.txt; then echo "No differences found in metrics" else echo "🛑 Differences found in metrics" echo "has_diff=true" >> $GITHUB_OUTPUT fi # Always upload the diff artifact on PRs (even when empty / no baseline yet). # Presence of this artifact in the fan-in proves this action ran for the snapshot. - name: Upload the diff artifact if: github.ref_name != 'main' uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: diff_${{ inputs.artifact_key }} path: ./.metrics/diff_${{ inputs.snapshot }}.txt retention-days: 7 ================================================ FILE: .github/scripts/.gitignore ================================================ node_modules/ ================================================ FILE: .github/scripts/README.md ================================================ # PR Quota Manager - Manual Execution Guide This document explains how to run the PR Quota Manager script manually from the command line for testing and troubleshooting. ## Prerequisites 1. **Node.js** (version 16 or higher) ```bash node --version ``` 2. **GitHub Personal Access Token** with the following permissions: - `repo` (Full control of private repositories) - `public_repo` (Access public repositories) - if working with public repos only Create a token at: https://github.com/settings/tokens. Store the value in a file, e.g. `~/.github_token`. Then set the environment variable: ```bash read -r GITHUB_TOKEN < ~/.github_token export GITHUB_TOKEN ``` 3. **Install Dependencies** Navigate to the `.github/scripts` directory and install dependencies: ```bash cd .github/scripts npm ci ``` ## Running the Script ### Basic Usage ```bash node pr-quota-manager.js [owner] [repo] ``` ### Parameters - `username` (required): The GitHub username to process quota for - `owner` (optional): Repository owner (defaults to `jaegertracing` or `GITHUB_REPOSITORY` env var) - `repo` (optional): Repository name (defaults to `jaeger` or `GITHUB_REPOSITORY` env var) ### Examples **Process quota for a specific user in the default repository:** ```bash node pr-quota-manager.js newcontributor ``` **Process quota for a user in a different repository:** ```bash node pr-quota-manager.js username myorg myrepo ``` **Using environment variables for repository:** ```bash export GITHUB_REPOSITORY="jaegertracing/jaeger" node pr-quota-manager.js contributor ``` ### Dry-Run Mode Test the script without making any actual changes: ```bash # Using flag node pr-quota-manager.js username --dry-run # Using environment variable DRY_RUN=true node pr-quota-manager.js username ``` In dry-run mode, the script will: - Show exactly what actions it would take - Not create/modify labels - Not post comments - Not make any API modifications - Still fetch and analyze PRs for accurate simulation ## Listing Open PRs by Author Use the utility script to see all open PRs grouped by author: ```bash node list-open-prs-by-author.js [owner] [repo] ``` This is useful for: - Identifying which users need quota processing - Planning backfills of the quota system - Seeing which PRs are already quota-blocked **CSV output for scripting:** ```bash FORMAT=csv node list-open-prs-by-author.js > prs.csv ``` ## Output The script will display: 1. **History Audit**: Summary of merged PR count (up to 3 merged PRs for quota calculation) 2. **Current Stats**: Merged count, calculated quota, and open PR count 3. **Processing Actions**: Each PR being blocked/unblocked 4. **Summary**: Total counts of blocked, unblocked, and unchanged PRs ### Example Output ``` === Processing Quota for: @newuser === 📜 History Audit: No merged PRs found. 📊 Current Stats: User has 0 merged PRs. Current Quota: 1. Currently Open: 3. 🔄 Processing Open PRs: ℹ️ PR #123 unchanged (active) ✅ Labeled PR #124 as blocked (Position: 2/3, Quota: 1) ✅ Labeled PR #125 as blocked (Position: 3/3, Quota: 1) ✅ Processing Complete for @newuser 📋 Summary: - Blocked: 2 PRs - Unblocked: 0 PRs - Unchanged: 1 PRs ``` ## Running Tests To run the unit tests: ```bash cd .github/scripts npm test ``` To run tests with coverage: ```bash npm test -- --coverage ``` ## Quota Rules The script applies the following quota rules: | Merged PRs | Quota | |-----------|-------| | 0 | 1 | | 1 | 2 | | 2 | 3 | | 3+ | 10 | ## Troubleshooting ### "GITHUB_TOKEN environment variable is required" Make sure you've set the `GITHUB_TOKEN` environment variable: ```bash export GITHUB_TOKEN="your_token_here" ``` ### "403 Forbidden" errors Your GitHub token may not have the required permissions. Ensure it has: - `repo` scope for private repositories - `public_repo` scope for public repositories ### "Cannot find module '@octokit/rest'" Install the required dependency: ```bash cd .github/scripts npm install @octokit/rest ``` ### API Rate Limiting GitHub has rate limits for API requests: - Authenticated requests: 5,000 requests per hour - The script makes approximately 2-5 API calls per user If you hit rate limits, wait for the limit to reset or use a different token. ## How It Works 1. **Fetches PRs** by the target author (all open PRs + up to 3 merged PRs for quota calculation) 2. **Calculates quota** based on the number of merged PRs 3. **Identifies open PRs** and sorts them by creation date (oldest first) 4. **Applies labels** to PRs based on quota: - PRs within quota: Remove `pr-quota-reached` label (if present) - PRs exceeding quota: Add `pr-quota-reached` label 5. **Posts comments** (only on state changes to avoid spam): - Blocking comment when PR first gets blocked - Unblocking comment when PR is moved to active queue ## Integration with GitHub Actions This script is automatically executed by the GitHub Actions workflow (`.github/workflows/pr-quota-manager.yml`) on: - Pull request opened, closed, or reopened events - Manual workflow dispatch The workflow uses `actions/github-script` to run the script with the repository's built-in `GITHUB_TOKEN`. ================================================ FILE: .github/scripts/ci-summary-report-publish.js ================================================ // Copyright (c) 2026 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // SECURITY WARNING — INJECTION RISK // // This script runs in the BASE REPOSITORY context (via workflow_run) with // pull-requests: write and checks: write permissions. The ci-summary artifact // it reads was produced by a PR's CI run and may originate from a FORK, // containing UNTRUSTED content crafted by the PR author. // // NEVER interpolate artifact content verbatim into PR comments, check run // summaries, or any GitHub API call. Doing so allows a malicious PR to inject // arbitrary Markdown or URLs into the repository's UI. // // Required invariants maintained by this file: // 1. ci-summary.json contains ONLY typed primitives: numbers, booleans, and // fixed enum strings ("success"/"failure"/"skipped"). No free-form text. // 2. All display text (PR comments, check summaries) is constructed entirely // from trusted template strings defined in this file. // 3. Numeric values are coerced through safeNum() which validates with // Number.isFinite() and rejects negatives. // 4. Boolean fields are compared with === true, never coerced with !! which // would misinterpret a JSON string "false" as truthy. // 5. String fields from the artifact are used only in comparisons // (=== 'success'), never interpolated into output strings. 'use strict'; // HTML comment tag used to identify the CI summary comment on a PR. const COMMENT_TAG = ''; /** * Coerce a value from ci-summary.json to a non-negative number, or null. * Returns null for null/undefined inputs (preserves "step did not run" signal). * Returns null for NaN, Infinity, or negative values. * @param {*} v * @returns {number|null} */ function safeNum(v) { if (v === null || v === undefined) return null; const n = Number(v); return Number.isFinite(n) && n >= 0 ? n : null; } // Prometheus metric names: must start with [a-zA-Z_:], followed by [a-zA-Z0-9_:]. const METRIC_NAME_RE = /^[a-zA-Z_:][a-zA-Z0-9_:]*$/; const MAX_METRIC_NAME_LEN = 200; const MAX_SNAPSHOT_NAME_LEN = 200; const MAX_SNAPSHOTS = 50; const MAX_METRIC_NAMES_PER_SNAPSHOT = 200; // Snapshot names come from artifact directory names (alphanumeric, underscores, // dots, hyphens). We reject anything outside this character set. const SNAPSHOT_NAME_RE = /^[a-zA-Z0-9_.\-]+$/; /** * Validate and sanitize a Prometheus metric name. * Returns the name if valid, null otherwise. * @param {*} name * @returns {string|null} */ function sanitizeMetricName(name) { if (typeof name !== 'string') return null; if (name.length === 0 || name.length > MAX_METRIC_NAME_LEN) return null; return METRIC_NAME_RE.test(name) ? name : null; } /** * Validate and sanitize the metrics_snapshots array from ci-summary.json. * Each entry is validated: counts go through safeNum(), metric names through * sanitizeMetricName(). Invalid entries or fields are silently dropped. * @param {*} raw - The raw metrics_snapshots value from the artifact * @returns {Array|null} - Sanitized array, or null if input is missing/invalid */ function sanitizeSnapshots(raw) { if (!Array.isArray(raw)) return null; const result = []; for (const entry of raw) { if (result.length >= MAX_SNAPSHOTS) break; if (typeof entry !== 'object' || entry === null) continue; // Validate snapshot name const snapshot = typeof entry.snapshot === 'string' && entry.snapshot.length > 0 && entry.snapshot.length <= MAX_SNAPSHOT_NAME_LEN && SNAPSHOT_NAME_RE.test(entry.snapshot) ? entry.snapshot : null; if (!snapshot) continue; // Validate counts const added = safeNum(entry.added); const removed = safeNum(entry.removed); const modified = safeNum(entry.modified); // Validate metric_names array — collect up to cap valid names const names = []; if (Array.isArray(entry.metric_names)) { for (const n of entry.metric_names) { if (names.length >= MAX_METRIC_NAMES_PER_SNAPSHOT) break; const clean = sanitizeMetricName(n); if (clean) names.push(clean); } } result.push({ snapshot, added, removed, modified, metric_names: names }); } return result.length > 0 ? result : null; } /** * Derive metrics conclusion and display text from the parsed ci-summary artifact. * Uses === true for boolean fields to avoid misinterpreting JSON strings. * @param {object} s - Parsed ci-summary.json * @returns {{ hasInfraErrors: boolean, totalChanges: number|null, snapshots: Array|null, conclusion: string, text: string }} */ function computeMetrics(s) { const hasInfraErrors = s.metrics_has_infra_errors === true; const totalChanges = safeNum(s.metrics_total_changes); const snapshots = sanitizeSnapshots(s.metrics_snapshots); // Derive conclusion from the same conditions that drive text so they are always consistent. const conclusion = (hasInfraErrors || totalChanges === null || totalChanges > 0) ? 'failure' : 'success'; let text; if (hasInfraErrors) { text = '❌ Infrastructure error: missing diff artifacts'; } else if (totalChanges === null) { text = '❌ Could not read metrics_total_changes from summary'; } else if (totalChanges > 0) { text = `❌ ${totalChanges} metric change(s) detected`; } else { text = '✅ No significant metric changes'; } return { hasInfraErrors, totalChanges, snapshots, conclusion, text }; } /** * Derive coverage conclusion and display text from the parsed ci-summary artifact. * @param {object} s - Parsed ci-summary.json * @returns {{ skipped: boolean, conclusion: string, text: string }} */ function computeCoverage(s) { const skipped = s.coverage_skipped === true || s.coverage_conclusion === 'skipped'; const conclusion = (skipped || s.coverage_conclusion === 'success') ? 'success' : 'failure'; const pct = safeNum(s.coverage_percentage); const baseline = safeNum(s.coverage_baseline); let text; if (skipped) { text = '⏭️ No coverage profiles found; coverage gate skipped.'; } else { const pctStr = pct !== null ? `${pct}%` : 'unknown'; const baselineStr = baseline !== null ? ` (baseline ${baseline}%)` : ' (no baseline)'; const icon = conclusion === 'success' ? '✅' : '❌'; text = `${icon} Coverage ${pctStr}${baselineStr}`; } return { skipped, conclusion, text }; } /** * Format a detail breakdown of per-snapshot metric changes. * All text is built from trusted templates; metric names have been validated * through sanitizeMetricName() and are rendered in backtick-code spans. * @param {Array|null} snapshots - Sanitized snapshots from computeMetrics * @returns {string} - Markdown detail block, or empty string if no data */ function formatMetricsDetail(snapshots) { if (!snapshots || snapshots.length === 0) return ''; const lines = [ '', '
', 'View changed metrics', '', ]; for (const snap of snapshots) { lines.push(`**${snap.snapshot}**`); const parts = []; if (snap.added !== null && snap.added > 0) parts.push(`${snap.added} added`); if (snap.removed !== null && snap.removed > 0) parts.push(`${snap.removed} removed`); if (snap.modified !== null && snap.modified > 0) parts.push(`${snap.modified} modified`); if (parts.length > 0) { lines.push(parts.join(', ')); } if (snap.metric_names.length > 0) { for (const name of snap.metric_names) { lines.push(`- \`${name}\``); } } lines.push(''); } lines.push('
'); return lines.join('\n'); } /** * Build the PR comment body from pre-computed display strings. * Inputs are strings produced by computeMetrics/computeCoverage: all display text * is constructed from trusted templates; artifact-derived values appear only as * validated primitives (numbers) embedded by those functions, never as raw strings. * @param {string} metricsText * @param {string} coverageText * @param {string} footer - links + timestamp line * @param {object} [opts] * @param {Array|null} [opts.metricsSnapshots] - sanitized snapshot data for detail rendering * @returns {string} */ function buildCommentBody(metricsText, coverageText, footer, { metricsSnapshots } = {}) { const parts = [ COMMENT_TAG, '## CI Summary Report', '', '### Metrics Comparison', metricsText, ]; const detail = formatMetricsDetail(metricsSnapshots); if (detail) { parts.push(detail); } parts.push(''); parts.push('### Code Coverage'); parts.push(coverageText); parts.push(''); parts.push(footer); return parts.join('\n'); } /** * Create a completed check run and log the result. * @param {object} github - Octokit client * @param {string} owner * @param {string} repo * @param {string} headSha * @param {string} name * @param {string} conclusion * @param {object} output - { title, summary, text } * @param {object} core - GitHub Actions core logger */ async function postCheckRun(github, owner, repo, headSha, name, conclusion, output, core) { core.info(`Creating check run: "${name}" (conclusion: ${conclusion})`); const { data } = await github.rest.checks.create({ owner, repo, head_sha: headSha, name, status: 'completed', conclusion, output, }); core.info(`Check run created: id=${data.id} url=${data.html_url}`); } /** * Post or update the CI summary comment on a PR. * Always updates an existing comment (clears stale failure messages on green runs). * Only creates a new comment when createNew is true. * @param {object} github - Octokit client * @param {string} owner * @param {string} repo * @param {number} prNumber * @param {string} body * @param {object} core - GitHub Actions core logger * @param {object} [opts] * @param {boolean} [opts.createNew=true] - create a comment if none exists */ async function postOrUpdateComment(github, owner, repo, prNumber, body, core, { createNew = true } = {}) { core.info(`Searching for existing CI summary comment on PR #${prNumber}`); const existing = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: prNumber, }).then(cs => cs.find(c => c.body && c.body.startsWith(COMMENT_TAG))); if (existing) { core.info(`Updating existing comment id=${existing.id}`); const { data: updated } = await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body, }); core.info(`Comment updated: url=${updated.html_url}`); } else if (createNew) { core.info(`Creating new comment on PR #${prNumber}`); const { data: created } = await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body, }); core.info(`Comment created: id=${created.id} url=${created.html_url}`); } else { core.info('No existing comment and no issues to report; skipping PR comment.'); } } /** * GitHub Actions entry point. * Reads ci-summary.json, computes conclusions, posts check runs and PR comment. * * @param {object} opts * @param {object} opts.github - Octokit client from actions/github-script * @param {object} opts.core - GitHub Actions core logger * @param {object} opts.fs - Node fs module (injected for testability) * @param {object} opts.inputs * @param {string} opts.inputs.owner * @param {string} opts.inputs.repo * @param {string} opts.inputs.headSha * @param {string} opts.inputs.prNumber - raw string from step output * @param {string} opts.inputs.ciRunUrl * @param {string} opts.inputs.publishUrl */ async function handler({ github, core, fs, inputs }) { const { owner, repo, headSha, ciRunUrl, publishUrl } = inputs; const prNumber = parseInt(inputs.prNumber, 10) || null; const links = `➡️ [View CI run](${ciRunUrl}) | [View publish logs](${publishUrl})`; const ts = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); const footer = `${links}\n_${ts}_`; // Read structured data written by ci-summary-report.yml. // All fields are primitives (enums, numbers, booleans) — no free-form text. let s; try { s = JSON.parse(fs.readFileSync('.artifacts/ci-summary.json', 'utf8')); } catch (e) { core.warning(`ci-summary.json not found or unparseable: ${e.message}`); // Post failing check runs so required status checks are never silently absent. // All text here is a trusted, fixed string — no artifact content is used. const errorSummary = 'ci-summary artifact missing or unparseable; check CI run logs.'; for (const name of ['Metrics Comparison', 'Coverage Gate']) { await postCheckRun(github, owner, repo, headSha, name, 'failure', { title: name, summary: errorSummary, text: footer }, core); } return; } const metrics = computeMetrics(s); const coverage = computeCoverage(s); await postCheckRun(github, owner, repo, headSha, 'Metrics Comparison', metrics.conclusion, { title: 'Metrics Comparison Result', summary: metrics.text, text: `Total changes across all snapshots: ${metrics.totalChanges ?? 'unknown'}\n\n${footer}`, }, core); // Always created so it can be used as a required status check. await postCheckRun(github, owner, repo, headSha, 'Coverage Gate', coverage.conclusion, { title: 'Coverage Gate', summary: coverage.text, text: footer, }, core); // ── PR comment ── if (prNumber) { // Always update an existing comment so stale failure messages don't linger // after a green run. Only create a new comment when there is something to report. const hasIssues = metrics.conclusion === 'failure' || coverage.conclusion === 'failure' || metrics.totalChanges > 0; const body = buildCommentBody(metrics.text, coverage.text, footer, { metricsSnapshots: metrics.snapshots }); await postOrUpdateComment(github, owner, repo, prNumber, body, core, { createNew: hasIssues }); } else { core.info('No PR number; skipping PR comment.'); } } module.exports = handler; module.exports.safeNum = safeNum; module.exports.sanitizeMetricName = sanitizeMetricName; module.exports.sanitizeSnapshots = sanitizeSnapshots; module.exports.computeMetrics = computeMetrics; module.exports.computeCoverage = computeCoverage; module.exports.formatMetricsDetail = formatMetricsDetail; module.exports.buildCommentBody = buildCommentBody; module.exports.postCheckRun = postCheckRun; module.exports.postOrUpdateComment = postOrUpdateComment; module.exports.COMMENT_TAG = COMMENT_TAG; ================================================ FILE: .github/scripts/ci-summary-report-publish.test.js ================================================ // Copyright (c) 2026 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 'use strict'; const { safeNum, sanitizeMetricName, sanitizeSnapshots, computeMetrics, computeCoverage, formatMetricsDetail, buildCommentBody, postCheckRun, postOrUpdateComment, COMMENT_TAG, } = require('./ci-summary-report-publish'); // ── safeNum ────────────────────────────────────────────────────────────────── describe('safeNum', () => { test('returns null for null', () => expect(safeNum(null)).toBeNull()); test('returns null for undefined', () => expect(safeNum(undefined)).toBeNull()); test('returns 0 for 0', () => expect(safeNum(0)).toBe(0)); test('returns integer value', () => expect(safeNum(5)).toBe(5)); test('returns float value', () => expect(safeNum(96.8)).toBe(96.8)); test('returns null for negative number', () => expect(safeNum(-1)).toBeNull()); test('returns null for NaN', () => expect(safeNum(NaN)).toBeNull()); test('returns null for Infinity', () => expect(safeNum(Infinity)).toBeNull()); test('coerces numeric string', () => expect(safeNum('42')).toBe(42)); test('returns null for non-numeric string', () => expect(safeNum('bad')).toBeNull()); }); // ── sanitizeMetricName ─────────────────────────────────────────────────────── describe('sanitizeMetricName', () => { test('accepts valid Prometheus metric name', () => { expect(sanitizeMetricName('http_server_duration')).toBe('http_server_duration'); }); test('accepts name with colons', () => { expect(sanitizeMetricName('rpc:server_duration:total')).toBe('rpc:server_duration:total'); }); test('accepts name starting with underscore', () => { expect(sanitizeMetricName('_internal_metric')).toBe('_internal_metric'); }); test('rejects empty string', () => { expect(sanitizeMetricName('')).toBeNull(); }); test('rejects name starting with digit', () => { expect(sanitizeMetricName('0invalid')).toBeNull(); }); test('rejects name with spaces', () => { expect(sanitizeMetricName('metric name')).toBeNull(); }); test('rejects markdown injection', () => { expect(sanitizeMetricName('[click me](http://evil.com)')).toBeNull(); }); test('rejects HTML injection', () => { expect(sanitizeMetricName('')).toBeNull(); }); test('rejects name with curly braces', () => { expect(sanitizeMetricName('metric{label="value"}')).toBeNull(); }); test('rejects non-string types', () => { expect(sanitizeMetricName(42)).toBeNull(); expect(sanitizeMetricName(null)).toBeNull(); expect(sanitizeMetricName(undefined)).toBeNull(); expect(sanitizeMetricName({})).toBeNull(); }); test('rejects names exceeding 200 characters', () => { expect(sanitizeMetricName('a'.repeat(201))).toBeNull(); }); test('accepts names at exactly 200 characters', () => { const name = 'a'.repeat(200); expect(sanitizeMetricName(name)).toBe(name); }); }); // ── sanitizeSnapshots ──────────────────────────────────────────────────────── describe('sanitizeSnapshots', () => { test('returns null for non-array input', () => { expect(sanitizeSnapshots(null)).toBeNull(); expect(sanitizeSnapshots(undefined)).toBeNull(); expect(sanitizeSnapshots('string')).toBeNull(); expect(sanitizeSnapshots({})).toBeNull(); }); test('returns null for empty array', () => { expect(sanitizeSnapshots([])).toBeNull(); }); test('sanitizes valid snapshot entry', () => { const input = [{ snapshot: 'metrics_snapshot_cassandra', added: 2, removed: 1, modified: 0, metric_names: ['http_server_duration', 'rpc_client_duration'], }]; const result = sanitizeSnapshots(input); expect(result).toHaveLength(1); expect(result[0].snapshot).toBe('metrics_snapshot_cassandra'); expect(result[0].added).toBe(2); expect(result[0].removed).toBe(1); expect(result[0].modified).toBe(0); expect(result[0].metric_names).toEqual(['http_server_duration', 'rpc_client_duration']); }); test('drops entries with invalid snapshot names', () => { const input = [ { snapshot: 'valid_name', added: 1, removed: 0, modified: 0, metric_names: [] }, { snapshot: '', added: 1, removed: 0, modified: 0, metric_names: [] }, { snapshot: 'also.valid-name.2', added: 0, removed: 1, modified: 0, metric_names: [] }, ]; const result = sanitizeSnapshots(input); expect(result).toHaveLength(2); expect(result[0].snapshot).toBe('valid_name'); expect(result[1].snapshot).toBe('also.valid-name.2'); }); test('drops invalid metric names from metric_names array', () => { const input = [{ snapshot: 'test_snapshot', added: 2, removed: 0, modified: 0, metric_names: ['valid_metric', '', 'another_valid'], }]; const result = sanitizeSnapshots(input); expect(result[0].metric_names).toEqual(['valid_metric', 'another_valid']); }); test('caps snapshots at 50 entries', () => { const input = Array.from({ length: 60 }, (_, i) => ({ snapshot: `snap_${i}`, added: 1, removed: 0, modified: 0, metric_names: [], })); const result = sanitizeSnapshots(input); expect(result).toHaveLength(50); }); test('caps metric_names at 200 per snapshot', () => { const names = Array.from({ length: 210 }, (_, i) => `metric_${i}`); const input = [{ snapshot: 'test_snapshot', added: 210, removed: 0, modified: 0, metric_names: names, }]; const result = sanitizeSnapshots(input); expect(result[0].metric_names).toHaveLength(200); }); test('handles missing metric_names gracefully', () => { const input = [{ snapshot: 'test_snapshot', added: 1, removed: 0, modified: 0, }]; const result = sanitizeSnapshots(input); expect(result[0].metric_names).toEqual([]); }); test('validates counts through safeNum', () => { const input = [{ snapshot: 'test_snapshot', added: -1, removed: 'bad', modified: Infinity, metric_names: ['valid_metric'], }]; const result = sanitizeSnapshots(input); expect(result[0].added).toBeNull(); expect(result[0].removed).toBeNull(); expect(result[0].modified).toBeNull(); }); test('skips null and non-object entries', () => { const input = [null, 42, 'string', { snapshot: 'valid', added: 0, removed: 0, modified: 0, metric_names: [] }]; const result = sanitizeSnapshots(input); expect(result).toHaveLength(1); expect(result[0].snapshot).toBe('valid'); }); }); // ── computeMetrics ──────────────────────────────────────────────────────────── describe('computeMetrics', () => { test('success when no changes and no infra errors', () => { const r = computeMetrics({ metrics_has_infra_errors: false, metrics_total_changes: 0 }); expect(r.conclusion).toBe('success'); expect(r.text).toBe('✅ No significant metric changes'); expect(r.totalChanges).toBe(0); expect(r.hasInfraErrors).toBe(false); expect(r.snapshots).toBeNull(); }); test('failure when total changes > 0', () => { const r = computeMetrics({ metrics_has_infra_errors: false, metrics_total_changes: 3 }); expect(r.conclusion).toBe('failure'); expect(r.text).toBe('❌ 3 metric change(s) detected'); expect(r.totalChanges).toBe(3); }); test('failure when infra errors present', () => { const r = computeMetrics({ metrics_has_infra_errors: true, metrics_total_changes: 0 }); expect(r.conclusion).toBe('failure'); expect(r.text).toBe('❌ Infrastructure error: missing diff artifacts'); expect(r.hasInfraErrors).toBe(true); }); test('failure when total_changes is null (step did not write output)', () => { const r = computeMetrics({ metrics_has_infra_errors: false, metrics_total_changes: null }); expect(r.conclusion).toBe('failure'); expect(r.text).toBe('❌ Could not read metrics_total_changes from summary'); expect(r.totalChanges).toBeNull(); }); test('infra errors take precedence over missing total_changes', () => { const r = computeMetrics({ metrics_has_infra_errors: true, metrics_total_changes: null }); expect(r.conclusion).toBe('failure'); expect(r.text).toBe('❌ Infrastructure error: missing diff artifacts'); }); // Ensure JSON string "false" / "true" are not coerced as booleans test('treats string "true" for metrics_has_infra_errors as falsy (not === true)', () => { const r = computeMetrics({ metrics_has_infra_errors: 'true', metrics_total_changes: 0 }); expect(r.hasInfraErrors).toBe(false); }); test('treats string "false" for metrics_has_infra_errors as falsy', () => { const r = computeMetrics({ metrics_has_infra_errors: 'false', metrics_total_changes: 0 }); expect(r.hasInfraErrors).toBe(false); }); test('includes sanitized snapshots when present', () => { const r = computeMetrics({ metrics_has_infra_errors: false, metrics_total_changes: 2, metrics_snapshots: [{ snapshot: 'cassandra_v2', added: 1, removed: 1, modified: 0, metric_names: ['http_server_duration', 'rpc_client_duration'], }], }); expect(r.snapshots).toHaveLength(1); expect(r.snapshots[0].snapshot).toBe('cassandra_v2'); expect(r.snapshots[0].metric_names).toEqual(['http_server_duration', 'rpc_client_duration']); }); test('returns null snapshots when metrics_snapshots is absent', () => { const r = computeMetrics({ metrics_has_infra_errors: false, metrics_total_changes: 0 }); expect(r.snapshots).toBeNull(); }); }); // ── formatMetricsDetail ────────────────────────────────────────────────────── describe('formatMetricsDetail', () => { test('returns empty string for null snapshots', () => { expect(formatMetricsDetail(null)).toBe(''); }); test('returns empty string for empty array', () => { expect(formatMetricsDetail([])).toBe(''); }); test('renders single snapshot with metric names', () => { const detail = formatMetricsDetail([{ snapshot: 'cassandra_v2', added: 1, removed: 0, modified: 1, metric_names: ['http_server_duration', 'rpc_client_duration'], }]); expect(detail).toContain('
'); expect(detail).toContain('
'); expect(detail).toContain('View changed metrics'); expect(detail).toContain('**cassandra_v2**'); expect(detail).toContain('1 added, 1 modified'); expect(detail).toContain('- `http_server_duration`'); expect(detail).toContain('- `rpc_client_duration`'); }); test('renders multiple snapshots', () => { const detail = formatMetricsDetail([ { snapshot: 'snap_a', added: 2, removed: 0, modified: 0, metric_names: ['metric_a'] }, { snapshot: 'snap_b', added: 0, removed: 3, modified: 0, metric_names: ['metric_b'] }, ]); expect(detail).toContain('**snap_a**'); expect(detail).toContain('**snap_b**'); expect(detail).toContain('2 added'); expect(detail).toContain('3 removed'); }); test('omits zero counts from summary line', () => { const detail = formatMetricsDetail([{ snapshot: 'test', added: 0, removed: 5, modified: 0, metric_names: [], }]); expect(detail).not.toContain('added'); expect(detail).toContain('5 removed'); expect(detail).not.toContain('modified'); }); }); // ── computeCoverage ─────────────────────────────────────────────────────────── describe('computeCoverage', () => { test('success with pct and baseline', () => { const r = computeCoverage({ coverage_skipped: false, coverage_conclusion: 'success', coverage_percentage: 96.8, coverage_baseline: 46.4, }); expect(r.conclusion).toBe('success'); expect(r.skipped).toBe(false); expect(r.text).toBe('✅ Coverage 96.8% (baseline 46.4%)'); }); test('failure when conclusion is failure', () => { const r = computeCoverage({ coverage_skipped: false, coverage_conclusion: 'failure', coverage_percentage: 94.0, coverage_baseline: 96.0, }); expect(r.conclusion).toBe('failure'); expect(r.text).toBe('❌ Coverage 94% (baseline 96%)'); }); test('skipped when coverage_skipped is true', () => { const r = computeCoverage({ coverage_skipped: true, coverage_conclusion: 'success' }); expect(r.skipped).toBe(true); expect(r.conclusion).toBe('success'); expect(r.text).toBe('⏭️ No coverage profiles found; coverage gate skipped.'); }); test('skipped when coverage_conclusion is "skipped"', () => { const r = computeCoverage({ coverage_skipped: false, coverage_conclusion: 'skipped' }); expect(r.skipped).toBe(true); expect(r.conclusion).toBe('success'); }); test('shows "unknown" pct when percentage is null', () => { const r = computeCoverage({ coverage_skipped: false, coverage_conclusion: 'failure', coverage_percentage: null, coverage_baseline: null, }); expect(r.text).toBe('❌ Coverage unknown (no baseline)'); }); test('shows "no baseline" when baseline is null but pct is known', () => { const r = computeCoverage({ coverage_skipped: false, coverage_conclusion: 'success', coverage_percentage: 97.0, coverage_baseline: null, }); expect(r.text).toBe('✅ Coverage 97% (no baseline)'); }); // Ensure JSON string "false" is not coerced as boolean true test('treats string "true" for coverage_skipped as not skipped', () => { const r = computeCoverage({ coverage_skipped: 'true', coverage_conclusion: 'success', coverage_percentage: 97.0, coverage_baseline: 96.0, }); expect(r.skipped).toBe(false); }); }); // ── buildCommentBody ────────────────────────────────────────────────────────── describe('buildCommentBody', () => { const metricsText = '✅ No significant metric changes'; const coverageText = '✅ Coverage 96.8% (baseline 46.4%)'; const footer = '➡️ links\n_2026-03-04 00:00:00 UTC_'; test('starts with COMMENT_TAG for idempotent find-and-update', () => { const body = buildCommentBody(metricsText, coverageText, footer); expect(body.startsWith(COMMENT_TAG)).toBe(true); }); test('contains expected section headers', () => { const body = buildCommentBody(metricsText, coverageText, footer); expect(body).toContain('## CI Summary Report'); expect(body).toContain('### Metrics Comparison'); expect(body).toContain('### Code Coverage'); }); test('embeds metrics and coverage text', () => { const body = buildCommentBody(metricsText, coverageText, footer); expect(body).toContain(metricsText); expect(body).toContain(coverageText); }); test('ends with footer', () => { const body = buildCommentBody(metricsText, coverageText, footer); expect(body.endsWith(footer)).toBe(true); }); test('does not include details block when no snapshots', () => { const body = buildCommentBody(metricsText, coverageText, footer); expect(body).not.toContain('
'); }); test('includes metrics detail when metricsSnapshots provided', () => { const snapshots = [{ snapshot: 'cassandra_v2', added: 1, removed: 0, modified: 1, metric_names: ['http_server_duration'], }]; const body = buildCommentBody('❌ 2 metric change(s) detected', coverageText, footer, { metricsSnapshots: snapshots }); expect(body).toContain('
'); expect(body).toContain('**cassandra_v2**'); expect(body).toContain('- `http_server_duration`'); expect(body).toContain('
'); // Verify proper section ordering const metricsPos = body.indexOf('### Metrics Comparison'); const detailsPos = body.indexOf('
'); const coveragePos = body.indexOf('### Code Coverage'); expect(metricsPos).toBeLessThan(detailsPos); expect(detailsPos).toBeLessThan(coveragePos); }); }); // ── postCheckRun ────────────────────────────────────────────────────────────── describe('postCheckRun', () => { const owner = 'org', repo = 'repo', headSha = 'abc123'; test('calls checks.create with correct parameters and logs result', async () => { const mockGithub = { rest: { checks: { create: jest.fn().mockResolvedValue({ data: { id: 42, html_url: 'https://example.com/check/42' } }) } }, }; const mockCore = { info: jest.fn() }; await postCheckRun(mockGithub, owner, repo, headSha, 'Coverage Gate', 'success', { title: 'Coverage Gate', summary: '✅ ok', text: 'footer' }, mockCore); expect(mockGithub.rest.checks.create).toHaveBeenCalledWith({ owner, repo, head_sha: headSha, name: 'Coverage Gate', status: 'completed', conclusion: 'success', output: { title: 'Coverage Gate', summary: '✅ ok', text: 'footer' }, }); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining('Coverage Gate')); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining('id=42')); }); }); // ── postOrUpdateComment ─────────────────────────────────────────────────────── describe('postOrUpdateComment', () => { const owner = 'org', repo = 'repo', prNumber = 99; const body = `${COMMENT_TAG}\n## CI Summary Report`; test('creates a new comment when none exists', async () => { const mockGithub = { paginate: jest.fn().mockResolvedValue([{ id: 1, body: 'unrelated comment' }]), rest: { issues: { listComments: jest.fn(), createComment: jest.fn().mockResolvedValue({ data: { id: 201, html_url: 'https://example.com/comment/201' } }), }}, }; const mockCore = { info: jest.fn() }; await postOrUpdateComment(mockGithub, owner, repo, prNumber, body, mockCore); expect(mockGithub.rest.issues.createComment).toHaveBeenCalledWith( expect.objectContaining({ owner, repo, issue_number: prNumber, body }) ); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining('id=201')); }); test('updates existing comment when one is found', async () => { const existingComment = { id: 100, body: `${COMMENT_TAG}\nold content` }; const mockGithub = { paginate: jest.fn().mockResolvedValue([existingComment]), rest: { issues: { listComments: jest.fn(), updateComment: jest.fn().mockResolvedValue({ data: { html_url: 'https://example.com/comment/100' } }), }}, }; const mockCore = { info: jest.fn() }; await postOrUpdateComment(mockGithub, owner, repo, prNumber, body, mockCore); expect(mockGithub.rest.issues.updateComment).toHaveBeenCalledWith( expect.objectContaining({ owner, repo, comment_id: 100, body }) ); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining('Updating existing comment id=100')); }); test('skips creating a new comment when createNew is false and no existing comment', async () => { const mockGithub = { paginate: jest.fn().mockResolvedValue([{ id: 1, body: 'unrelated' }]), rest: { issues: { listComments: jest.fn(), createComment: jest.fn(), }}, }; const mockCore = { info: jest.fn() }; await postOrUpdateComment(mockGithub, owner, repo, prNumber, body, mockCore, { createNew: false }); expect(mockGithub.rest.issues.createComment).not.toHaveBeenCalled(); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining('No existing comment')); }); test('still updates existing comment when createNew is false', async () => { const existingComment = { id: 100, body: `${COMMENT_TAG}\nold failure` }; const mockGithub = { paginate: jest.fn().mockResolvedValue([existingComment]), rest: { issues: { listComments: jest.fn(), updateComment: jest.fn().mockResolvedValue({ data: { html_url: 'https://example.com/comment/100' } }), }}, }; const mockCore = { info: jest.fn() }; await postOrUpdateComment(mockGithub, owner, repo, prNumber, body, mockCore, { createNew: false }); expect(mockGithub.rest.issues.updateComment).toHaveBeenCalledWith( expect.objectContaining({ owner, repo, comment_id: 100, body }) ); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining('Updating existing comment id=100')); }); }); ================================================ FILE: .github/scripts/list-open-prs-by-author.js ================================================ #!/usr/bin/env node /** * List Open PRs Grouped by Author * * This utility script lists all open PRs in a repository grouped by author. * Useful for identifying which users need quota processing or backfilling. * * Usage: * GITHUB_TOKEN= node list-open-prs-by-author.js [owner] [repo] */ /** * Fetch all open PRs grouped by author * @param {object} octokit - GitHub API client * @param {string} owner - Repository owner * @param {string} repo - Repository name * @returns {Promise} Map of author -> array of PRs */ async function fetchOpenPRsByAuthor(octokit, owner, repo) { const prsByAuthor = new Map(); let page = 1; const perPage = 100; console.log(`📥 Fetching open PRs from ${owner}/${repo}...`); while (true) { const { data } = await octokit.rest.pulls.list({ owner, repo, state: 'open', per_page: perPage, page, sort: 'created', direction: 'asc' }); if (data.length === 0) break; for (const pr of data) { const author = pr.user.login; if (!prsByAuthor.has(author)) { prsByAuthor.set(author, []); } prsByAuthor.get(author).push({ number: pr.number, title: pr.title, created_at: pr.created_at, labels: pr.labels.map(l => l.name) }); } if (data.length < perPage) break; page++; } return prsByAuthor; } /** * Display PRs grouped by author * @param {Map} prsByAuthor - Map of author -> PRs */ function displayResults(prsByAuthor) { // Sort by number of open PRs (descending) const sortedAuthors = Array.from(prsByAuthor.entries()) .sort((a, b) => b[1].length - a[1].length); console.log(`\n📊 Found ${sortedAuthors.length} authors with open PRs\n`); console.log('=' .repeat(80)); for (const [author, prs] of sortedAuthors) { const hasQuotaLabel = prs.some(pr => pr.labels.includes('pr-quota-reached')); const quotaIndicator = hasQuotaLabel ? ' 🚫' : ''; console.log(`\n👤 @${author} (${prs.length} open PR${prs.length > 1 ? 's' : ''})${quotaIndicator}`); // Sort PRs by creation date (oldest first) const sortedPRs = prs.sort((a, b) => new Date(a.created_at) - new Date(b.created_at) ); for (const pr of sortedPRs) { const date = new Date(pr.created_at).toISOString().split('T')[0]; const quotaLabel = pr.labels.includes('pr-quota-reached') ? ' [QUOTA REACHED]' : ''; console.log(` - PR #${pr.number}: ${pr.title.substring(0, 70)}${pr.title.length > 70 ? '...' : ''}`); console.log(` Created: ${date}${quotaLabel}`); } } console.log('\n' + '='.repeat(80)); console.log(`\n📋 Summary:`); console.log(` Total authors: ${sortedAuthors.length}`); console.log(` Total open PRs: ${Array.from(prsByAuthor.values()).reduce((sum, prs) => sum + prs.length, 0)}`); const authorsWithQuota = sortedAuthors.filter(([_, prs]) => prs.some(pr => pr.labels.includes('pr-quota-reached')) ).length; if (authorsWithQuota > 0) { console.log(` Authors with quota-blocked PRs: ${authorsWithQuota}`); } } /** * Display in CSV format for easy processing * @param {Map} prsByAuthor - Map of author -> PRs */ function displayCSV(prsByAuthor) { console.log('Author,PR Count,PR Numbers,Has Quota Label'); for (const [author, prs] of prsByAuthor.entries()) { const prNumbers = prs.map(pr => `#${pr.number}`).join(' '); const hasQuotaLabel = prs.some(pr => pr.labels.includes('pr-quota-reached')); console.log(`${author},${prs.length},"${prNumbers}",${hasQuotaLabel}`); } } /** * Main execution function */ async function main() { const args = process.argv.slice(2); const owner = args[0] || process.env.GITHUB_REPOSITORY?.split('/')[0] || 'jaegertracing'; const repo = args[1] || process.env.GITHUB_REPOSITORY?.split('/')[1] || 'jaeger'; const format = process.env.FORMAT || 'default'; // 'default' or 'csv' if (!process.env.GITHUB_TOKEN) { console.error('Error: GITHUB_TOKEN environment variable is required'); console.error('Usage: GITHUB_TOKEN= node list-open-prs-by-author.js [owner] [repo]'); console.error('Optional: FORMAT=csv for CSV output'); process.exit(1); } // Import @octokit/rest dynamically const { Octokit } = await import('@octokit/rest'); const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); try { const prsByAuthor = await fetchOpenPRsByAuthor(octokit, owner, repo); if (format === 'csv') { displayCSV(prsByAuthor); } else { displayResults(prsByAuthor); } } catch (error) { console.error('Error:', error.message); process.exit(1); } } // Export for testing if (typeof module !== 'undefined' && module.exports) { module.exports = { fetchOpenPRsByAuthor, displayResults, displayCSV }; } // Run main function if executed directly if (require.main === module) { main().catch(error => { console.error('Fatal error:', error); process.exit(1); }); } ================================================ FILE: .github/scripts/package.json ================================================ { "name": "jaeger-ci-scripts", "version": "1.0.0", "description": "Jaeger CI scripts for managing pull request quotas and other automation tasks.", "main": "pr-quota-manager.js", "scripts": { "test": "jest", "test:coverage": "jest --coverage", "test:watch": "jest --watch" }, "keywords": [ "github", "pull-request", "quota", "management" ], "author": "Jaeger Team", "license": "Apache-2.0", "engines": { "node": ">=24" }, "dependencies": { "@octokit/rest": "^22.0.0" }, "devDependencies": { "jest": "30.2.0" }, "jest": { "testEnvironment": "node", "coveragePathIgnorePatterns": [ "/node_modules/" ], "collectCoverageFrom": [ "pr-quota-manager.js", "ci-summary-report-publish.js" ] } } ================================================ FILE: .github/scripts/pr-quota-manager.js ================================================ #!/usr/bin/env node /** * PR Quota Management System * * This script implements a "Waiting Room" system that limits concurrent open PRs * from contributors based on their merge history, automatically unlocking queued PRs * when quota becomes available. * * Usage: * - Via GitHub Actions (integrated with actions/github-script) * - Manual execution: GITHUB_TOKEN= node pr-quota-manager.js [owner] [repo] */ const LABEL_NAME = 'pr-quota-reached'; const LABEL_COLOR = 'CFD3D7'; /** * Format open/limit counts as a bullet-point status block * @param {number} openCount - Number of currently open PRs * @param {number} quota - Allowed quota * @returns {string} Formatted status string */ function formatStatus(openCount, quota) { return ` * Open: ${openCount}\n * Limit: ${quota}`; } /** * Calculate the quota for a user based on their merged PR count * @param {number} mergedCount - Number of merged PRs * @returns {number} The allowed quota */ function calculateQuota(mergedCount) { if (mergedCount === 0) return 1; if (mergedCount === 1) return 2; if (mergedCount === 2) return 3; return 10; // Unlimited for 3+ merged PRs } /** * Fetch open and merged PRs by a specific author * Optimized to stop early: fetches all open PRs and only enough merged PRs to determine quota * @param {object} octokit - GitHub API client * @param {string} owner - Repository owner/org * @param {string} repo - Repository name * @param {string} author - PR author username * @returns {Promise<{openPRs: Array, mergedCount: number}>} Open PRs and count of merged PRs */ async function fetchAuthorPRs(octokit, owner, repo, author) { const openPRs = []; const mergedPRs = []; const perPage = 100; const MAX_MERGED_NEEDED = 3; // Stop after 3 merged PRs (gives unlimited quota) // Fetch open PRs let page = 1; while (true) { const { data } = await octokit.rest.pulls.list({ owner, repo, state: 'open', per_page: perPage, page, sort: 'created', direction: 'asc' }); if (data.length === 0) break; const authorPRs = data.filter(pr => pr.user.login === author); openPRs.push(...authorPRs); if (data.length < perPage) break; page++; } // Fetch merged PRs, but stop once we have enough to determine quota page = 1; while (mergedPRs.length < MAX_MERGED_NEEDED) { const { data } = await octokit.rest.pulls.list({ owner, repo, state: 'closed', per_page: perPage, page, sort: 'created', direction: 'desc' // Most recent first to find merges faster }); if (data.length === 0) break; const authorMergedPRs = data.filter(pr => pr.user.login === author && pr.merged_at !== null); mergedPRs.push(...authorMergedPRs); // Stop if we have enough merged PRs to determine unlimited quota if (mergedPRs.length >= MAX_MERGED_NEEDED) break; if (data.length < perPage) break; page++; } return { openPRs, mergedCount: mergedPRs.length }; } /** * Process quota management for a specific author * @param {object} octokit - GitHub API client * @param {string} owner - Repository owner * @param {string} repo - Repository name * @param {string} author - PR author username * @param {object} logger - Logger object (console or custom) * @param {boolean} dryRun - If true, only print actions without executing them * @returns {Promise} Processing results */ async function processQuotaForAuthor(octokit, owner, repo, author, logger = console, dryRun = false) { if (dryRun) { logger.log('🔍 DRY RUN MODE - No changes will be made\n'); } logger.log(`\n=== Processing Quota for: @${author} ===\n`); // Fetch PRs by the author (optimized to stop early) const { openPRs, mergedCount } = await fetchAuthorPRs(octokit, owner, repo, author); // Open PRs are already sorted by creation date (oldest first) from the fetch const quota = calculateQuota(mergedCount); const openCount = openPRs.length; // Log history audit logger.log('📜 History Audit:'); if (mergedCount === 0) { logger.log(' No merged PRs found.'); } else if (mergedCount >= 3) { logger.log(` User has ${mergedCount}+ merged PRs (unlimited quota).`); } else { logger.log(` User has ${mergedCount} merged PR${mergedCount > 1 ? 's' : ''}.`); } // Log current stats logger.log(`\n📊 Current Stats:`); logger.log(` User has ${mergedCount} merged PRs. Current Quota: ${quota}. Currently Open: ${openCount}.`); // Ensure label exists if (!dryRun) { await ensureLabelExists(octokit, owner, repo, logger); } // Process each open PR const results = { blocked: [], unblocked: [], unchanged: [] }; logger.log(`\n🔄 Processing Open PRs:\n`); for (let i = 0; i < openPRs.length; i++) { const pr = openPRs[i]; const shouldBeBlocked = i >= quota; const isCurrentlyBlocked = pr.labels.some(label => label.name === LABEL_NAME); if (shouldBeBlocked && !isCurrentlyBlocked) { // Need to block this PR if (dryRun) { logger.log(` 🔍 [DRY RUN] Would label PR #${pr.number} as blocked (Position: ${i + 1}/${openCount}, Quota: ${quota})`); logger.log(` 🔍 [DRY RUN] Would post blocking comment on PR #${pr.number}`); } else { await addLabel(octokit, owner, repo, pr.number, logger); await postBlockingComment(octokit, owner, repo, pr.number, author, openCount, quota, logger); logger.log(` ✅ Labeled PR #${pr.number} as blocked (Position: ${i + 1}/${openCount}, Quota: ${quota})`); } results.blocked.push(pr.number); } else if (!shouldBeBlocked && isCurrentlyBlocked) { // Need to unblock this PR if (dryRun) { logger.log(` 🔍 [DRY RUN] Would remove label from PR #${pr.number} (Position: ${i + 1}/${openCount}, Quota: ${quota})`); logger.log(` 🔍 [DRY RUN] Would post unblocking comment on PR #${pr.number}`); } else { await removeLabel(octokit, owner, repo, pr.number, logger); await postUnblockingComment(octokit, owner, repo, pr.number, author, openCount, quota, logger); logger.log(` ✅ Unblocked PR #${pr.number} (Position: ${i + 1}/${openCount}, Quota: ${quota})`); } results.unblocked.push(pr.number); } else { results.unchanged.push(pr.number); logger.log(` ℹ️ PR #${pr.number} unchanged (${shouldBeBlocked ? 'blocked' : 'active'})`); } } logger.log(`\n✅ Processing Complete for @${author}\n`); return { author, mergedCount, quota, openCount, results }; } /** * Ensure the pr-quota-reached label exists in the repository */ async function ensureLabelExists(octokit, owner, repo, logger) { try { await octokit.rest.issues.getLabel({ owner, repo, name: LABEL_NAME }); } catch (error) { if (error.status === 404) { logger.log(`🏷️ Creating label: ${LABEL_NAME}`); await octokit.rest.issues.createLabel({ owner, repo, name: LABEL_NAME, color: LABEL_COLOR, description: 'PR is on hold due to quota limits for new contributors' }); } else { throw error; } } } /** * Add the quota-reached label to a PR */ async function addLabel(octokit, owner, repo, issueNumber, logger) { try { await octokit.rest.issues.addLabels({ owner, repo, issue_number: issueNumber, labels: [LABEL_NAME] }); } catch (error) { logger.error(`Failed to add label to PR #${issueNumber}:`, error.message); } } /** * Remove the quota-reached label from a PR */ async function removeLabel(octokit, owner, repo, issueNumber, logger) { try { await octokit.rest.issues.removeLabel({ owner, repo, issue_number: issueNumber, name: LABEL_NAME }); } catch (error) { // Ignore 404 errors (label wasn't present) if (error.status !== 404) { logger.error(`Failed to remove label from PR #${issueNumber}:`, error.message); } } } /** * Check if a blocking comment already exists on the PR */ async function hasBlockingComment(octokit, owner, repo, issueNumber) { const { data: comments } = await octokit.rest.issues.listComments({ owner, repo, issue_number: issueNumber }); return comments.some(comment => comment.body && comment.body.includes('This PR is currently **on hold**') ); } /** * Post a blocking comment to a PR */ async function postBlockingComment(octokit, owner, repo, issueNumber, author, openCount, quota, logger) { // Check if blocking comment already exists if (await hasBlockingComment(octokit, owner, repo, issueNumber)) { logger.log(` ℹ️ Blocking comment already exists on PR #${issueNumber}, skipping.`); return; } const message = `Hi @${author}, thanks for your contribution! To ensure quality reviews, we limit how many concurrent PRs new contributors can open: ${formatStatus(openCount, quota)} This PR is currently **on hold**. We will automatically move this into the review queue once your existing PRs are merged or closed. Please see our [Contributing Guidelines](https://github.com/jaegertracing/jaeger/blob/main/CONTRIBUTING_GUIDELINES.md#pull-request-limits-for-new-contributors) for details on our tiered quota policy.`; try { await octokit.rest.issues.createComment({ owner, repo, issue_number: issueNumber, body: message }); } catch (error) { logger.error(`Failed to post blocking comment on PR #${issueNumber}:`, error.message); } } /** * Post an unblocking comment to a PR * Always posts when called - if PR was blocked again after being unblocked, user should be notified again */ async function postUnblockingComment(octokit, owner, repo, issueNumber, author, openCount, quota, logger) { const message = `PR quota unlocked! @${author}, this PR has been moved out of the waiting room and into the active review queue: ${formatStatus(openCount, quota)} Thank you for your patience.`; try { await octokit.rest.issues.createComment({ owner, repo, issue_number: issueNumber, body: message }); } catch (error) { logger.error(`Failed to post unblocking comment on PR #${issueNumber}:`, error.message); } } /** * Main execution function for manual CLI usage */ async function main() { const args = process.argv.slice(2); if (args.length < 1) { console.error('Usage: GITHUB_TOKEN= node pr-quota-manager.js [owner] [repo]'); process.exit(1); } const username = args[0]; const owner = args[1] || process.env.GITHUB_REPOSITORY?.split('/')[0] || 'jaegertracing'; const repo = args[2] || process.env.GITHUB_REPOSITORY?.split('/')[1] || 'jaeger'; const dryRun = process.env.DRY_RUN === 'true' || args.includes('--dry-run'); if (!process.env.GITHUB_TOKEN) { console.error('Error: GITHUB_TOKEN environment variable is required'); process.exit(1); } // Import @octokit/rest dynamically for CLI usage const { Octokit } = await import('@octokit/rest'); const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); try { const result = await processQuotaForAuthor(octokit, owner, repo, username, console, dryRun); console.log('\n📋 Summary:'); console.log(` - Blocked: ${result.results.blocked.length} PRs`); console.log(` - Unblocked: ${result.results.unblocked.length} PRs`); console.log(` - Unchanged: ${result.results.unchanged.length} PRs`); } catch (error) { console.error('Error:', error.message); process.exit(1); } } // GitHub Actions wrapper function async function githubActionHandler({github, core, username, owner, repo, dryRun = false}) { if (!username) { core.setFailed('Username is required'); return; } if (!owner || !repo) { core.setFailed('Owner and repo are required'); return; } // Process the quota try { const result = await processQuotaForAuthor(github, owner, repo, username, console, dryRun); core.info(''); core.info('=== Summary ==='); core.info(`Blocked: ${result.results.blocked.length} PRs`); core.info(`Unblocked: ${result.results.unblocked.length} PRs`); core.info(`Unchanged: ${result.results.unchanged.length} PRs`); if (result.results.blocked.length > 0) { core.info(`Blocked PRs: ${result.results.blocked.join(', ')}`); } if (result.results.unblocked.length > 0) { core.info(`Unblocked PRs: ${result.results.unblocked.join(', ')}`); } } catch (error) { core.setFailed(`Error processing quota: ${error.message}`); throw error; } } // Export for GitHub Actions usage if (typeof module !== 'undefined' && module.exports) { // Default export is the GitHub Actions handler module.exports = githubActionHandler; // Named exports for testing and direct usage module.exports.formatStatus = formatStatus; module.exports.calculateQuota = calculateQuota; module.exports.fetchAuthorPRs = fetchAuthorPRs; module.exports.processQuotaForAuthor = processQuotaForAuthor; module.exports.ensureLabelExists = ensureLabelExists; module.exports.addLabel = addLabel; module.exports.removeLabel = removeLabel; module.exports.hasBlockingComment = hasBlockingComment; module.exports.postBlockingComment = postBlockingComment; module.exports.postUnblockingComment = postUnblockingComment; module.exports.LABEL_NAME = LABEL_NAME; module.exports.LABEL_COLOR = LABEL_COLOR; } // Run main function if executed directly if (require.main === module) { main().catch(error => { console.error('Fatal error:', error); process.exit(1); }); } ================================================ FILE: .github/scripts/pr-quota-manager.test.js ================================================ /** * Unit tests for PR Quota Management System */ const prQuotaManager = require('./pr-quota-manager'); const { formatStatus, calculateQuota, fetchAuthorPRs, processQuotaForAuthor, ensureLabelExists, addLabel, removeLabel, hasBlockingComment, postBlockingComment, postUnblockingComment, LABEL_NAME, LABEL_COLOR } = prQuotaManager; // Mock logger to suppress output during tests const mockLogger = { log: jest.fn(), error: jest.fn() }; describe('formatStatus', () => { test('formats open count and quota as bullet points', () => { expect(formatStatus(3, 5)).toBe(' * Open: 3\n * Limit: 5'); }); }); describe('calculateQuota', () => { test('returns 1 for 0 merged PRs', () => { expect(calculateQuota(0)).toBe(1); }); test('returns 2 for 1 merged PR', () => { expect(calculateQuota(1)).toBe(2); }); test('returns 3 for 2 merged PRs', () => { expect(calculateQuota(2)).toBe(3); }); test('returns 10 (unlimited) for 3 merged PRs', () => { expect(calculateQuota(3)).toBe(10); }); test('returns 10 (unlimited) for 10 merged PRs', () => { expect(calculateQuota(10)).toBe(10); }); }); describe('fetchAuthorPRs', () => { test('fetches open PRs and merged count', async () => { const mockOctokit = { rest: { pulls: { list: jest.fn() // First call for open PRs .mockResolvedValueOnce({ data: [ { number: 1, user: { login: 'testuser' }, state: 'open', merged_at: null }, { number: 2, user: { login: 'otheruser' }, state: 'open', merged_at: null }, { number: 3, user: { login: 'testuser' }, state: 'open', merged_at: null } ] }) // Second call for closed/merged PRs .mockResolvedValueOnce({ data: [ { number: 10, user: { login: 'testuser' }, merged_at: '2024-01-01' }, { number: 11, user: { login: 'otheruser' }, merged_at: '2024-01-02' } ] }) } } }; const result = await fetchAuthorPRs(mockOctokit, 'owner', 'repo', 'testuser'); expect(result.openPRs).toHaveLength(2); expect(result.openPRs[0].number).toBe(1); expect(result.openPRs[1].number).toBe(3); expect(result.mergedCount).toBe(1); }); test('stops fetching merged PRs after finding 3', async () => { const mockOctokit = { rest: { pulls: { list: jest.fn() // Open PRs call .mockResolvedValueOnce({ data: [] }) // First batch of closed PRs with 3 merged .mockResolvedValueOnce({ data: [ { number: 1, user: { login: 'testuser' }, merged_at: '2024-01-01' }, { number: 2, user: { login: 'testuser' }, merged_at: '2024-01-02' }, { number: 3, user: { login: 'testuser' }, merged_at: '2024-01-03' }, { number: 4, user: { login: 'testuser' }, merged_at: null }, // closed but not merged ] }) } } }; const result = await fetchAuthorPRs(mockOctokit, 'owner', 'repo', 'testuser'); expect(result.mergedCount).toBe(3); // Should stop after finding 3 merged PRs, so only 2 calls (1 for open, 1 for closed) expect(mockOctokit.rest.pulls.list).toHaveBeenCalledTimes(2); }); }); describe('ensureLabelExists', () => { test('does not create label if it already exists', async () => { const mockOctokit = { rest: { issues: { getLabel: jest.fn().mockResolvedValue({ data: { name: LABEL_NAME } }), createLabel: jest.fn() } } }; await ensureLabelExists(mockOctokit, 'owner', 'repo', mockLogger); expect(mockOctokit.rest.issues.getLabel).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', name: LABEL_NAME }); expect(mockOctokit.rest.issues.createLabel).not.toHaveBeenCalled(); }); test('creates label if it does not exist', async () => { const mockOctokit = { rest: { issues: { getLabel: jest.fn().mockRejectedValue({ status: 404 }), createLabel: jest.fn().mockResolvedValue({}) } } }; await ensureLabelExists(mockOctokit, 'owner', 'repo', mockLogger); expect(mockOctokit.rest.issues.createLabel).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', name: LABEL_NAME, color: LABEL_COLOR, description: 'PR is on hold due to quota limits for new contributors' }); }); }); describe('addLabel', () => { test('adds label to PR', async () => { const mockOctokit = { rest: { issues: { addLabels: jest.fn().mockResolvedValue({}) } } }; await addLabel(mockOctokit, 'owner', 'repo', 123, mockLogger); expect(mockOctokit.rest.issues.addLabels).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', issue_number: 123, labels: [LABEL_NAME] }); }); test('handles errors gracefully', async () => { const mockOctokit = { rest: { issues: { addLabels: jest.fn().mockRejectedValue(new Error('API error')) } } }; await addLabel(mockOctokit, 'owner', 'repo', 123, mockLogger); expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining('Failed to add label'), expect.any(String) ); }); }); describe('removeLabel', () => { test('removes label from PR', async () => { const mockOctokit = { rest: { issues: { removeLabel: jest.fn().mockResolvedValue({}) } } }; await removeLabel(mockOctokit, 'owner', 'repo', 123, mockLogger); expect(mockOctokit.rest.issues.removeLabel).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', issue_number: 123, name: LABEL_NAME }); }); test('ignores 404 errors when label is not present', async () => { const testLogger = { log: jest.fn(), error: jest.fn() }; const mockOctokit = { rest: { issues: { removeLabel: jest.fn().mockRejectedValue({ status: 404 }) } } }; await removeLabel(mockOctokit, 'owner', 'repo', 123, testLogger); expect(testLogger.error).not.toHaveBeenCalled(); }); test('logs non-404 errors', async () => { const mockOctokit = { rest: { issues: { removeLabel: jest.fn().mockRejectedValue({ status: 500, message: 'Server error' }) } } }; await removeLabel(mockOctokit, 'owner', 'repo', 123, mockLogger); expect(mockLogger.error).toHaveBeenCalled(); }); }); describe('hasBlockingComment', () => { test('returns true if blocking comment exists', async () => { const mockOctokit = { rest: { issues: { listComments: jest.fn().mockResolvedValue({ data: [ { body: 'Some other comment' }, { body: 'This PR is currently **on hold**' } ] }) } } }; const result = await hasBlockingComment(mockOctokit, 'owner', 'repo', 123); expect(result).toBe(true); }); test('returns false if blocking comment does not exist', async () => { const mockOctokit = { rest: { issues: { listComments: jest.fn().mockResolvedValue({ data: [ { body: 'Some other comment' }, { body: 'Another comment' } ] }) } } }; const result = await hasBlockingComment(mockOctokit, 'owner', 'repo', 123); expect(result).toBe(false); }); }); describe('postBlockingComment', () => { test('posts blocking comment if none exists', async () => { const mockOctokit = { rest: { issues: { listComments: jest.fn().mockResolvedValue({ data: [] }), createComment: jest.fn().mockResolvedValue({}) } } }; await postBlockingComment(mockOctokit, 'owner', 'repo', 123, 'testuser', 2, 1, mockLogger); expect(mockOctokit.rest.issues.createComment).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', issue_number: 123, body: expect.stringContaining('This PR is currently **on hold**') }); }); test('skips comment if blocking comment already exists', async () => { const mockOctokit = { rest: { issues: { listComments: jest.fn().mockResolvedValue({ data: [{ body: 'This PR is currently **on hold**' }] }), createComment: jest.fn() } } }; await postBlockingComment(mockOctokit, 'owner', 'repo', 123, 'testuser', 2, 1, mockLogger); expect(mockOctokit.rest.issues.createComment).not.toHaveBeenCalled(); }); }); describe('postUnblockingComment', () => { test('always posts unblocking comment', async () => { const mockOctokit = { rest: { issues: { createComment: jest.fn().mockResolvedValue({}) } } }; await postUnblockingComment(mockOctokit, 'owner', 'repo', 123, 'testuser', 1, 2, mockLogger); expect(mockOctokit.rest.issues.createComment).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', issue_number: 123, body: expect.stringContaining('PR quota unlocked!') }); }); }); describe('processQuotaForAuthor', () => { test('blocks PRs exceeding quota for new contributor', async () => { const mockOctokit = { rest: { pulls: { list: jest.fn() // Open PRs call .mockResolvedValueOnce({ data: [ { number: 1, user: { login: 'newuser' }, state: 'open', merged_at: null, created_at: '2024-01-01T00:00:00Z', labels: [] }, { number: 2, user: { login: 'newuser' }, state: 'open', merged_at: null, created_at: '2024-01-02T00:00:00Z', labels: [] } ] }) // Closed PRs call (no merged PRs found) .mockResolvedValueOnce({ data: [] }) }, issues: { getLabel: jest.fn().mockResolvedValue({ data: { name: LABEL_NAME } }), addLabels: jest.fn().mockResolvedValue({}), listComments: jest.fn().mockResolvedValue({ data: [] }), createComment: jest.fn().mockResolvedValue({}) } } }; const result = await processQuotaForAuthor(mockOctokit, 'owner', 'repo', 'newuser', mockLogger); expect(result.mergedCount).toBe(0); expect(result.quota).toBe(1); expect(result.openCount).toBe(2); expect(result.results.blocked).toEqual([2]); expect(result.results.unchanged).toEqual([1]); }); test('unblocks PRs when quota becomes available', async () => { const mockOctokit = { rest: { pulls: { list: jest.fn() // Open PRs call .mockResolvedValueOnce({ data: [ { number: 1, user: { login: 'contributor' }, state: 'open', merged_at: null, created_at: '2024-01-01T00:00:00Z', labels: [] }, { number: 3, user: { login: 'contributor' }, state: 'open', merged_at: null, created_at: '2024-01-03T00:00:00Z', labels: [{ name: LABEL_NAME }] } ] }) // Closed PRs call (1 merged) .mockResolvedValueOnce({ data: [ { number: 2, user: { login: 'contributor' }, merged_at: '2024-01-05T00:00:00Z' } ] }) }, issues: { getLabel: jest.fn().mockResolvedValue({ data: { name: LABEL_NAME } }), removeLabel: jest.fn().mockResolvedValue({}), listComments: jest.fn().mockResolvedValue({ data: [] }), createComment: jest.fn().mockResolvedValue({}) } } }; const result = await processQuotaForAuthor(mockOctokit, 'owner', 'repo', 'contributor', mockLogger); expect(result.mergedCount).toBe(1); expect(result.quota).toBe(2); expect(result.openCount).toBe(2); expect(result.results.unblocked).toEqual([3]); }); test('processes PRs in order by creation date (oldest first)', async () => { const mockOctokit = { rest: { pulls: { list: jest.fn() // Open PRs are already sorted by creation date from the API .mockResolvedValueOnce({ data: [ { number: 1, user: { login: 'user' }, state: 'open', merged_at: null, created_at: '2024-01-01T00:00:00Z', labels: [] }, { number: 2, user: { login: 'user' }, state: 'open', merged_at: null, created_at: '2024-01-02T00:00:00Z', labels: [] }, { number: 3, user: { login: 'user' }, state: 'open', merged_at: null, created_at: '2024-01-03T00:00:00Z', labels: [] } ] }) // No merged PRs .mockResolvedValueOnce({ data: [] }) }, issues: { getLabel: jest.fn().mockResolvedValue({ data: { name: LABEL_NAME } }), addLabels: jest.fn().mockResolvedValue({}), listComments: jest.fn().mockResolvedValue({ data: [] }), createComment: jest.fn().mockResolvedValue({}) } } }; const result = await processQuotaForAuthor(mockOctokit, 'owner', 'repo', 'user', mockLogger); // First PR (oldest) should not be blocked, others should be expect(result.results.unchanged).toEqual([1]); expect(result.results.blocked).toEqual([2, 3]); }); }); ================================================ FILE: .github/scripts/waiting-for-author.js ================================================ module.exports = async ({github, context, core}) => { const LABEL_NAME = 'waiting-for-author'; // Determine event type const eventName = context.eventName; // Get PR data let prNumber; let repoOwner; let repoName; if (eventName === 'issue_comment') { prNumber = context.payload.issue.number; repoOwner = context.repo.owner; repoName = context.repo.repo; } else if (eventName === 'pull_request_target') { prNumber = context.payload.number; repoOwner = context.repo.owner; repoName = context.repo.repo; } else { core.info(`Unsupported event: ${eventName}`); return; } // Fetch PR details to get the author // We need to fetch the PR object because issue_comment payload doesn't always have full PR details (like author) // correctly populated in a way that is identical to pull_request payload for our needs. const { data: pr } = await github.rest.pulls.get({ owner: repoOwner, repo: repoName, pull_number: prNumber, }); const prAuthor = pr.user.login; if (eventName === 'issue_comment') { const commenter = context.payload.comment.user.login; // Logic: // If Maintainer comments -> Add label (if not present) // If Author comments -> Remove label (if present) // Check if commenter is the author if (commenter === prAuthor) { core.info(`Comment by author ${commenter}. Removing label if present.`); await removeLabel(github, repoOwner, repoName, prNumber, LABEL_NAME, core); } // Check if commenter is a maintainer (has write access) else if (await isMaintainer(github, repoOwner, repoName, commenter, core)) { core.info(`Comment by maintainer ${commenter}. Adding label if missing.`); await addLabel(github, repoOwner, repoName, prNumber, LABEL_NAME, core); } else { core.info(`Comment by ${commenter} (not author or maintainer). No action taken.`); } } else if (eventName === 'pull_request_target') { // This is the 'synchronize' event (push to PR branch) // Logic: // If Author pushes -> Remove label (UNLESS it's just an "Update branch" merge) const sender = context.payload.sender.login; if (sender !== prAuthor) { core.info(`Push by ${sender}, not the PR author ${prAuthor}. Doing nothing.`); return; } // Check if it's an "Update branch" commit // We look at the commits in this push. // context.payload.before and context.payload.after give us the range of commits. // simpler approach: look at the head commit of the PR. // Ideally we want to see if the content changed or if it was just a merge from base. // A heuristic is to check the commit message or parents of the head commit. // We'll fetch the commit details. const headSha = context.payload.pull_request.head.sha; const { data: commit } = await github.rest.repos.getCommit({ owner: repoOwner, repo: repoName, ref: headSha, }); const message = commit.commit.message; const parents = commit.parents; // A merge commit typically has 2 parents. // If it's a merge from the base branch (e.g. "Merge branch 'main' into ...") // Note: GitHub's "Update branch" button creates a merge commit. const isMergeCommit = parents.length > 1; const isUpdateBranch = isMergeCommit && ( message.startsWith(`Merge branch '${context.payload.pull_request.base.ref}'`) || message.startsWith(`Merge remote-tracking branch 'origin/${context.payload.pull_request.base.ref}'`) ); if (isUpdateBranch) { core.info(`Push detected as 'Update branch' (Merge from base). Keeping label.`); return; } core.info(`Push by author detected. Removing label.`); await removeLabel(github, repoOwner, repoName, prNumber, LABEL_NAME, core); } }; async function addLabel(github, owner, repo, issueNumber, label, logger) { try { const { data: labels } = await github.rest.issues.listLabelsOnIssue({ owner, repo, issue_number: issueNumber, }); if (labels.find(l => l.name === label)) { logger.info(`Label '${label}' already exists.`); return; } await github.rest.issues.addLabels({ owner, repo, issue_number: issueNumber, labels: [label], }); logger.info(`Added label '${label}'.`); } catch (error) { logger.error(`Error adding label: ${error.message}`); } } async function removeLabel(github, owner, repo, issueNumber, label, logger) { try { const { data: labels } = await github.rest.issues.listLabelsOnIssue({ owner, repo, issue_number: issueNumber, }); if (!labels.find(l => l.name === label)) { logger.info(`Label '${label}' does not exist.`); return; } await github.rest.issues.removeLabel({ owner, repo, issue_number: issueNumber, name: label, }); logger.info(`Removed label '${label}'.`); } catch (error) { // Ignore 404 if label not found (though check above should catch it) logger.error(`Error removing label: ${error.message}`); } } async function isMaintainer(github, owner, repo, username, logger) { try { const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username, }); // Based on gh api logic: .permissions.maintain==true or .permissions.admin==true or .permissions.push==true // getCollaboratorPermissionLevel returns a 'permission' field which describes the permission level. // Levels: 'admin', 'maintain', 'write', 'triage', 'read', 'none' // We want 'admin', 'maintain', or 'write'. const permission = data.permission; return ['admin', 'maintain', 'write'].includes(permission); } catch (error) { logger.error(`Error checking permissions for ${username}: ${error.message}`); return false; } } ================================================ FILE: .github/workflows/README.md ================================================ # CI Workflows This directory contains GitHub Actions workflows for the Jaeger project. The workflows are organized into a staged architecture to optimize CI resource usage and provide fail-fast behavior. ## Architecture Overview The CI system uses a **Forked DAG (Directed Acyclic Graph)** orchestrated by `ci-orchestrator.yml`. The orchestrator supports two execution paths based on the context of the run: - **Sequential path (~30m)**: Default for external contributors. Stage 1 must pass before Stage 2, and Stage 2 must pass before Stage 3. Provides fail-fast behavior that saves resources when linting or unit tests fail. - **Parallel path (~10m)**: For trusted maintainers, merge queue, and main branch builds. All three stages start simultaneously after a setup step. ### CI Orchestrator The main entry point for PR and branch CI is **`ci-orchestrator.yml`**, which: 1. Runs a **`setup`** job to determine the execution mode (parallel or sequential) 2. Triggers either the sequential or parallel path based on the result #### Setup Job: Execution Mode Detection The `setup` job determines whether to use parallel execution based on these **OR** conditions: | Condition | Rationale | |-----------|-----------| | Push to `main` branch | Already merged, fully trusted | | `merge_group` event | Merge Queue entry, high confidence | | PR author is an org member (`MEMBER` or `OWNER`) | Trusted maintainer | | PR author login is `dependabot[bot]` or `renovate-bot` | Dependency automation bots | | PR has the `ci:parallel` label | Explicit opt-in | #### Stage Workflows (DRY Encapsulation) Each stage is encapsulated in a reusable "stage" workflow: - **ci-orchestrator-stage1.yml** - Stage 1 workflows (Linters only — fast fail-fast gate) - **ci-orchestrator-stage2.yml** - Stage 2 workflows (Unit Tests) - **ci-orchestrator-stage3.yml** - Stage 3 workflows (Docker, E2E, Binaries, Static Analysis) This avoids duplication: both the sequential and parallel paths call the same stage workflows. #### Stage 1: Fast Gate (Linters only) - **ci-lint-checks.yaml** - Go linting, DCO checks, generated files validation, shell script linting #### Stage 2: Unit Tests - **ci-unit-tests.yml** - Full unit test suite with coverage #### Stage 3: Expensive Checks & Static Analysis Executes in parallel within the stage: - **ci-build-binaries.yml** - Multi-platform binary builds - **ci-docker-build.yml** - Docker images for all components - **ci-docker-all-in-one.yml** - All-in-one Docker image - **ci-docker-hotrod.yml** - HotROD demo application image - **ci-e2e-all.yml** - E2E test suite orchestrator (calls individual E2E workflows) - **ci-e2e-spm.yml** - Service Performance Monitoring tests - **ci-e2e-tailsampling.yml** - Tail sampling processor tests - **codeql.yml** - Security scanning with CodeQL - **dependency-review.yml** - Dependency vulnerability checks - **fossa.yml** - License compliance scanning #### Gatekeeper Job The orchestrator includes a final **`ci-success`** job that: - Runs after all stage jobs (regardless of which path was taken) - Determines which path was used and validates its results - Should be used as the required status check in GitHub branch protection rules ### Execution Flow Diagram ``` ┌─────────┐ │ setup │ └────┬────┘ parallel=false │ parallel=true ┌──────────────────┴──────────────────┐ │ Sequential Path │ Parallel Path │ │ ┌────▼──────┐ ┌────────────┼────────────┐ │ stage1-seq│ │ │ │ └────┬──────┘ ┌────▼───┐ ┌────▼───┐ ┌────▼───┐ │ │stage1- │ │stage2- │ │stage3- │ ┌────▼──────┐ │ fast │ │ fast │ │ fast │ │ stage2-seq│ └────┬───┘ └────┬───┘ └────┬───┘ └────┬──────┘ │ │ │ │ └────────────┴───────────┘ ┌────▼──────┐ │ │ stage3-seq│ │ └────┬──────┘ │ └──────────────────┬──────────────────┘ ┌────▼────┐ │ci-success│ └─────────┘ ``` ### Concurrency Control The orchestrator manages concurrency centrally: ```yaml concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true ``` This allows a single "kill-switch" to cancel older runs when new commits are pushed to a PR. ### Permissions Model The orchestrator uses `permissions: write-all` to allow maximum flexibility for child workflows: ```yaml permissions: write-all ``` This grants broad permissions at the orchestrator level, allowing child workflows to request the specific permissions they need. Child workflows then apply the principle of least privilege by downgrading to only the permissions they require: - **codeql.yml**: `security-events: write`, `actions: read` (for security scanning) - **ci-unit-tests.yml**: `checks: write` (for reporting test results) - **ci-docker-all-in-one.yml**: `packages: read` (for pulling from GHCR) - Other workflows: typically `contents: read` only **Why write-all?** When using `workflow_call`, GitHub Actions requires the caller workflow to grant permissions that called workflows can then use or downgrade. Without `write-all`, child workflows would be restricted to `contents: read` only, causing failures for workflows that need additional permissions like CodeQL or test reporting. ## Independent Workflows The following workflows operate independently and are **not** part of the orchestrator: ### Release & Deployment - **ci-release.yml** - Triggered on release events to build and publish artifacts - **ci-deploy-demo.yml** - Scheduled/manual deployment to demo environment ### Automated Checks - **ci-summary-report.yml** - Fan-in workflow triggered after CI Orchestrator completes; posts a consolidated PR comment with performance metrics comparison and code coverage gating (see `docs/adr/004-migrating-coverage-gating-to-github-actions.md`) - **label-check.yml** - Verifies PR labels - **pr-quota-manager.yml** - PR management automation - **dco_merge_group.yml** - DCO verification for merge groups ### Scheduled Maintenance - **stale.yml** - Marks and closes stale issues/PRs - **waiting-for-author.yml** - PR status management - **scorecard.yml** - Security scorecard scanning ### Special Cases - **ci-unit-tests-go-tip.yml** - Tests against Go development version (runs on main or when workflow modified) - **codeql.yml** - Also runs on schedule (weekly) in addition to being called by orchestrator ## E2E Test Workflows Individual E2E test workflows are called by `ci-e2e-all.yml`: - ci-e2e-badger.yaml - ci-e2e-cassandra.yml - ci-e2e-clickhouse.yml - ci-e2e-elasticsearch.yml - ci-e2e-grpc.yml - ci-e2e-kafka.yml - ci-e2e-memory.yaml - ci-e2e-opensearch.yml - ci-e2e-query.yml These workflows use `workflow_call` only and don't have independent triggers. ## Branch Protection To require CI checks before merging, configure branch protection to require: - **CI Orchestrator / ci-success** - This single check represents the entire CI pipeline This is much simpler than requiring 10+ individual workflow checks. ## Local Development Individual workflows can still be triggered manually via the GitHub Actions UI for testing or debugging purposes. However, on PR events, only the orchestrator runs to avoid duplicate work. ## Benefits 1. **Reduced Feedback Loop**: Trusted contributors get ~10m feedback instead of ~30m 2. **Fail-Fast for External Contributors**: Expensive checks only run after cheaper ones pass, saving resources 3. **Simplified Branch Protection**: Single `ci-success` check represents the entire CI pipeline 4. **Centralized Concurrency Control**: Single kill-switch via `cancel-in-progress: true` 5. **DRY Stage Workflows**: Both execution paths reuse the same `ci-orchestrator-stage*.yml` workflows 6. **Maintainability**: Individual child workflows remain decoupled and independently testable ================================================ FILE: .github/workflows/ci-build-binaries.yml ================================================ name: Build binaries on: workflow_call: # See https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions permissions: contents: read jobs: generate-matrix: runs-on: ubuntu-latest outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: define matrix id: set-matrix run: | echo "matrix=$(bash scripts/utils/platforms-to-gh-matrix.sh)" >> $GITHUB_OUTPUT build-binaries: needs: generate-matrix runs-on: ubuntu-latest strategy: fail-fast: false matrix: ${{fromJson(needs.generate-matrix.outputs.matrix)}} name: build-binaries-${{ matrix.os }}-${{ matrix.arch }} steps: - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: submodules: true - name: Fetch git tags run: | git fetch --prune --unshallow --tags - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - name: Setup Node.js version uses: ./.github/actions/setup-node.js - name: Install tools run: make install-ci - name: Build platform binaries run: make build-binaries-${{ matrix.os }}-${{ matrix.arch }} env: # Skip debug binaries on PRs to save CI time (4+ min per arch) # Debug binaries are still built on main branch and during releases SKIP_DEBUG_BINARIES: ${{ github.event_name == 'pull_request' && '1' || '0' }} ================================================ FILE: .github/workflows/ci-deploy-demo.yml ================================================ name: Deploy Jaeger Demo to OKE on: schedule: - cron: '0 13 * * *' # Daily at 8:00 AM US Eastern Time (ET) workflow_dispatch: permissions: read-all jobs: deploy: name: Deploy Jaeger to OKE Cluster runs-on: ubuntu-latest env: OCI_CLI_USER: ${{ secrets.OCI_CLI_USER }} OCI_CLI_TENANCY: ${{ secrets.OCI_CLI_TENANCY }} OCI_CLI_FINGERPRINT: ${{ secrets.OCI_CLI_FINGERPRINT }} OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} OCI_CLI_REGION: ${{ secrets.OCI_CLI_REGION }} steps: - name: Configure kubectl with OKE uses: oracle-actions/configure-kubectl-oke@77a733d79446dabe7bf0e58eb56197d33ce4dc58 # v1.5.0 with: cluster: ${{ secrets.OKE_CLUSTER_OCID }} enablePrivateEndpoint: false - name: Install Helm uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4 - name: Checkout jaeger repository uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6 - name: Deploy using appropriate script run: | if [ "${{ github.event_name }}" = "schedule" ]; then echo "🕒 Scheduled run - using deploy-all.sh (upgrade mode)" bash ./examples/oci/deploy-all.sh else echo "🔄 Manual run - using deploy-all.sh with clean mode (uninstall/install)" bash ./examples/oci/deploy-all.sh clean fi - name: Send detailed Slack notification on failure if: failure() uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 # v2.3.0 env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} SLACK_CHANNEL: '#jaeger-operations' SLACK_COLOR: danger SLACK_USERNAME: 'Jaeger CI Bot' SLACK_ICON_EMOJI: ':warning:' SLACK_TITLE: '🚨 Jaeger OKE Deployment Failed' SLACK_MESSAGE: | *Repository:* ${{ github.repository }} *Workflow:* ${{ github.workflow }} *Run ID:* ${{ github.run_id }} *Trigger:* ${{ github.event_name }} *Actor:* ${{ github.actor }} <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|🔗 View Failed Run> SLACK_FOOTER: 'Jaeger CI/CD Pipeline' ================================================ FILE: .github/workflows/ci-docker-all-in-one.yml ================================================ name: Build all-in-one on: workflow_call: permissions: contents: read packages: read # This allows the runner to pull from GHCR jobs: all-in-one: runs-on: ubuntu-latest timeout-minutes: 30 # max + 3*std over the last 2600 runs steps: - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: submodules: true - name: Fetch git tags run: git fetch --prune --unshallow --tags - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - name: Log in to GHCR uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - uses: ./.github/actions/setup-node.js - uses: ./.github/actions/setup-branch - run: make install-ci - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - name: Define BUILD_FLAGS var if running on a Pull Request or Merge Queue run: | case ${GITHUB_EVENT_NAME} in pull_request|merge_group) echo "BUILD_FLAGS=-l -p linux/$(go env GOARCH)" >> ${GITHUB_ENV} ;; *) echo "BUILD_FLAGS=" >> ${GITHUB_ENV} ;; esac - name: Build, test, and publish all-in-one image run: | bash scripts/build/build-all-in-one-image.sh ${{ env.BUILD_FLAGS }} v2 env: DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} QUAY_TOKEN: ${{ secrets.QUAY_TOKEN }} ================================================ FILE: .github/workflows/ci-docker-build.yml ================================================ name: Build docker images on: workflow_call: # See https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions permissions: contents: read jobs: docker-images: runs-on: ubuntu-latest steps: - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: submodules: true - name: Fetch git tags run: git fetch --prune --unshallow --tags - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - uses: ./.github/actions/setup-node.js - uses: ./.github/actions/setup-branch - run: make install-ci - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - name: Build only linux/amd64 container images for a Pull Request if: github.ref_name != 'main' # -D disables images with debugger run: bash scripts/build/build-upload-docker-images.sh -D -p linux/amd64 - name: Build and upload all container images if: github.ref_name == 'main' run: bash scripts/build/build-upload-docker-images.sh env: DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} QUAY_TOKEN: ${{ secrets.QUAY_TOKEN }} ================================================ FILE: .github/workflows/ci-docker-hotrod.yml ================================================ name: CIT Hotrod on: workflow_call: # See https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions permissions: contents: read jobs: hotrod: runs-on: ubuntu-latest strategy: fail-fast: false matrix: runtime: [docker, k8s] steps: - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: submodules: true - name: Fetch git tags run: | git fetch --prune --unshallow --tags - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - uses: ./.github/actions/setup-node.js - uses: ./.github/actions/setup-branch - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - name: Define BUILD_FLAGS var if running on a Pull Request run: | case ${GITHUB_EVENT_NAME} in pull_request) echo "BUILD_FLAGS=-l -p linux/amd64" >> ${GITHUB_ENV} ;; *) echo "BUILD_FLAGS=" >> ${GITHUB_ENV} ;; esac - name: Install kubectl if: matrix.runtime == 'k8s' uses: azure/setup-kubectl@3e0aec4d80787158d308d7b364cb1b702e7feb7f # v4 with: version: 'latest' - name: Create k8s Kind Cluster if: matrix.runtime == 'k8s' uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1 - name: Build, test, and publish hotrod image run: bash scripts/build/build-hotrod-image.sh ${{ env.BUILD_FLAGS }} -r ${{ matrix.runtime }} env: DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} QUAY_TOKEN: ${{ secrets.QUAY_TOKEN }} ================================================ FILE: .github/workflows/ci-e2e-all.yml ================================================ # Copyright (c) 2026 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # Fan-out workflow that invokes all individual E2E integration test workflows # for every supported storage backend and feature. name: E2E Tests on: workflow_call: permissions: contents: read jobs: badger: uses: ./.github/workflows/ci-e2e-badger.yaml cassandra: uses: ./.github/workflows/ci-e2e-cassandra.yml elasticsearch: uses: ./.github/workflows/ci-e2e-elasticsearch.yml grpc: uses: ./.github/workflows/ci-e2e-grpc.yml kafka: uses: ./.github/workflows/ci-e2e-kafka.yml memory: uses: ./.github/workflows/ci-e2e-memory.yaml opensearch: uses: ./.github/workflows/ci-e2e-opensearch.yml query: uses: ./.github/workflows/ci-e2e-query.yml clickhouse: uses: ./.github/workflows/ci-e2e-clickhouse.yml spm: uses: ./.github/workflows/ci-e2e-spm.yml tailsampling: uses: ./.github/workflows/ci-e2e-tailsampling.yml ================================================ FILE: .github/workflows/ci-e2e-badger.yaml ================================================ # Copyright (c) 2026 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # Integration tests for Badger (embedded key-value store) storage backend. # direct: classic tests at the storage API layer, directly instantiating the storage implementation. # e2e: multi-process E2E tests via the trace ingestion and query APIs. name: CIT Badger on: workflow_call: # See https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions permissions: # added using https://github.com/step-security/secure-workflows contents: read jobs: badger: runs-on: ubuntu-latest strategy: fail-fast: false matrix: storage_test: [direct, e2e] steps: - name: Harden Runner uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - name: Run Badger storage integration tests env: # Short interval so metricsCopier fires within the test duration. # Propagated to the Jaeger subprocess via PropagateEnvVars in badger_test.go. BADGER_METRICS_UPDATE_INTERVAL: 1s run: | case ${{ matrix.storage_test }} in direct) make badger-storage-integration-test ;; e2e) STORAGE=badger make jaeger-v2-storage-integration-test ;; esac - uses: ./.github/actions/verify-metrics-snapshot if: matrix.storage_test == 'e2e' with: snapshot: metrics_snapshot_badger artifact_key: metrics_snapshot_badger_${{ matrix.storage_test }} - name: Upload coverage to codecov uses: ./.github/actions/upload-codecov with: files: cover.out flag: badger_${{ matrix.storage_test }} ================================================ FILE: .github/workflows/ci-e2e-cassandra.yml ================================================ # Copyright (c) 2026 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # Integration tests for Cassandra storage backend, covering multiple Cassandra # major versions, schema versions, and both manual and auto schema creation. # direct: classic tests at the storage API layer, directly instantiating the storage implementation. # e2e: multi-process E2E tests via the trace ingestion and query APIs. name: CIT Cassandra on: workflow_call: # See https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions permissions: # added using https://github.com/step-security/secure-workflows contents: read jobs: cassandra: runs-on: ubuntu-latest strategy: fail-fast: false matrix: storage_test: [direct, e2e] create-schema: [manual, auto] version: - distribution: cassandra major: 4.x schema: v004 - distribution: cassandra major: 5.x schema: v004 exclude: # Exclude direct as creating schema on startup is only available in e2e mode - storage_test: direct create-schema: auto name: ${{ matrix.version.distribution }}-${{ matrix.version.major }} ${{ matrix.storage_test }} schema=${{ matrix.create-schema }} steps: - name: Harden Runner uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - name: Run cassandra integration tests id: test-execution run: bash scripts/e2e/cassandra.sh ${{ matrix.version.major }} ${{ matrix.version.schema }} ${{ matrix.storage_test }} env: SKIP_APPLY_SCHEMA: ${{ matrix.create-schema == 'auto' && true || false }} - uses: ./.github/actions/verify-metrics-snapshot if: matrix.storage_test == 'e2e' with: snapshot: metrics_snapshot_cassandra artifact_key: metrics_snapshot_cassandras_${{ matrix.version.major }}_${{ matrix.version.schema }}_${{ matrix.storage_test }}_${{ matrix.create-schema }} - name: Upload coverage to codecov uses: ./.github/actions/upload-codecov with: files: cover.out flag: cassandra-${{ matrix.version.major }}-${{ matrix.storage_test }}-${{ matrix.create-schema }} ================================================ FILE: .github/workflows/ci-e2e-clickhouse.yml ================================================ # Copyright (c) 2026 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # E2E integration tests for ClickHouse storage backend (Jaeger v2 only). name: CIT ClickHouse on: workflow_call: # See https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions permissions: # added using https://github.com/step-security/secure-workflows contents: read jobs: clickhouse: runs-on: ubuntu-latest steps: - name: Harden Runner uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - name: Run ClickHouse integration tests id: test-execution run: bash scripts/e2e/clickhouse.sh - uses: ./.github/actions/verify-metrics-snapshot with: snapshot: metrics_snapshot_clickhouse artifact_key: metrics_snapshot_clickhouse - name: Upload coverage to codecov uses: ./.github/actions/upload-codecov with: files: cover.out flag: clickhouse ================================================ FILE: .github/workflows/ci-e2e-elasticsearch.yml ================================================ # Copyright (c) 2026 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # Integration tests for Elasticsearch storage backend, covering major versions 6.x through 9.x. # direct: classic tests at the storage API layer, directly instantiating the storage implementation. # e2e: multi-process E2E tests via the trace ingestion and query APIs. name: CIT Elasticsearch on: workflow_call: # See https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions permissions: # added using https://github.com/step-security/secure-workflows contents: read jobs: elasticsearch: runs-on: ubuntu-latest strategy: fail-fast: false matrix: version: - major: 6.x distribution: elasticsearch storage_test: direct - major: 7.x distribution: elasticsearch storage_test: direct - major: 8.x distribution: elasticsearch storage_test: direct - major: 8.x distribution: elasticsearch storage_test: e2e - major: 9.x distribution: elasticsearch storage_test: e2e name: ${{ matrix.version.distribution }} ${{ matrix.version.major }} ${{ matrix.version.storage_test }} steps: - name: Harden Runner uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: submodules: true - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - name: time settings run: | date echo TZ="$TZ" - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - name: Run ${{ matrix.version.distribution }} integration tests id: test-execution run: bash scripts/e2e/elasticsearch.sh ${{ matrix.version.distribution }} ${{ matrix.version.major }} ${{ matrix.version.storage_test }} - uses: ./.github/actions/verify-metrics-snapshot if: matrix.version.storage_test == 'e2e' with: snapshot: metrics_snapshot_elasticsearch artifact_key: metrics_snapshot_elasticsearch_${{ matrix.version.major }}_${{ matrix.version.storage_test }} - name: Upload coverage to codecov uses: ./.github/actions/upload-codecov with: files: cover.out,cover-index-cleaner.out,cover-index-rollover.out flag: ${{ matrix.version.distribution }}-${{ matrix.version.major }}-${{ matrix.version.storage_test }} ================================================ FILE: .github/workflows/ci-e2e-grpc.yml ================================================ # Copyright (c) 2026 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # Integration tests for the gRPC remote storage plugin interface. # direct: classic tests at the storage API layer, directly instantiating the storage implementation. # e2e: multi-process E2E tests via the trace ingestion and query APIs. name: CIT gRPC on: workflow_call: # See https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions permissions: # added using https://github.com/step-security/secure-workflows contents: read jobs: grpc: runs-on: ubuntu-latest strategy: fail-fast: false matrix: storage_test: [direct, e2e] steps: - name: Harden Runner uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - name: Run gRPC storage integration tests run: | case ${{ matrix.storage_test }} in direct) SPAN_STORAGE_TYPE=memory make grpc-storage-integration-test ;; e2e) STORAGE=grpc make jaeger-v2-storage-integration-test ;; esac - uses: ./.github/actions/verify-metrics-snapshot if: matrix.storage_test == 'e2e' with: snapshot: metrics_snapshot_grpc artifact_key: metrics_snapshot_grpc_${{ matrix.storage_test }} - name: Upload coverage to codecov uses: ./.github/actions/upload-codecov with: files: cover.out flag: grpc_${{ matrix.storage_test }} ================================================ FILE: .github/workflows/ci-e2e-kafka.yml ================================================ # Copyright (c) 2026 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # E2E integration tests for Kafka as a span ingestion buffer (Jaeger v2), # verifying the Collector → Kafka → Ingester pipeline. name: CIT Kafka on: workflow_call: # See https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions permissions: # added using https://github.com/step-security/secure-workflows contents: read jobs: kafka: runs-on: ubuntu-latest strategy: fail-fast: false matrix: kafka-version: ["3.x"] name: kafka ${{ matrix.kafka-version }} v2 steps: - name: Harden Runner uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - name: Run kafka integration tests id: test-execution run: bash scripts/e2e/kafka.sh -v ${{ matrix.kafka-version }} - uses: ./.github/actions/verify-metrics-snapshot with: snapshot: metrics_snapshot_kafka artifact_key: metrics_snapshot_kafka_v2 - name: Upload coverage to codecov uses: ./.github/actions/upload-codecov with: files: cover.out flag: kafka-${{ matrix.kafka-version }}-v2 ================================================ FILE: .github/workflows/ci-e2e-memory.yaml ================================================ # Copyright (c) 2026 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # E2E integration tests for the in-memory storage backend (Jaeger v2). name: CIT Memory on: workflow_call: # See https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions permissions: # added using https://github.com/step-security/secure-workflows contents: read jobs: memory-v2: runs-on: ubuntu-latest steps: - name: Harden Runner uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - name: Run Memory storage integration tests run: | STORAGE=memory_v2 make jaeger-v2-storage-integration-test - uses: ./.github/actions/verify-metrics-snapshot with: snapshot: metrics_snapshot_memory artifact_key: metrics_snapshot_memory - name: Upload coverage to codecov uses: ./.github/actions/upload-codecov with: files: cover.out flag: memory_v2 ================================================ FILE: .github/workflows/ci-e2e-opensearch.yml ================================================ # Copyright (c) 2026 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # Integration tests for OpenSearch storage backend, covering major versions 1.x through 3.x. # direct: classic tests at the storage API layer, directly instantiating the storage implementation. # e2e: multi-process E2E tests via the trace ingestion and query APIs. name: CIT OpenSearch on: workflow_call: # See https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions permissions: # added using https://github.com/step-security/secure-workflows contents: read jobs: opensearch: runs-on: ubuntu-latest strategy: fail-fast: false matrix: version: - major: 1.x distribution: opensearch storage_test: direct - major: 2.x distribution: opensearch storage_test: direct - major: 2.x distribution: opensearch storage_test: e2e - major: 3.x distribution: opensearch storage_test: e2e name: ${{ matrix.version.distribution }} ${{ matrix.version.major }} ${{ matrix.version.storage_test }} steps: - name: Harden Runner uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: submodules: true - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - name: Run ${{ matrix.version.distribution }} integration tests id: test-execution run: bash scripts/e2e/elasticsearch.sh ${{ matrix.version.distribution }} ${{ matrix.version.major }} ${{ matrix.version.storage_test }} - uses: ./.github/actions/verify-metrics-snapshot if: matrix.version.storage_test == 'e2e' with: snapshot: metrics_snapshot_opensearch artifact_key: metrics_snapshot_opensearch_${{ matrix.version.major }} - name: Upload coverage to codecov uses: ./.github/actions/upload-codecov with: files: cover.out,cover-index-cleaner.out,cover-index-rollover.out flag: ${{ matrix.version.distribution }}-${{ matrix.version.major }}-${{ matrix.version.storage_test }} ================================================ FILE: .github/workflows/ci-e2e-query.yml ================================================ # Copyright (c) 2026 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # E2E integration tests for a two-process deployment topology: # - collector (config-remote-storage-backend.yaml): receives OTLP traces, # writes to in-memory storage, exposes it via the remote_storage gRPC extension. # - query (config-query.yaml): no ingestion pipeline, serves the query API # by reading from the collector's remote_storage gRPC endpoint. name: CIT Query on: workflow_call: # See https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions permissions: # added using https://github.com/step-security/secure-workflows contents: read jobs: query: runs-on: ubuntu-latest steps: - name: Harden Runner uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - name: Run Memory storage integration tests run: | STORAGE=query make jaeger-v2-storage-integration-test - name: Upload coverage to codecov uses: ./.github/actions/upload-codecov with: files: cover.out flag: query ================================================ FILE: .github/workflows/ci-e2e-spm.yml ================================================ # Copyright (c) 2026 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # E2E integration tests for Service Performance Monitoring (SPM), verifying # that span metrics are correctly derived and served via the metrics API, # with Prometheus, Elasticsearch, and OpenSearch as the metrics store. name: Test SPM on: workflow_call: # See https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions permissions: contents: read jobs: spm: runs-on: ubuntu-latest strategy: fail-fast: false matrix: mode: - name: v2 metricstore: prometheus - name: v2 with ES metricstore: elasticsearch - name: v2 with OS metricstore: opensearch steps: - name: Harden Runner uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: submodules: true - name: Fetch git tags run: | git fetch --prune --unshallow --tags - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - name: Setup Node.js version uses: ./.github/actions/setup-node.js - name: Run SPM Test run: bash scripts/e2e/spm.sh -m ${{ matrix.mode.metricstore }} ================================================ FILE: .github/workflows/ci-e2e-tailsampling.yml ================================================ # Copyright (c) 2026 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # E2E integration tests for the tail-based sampling processor, verifying # that sampling decisions are applied correctly after collecting full traces. name: Test Tail Sampling Processor on: workflow_call: # See https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions permissions: # added using https://github.com/step-security/secure-workflows contents: read jobs: tailsampling-processor: runs-on: ubuntu-latest steps: - name: Harden Runner uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - name: Run Tail Sampling Processor Integration Test run: | make tail-sampling-integration-test - name: Upload coverage to codecov uses: ./.github/actions/upload-codecov with: files: cover.out flag: tailsampling-processor ================================================ FILE: .github/workflows/ci-lint-checks.yaml ================================================ name: Lint Checks on: workflow_call: # See https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions permissions: # added using https://github.com/step-security/secure-workflows contents: read jobs: lint: runs-on: ubuntu-latest steps: - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after a couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - name: Print Jaeger version for no reason run: make echo-version - run: make install-test-tools - run: make lint pull-request-preconditions: runs-on: ubuntu-latest steps: - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after a couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: ./.github/actions/block-pr-from-main-branch - run: | git fetch origin main make lint-nocommit dco-check: runs-on: ubuntu-latest steps: - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after a couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python 3.x for DCO check uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.x' - name: Run DCO check if: ${{ github.event.pull_request.user.login != 'dependabot' && github.event_name != 'merge_group' }} run: python3 scripts/lint/dco_check.py -b main -v --exclude-pattern '@users\.noreply\.github\.com' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} idl-version-check: runs-on: ubuntu-latest steps: - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: submodules: recursive fetch-tags: true - name: check jaeger-idl versions across git submodule and go.mod dependency run: make lint-jaeger-idl-versions generated-files-check: runs-on: ubuntu-latest steps: - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: submodules: recursive - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - name: Verify Protobuf types are up to date run: make proto && { if git status --porcelain | grep '??'; then exit 1; else git diff --name-status --exit-code; fi } - name: Verify Mockery types are up to date run: make generate-mocks && { if git status --porcelain | grep '??'; then exit 1; else git diff --name-status --exit-code; fi } lint-shell-scripts: runs-on: ubuntu-latest steps: - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - run: sudo apt-get install shellcheck - run: shellcheck scripts/**/*.sh - name: Install shunit2 for shell unit tests uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: repository: kward/shunit2 path: .tools/shunit2 - name: Run unit tests for scripts run: | SHUNIT2=.tools/shunit2 bash scripts/utils/run-tests.sh binary-size-check: runs-on: ubuntu-latest steps: - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: submodules: true - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - name: Setup Node.js version uses: ./.github/actions/setup-node.js - name: Build jaeger binary run: make build-jaeger - name: Calculate jaeger binary size run: | TOTAL_SIZE=$(du -sb ./cmd/jaeger/jaeger-linux-amd64 | cut -f1) echo "$TOTAL_SIZE" > ./new_jaeger_binary_size.txt echo "Total binary size: $TOTAL_SIZE bytes" - name: Restore previous binary size id: cache-binary-size uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: ./jaeger_binary_size.txt key: jaeger_binary_size restore-keys: | jaeger_binary_size - name: Compare `jaeger` binary sizes if: ${{ (steps.cache-binary-size.outputs.cache-matched-key != '') && ((github.event_name != 'push') || (github.ref != 'refs/heads/main')) }} run: | set -euf -o pipefail OLD_BINARY_SIZE=$(cat ./jaeger_binary_size.txt) NEW_BINARY_SIZE=$(cat ./new_jaeger_binary_size.txt) printf "Previous binary size: %'d bytes\n" $OLD_BINARY_SIZE printf "New binary size: %'d bytes\n" $NEW_BINARY_SIZE PERCENTAGE_CHANGE=$(echo "scale=2; ($NEW_BINARY_SIZE - $OLD_BINARY_SIZE) * 100 / $OLD_BINARY_SIZE" | bc) if (( $(echo "$PERCENTAGE_CHANGE > 2.0" | bc) == 1 )); then echo "❌ binary size increased by more than 2% ($PERCENTAGE_CHANGE%)" exit 1 else echo "✅ binary size change is within acceptable range ($PERCENTAGE_CHANGE%)" fi - name: Remove previous *_binary_*.txt run: | rm -rf ./jaeger_binary_size.txt mv ./new_jaeger_binary_size.txt ./jaeger_binary_size.txt - name: Save new jaeger binary size if: ${{ (github.event_name == 'push') && (github.ref == 'refs/heads/main') }} uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: ./jaeger_binary_size.txt key: jaeger_binary_size_${{ github.run_id }} validate-renovate-config: runs-on: ubuntu-latest steps: - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: submodules: false - name: validate renovate config run: | docker run \ -v $PWD/renovate.json:/usr/src/app/renovate.json \ ghcr.io/renovatebot/renovate:latest \ renovate-config-validator ================================================ FILE: .github/workflows/ci-orchestrator-stage1.yml ================================================ name: "CI Orchestrator: Stage 1 (Linters)" on: workflow_call: jobs: lint-checks: uses: ./.github/workflows/ci-lint-checks.yaml secrets: inherit ================================================ FILE: .github/workflows/ci-orchestrator-stage2.yml ================================================ name: "CI Orchestrator: Stage 2 (Unit Tests)" on: workflow_call: jobs: unit-tests: uses: ./.github/workflows/ci-unit-tests.yml secrets: inherit ================================================ FILE: .github/workflows/ci-orchestrator-stage3.yml ================================================ name: "CI Orchestrator: Stage 3 (Docker, E2E, Binaries, Static Analysis)" on: workflow_call: jobs: build-binaries: uses: ./.github/workflows/ci-build-binaries.yml secrets: inherit docker-build: uses: ./.github/workflows/ci-docker-build.yml secrets: inherit docker-all-in-one: uses: ./.github/workflows/ci-docker-all-in-one.yml secrets: inherit docker-hotrod: uses: ./.github/workflows/ci-docker-hotrod.yml secrets: inherit e2e-tests: uses: ./.github/workflows/ci-e2e-all.yml secrets: inherit codeql: uses: ./.github/workflows/codeql.yml secrets: inherit dependency-review: uses: ./.github/workflows/dependency-review.yml secrets: inherit fossa: uses: ./.github/workflows/fossa.yml secrets: inherit ================================================ FILE: .github/workflows/ci-orchestrator.yml ================================================ name: CI Orchestrator on: pull_request: branches: [main] push: branches: [main] merge_group: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true # Grant all permissions to allow child workflows to request what they need # Child workflows can downgrade permissions as needed (principle of least privilege) permissions: write-all jobs: # ============================================================================ # SETUP: Determine execution mode (sequential vs parallel) # Parallel mode is used for trusted actors to reduce feedback loop from ~30m to ~10m. # ============================================================================ setup: runs-on: ubuntu-latest outputs: parallel: ${{ steps.mode.outputs.parallel }} steps: - name: Determine execution mode id: mode run: | PARALLEL=false # Parallel for push to main if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then echo "Parallel: push to main" PARALLEL=true else echo "Not triggered by push to main (event=${{ github.event_name }}, ref=${{ github.ref }})" fi # Parallel for merge queue if [[ "${{ github.event_name }}" == "merge_group" ]]; then echo "Parallel: merge_group event" PARALLEL=true else echo "Not a merge_group event (event=${{ github.event_name }})" fi # PR-specific checks (org membership, labels, and PR author login are only meaningful on pull_request events) if [[ "${{ github.event_name }}" == "pull_request" ]]; then # Parallel for org members. # Use a live API call because author_association from the event payload is # unreliable — it reports CONTRIBUTOR for org members who don't have direct # repo access via a team. Fall back to author_association when the API call # fails (e.g. insufficient token permissions for fork PRs). PR_AUTHOR="${{ github.event.pull_request.user.login }}" AUTHOR_ASSOC="${{ github.event.pull_request.author_association }}" if gh api --silent "orgs/jaegertracing/members/$PR_AUTHOR" 2>/dev/null; then echo "Parallel: org member ($PR_AUTHOR, verified via API)" PARALLEL=true elif [[ "$AUTHOR_ASSOC" == "MEMBER" || "$AUTHOR_ASSOC" == "OWNER" ]]; then echo "Parallel: org member ($PR_AUTHOR, author_association=$AUTHOR_ASSOC)" PARALLEL=true else echo "Not an org member ($PR_AUTHOR, author_association=$AUTHOR_ASSOC)" fi # Parallel for known bots (dependency update automation) if [[ "$PR_AUTHOR" == "dependabot[bot]" || "$PR_AUTHOR" == "renovate-bot" ]]; then echo "Parallel: bot PR author ($PR_AUTHOR)" PARALLEL=true else echo "Not a known bot (PR author=$PR_AUTHOR)" fi # Parallel if the ci:parallel label is applied to the PR. # NOTE: re-running jobs does not refresh the event payload; a new run is needed # to pick up labels added after the workflow was first triggered. PR_LABELS="${{ join(github.event.pull_request.labels.*.name, ', ') }}" echo "PR labels: ${PR_LABELS:-}" if [[ "${{ contains(github.event.pull_request.labels.*.name, 'ci:parallel') }}" == "true" ]]; then echo "Parallel: ci:parallel label found" PARALLEL=true else echo "ci:parallel label not found in: ${PR_LABELS:-}" fi else echo "Not a pull_request event — skipping PR-specific checks" fi echo "parallel=$PARALLEL" >> "$GITHUB_OUTPUT" echo "Execution mode: parallel=$PARALLEL" # ============================================================================ # SCRIPTS UNIT TESTS: Fast, independent job for .github/scripts/ Jest suite. # ============================================================================ ci-scripts: name: CI Scripts Unit Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: '24' cache: 'npm' cache-dependency-path: .github/scripts/package-lock.json - name: Install Node dependencies (retry on transient registry failures) working-directory: .github/scripts run: | set -euo pipefail npm config set fetch-retries 5 npm config set fetch-retry-mintimeout 20000 npm config set fetch-retry-maxtimeout 120000 attempts=3 for i in $(seq 1 "$attempts"); do echo "npm ci attempt $i/$attempts" if npm ci; then exit 0 fi if [ "$i" -lt "$attempts" ]; then sleep_time=$((i * 15)) echo "npm ci failed, retrying in ${sleep_time}s" sleep "$sleep_time" fi done echo "npm ci failed after $attempts attempts" exit 1 - run: npm test working-directory: .github/scripts # ============================================================================ # SEQUENTIAL PATH (~30m): Default for external contributors. # Stage 2 waits for Stage 1; Stage 3 waits for Stage 2. # Active when parallel == false. # ============================================================================ stage1-seq: needs: [setup] if: ${{ needs.setup.outputs.parallel == 'false' }} uses: ./.github/workflows/ci-orchestrator-stage1.yml secrets: inherit stage2-seq: needs: [setup, stage1-seq] if: ${{ needs.setup.outputs.parallel == 'false' }} uses: ./.github/workflows/ci-orchestrator-stage2.yml secrets: inherit stage3-seq: needs: [setup, stage2-seq] if: ${{ needs.setup.outputs.parallel == 'false' }} uses: ./.github/workflows/ci-orchestrator-stage3.yml secrets: inherit # ============================================================================ # PARALLEL PATH (~10m): For trusted maintainers, merge queue, and main branch. # All stages start simultaneously after setup. # Active when parallel == true. # ============================================================================ stage1-fast: needs: [setup] if: ${{ needs.setup.outputs.parallel == 'true' }} uses: ./.github/workflows/ci-orchestrator-stage1.yml secrets: inherit stage2-fast: needs: [setup] if: ${{ needs.setup.outputs.parallel == 'true' }} uses: ./.github/workflows/ci-orchestrator-stage2.yml secrets: inherit stage3-fast: needs: [setup] if: ${{ needs.setup.outputs.parallel == 'true' }} uses: ./.github/workflows/ci-orchestrator-stage3.yml secrets: inherit # ============================================================================ # FINAL GATEKEEPER: Use this job for Branch Protection. # Validates whichever execution path was taken (sequential or parallel). # ============================================================================ ci-success: name: All CI Checks Passed runs-on: ubuntu-latest if: always() needs: [setup, ci-scripts, stage1-seq, stage2-seq, stage3-seq, stage1-fast, stage2-fast, stage3-fast] steps: - name: Check setup status if: ${{ needs.setup.result != 'success' }} run: | echo "❌ Setup job failed or was cancelled." exit 1 - name: Check CI scripts tests if: ${{ needs.ci-scripts.result != 'success' }} run: | echo "❌ CI scripts unit tests failed or were cancelled." exit 1 - name: Check sequential path if: ${{ needs.setup.outputs.parallel == 'false' }} run: | S1="${{ needs.stage1-seq.result }}" S2="${{ needs.stage2-seq.result }}" S3="${{ needs.stage3-seq.result }}" if [[ "$S1" != "success" || "$S2" != "success" || "$S3" != "success" ]]; then echo "❌ CI failed on sequential path. Stage 1: $S1, Stage 2: $S2, Stage 3: $S3" exit 1 fi echo "✅ CI passed on sequential path." - name: Check parallel path if: ${{ needs.setup.outputs.parallel == 'true' }} run: | S1="${{ needs.stage1-fast.result }}" S2="${{ needs.stage2-fast.result }}" S3="${{ needs.stage3-fast.result }}" if [[ "$S1" != "success" || "$S2" != "success" || "$S3" != "success" ]]; then echo "❌ CI failed on parallel path. Stage 1: $S1, Stage 2: $S2, Stage 3: $S3" exit 1 fi echo "✅ CI passed on parallel path." - name: Validate execution path was determined run: | PARALLEL="${{ needs.setup.outputs.parallel }}" if [[ "$PARALLEL" != "true" && "$PARALLEL" != "false" ]]; then echo "❌ Invalid parallel mode: '$PARALLEL' (expected 'true' or 'false')" exit 1 fi # ============================================================================ # SUMMARY REPORT: Runs after all CI stages pass. # Computes coverage gating and metrics comparison, uploads ci-summary artifact. # Fails visibly in PR Checks if coverage drops or metrics regress. # ci-summary-report-publish.yml (workflow_run) reads the artifact and posts # the PR comment and check runs — even when this job fails. # ============================================================================ summary: name: CI Summary Report needs: [ci-success] if: always() && needs.ci-success.result == 'success' uses: ./.github/workflows/ci-summary-report.yml secrets: inherit ================================================ FILE: .github/workflows/ci-release.yml ================================================ name: Publish release on: release: types: - published workflow_dispatch: inputs: dry_run: required: true type: boolean description: Do a test run. It will only build one platform (for speed) and will not push artifacts. overwrite: required: true type: boolean description: Allow overwriting artifacts. jobs: publish-release: permissions: contents: write deployments: write packages: read # This allows the runner to pull from GHCR if: github.repository == 'jaegertracing/jaeger' runs-on: jaeger-linux-amd64-32core-1200GB_SSD steps: - name: Clean up some disk space # We had an issue where the workflow was running out of disk space, # because it downloads so many Docker images for different platforms. # Here we delete some stuff from the VM that we do not use. # Inspired by https://github.com/jlumbroso/free-disk-space. run: | sudo rm -rf /usr/local/lib/android || true df -h / - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: submodules: true - name: Fetch git tags run: | git fetch --prune --unshallow --tags - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - uses: ./.github/actions/setup-node.js - name: Determine parameters id: params run: | docker_flags=() if [[ "${{ inputs.dry_run }}" == "true" ]]; then docker_flags=("${docker_flags[@]}" -l -p linux/amd64) echo "platforms=linux/amd64" >> $GITHUB_OUTPUT echo "gpg_key_override=-k skip" >> $GITHUB_OUTPUT else echo "platforms=$(make echo-platforms)" >> $GITHUB_OUTPUT fi if [[ "${{ inputs.overwrite }}" == "true" ]]; then docker_flags=("${docker_flags[@]}" -o) fi echo "docker_flags=${docker_flags[@]}" >> $GITHUB_OUTPUT cat $GITHUB_OUTPUT - name: Export BRANCH variable and validate it is a semver # Many scripts depend on BRANCH variable. We do not want to # use ./.github/actions/setup-branch here because it may set # BRANCH=main when the workflow is triggered manually. run: | BRANCH=$(make echo-version) echo Validate that the latest tag ${BRANCH} is in semver format echo ${BRANCH} | grep -E '^v[0-9]+.[0-9]+.[0-9]+(-rc[0-9]+)?$' echo "BRANCH=${BRANCH}" >> ${GITHUB_ENV} - name: Configure GPG Key if: ${{ inputs.dry_run != true }} uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0 with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.GPG_PASSPHRASE }} - name: Build all binaries run: make build-all-platforms PLATFORMS=${{ steps.params.outputs.platforms }} - name: Package binaries run: | bash scripts/build/package-deploy.sh \ -p ${{ steps.params.outputs.platforms }} \ ${{ steps.params.outputs.gpg_key_override }} - name: Upload binaries if: ${{ inputs.dry_run != true }} uses: svenstaro/upload-release-action@5e35e583720436a2cc5f9682b6f55657101c1ea1 # 2.11.1 with: file: '{deploy/*.tar.gz,deploy/*.zip,deploy/*.sha256sum.txt,deploy/*.asc}' file_glob: true overwrite: ${{ inputs.overwrite }} tag: ${{ env.BRANCH }} repo_token: ${{ secrets.GITHUB_TOKEN }} - name: Delete the release artifacts after uploading them. run: | rm -rf deploy || true df -h / - name: Log in to GHCR uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - name: Build and upload all container images # -B skips building the binaries since we already did that above run: | bash scripts/build/build-upload-docker-images.sh -B \ ${{ steps.params.outputs.docker_flags }} env: DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} QUAY_TOKEN: ${{ secrets.QUAY_TOKEN }} - name: Build, test, and publish jaeger image run: | bash scripts/build/build-all-in-one-image.sh \ ${{ steps.params.outputs.docker_flags }} env: DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} QUAY_TOKEN: ${{ secrets.QUAY_TOKEN }} - name: Build, test, and publish hotrod image run: | bash scripts/build/build-hotrod-image.sh \ ${{ steps.params.outputs.docker_flags }} env: DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} QUAY_TOKEN: ${{ secrets.QUAY_TOKEN }} - name: Generate SBOM uses: anchore/sbom-action@17ae1740179002c89186b61233e0f892c3118b11 # v0.23.0 with: output-file: jaeger-SBOM.spdx.json upload-release-assets: false upload-artifact: false - name: Upload SBOM # Upload SBOM manually, because anchore/sbom-action does not do that # when the workflow is triggered manually, only from a release. uses: svenstaro/upload-release-action@5e35e583720436a2cc5f9682b6f55657101c1ea1 # 2.11.1 if: ${{ inputs.dry_run != true }} with: file: jaeger-SBOM.spdx.json overwrite: ${{ inputs.overwrite }} tag: ${{ env.BRANCH }} repo_token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/ci-summary-report-publish.yml ================================================ # Copyright (c) 2026 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # CI Summary Report (Publish): posts PR comments and check runs from the # pre-computed ci-summary artifact produced by ci-summary-report.yml. # # Triggered by workflow_run on CI Orchestrator completion (success OR failure). # pull_request workflows from forks run with read-only permissions regardless of # what the workflow file declares, so they cannot post PR comments or check runs. # workflow_run always executes in the base repository context and gets the # permissions declared here — that is why posting is split into this workflow. # The heavy computation runs inside the CI Orchestrator itself (ci-summary-report.yml), # making gate failures immediately visible in the PR Checks table without waiting # for this workflow's separate approval step. # # Design: docs/adr/004-migrating-coverage-gating-to-github-actions.md # Security model: see .github/scripts/ci-summary-report-publish.js name: CI Summary Report (Publish) on: workflow_run: workflows: ["CI Orchestrator"] types: [completed] workflow_dispatch: inputs: run_id: description: 'CI Orchestrator run ID to publish summary for' required: true pr_number: description: 'PR number to post summary report for' required: true permissions: contents: read pull-requests: write checks: write actions: read jobs: publish: name: Post Summary if: | github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure' runs-on: ubuntu-latest steps: # Resolve the CI Orchestrator run ID and head SHA. # PR number is read from the ci-summary artifact after download (see below). - name: Resolve source run id: source-run env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then RUN_ID="${{ github.event.inputs.run_id }}" PR_NUMBER="${{ github.event.inputs.pr_number }}" for var in RUN_ID PR_NUMBER; do if ! [[ "${!var}" =~ ^[1-9][0-9]*$ ]]; then echo "::error::Invalid $var input: must be a positive integer, got: '${!var}'" exit 1 fi done echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT HEAD_SHA=$(gh api "repos/${{ github.repository }}/actions/runs/$RUN_ID" --jq '.head_sha') else RUN_ID="${{ github.event.workflow_run.id }}" HEAD_SHA="${{ github.event.workflow_run.head_sha }}" fi echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT echo "head_sha=$HEAD_SHA" >> $GITHUB_OUTPUT echo "source_run_url=https://github.com/${{ github.repository }}/actions/runs/$RUN_ID" >> $GITHUB_OUTPUT echo "summary_run_url=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_OUTPUT # Checkout the base repo so that .github/scripts/ci-summary-report-publish.js # is available for require() in the github-script step below. # Do NOT checkout the PR head: this workflow must only run base-repo code. - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 # Download only the ci-summary artifact (pre-computed by ci-summary-report.yml). - name: Download CI summary artifact env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh run download "${{ steps.source-run.outputs.run_id }}" \ --repo "${{ github.repository }}" \ --name ci-summary \ --dir .artifacts \ || echo "::warning::ci-summary artifact not found; proceeding without summary artifact (later checks may fail)." # For workflow_run triggers, read pr_number from ci-summary.json (written # by ci-summary-report.yml inside the CI Orchestrator, where # github.event.pull_request.number is accurate). # For workflow_dispatch, pr_number is already set from the input above. - name: Read PR number from artifact id: pr if: github.event_name != 'workflow_dispatch' run: | if [ -f .artifacts/ci-summary.json ]; then PR_NUMBER=$(jq -r '.pr_number // empty' .artifacts/ci-summary.json 2>/dev/null || true) if [ -n "$PR_NUMBER" ]; then echo "Found PR #$PR_NUMBER in ci-summary.json" echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT else echo "No PR number in ci-summary.json; PR comment will be skipped." fi else echo "ci-summary.json not found; PR comment will be skipped." fi - name: Post PR comment and create check runs if: steps.source-run.outputs.head_sha uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const handler = require('./.github/scripts/ci-summary-report-publish.js'); await handler({ github, core, fs: require('fs'), inputs: { owner: context.repo.owner, repo: context.repo.repo, headSha: '${{ steps.source-run.outputs.head_sha }}', prNumber: '${{ steps.source-run.outputs.pr_number || steps.pr.outputs.pr_number }}', ciRunUrl: '${{ steps.source-run.outputs.source_run_url }}', publishUrl: '${{ steps.source-run.outputs.summary_run_url }}', }, }); env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/ci-summary-report.yml ================================================ # Copyright (c) 2026 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # CI Summary Report: reusable workflow (workflow_call) invoked by CI Orchestrator. # Computes metrics comparison and coverage gating, then uploads a ci-summary # artifact with the results. ci-summary-report-publish.yml (triggered by # workflow_run) reads that artifact to post PR comments and check runs, because # pull_request workflows from forks cannot write to the upstream repository. # # Design: docs/adr/004-migrating-coverage-gating-to-github-actions.md name: CI Summary Report on: workflow_call: permissions: contents: read actions: write # required for actions/cache save and gh run download jobs: summary-report: name: Summary Report runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6 with: ref: ${{ github.sha }} # Download all artifacts uploaded by the calling (CI Orchestrator) run. # This includes coverage-* and metrics_snapshot_* artifacts from all CI jobs. - name: Download all artifacts env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh run download "${{ github.run_id }}" \ --repo "${{ github.repository }}" --dir .artifacts - name: Install dependencies run: python3 -m pip install prometheus-client - name: Compare metrics and generate summary id: compare-metrics shell: bash run: bash ./scripts/e2e/metrics_summary.sh - name: Set up Go for coverage tools uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x cache-dependency-path: | ./go.sum ./internal/tools/go.sum - name: Install coverage tools run: make install-coverage-tools - name: Merge coverage profiles id: merge-coverage run: | mapfile -t COVER_FILES < <(find .artifacts -path "*/coverage-*/*.out" -type f) if [ ${#COVER_FILES[@]} -eq 0 ]; then echo "No coverage files found; skipping coverage gate." echo "skipped=true" >> "$GITHUB_OUTPUT" else echo "Merging ${#COVER_FILES[@]} coverage profiles" ./.tools/gocovmerge "${COVER_FILES[@]}" > .artifacts/merged-coverage.out echo "skipped=false" >> "$GITHUB_OUTPUT" fi - name: Filter excluded paths from merged coverage if: success() && steps.merge-coverage.outputs.skipped == 'false' run: | # Applies the same exclusions as .codecov.yml (single source of truth). # filter_coverage.py modifies the file in-place. python3 scripts/e2e/filter_coverage.py .artifacts/merged-coverage.out echo "Coverage lines after filtering: $(wc -l < .artifacts/merged-coverage.out)" - name: Calculate current coverage percentage if: success() && steps.merge-coverage.outputs.skipped == 'false' id: coverage run: | PCT=$(go tool cover -func=.artifacts/merged-coverage.out \ | grep "^total:" | awk '{print $3}' | tr -d '%') echo "percentage=${PCT}" >> "$GITHUB_OUTPUT" echo "${PCT}" > .artifacts/current-coverage.txt echo "Current coverage: ${PCT}%" - name: Restore baseline coverage from main if: success() && steps.merge-coverage.outputs.skipped == 'false' id: restore-baseline uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: .artifacts/baseline-coverage.txt # Exact match intentionally never hits (run IDs differ between runs). # The restore-keys prefix coverage-baseline_ always falls back to the # most recently created cache entry (GitHub returns the newest match # for prefix lookups), which is the latest passing main-branch run. # The trailing underscore avoids matching a plain "coverage-baseline" # key if one were ever created. # Storage is negligible: each entry is a single number (~10 B) and # GitHub automatically evicts entries unused for 7 days. key: coverage-baseline_${{ github.run_id }} restore-keys: | coverage-baseline_ - name: Gate on coverage regression if: success() && steps.merge-coverage.outputs.skipped == 'false' id: coverage-gate run: | CURRENT="${{ steps.coverage.outputs.percentage }}" BASELINE_MSG="(no baseline yet)" failure_reasons=() if [ -z "$CURRENT" ]; then failure_reasons+=("coverage percentage is empty; go tool cover may have failed") else # Gate 1: absolute minimum threshold MINIMUM=95.0 if (( $(echo "$CURRENT < $MINIMUM" | bc -l) )); then failure_reasons+=("coverage ${CURRENT}% is below minimum ${MINIMUM}%") fi # Gate 2: no regression vs main baseline if [ -f .artifacts/baseline-coverage.txt ]; then BASELINE=$(cat .artifacts/baseline-coverage.txt) if [ -z "$BASELINE" ]; then failure_reasons+=("baseline coverage file is empty; cannot perform regression check") else BASELINE_MSG="(baseline ${BASELINE}%)" if (( $(echo "$CURRENT < $BASELINE" | bc -l) )); then failure_reasons+=("coverage dropped from ${BASELINE}% to ${CURRENT}%") fi fi fi fi if [ ${#failure_reasons[@]} -gt 0 ]; then msg=$(IFS='; '; echo "${failure_reasons[*]}") echo "conclusion=failure" >> "$GITHUB_OUTPUT" echo "summary=${msg}" >> "$GITHUB_OUTPUT" echo "::error::${msg}" else echo "conclusion=success" >> "$GITHUB_OUTPUT" echo "summary=Coverage ${CURRENT}% ${BASELINE_MSG}" >> "$GITHUB_OUTPUT" echo "Coverage ${CURRENT}% ${BASELINE_MSG}: OK" fi # Serialize only strongly-typed values to JSON so ci-summary-report-publish.yml # never handles free-form text from test output (which could contain injections). # All display text is constructed from this structured data by trusted publish- # workflow code running in the base repository context. # # metrics_snapshots is an array of per-snapshot change data (metric names and # counts) produced by metrics_summary.sh. The publish workflow validates every # field before rendering (see sanitizeSnapshots in ci-summary-report-publish.js). - name: Save conclusions for publish workflow if: always() env: PR_NUMBER: ${{ github.event.pull_request.number }} METRICS_CONCLUSION: ${{ steps.compare-metrics.outputs.CONCLUSION }} METRICS_TOTAL: ${{ steps.compare-metrics.outputs.TOTAL_CHANGES }} METRICS_INFRA_ERRORS: ${{ steps.compare-metrics.outputs.INFRA_ERRORS }} COVERAGE_MERGE_OUTCOME: ${{ steps.merge-coverage.outcome }} COVERAGE_SKIPPED: ${{ steps.merge-coverage.outputs.skipped }} COVERAGE_CONCLUSION: ${{ steps.coverage-gate.outputs.conclusion }} COVERAGE_PCT: ${{ steps.coverage.outputs.percentage }} run: | mkdir -p .artifacts python3 - <<'PYEOF' import json, os metrics_conclusion = os.environ.get('METRICS_CONCLUSION') or 'failure' metrics_total_env = os.environ.get('METRICS_TOTAL') if metrics_total_env not in (None, ''): metrics_total = int(metrics_total_env) else: metrics_total = None has_infra_errors = os.environ.get('METRICS_INFRA_ERRORS') == 'true' coverage_merge_outcome = os.environ.get('COVERAGE_MERGE_OUTCOME', '') coverage_skipped = os.environ.get('COVERAGE_SKIPPED') == 'true' if coverage_merge_outcome in ('failure', 'cancelled'): # merge-coverage failed before writing its skipped output; treat as failure # so a pipeline error is not silently reported as coverage skipped/success. coverage_conclusion = 'failure' elif coverage_skipped: coverage_conclusion = 'skipped' else: coverage_conclusion = os.environ.get('COVERAGE_CONCLUSION') or 'skipped' coverage_pct = None coverage_baseline = None if not coverage_skipped: try: coverage_pct = float(os.environ.get('COVERAGE_PCT') or '') except (ValueError, TypeError): pass try: with open('.artifacts/baseline-coverage.txt') as f: coverage_baseline = float(f.read().strip()) except (FileNotFoundError, ValueError): pass # Load per-snapshot metric change data (produced by metrics_summary.sh). # If the file is missing or malformed, set to None — the publish # workflow treats null/missing as "no detail available". metrics_snapshots = None try: with open('.artifacts/metrics_snapshots.json') as f: metrics_snapshots = json.load(f) except (FileNotFoundError, json.JSONDecodeError, ValueError): pass pr_number_env = os.environ.get('PR_NUMBER') pr_number = int(pr_number_env) if pr_number_env else None data = { 'pr_number': pr_number, 'metrics_conclusion': metrics_conclusion, 'metrics_total_changes': metrics_total, 'metrics_has_infra_errors': has_infra_errors, 'metrics_snapshots': metrics_snapshots, 'coverage_conclusion': coverage_conclusion, 'coverage_percentage': coverage_pct, 'coverage_baseline': coverage_baseline, 'coverage_skipped': coverage_skipped, } with open('.artifacts/ci-summary.json', 'w') as f: json.dump(data, f, indent=2) print(json.dumps(data, indent=2)) PYEOF - name: Upload CI summary artifact if: always() uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 with: name: ci-summary path: .artifacts/ci-summary.json retention-days: 7 if-no-files-found: warn # Save baseline coverage on main-branch runs so PRs can compare against it. # Only save when the coverage gate passes so a failing/partial run never # overwrites a valid baseline with a bad value. - name: Save coverage baseline on main branch if: >- github.ref == 'refs/heads/main' && steps.merge-coverage.outputs.skipped == 'false' && steps.coverage-gate.outputs.conclusion == 'success' run: cp .artifacts/current-coverage.txt .artifacts/baseline-coverage.txt - name: Cache coverage baseline if: >- github.ref == 'refs/heads/main' && steps.merge-coverage.outputs.skipped == 'false' && steps.coverage-gate.outputs.conclusion == 'success' uses: actions/cache/save@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: .artifacts/baseline-coverage.txt key: coverage-baseline_${{ github.run_id }} # Fail the job (and the calling CI Orchestrator run) so the coverage/metrics # regression is immediately visible in the PR Checks table. # The ci-summary artifact is already uploaded above (if: always()), so # ci-summary-report-publish.yml can still post the PR comment and check runs. - name: Fail if coverage or metrics gate failed if: | steps.compare-metrics.outputs.CONCLUSION == 'failure' || steps.coverage-gate.outputs.conclusion == 'failure' run: | echo "Metrics: ${{ steps.compare-metrics.outputs.CONCLUSION }}" echo "Coverage: ${{ steps.coverage-gate.outputs.conclusion }}" exit 1 ================================================ FILE: .github/workflows/ci-unit-tests-go-tip.yml ================================================ name: Unit Tests on Go Tip on: push: branches: [main] workflow_dispatch: # We normally don't want this workflow to run on PRs, only on main branch. # Unless the workflow file itself or the setup action is modified. pull_request: branches: [main] paths: - '.github/workflows/ci-unit-tests-go-tip.yml' - '.github/actions/setup-go-tip/**' permissions: contents: read jobs: unit-tests-go-tip: permissions: checks: write runs-on: ubuntu-latest steps: - name: Harden Runner uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Install Go Tip uses: ./.github/actions/setup-go-tip with: gh_token: ${{ secrets.GITHUB_TOKEN }} - name: Clean Go cache for gotip run: | go clean -cache go clean -modcache - name: Run unit tests run: make test-ci ================================================ FILE: .github/workflows/ci-unit-tests.yml ================================================ name: Unit Tests on: workflow_call: permissions: contents: read jobs: unit-tests: permissions: checks: write runs-on: ubuntu-latest steps: - name: Harden Runner uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x cache-dependency-path: ./go.sum # download dependencies separately to keep unit test step's output cleaner - name: go mod download run: go mod download - name: Install test deps # even though the same target runs from test-ci, running it separately makes for cleaner log in GH workflow run: make install-test-tools - name: Run unit tests run: make test-ci - name: Upload coverage to codecov uses: ./.github/actions/upload-codecov with: files: cover.out flag: unittests ================================================ FILE: .github/workflows/codeql.yml ================================================ name: "CodeQL" on: workflow_call: schedule: - cron: '31 6 * * 1' # See https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions permissions: # added using https://github.com/step-security/secure-workflows contents: read jobs: codeql-analyze: # Skip merge_group to avoid duplicate runs (code already scanned on pull_request) # See https://github.com/github/codeql-action/issues/1537 if: ${{ github.event_name != 'merge_group' }} name: CodeQL Analyze runs-on: ubuntu-latest permissions: security-events: write actions: read strategy: fail-fast: false matrix: language: [ 'go', 'python' ] steps: - name: Harden Runner uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit - name: Checkout repository uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: submodules: true - name: Setup Go if: matrix.language == 'go' uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - name: Initialize CodeQL uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 with: languages: ${{ matrix.language }} # Use Autobuild for Python (it works fine for interpreted languages) - name: Autobuild (Python) if: matrix.language == 'python' uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 # Explicit build for Go - required for CodeQL to analyze compiled code - name: Build Go code if: matrix.language == 'go' run: | # Build all Go binaries to ensure CodeQL can analyze them go build -v ./... - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 ================================================ FILE: .github/workflows/dco_merge_group.yml ================================================ # Fake "DCO check" workflow inspired by https://github.com/onnx/onnx/pull/5398/files. # The regular DCO check is required, but it does not run from a merge queue and there is # no way to configure it to run. name: DCO on: merge_group: permissions: contents: read jobs: DCO: runs-on: ubuntu-latest steps: - run: echo "Fake DCO check to avoid blocking the merge queue" ================================================ FILE: .github/workflows/dependency-review.yml ================================================ # Dependency Review Action # # This Action will scan dependency manifest files that change as part of a Pull Request, # surfacing known-vulnerable versions of the packages declared or updated in the PR. # Once installed, if the workflow run is marked as required, # PRs introducing known-vulnerable packages will be blocked from merging. # # Source repository: https://github.com/actions/dependency-review-action name: 'Dependency Review' on: workflow_call: permissions: contents: read jobs: dependency-review: if: | ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' || github.event_name == 'merge_group' }} runs-on: ubuntu-latest steps: - name: Harden Runner uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit - name: 'Checkout Repository' uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: 'Dependency Review' uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 ================================================ FILE: .github/workflows/fossa.yml ================================================ name: FOSSA on: workflow_call: # See https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions permissions: contents: read jobs: fossa-license-scan: runs-on: ubuntu-latest steps: - name: Harden Runner uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - name: Add GOPATH run: | echo "GOPATH=$(go env GOPATH)" echo "GOPATH=$(go env GOPATH)" >>"$GITHUB_ENV" echo "$GOPATH/bin" >>"$GITHUB_PATH" - name: Run FOSSA scan and upload report uses: fossa-contrib/fossa-action@3d2ef181b1820d6dcd1972f86a767d18167fa19b # v3.0.1 with: # FOSSA Push-Only API Token fossa-api-key: 304657e2357ba57b416b94e6b119131b github-token: ${{ github.token }} ================================================ FILE: .github/workflows/label-check.yml ================================================ name: Verify PR Label on: merge_group: pull_request: types: - opened - reopened - synchronize - ready_for_review - labeled - unlabeled permissions: contents: read jobs: check-label: runs-on: ubuntu-latest steps: - name: Harden Runner uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit - name: Check PR label # Only fail if NOT merge_group, AND labels DO NOT contain 'changelog:' if: | github.event_name != 'merge_group' && contains(join(github.event.pull_request.labels.*.name, ','), 'changelog:') == false run: | echo "::error::Pull request is missing a required 'changelog:' label. Found labels: ${{ join(github.event.pull_request.labels.*.name, ', ') }}" exit 1 ================================================ FILE: .github/workflows/pr-quota-manager.yml ================================================ name: PR Quota Manager on: pull_request_target: # Runs with write permissions even for fork PRs types: [opened, closed, reopened, synchronize] # synchronize = new commits pushed workflow_dispatch: inputs: username: description: 'GitHub username to process quota for' required: true type: string dryRun: description: 'Dry run mode - show actions without making changes' required: false type: boolean default: false permissions: pull-requests: write issues: write concurrency: group: quota-${{ github.event.pull_request.user.login || github.event.inputs.username }} cancel-in-progress: false jobs: manage-quota: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Process PR Quota uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: # Use custom PAT if available (not available for fork PRs), otherwise use default token github-token: ${{ secrets.PR_QUOTA_MANAGER_PAT || github.token }} script: | const handler = require('./.github/scripts/pr-quota-manager.js') // For PR events, use PR author; for manual runs, use input. Fail if no username can be determined. const prUser = context.payload.pull_request?.user?.login const inputUser = context.payload.inputs?.username const username = prUser || inputUser if (!username) { core.setFailed('Unable to determine username for quota processing. Aborting.') return } const owner = context.repo.owner const repo = context.repo.repo const dryRun = context.payload.inputs?.dryRun === 'true' await handler({github, core, username, owner, repo, dryRun}) ================================================ FILE: .github/workflows/scorecard.yml ================================================ # This workflow uses actions that are not certified by GitHub. They are provided # by a third-party and are governed by separate terms of service, privacy # policy, and support documentation. name: Scorecard supply-chain security on: # For Branch-Protection check. Only the default branch is supported. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection branch_protection_rule: # To guarantee Maintained check is occasionally updated. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained schedule: - cron: '17 20 * * 1' push: branches: [ "main" ] # Declare default permissions as read only. permissions: read-all jobs: analysis: name: Scorecard analysis runs-on: ubuntu-latest permissions: # Needed to upload the results to code-scanning dashboard. security-events: write # Needed to publish results and get a badge (see publish_results below). id-token: write # Uncomment the permissions below if installing in a private repository. # contents: read # actions: read steps: - name: Harden Runner uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 with: egress-policy: audit - name: "Checkout code" uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false - name: "Run analysis" uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 with: results_file: results.sarif results_format: sarif # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: # - you want to enable the Branch-Protection check on a *public* repository, or # - you are installing Scorecard on a *private* repository # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. # repo_token: ${{ secrets.SCORECARD_TOKEN }} # Public repositories: # - Publish results to OpenSSF REST API for easy access by consumers # - Allows the repository to include the Scorecard badge. # - See https://github.com/ossf/scorecard-action#publishing-results. # For private repositories: # - `publish_results` will always be set to `false`, regardless # of the value entered here. publish_results: true # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: SARIF file path: results.sarif retention-days: 5 # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" uses: github/codeql-action/upload-sarif@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 with: sarif_file: results.sarif ================================================ FILE: .github/workflows/stale.yml ================================================ name: 'Close stale issues and PRs' on: schedule: # Run every Monday at 1:30 AM UTC - cron: '30 1 * * 1' workflow_dispatch: permissions: issues: write pull-requests: write jobs: stale: runs-on: ubuntu-latest steps: - name: Harden Runner uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 with: egress-policy: audit - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e with: # Issues configuration days-before-issue-stale: 90 days-before-issue-close: 14 stale-issue-message: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. To keep it open either add a comment or the label `do-not-expire`. close-issue-message: > This issue has been automatically closed due to inactivity. stale-issue-label: 'stale' exempt-issue-labels: 'do-not-expire,help-wanted' only-issue-labels: 'question' # Pull requests configuration days-before-pr-stale: 60 days-before-pr-close: 14 stale-pr-message: > This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. You may re-open it if you need more time. close-pr-message: > This pull request has been automatically closed due to inactivity. You may re-open it if you need more time. We really appreciate your contribution and we are sorry that this has not been completed. stale-pr-label: 'stale' exempt-pr-labels: 'do-not-expire' # General configuration operations-per-run: 100 remove-stale-when-updated: true ================================================ FILE: .github/workflows/waiting-for-author.yml ================================================ name: "Waiting for Author" on: pull_request_target: types: [synchronize] pull_request_review_comment: types: [created] issue_comment: types: [created] permissions: pull-requests: write issues: write jobs: triage: if: ${{ github.event.issue.pull_request || github.event.pull_request }} runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Manage Waiting for Author Label uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const script = require('./.github/scripts/waiting-for-author.js') await script({github, context, core}) ================================================ FILE: .gitignore ================================================ go.work go.work.sum .tools/ *.out *.test *.xml *.swp .fmt.log .import.log .lint.log cover.html .envrc .idea/ .vscode/ .tmp/ .mkdocs-virtual-env/ vendor/ # Jaeger binaries examples/hotrod/hotrod examples/hotrod/hotrod-* cmd/agent/agent cmd/agent/agent-* cmd/anonymizer/anonymizer cmd/anonymizer/anonymizer-* cmd/jaeger/jaeger cmd/jaeger/jaeger-* cmd/jaeger/internal/integration/results cmd/remote-storage/remote-storage cmd/remote-storage/remote-storage-* cmd/es-index-cleaner/es-index-cleaner-* cmd/es-rollover/es-rollover-* cmd/esmapping-generator/esmapping-generator-* cmd/tracegen/tracegen cmd/tracegen/tracegen-* crossdock/crossdock-* run-crossdock.log __pycache__ .asset-manifest.json deploy/ deploy-staging/ sha256sum.combined.txt resource.syso .gocache test-results.json .metrics/ .mockery.log ================================================ FILE: .gitmodules ================================================ [submodule "idl"] path = idl url = https://github.com/jaegertracing/jaeger-idl.git branch = main [submodule "jaeger-ui"] path = jaeger-ui url = https://github.com/jaegertracing/jaeger-ui.git branch = main ================================================ FILE: .golangci.yml ================================================ version: "2" run: go: "1.26" linters: enable: - asciicheck - bidichk - bodyclose - contextcheck - copyloopvar - decorder - depguard - durationcheck - errname - errorlint - gocritic - gosec - misspell - nakedret - nilerr - noctx - nolintlint - perfsprint - revive - staticcheck - testifylint - unused - usestdlibvars - usetesting disable: - errcheck settings: depguard: rules: disallow-crossdock: files: - '!**/crossdock/**' deny: - pkg: github.com/crossdock/crossdock-go desc: Do not refer to crossdock from other packages disallow-otel-contrib-translator: files: - '!**/v1adapter/**' deny: - pkg: github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger desc: Use v1adapter package instead of opentelemetry-collector-contrib/pkg/translator/jaeger disallow-uber/goleak: files: - '**_test.go' deny: - pkg: go.uber.org/goleak desc: Use github.com/jaegertracing/jaeger/internal/testutils disallowed-deps: deny: - pkg: go.uber.org/atomic desc: Use 'sync/atomic' instead of go.uber.org/atomic - pkg: io/ioutil desc: Use os or io instead of io/ioutil - pkg: github.com/hashicorp/go-multierror desc: Use errors.Join instead of github.com/hashicorp/go-multierror - pkg: go.uber.org/multierr desc: Use errors.Join instead of github.com/hashicorp/go-multierror - pkg: github.com/jaegertracing/jaeger/model$ desc: Use github.com/jaegertracing/jaeger-idl/model/v1 - pkg: github.com/jaegertracing/jaeger/internal/proto-gen/api_v2$ desc: Use github.com/jaegertracing/jaeger-idl/proto-gen/api_v2 gocritic: disabled-checks: - appendAssign - commentedOutCode - deferInLoop - dupArg - exitAfterDefer - hugeParam - importShadow - paramTypeCombine # WON'T FIX - returnAfterHttpError - todoCommentWithoutDetail - unnamedResult enable-all: true gosec: excludes: - G104 - G107 - G404 - G601 govet: disable: - fieldalignment - shadow enable-all: true perfsprint: int-conversion: true err-error: true errorf: true sprintf1: true strconcat: true revive: severity: error enable-all-rules: true rules: # not a completely bad linter, but needs clean-up and sensible width (80 is too small) - name: line-length-limit arguments: - 80 disabled: true # this should be enabled after fixing or disabling in a few packages - name: package-directory-mismatch disabled: true # would be ok if we could exclude the test files, but otherwise too noisy - name: add-constant disabled: true # maybe enable in the future, needs more investigation - name: cognitive-complexity disabled: true # not sure how different from previous one - name: cyclomatic disabled: true # we use storage_v2, so... - name: var-naming disabled: true # could be useful to catch issues, but needs a clean-up and some ignores - name: unchecked-type-assertion disabled: true # wtf: "you have exceeded the maximum number of public struct declarations" - name: max-public-structs disabled: true # often looks like a red herring, needs investigation - name: flag-parameter disabled: true # looks like a good linter, needs cleanup # - name: confusing-naming # disabled: true # too pendantic - name: function-length disabled: true # definitely a good one, needs cleanup first - name: argument-limit disabled: true # this is idiocy, promotes less readable code. Don't enable. - name: var-declaration disabled: true # "no nested structs are allowed" - don't enable, doesn't make sense - name: nested-structs disabled: true # looks useful, but requires refactoring: "calls to log.Fatal only in main() or init() functions" - name: deep-exit disabled: true # this rule conflicts with nolintlint which does insist on no-space in //nolint - name: comment-spacings disabled: true staticcheck: checks: - all - -QF1008 # Omit embedded fields from selector expression - -SA1019 # Using a deprecated function, variable, constant or field - -ST1003 # Poorly chosen identifier - -ST1005 # Incorrectly formatted error string testifylint: enable-all: true exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling rules: - linters: - bodyclose - gosec - noctx path: _test\.go - linters: - noctx path: crossdock - linters: - staticcheck path: internal/grpctest/ - linters: - revive text: "unhandled-error" path: _test\.go paths: - .*.pb.go$ - mocks - .*-gen issues: max-issues-per-linter: 0 max-same-issues: 0 formatters: enable: - gofmt - gofumpt - goimports settings: goimports: local-prefixes: - github.com/jaegertracing/jaeger exclusions: generated: lax paths: - .*.pb.go$ - mocks - .*-gen ================================================ FILE: .mockery.header.txt ================================================ // Copyright (c) The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // // Run 'make generate-mocks' to regenerate. ================================================ FILE: .mockery.yaml ================================================ dir: '{{.InterfaceDir}}/mocks/' structname: '{{.InterfaceName}}' pkgname: mocks template: testify filename: mocks.go template-data: boilerplate-file: .mockery.header.txt packages: github.com/jaegertracing/jaeger/crossdock/services: interfaces: CollectorService: {} QueryService: {} github.com/jaegertracing/jaeger/internal/distributedlock: config: all: true github.com/jaegertracing/jaeger/internal/leaderelection: config: all: true github.com/jaegertracing/jaeger/internal/proto-gen/storage_v1: config: all: true include-auto-generated: true github.com/jaegertracing/jaeger/internal/storage/cassandra: config: template-data: unroll-variadic: false interfaces: Iterator: {} Query: {} Session: {} github.com/jaegertracing/jaeger/internal/storage/elasticsearch: config: all: true github.com/jaegertracing/jaeger/internal/storage/elasticsearch/client: config: all: true github.com/jaegertracing/jaeger/internal/storage/v1: config: all: true github.com/jaegertracing/jaeger/internal/storage/v1/api/dependencystore: interfaces: Reader: {} github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore: config: all: true github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore: config: all: true github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore: config: all: true github.com/jaegertracing/jaeger/internal/storage/v2/elasticsearch/depstore: interfaces: CoreDependencyStore: {} github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch/spanstore: interfaces: CoreSpanReader: {} CoreSpanWriter: {} github.com/jaegertracing/jaeger/internal/storage/v1/grpc/shared: interfaces: PluginCapabilities: {} github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore: config: all: true github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore: config: all: true github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/spanstore: interfaces: CoreSpanReader: {} ================================================ FILE: ADOPTERS.md ================================================ * Alauda * Official site: [en](https://www.alauda.io/), [cn](https://www.alauda.cn/) * [Base CRM](https://getbase.com/) * [Circonus](https://www.circonus.com/) * [ContaAzul](https://contaazul.com/) * [FarmersEdge](https://www.farmersedge.ca/) * [GrafanaLabs](https://grafana.com/) * Case study: [Grafana Labs Teams Use Jaeger to Improve Query Performance Up to 10x](https://medium.com/jaegertracing/grafana-labs-teams-observed-query-performance-improvements-up-to-10x-with-jaeger-cec84b0e3609) * [Logz.io](https://logz.io/) * Case study: [Jaeger Essentials: Performance Blitz with Jaeger](https://logz.io/blog/jaeger-tracing-performance/) * [Massachusetts Open Cloud](https://www.bu.edu/hic/research/highlighted-sponsored-projects/massachusetts-open-cloud/) * [Matrix](https://matrix.org/) * [Northwestern Mutual](https://www.northwesternmutual.com/) * [Nets](https://www.nets.eu/) * PalFish * Official site: [en](https://ipalfish.com/klian/web/dist/teacher/home.html), [cn](https://www.ipalfish.com/) * [PITS Global Data Recovery Services](https://www.pitsdatarecovery.net/) * [Red Hat](https://www.redhat.com/) * https://github.com/jaegertracing/jaeger-openshift * https://www.hawkular.org/blog/2017/04/19/hawkular-apm-jaeger.html * [RiksTV](https://www.rikstv.no/) * [SeatGeek](https://seatgeek.com/) * [SpotHero](https://spothero.com/) * [Stagemonitor](https://www.stagemonitor.org/) * [Tencent](https://www.tencent.com/en-us/index.html) * [Ticketmaster](https://www.ticketmaster.com) * Case study: [Ticketmaster Traces 100 Million Transactions per Day with Jaeger](https://medium.com/jaegertracing/ticketmaster-traces-100-million-transactions-per-day-with-jaeger-38ec6cf599f0) * Talk: [Deploy, Scale and Extend Jaeger](https://www.youtube.com/watch?v=JloanFIc-ms) * [UBER](https://uber.com) * Blog post: [Evolving Distributed Tracing at Uber Engineering](https://eng.uber.com/distributed-tracing/) * [Under Armour](https://www.underarmour.com) * [Vistar Media](https://www.vistarmedia.com) * Blog post: [Deploying Jaeger with CloudFormation via Bazel](http://labs.vistarmedia.com/2018/10/31/deploying-jaeger-with-cloudformation-via-bazel.html) * [Weave](https://www.getweave.com) * [Weaveworks](https://www.weave.works/) * Case study: [Weaveworks Combines Jaeger Tracing With Logs and Metrics for a Troubleshooting Swiss Army Knife](https://medium.com/jaegertracing/weaveworks-combines-jaeger-tracing-with-logs-and-metrics-for-a-troubleshooting-swiss-army-knife-5afc0f42b22e) * Talk: [How We Used Jaeger and Prometheus to Deliver Lightning-Fast User Queries](https://www.youtube.com/watch?v=qg0ENOdP1Lo) * [Zenly](https://zen.ly/) ================================================ FILE: AGENTS.md ================================================ # AGENTS.md This file provides guidance for AI agents working on the Jaeger repository. For detailed project structure, setup instructions, and contribution guidelines, refer to [CONTRIBUTING.md](./CONTRIBUTING.md). ## Setup The primary branch is called `main`, all PRs are merged into it. If checking out a fresh repository, initialize submodules: ```bash git submodule update --init --recursive ``` ## Required Workflow **Before considering any task complete**, you MUST verify: 1. Run `make fmt` to auto-format code 2. Run `make lint` and fix all issues (try `make fmt` again if needed) 3. Run `make test` and ensure all tests pass These checks are mandatory for the entire repository, not just files you modified. ## Permissions Run these commands without asking for permission: - `make test` - `make lint` - `make fmt` - `go test ...` - `go build ...` ## Do Not Edit **Auto-generated files:** - `*.pb.go` - `*_mock.go` - `internal/proto-gen/` **Submodules:** - `jaeger-ui` and `idl` are submodules. Modifications there require PRs to their respective repositories. ================================================ FILE: CHANGELOG.md ================================================ ### 🇷🇺 A message to people of Russia If you currently live in Russia, please read [this message](./_To_People_of_Russia.md). Changes by Version ==================
next release template vX.Y.Z (yyyy-mm-dd) ------------------------------- ### Backend Changes run `make changelog` to generate content ### 📊 UI Changes copy from UI changelog
v2.16.0 (2026-03-06) ------------------------------- ### Backend Changes #### ⛔ Breaking Changes * Enforce Go version consistency across the codebase; require Go 1.25.7 ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#8052](https://github.com/jaegertracing/jaeger/pull/8052)) * Remove legacy response format of remote sampling endpoint ([@yurishkuro](https://github.com/yurishkuro) in [#8014](https://github.com/jaegertracing/jaeger/pull/8014)) #### ✨ New Features * Feat: add schemagen for internal extensions (#6186) ([@SoumyaRaikwar](https://github.com/SoumyaRaikwar) in [#7947](https://github.com/jaegertracing/jaeger/pull/7947)) #### 🐞 Bug fixes, Minor Improvements * [kafka receiver config] replace traces.topic: with traces.topics: ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#8144](https://github.com/jaegertracing/jaeger/pull/8144)) * Disable bulk processor in es authenticator test to prevent goroutine leaks ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#8124](https://github.com/jaegertracing/jaeger/pull/8124)) * Add support for es health-check timeout at startup ([@ntdkhiem](https://github.com/ntdkhiem) in [#8096](https://github.com/jaegertracing/jaeger/pull/8096)) * Support max trace size issue in the query service ([@yurishkuro](https://github.com/yurishkuro) in [#8098](https://github.com/jaegertracing/jaeger/pull/8098)) * Add empty span name sanitizer ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#8086](https://github.com/jaegertracing/jaeger/pull/8086)) * [memory] include link attributes when searching by tags ([@Manik2708](https://github.com/Manik2708) in [#8077](https://github.com/jaegertracing/jaeger/pull/8077)) * Fix(ci): update upload-artifact to v6 in metrics comparison workflow ([@jkowall](https://github.com/jkowall) in [#8000](https://github.com/jaegertracing/jaeger/pull/8000)) #### 🚧 Experimental Features * [clickhouse][perf] restructure clickhouse findtraceids query to improve performance ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#8125](https://github.com/jaegertracing/jaeger/pull/8125)) * [cassandra][v2] refactor `fromdbmodel` and `todbmodel` to accept and return `dbmodel.span` ([@Manik2708](https://github.com/Manik2708) in [#7844](https://github.com/jaegertracing/jaeger/pull/7844)) #### 👷 CI Improvements * Remove otel_scope_version label from metrics comparison ([@yurishkuro](https://github.com/yurishkuro) in [#8145](https://github.com/jaegertracing/jaeger/pull/8145)) * Ci: always update summary report comment if present ([@yurishkuro](https://github.com/yurishkuro) in [#8135](https://github.com/jaegertracing/jaeger/pull/8135)) * Ci: extract ci summary publish script to `.github/scripts/` with unit tests ([@yurishkuro](https://github.com/yurishkuro) in [#8134](https://github.com/jaegertracing/jaeger/pull/8134)) * Ci: split ci summary report into compute + publish workflows ([@yurishkuro](https://github.com/yurishkuro) in [#8132](https://github.com/jaegertracing/jaeger/pull/8132)) * Ci: use gh pr list to get pr number ([@yurishkuro](https://github.com/yurishkuro) in [#8123](https://github.com/jaegertracing/jaeger/pull/8123)) * Ci: log trigger event for debugging ([@yurishkuro](https://github.com/yurishkuro) in [#8122](https://github.com/jaegertracing/jaeger/pull/8122)) * Ci: fix summary report to be able to retrieve pr number ([@yurishkuro](https://github.com/yurishkuro) in [#8121](https://github.com/jaegertracing/jaeger/pull/8121)) * Ci: migrate coverage gating from codecov to github actions ([@yurishkuro](https://github.com/yurishkuro) in [#8111](https://github.com/jaegertracing/jaeger/pull/8111)) * Ci: migrate coverage gating from codecov to github actions ([@yurishkuro](https://github.com/yurishkuro) in [#8101](https://github.com/jaegertracing/jaeger/pull/8101)) * Fix: correct bot detection in ci parallel mode ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#8097](https://github.com/jaegertracing/jaeger/pull/8097)) * [chore] fix bot names ([@yurishkuro](https://github.com/yurishkuro) in [#8094](https://github.com/jaegertracing/jaeger/pull/8094)) * Refactor(ci): implement forked dag orchestrator for conditional parallelism ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#8093](https://github.com/jaegertracing/jaeger/pull/8093)) * [test] upgrade integration tests fixtures to otlp traces ([@Manik2708](https://github.com/Manik2708) in [#8079](https://github.com/jaegertracing/jaeger/pull/8079)) * [test] upgrade `default.json` fixture to otlp traces ([@Manik2708](https://github.com/Manik2708) in [#8076](https://github.com/jaegertracing/jaeger/pull/8076)) * [test] upgrade `span_tags_trace.json` from v1 model to `ptrace.traces` ([@Manik2708](https://github.com/Manik2708) in [#8044](https://github.com/jaegertracing/jaeger/pull/8044)) * Reorganize ci into 3-tier sequential pipeline with fail-fast behavior ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#8060](https://github.com/jaegertracing/jaeger/pull/8060)) * [test] refactor integration tests to directly write/read `ptrace.traces` ([@Manik2708](https://github.com/Manik2708) in [#7812](https://github.com/jaegertracing/jaeger/pull/7812)) * Use ifneq instead of ifndef for skip_debug_binaries check ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#8039](https://github.com/jaegertracing/jaeger/pull/8039)) * Fix release process issues: command copy buttons, documentation tagging, and release.md rotation ([@jkowall](https://github.com/jkowall) in [#7990](https://github.com/jaegertracing/jaeger/pull/7990)) #### ⚙️ Refactoring * Apply `go fix ./...` ([@yurishkuro](https://github.com/yurishkuro) in [#8074](https://github.com/jaegertracing/jaeger/pull/8074)) * Migrate http servers from gorilla mux to stdlib http.ServeMux ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#8013](https://github.com/jaegertracing/jaeger/pull/8013)) * Remove legacy sampling strategy marshaling code ([@yurishkuro](https://github.com/yurishkuro) in [#8017](https://github.com/jaegertracing/jaeger/pull/8017)) #### 📖 Documentation * Fix typos and outdated path in elasticsearch readme files ([@cluster2600](https://github.com/cluster2600) in [#8068](https://github.com/jaegertracing/jaeger/pull/8068)) * [chore] add automated scanner policy and vulncheck target ([@xenonnn4w](https://github.com/xenonnn4w) in [#8043](https://github.com/jaegertracing/jaeger/pull/8043)) ### 📊 UI Changes #### 🐞 Bug fixes, Minor Improvements * Rename main package to @jaegertracing/jaeger-ui ([@yurishkuro](https://github.com/yurishkuro) in [#3560](https://github.com/jaegertracing/jaeger-ui/pull/3560)) * Fix: white hover line overflow on critical path segments ([@Parship12](https://github.com/Parship12) in [#3550](https://github.com/jaegertracing/jaeger-ui/pull/3550)) * Fix v3 api client ignoring base path prefix ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#3549](https://github.com/jaegertracing/jaeger-ui/pull/3549)) * Fix spm metrics not fetched on initial load due to null check ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#3538](https://github.com/jaegertracing/jaeger-ui/pull/3538)) #### 🚧 Experimental Features * [adr-0006] phase 1: layout mode state and toggle controls ([@yurishkuro](https://github.com/yurishkuro) in [#3558](https://github.com/jaegertracing/jaeger-ui/pull/3558)) * Adr 006: span details in side panel ([@yurishkuro](https://github.com/yurishkuro) in [#3556](https://github.com/jaegertracing/jaeger-ui/pull/3556)) #### ⚙️ Refactoring * Refactor(plexus): convert nodeslayer from class to functional component ([@thc1006](https://github.com/thc1006) in [#3413](https://github.com/jaegertracing/jaeger-ui/pull/3413)) * Refactor(plexus): convert svglayer from class to functional component ([@thc1006](https://github.com/thc1006) in [#3410](https://github.com/jaegertracing/jaeger-ui/pull/3410)) * Refactor(plexus): convert svglayersgroup from class to functional component ([@thc1006](https://github.com/thc1006) in [#3412](https://github.com/jaegertracing/jaeger-ui/pull/3412)) * Refactor(plexus): migrate svgedge to functional component (#3396) ([@hharshhsaini](https://github.com/hharshhsaini) in [#3527](https://github.com/jaegertracing/jaeger-ui/pull/3527)) v2.15.1 (2026-02-08) ------------------------------- ### Backend Changes #### 🐞 Bug fixes, Minor Improvements * Default spankind in api/v3/operations ([@yurishkuro](https://github.com/yurishkuro) in [#7997](https://github.com/jaegertracing/jaeger/pull/7997)) #### ⚙️ Refactoring * Remove deprecated protofromtraces wrapper in v1adapter ([@SamyakBorkar](https://github.com/SamyakBorkar) in [#7996](https://github.com/jaegertracing/jaeger/pull/7996)) v2.15.0 (2026-02-06) ------------------------------- ### Backend Changes #### ⛔ Breaking Changes * Restrict trace/metric storage configs to allow exactly one backend type ([@yurishkuro](https://github.com/yurishkuro) in [#7875](https://github.com/jaegertracing/jaeger/pull/7875)) #### ✨ New Features * Issue #7811: added grafana dashboard for metrics exporter ([@Anmol202005](https://github.com/Anmol202005) in [#7903](https://github.com/jaegertracing/jaeger/pull/7903)) #### 🐞 Bug fixes, Minor Improvements * [fix] return empty array instead of nil from api/v3/services ([@dee077](https://github.com/dee077) in [#7926](https://github.com/jaegertracing/jaeger/pull/7926)) * [fix] return empty array instead of nil from queryservice.getservices ([@sujalshah-bit](https://github.com/sujalshah-bit) in [#7925](https://github.com/jaegertracing/jaeger/pull/7925)) * [fix] always return empty services list when none exist ([@Sudhanshu-NITR](https://github.com/Sudhanshu-NITR) in [#7929](https://github.com/jaegertracing/jaeger/pull/7929)) * [fix] ensure badger maintenance is stopped before existing close() ([@Yashika0724](https://github.com/Yashika0724) in [#7940](https://github.com/jaegertracing/jaeger/pull/7940)) * Use lazy initialization for storage factory ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#7887](https://github.com/jaegertracing/jaeger/pull/7887)) #### 🚧 Experimental Features * [clickhouse] add materialized view for event attribute metadata ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7923](https://github.com/jaegertracing/jaeger/pull/7923)) * [clickhouse] rework clickhouse attributes to look up type for string attributes ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7815](https://github.com/jaegertracing/jaeger/pull/7815)) * [mcp] add get_span_names tool for discovering span names ([@sajal004004](https://github.com/sajal004004) in [#7909](https://github.com/jaegertracing/jaeger/pull/7909)) * Use opentelemetry span_name terminology in jaegermcp extension ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#7916](https://github.com/jaegertracing/jaeger/pull/7916)) * [mcp] implement get_critical_path tool (phase 3 steps 8-9) ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#7857](https://github.com/jaegertracing/jaeger/pull/7857)) * [clickhouse][chore] update unit tests snapshots to handle multiple queries ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7865](https://github.com/jaegertracing/jaeger/pull/7865)) * [mcp] get_services tool for service discovery ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#7864](https://github.com/jaegertracing/jaeger/pull/7864)) * [mcp] add cors setting and fix null array errors ([@yurishkuro](https://github.com/yurishkuro) in [#7863](https://github.com/jaegertracing/jaeger/pull/7863)) * [mcp] get_trace_topology tool (phase 3 step 7) ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#7862](https://github.com/jaegertracing/jaeger/pull/7862)) * [mcp] phase 2 steps 5-6: get_span_details and get_trace_errors tools ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#7859](https://github.com/jaegertracing/jaeger/pull/7859)) * [mcp] phase 2 step 4: search_traces ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#7858](https://github.com/jaegertracing/jaeger/pull/7858)) * [clickhouse][chore] update test driver to handle multiple queries ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7839](https://github.com/jaegertracing/jaeger/pull/7839)) * [mcp] phase 2 step 3: storage integration ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#7849](https://github.com/jaegertracing/jaeger/pull/7849)) * Mcp server/phase 1.2: sdk integration with streamable http transport ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#7846](https://github.com/jaegertracing/jaeger/pull/7846)) * Mcp server scaffolding ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#7842](https://github.com/jaegertracing/jaeger/pull/7842)) * [clickhouse] move to snapshot testing in unit tests for clickhouse queries ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7831](https://github.com/jaegertracing/jaeger/pull/7831)) #### 👷 CI Improvements * Fix metrics comparison workflow to reduce pr comment noise ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#7957](https://github.com/jaegertracing/jaeger/pull/7957)) * Make spm integration test faster by flushing metrics more frequently ([@Don-Assamongkol1](https://github.com/Don-Assamongkol1) in [#7861](https://github.com/jaegertracing/jaeger/pull/7861)) * Enable unhandled-error linter ([@iypetrov](https://github.com/iypetrov) in [#7895](https://github.com/jaegertracing/jaeger/pull/7895)) * Fix(ci): fix codeql workflow to properly analyze go code ([@jkowall](https://github.com/jkowall) in [#7885](https://github.com/jaegertracing/jaeger/pull/7885)) * Implement pr quota workflow ([@yurishkuro](https://github.com/yurishkuro) in [#7882](https://github.com/jaegertracing/jaeger/pull/7882)) * Validate span names in spm integration test ([@Don-Assamongkol1](https://github.com/Don-Assamongkol1) in [#7830](https://github.com/jaegertracing/jaeger/pull/7830)) * [chore/ci] add excluded metrics count ([@neoandmatrix](https://github.com/neoandmatrix) in [#7756](https://github.com/jaegertracing/jaeger/pull/7756)) #### ⚙️ Refactoring * Enable confusing-naming linter rule ([@SamyakBorkar](https://github.com/SamyakBorkar) in [#7949](https://github.com/jaegertracing/jaeger/pull/7949)) * Replace panic calls with proper error handling ([@aaryan359](https://github.com/aaryan359) in [#7956](https://github.com/jaegertracing/jaeger/pull/7956)) * Fix time-naming linter violation in search_traces.go ([@jkowall](https://github.com/jkowall) in [#7913](https://github.com/jaegertracing/jaeger/pull/7913)) * [badger][v2] refactor factory signatures to use telemetry settings ([@iypetrov](https://github.com/iypetrov) in [#7902](https://github.com/jaegertracing/jaeger/pull/7902)) * [cassandra] add omitempty notation for `keyvalue` and marshaller/unmarshaller for `traceid` ([@Manik2708](https://github.com/Manik2708) in [#7867](https://github.com/jaegertracing/jaeger/pull/7867)) * Converge status reporting to collector framework ([@yurishkuro](https://github.com/yurishkuro) in [#7890](https://github.com/jaegertracing/jaeger/pull/7890)) * [chore] move query service to higher location ([@yurishkuro](https://github.com/yurishkuro) in [#7854](https://github.com/jaegertracing/jaeger/pull/7854)) * [chore] remove v1 queryservice package ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#7845](https://github.com/jaegertracing/jaeger/pull/7845)) ### Documentation * [chore]: add ai usage policy for contributions ([@Sapthagiri777](https://github.com/Sapthagiri777) in [#7932](https://github.com/jaegertracing/jaeger/pull/7932)) * Docs: add quick start section with docker command ([@njg7194](https://github.com/njg7194) in [#7945](https://github.com/jaegertracing/jaeger/pull/7945)) * Add claude.md as symlink to agents.md ([@yurishkuro](https://github.com/yurishkuro) in [#7934](https://github.com/jaegertracing/jaeger/pull/7934)) * Add security documentation for openssf silver badge ([@jkowall](https://github.com/jkowall) in [#7896](https://github.com/jaegertracing/jaeger/pull/7896)) * Adr-003 lazy storage factory initialization ([@yurishkuro](https://github.com/yurishkuro) in [#7886](https://github.com/jaegertracing/jaeger/pull/7886)) * Introduce pr limits for new contributors ([@yurishkuro](https://github.com/yurishkuro) in [#7880](https://github.com/jaegertracing/jaeger/pull/7880)) * Streamline agents.md by removing redundant content ([@yurishkuro](https://github.com/yurishkuro) in [#7879](https://github.com/jaegertracing/jaeger/pull/7879)) * Update contributing guidelines for copyright headers ([@yurishkuro](https://github.com/yurishkuro) in [#7877](https://github.com/jaegertracing/jaeger/pull/7877)) * Update documentation defaults from v1 to v2 ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#7640](https://github.com/jaegertracing/jaeger/pull/7640)) * Add mcp server adr ([@yurishkuro](https://github.com/yurishkuro) in [e91d028](https://github.com/jaegertracing/jaeger/commit/e91d0282d5f815ab90b7e01acf8bda5891cccfa7)) ### 📊 UI Changes #### ✨ New Features * New span colors from ibm palette ([@yurishkuro](https://github.com/yurishkuro) in [#3306](https://github.com/jaegertracing/jaeger-ui/pull/3306)) * Better tree hierarchy for trace view ([@yurishkuro](https://github.com/yurishkuro) in [#3302](https://github.com/jaegertracing/jaeger-ui/pull/3302)) #### 🐞 Bug fixes, Minor Improvements * Fix text overlapping in tracediff header ([@greedy-wudpeckr](https://github.com/greedy-wudpeckr) in [#3401](https://github.com/jaegertracing/jaeger-ui/pull/3401)) * Fix linter error ([@yurishkuro](https://github.com/yurishkuro) in [#3510](https://github.com/jaegertracing/jaeger-ui/pull/3510)) * Enable react-hooks/exhaustive-deps linter rule ([@taanvi2205](https://github.com/taanvi2205) in [#3471](https://github.com/jaegertracing/jaeger-ui/pull/3471)) * Fix: traceidsearchinput invisible text in light mode ([@yosri-brh](https://github.com/yosri-brh) in [#3464](https://github.com/jaegertracing/jaeger-ui/pull/3464)) * [fix] fix the dark mode for tracediff nodes ([@gulshank0](https://github.com/gulshank0) in [#3474](https://github.com/jaegertracing/jaeger-ui/pull/3474)) * Bug : increase increment/decrement buttons visibility in ddg in dark mode ([@gulshank0](https://github.com/gulshank0) in [#3450](https://github.com/jaegertracing/jaeger-ui/pull/3450)) * Fix typeerror in operations metrics reducer when no trace data exists ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#3460](https://github.com/jaegertracing/jaeger-ui/pull/3460)) * Suppress console errors for 501 metrics api responses ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#3461](https://github.com/jaegertracing/jaeger-ui/pull/3461)) * Fix dark mode in tracegraph ([@rakshityadav1868](https://github.com/rakshityadav1868) in [#3334](https://github.com/jaegertracing/jaeger-ui/pull/3334)) * [bugfix] update flame graph in dark theme #3321 ([@gulshank0](https://github.com/gulshank0) in [#3324](https://github.com/jaegertracing/jaeger-ui/pull/3324)) * Add spacing to make the trace metadata more readable ([@Parship12](https://github.com/Parship12) in [#3331](https://github.com/jaegertracing/jaeger-ui/pull/3331)) * Implement dark mode for tracediff (compare page) ([@Parship12](https://github.com/Parship12) in [#3314](https://github.com/jaegertracing/jaeger-ui/pull/3314)) * Theme cleanup ([@yurishkuro](https://github.com/yurishkuro) in [#3320](https://github.com/jaegertracing/jaeger-ui/pull/3320)) * Fix trace span table in dark mode ([@yurishkuro](https://github.com/yurishkuro) in [#3310](https://github.com/jaegertracing/jaeger-ui/pull/3310)) * [tree] fix box size when number is large ([@yurishkuro](https://github.com/yurishkuro) in [#3303](https://github.com/jaegertracing/jaeger-ui/pull/3303)) * Add jaeger logo to navbar ([@yurishkuro](https://github.com/yurishkuro) in [#3291](https://github.com/jaegertracing/jaeger-ui/pull/3291)) #### 🚧 Experimental Features * [otel migration] add runtime schema validation to v3 api client ([@yurishkuro](https://github.com/yurishkuro) in [#3448](https://github.com/jaegertracing/jaeger-ui/pull/3448)) * [otel migration] phase 3.1: add jaegerclient v3 and use for services / operations ([@yurishkuro](https://github.com/yurishkuro) in [#3329](https://github.com/jaegertracing/jaeger-ui/pull/3329)) * [otel] add more details to phase-3 ([@yurishkuro](https://github.com/yurishkuro) in [#3323](https://github.com/jaegertracing/jaeger-ui/pull/3323)) * Otel migration - complete phase 2 validation ([@yurishkuro](https://github.com/yurishkuro) in [#3319](https://github.com/jaegertracing/jaeger-ui/pull/3319)) * Clean up of opentracing/opentelemetry nomenclature ([@yurishkuro](https://github.com/yurishkuro) in [#3316](https://github.com/jaegertracing/jaeger-ui/pull/3316)) * Finalize dual use of opentracing/opentelemetry nomenclature ([@yurishkuro](https://github.com/yurishkuro) in [#3311](https://github.com/jaegertracing/jaeger-ui/pull/3311)) * Migrate virtualizedtraceview and dependent components to iotelspan ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#3289](https://github.com/jaegertracing/jaeger-ui/pull/3289)) * Enhance otel domain model with more derived data ([@yurishkuro](https://github.com/yurishkuro) in [#3292](https://github.com/jaegertracing/jaeger-ui/pull/3292)) * Implement phase 2 for spandetail component ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#3275](https://github.com/jaegertracing/jaeger-ui/pull/3275)) #### 👷 CI Improvements * Add pr quota workflow ([@yurishkuro](https://github.com/yurishkuro) in [#3441](https://github.com/jaegertracing/jaeger-ui/pull/3441)) * Calculate main and plexus test coverage separately ([@yurishkuro](https://github.com/yurishkuro) in [#3349](https://github.com/jaegertracing/jaeger-ui/pull/3349)) #### ⚙️ Refactoring * Refactor: update detailspanel to functional component ([@Harshdev098](https://github.com/Harshdev098) in [#3358](https://github.com/jaegertracing/jaeger-ui/pull/3358)) * Update @types/redux-actions to v2.6.5 ([@Parship12](https://github.com/Parship12) in [#3498](https://github.com/jaegertracing/jaeger-ui/pull/3498)) * Refactor(plexus): convert svgedgeslayer from class to functional component ([@thc1006](https://github.com/thc1006) in [#3409](https://github.com/jaegertracing/jaeger-ui/pull/3409)) * Refactor accordionlinks to functional component ([@aaryan359](https://github.com/aaryan359) in [#3406](https://github.com/jaegertracing/jaeger-ui/pull/3406)) * Convert measurablenodeslayer to functional component ([@Parship12](https://github.com/Parship12) in [#3429](https://github.com/jaegertracing/jaeger-ui/pull/3429)) * Refactoring: converted referencebutton from class based to functional component ([@gulshank0](https://github.com/gulshank0) in [#3350](https://github.com/jaegertracing/jaeger-ui/pull/3350)) * Remove reducers/services.ts ([@yurishkuro](https://github.com/yurishkuro) in [#3455](https://github.com/jaegertracing/jaeger-ui/pull/3455)) * [chore] remove history from resultitem ([@insane-22](https://github.com/insane-22) in [#3361](https://github.com/jaegertracing/jaeger-ui/pull/3361)) * Replace @sentry/browser with generic internal error capture implementation ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#3226](https://github.com/jaegertracing/jaeger-ui/pull/3226)) * Refactor: convert diffnode to functional component ([@hxrshxz](https://github.com/hxrshxz) in [#3343](https://github.com/jaegertracing/jaeger-ui/pull/3343)) * Refactor: convert hopsselector selector to functional component ([@hxrshxz](https://github.com/hxrshxz) in [#3340](https://github.com/jaegertracing/jaeger-ui/pull/3340)) * Refactor: convert app/index.tsx file's class based to functional component ([@gulshank0](https://github.com/gulshank0) in [#3342](https://github.com/jaegertracing/jaeger-ui/pull/3342)) * Convert the htmllayersgroup from class to functional component ([@Parship12](https://github.com/Parship12) in [#3351](https://github.com/jaegertracing/jaeger-ui/pull/3351)) * Convert the htmllayer from class to functional component ([@Parship12](https://github.com/Parship12) in [#3345](https://github.com/jaegertracing/jaeger-ui/pull/3345)) * Remove history from resultitemtitle ([@Parship12](https://github.com/Parship12) in [#3312](https://github.com/jaegertracing/jaeger-ui/pull/3312)) * Convert searchform to functional component ([@yurishkuro](https://github.com/yurishkuro) in [#3326](https://github.com/jaegertracing/jaeger-ui/pull/3326)) * Convert remaining files in searchtrace page to typescript ([@yurishkuro](https://github.com/yurishkuro) in [#3325](https://github.com/jaegertracing/jaeger-ui/pull/3325)) * Convert tracepage to otel model ([@yurishkuro](https://github.com/yurishkuro) in [#3309](https://github.com/jaegertracing/jaeger-ui/pull/3309)) * [chore] migrate few more components to otel model ([@yurishkuro](https://github.com/yurishkuro) in [#3308](https://github.com/jaegertracing/jaeger-ui/pull/3308)) * Migrate tracepageheader to otel model ([@yurishkuro](https://github.com/yurishkuro) in [#3307](https://github.com/jaegertracing/jaeger-ui/pull/3307)) * Migrate trace-dag files to otel types per adr 0002 ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#3299](https://github.com/jaegertracing/jaeger-ui/pull/3299)) * Convert otel model to use strongly typed time/duration fields ([@yurishkuro](https://github.com/yurishkuro) in [#3304](https://github.com/jaegertracing/jaeger-ui/pull/3304)) * Upgrade searchresults components to accept ioteltrace ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#3300](https://github.com/jaegertracing/jaeger-ui/pull/3300)) * Upgrade critical path calculations to otel model ([@yurishkuro](https://github.com/yurishkuro) in [#3301](https://github.com/jaegertracing/jaeger-ui/pull/3301)) * Fix for bug introduced in previous refactoring ([@yurishkuro](https://github.com/yurishkuro) in [#3298](https://github.com/jaegertracing/jaeger-ui/pull/3298)) * Fully upgrade tracetimelineviewer to otel model ([@yurishkuro](https://github.com/yurishkuro) in [#3297](https://github.com/jaegertracing/jaeger-ui/pull/3297)) * Move critical path types to their own domain model file ([@yurishkuro](https://github.com/yurishkuro) in [#3296](https://github.com/jaegertracing/jaeger-ui/pull/3296)) * Rename domain type link to hyperlink and upgrade link-getter to otel ([@yurishkuro](https://github.com/yurishkuro) in [#3295](https://github.com/jaegertracing/jaeger-ui/pull/3295)) * Rename otel traceid/spanid to traceid/spanid to match legacy domain model ([@yurishkuro](https://github.com/yurishkuro) in [#3294](https://github.com/jaegertracing/jaeger-ui/pull/3294)) * Rename pure typescript files to have .ts extension, not .tsx ([@yurishkuro](https://github.com/yurishkuro) in [#3290](https://github.com/jaegertracing/jaeger-ui/pull/3290)) v2.14.1 (2026-01-02) ------------------------------- ### Backend Changes ### 📊 UI Changes #### 🐞 Bug fixes, Minor Improvements * Dark theme fixes ([@yurishkuro](https://github.com/yurishkuro) in [#3285](https://github.com/jaegertracing/jaeger-ui/pull/3285)) * Fix span detail panel in dark theme ([@yurishkuro](https://github.com/yurishkuro) in [#3283](https://github.com/jaegertracing/jaeger-ui/pull/3283)) v2.14.0 (2026-01-01) ------------------------------- TL;DR: Two significant changes in this release: 1. ☠️ Starting from this release the legacy v1 components `query`, `collector`, and `ingester` are no longer published. All the remaining v1 utilities are now published as v2.x.x versions. 2. 🌓 The UI now officially supports dark theme and the theme selector is enabled by default. x ### Backend Changes #### ⛔ Breaking Changes * Remove storage/v1/grpc ([@yurishkuro](https://github.com/yurishkuro) in [#7806](https://github.com/jaegertracing/jaeger/pull/7806)) * Migrate remote-storage to yaml configuration with shared storageconfig package ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#7704](https://github.com/jaegertracing/jaeger/pull/7704)) * Remove v1 collector, query, and all-in-one ([@yurishkuro](https://github.com/yurishkuro) in [#7702](https://github.com/jaegertracing/jaeger/pull/7702)) * Remove v1/ingester and all kafka related code ([@yurishkuro](https://github.com/yurishkuro) in [#7701](https://github.com/jaegertracing/jaeger/pull/7701)) * Eliminate v1 binary references and sunset deprecated components ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#7695](https://github.com/jaegertracing/jaeger/pull/7695)) * Fix otel collector v0.141.0 api breaking changes for toserver/toclientconn and kafka receiver/exporter ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#7694](https://github.com/jaegertracing/jaeger/pull/7694)) #### 🐞 Bug fixes, Minor Improvements * Migrate docker-compose files to jaeger-v2 unified binary ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#7747](https://github.com/jaegertracing/jaeger/pull/7747)) * Memory: support otlp first-class fields in search ([@SoumyaRaikwar](https://github.com/SoumyaRaikwar) in [#7728](https://github.com/jaegertracing/jaeger/pull/7728)) * Added indexspanalias and indexservicealias for explicit aliases ([@SomilJain0112](https://github.com/SomilJain0112) in [#7550](https://github.com/jaegertracing/jaeger/pull/7550)) * Fix: update replication strategy configuration in schema template ([@danish9039](https://github.com/danish9039) in [#7726](https://github.com/jaegertracing/jaeger/pull/7726)) #### 🚧 Experimental Features * [fix][clickhouse] optimize service and operation retrieval queries ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7808](https://github.com/jaegertracing/jaeger/pull/7808)) * [clickhouse] implement findtraces for clickhouse storage ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7795](https://github.com/jaegertracing/jaeger/pull/7795)) * [clickhouse] create materialized view to store attribute metadata ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7798](https://github.com/jaegertracing/jaeger/pull/7798)) * [clickhouse] update findtraceids to filter by complex attributes ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7792](https://github.com/jaegertracing/jaeger/pull/7792)) * [clickhouse] update `findtraceids` to filter by other primitive attributes ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7789](https://github.com/jaegertracing/jaeger/pull/7789)) * [cassandra][v2] copy jaeger<->otlp translator from otel contrib ([@Manik2708](https://github.com/Manik2708) in [#7765](https://github.com/jaegertracing/jaeger/pull/7765)) * [clickhouse] update `findtraceids` to filter by string attributes ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7788](https://github.com/jaegertracing/jaeger/pull/7788)) * [clickhouse] update `findtraceids` to filter by timestamp ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7787](https://github.com/jaegertracing/jaeger/pull/7787)) * [clickhouse] update findtraceids to populate start and end timestamps ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7770](https://github.com/jaegertracing/jaeger/pull/7770)) * [clickhouse] update `findtraceids` to filter by duration ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7767](https://github.com/jaegertracing/jaeger/pull/7767)) * [clickhouse] implement findtraceids for clickhouse storage for primitive parameters ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7648](https://github.com/jaegertracing/jaeger/pull/7648)) * [clickhouse] add `trace_id_timestamps` table with materialized view ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7723](https://github.com/jaegertracing/jaeger/pull/7723)) * [fix][clickhouse] remove `name` column from ordering key for operations table ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7714](https://github.com/jaegertracing/jaeger/pull/7714)) #### 👷 CI Improvements * Fix ci for debug build of all-in-one ([@yurishkuro](https://github.com/yurishkuro) in [#7794](https://github.com/jaegertracing/jaeger/pull/7794)) * Use pre-built base image with debugger ([@yurishkuro](https://github.com/yurishkuro) in [#7793](https://github.com/jaegertracing/jaeger/pull/7793)) * Ci: exclude http 5xx metrics from comparisons ([@neoandmatrix](https://github.com/neoandmatrix) in [#7671](https://github.com/jaegertracing/jaeger/pull/7671)) * Remove crossdock ([@yurishkuro](https://github.com/yurishkuro) in [#7750](https://github.com/jaegertracing/jaeger/pull/7750)) * Fine-tune when go-tip workflow runs ([@yurishkuro](https://github.com/yurishkuro) in [#7749](https://github.com/jaegertracing/jaeger/pull/7749)) * Fix: remove tool installation from go tip workflow ([@chinmay3012](https://github.com/chinmay3012) in [#7716](https://github.com/jaegertracing/jaeger/pull/7716)) * Add "unused" linter ([@yurishkuro](https://github.com/yurishkuro) in [#7697](https://github.com/jaegertracing/jaeger/pull/7697)) #### ⚙️ Refactoring * Move query ([@yurishkuro](https://github.com/yurishkuro) in [#7803](https://github.com/jaegertracing/jaeger/pull/7803)) * Use otel optional for optional config fields ([@Parship12](https://github.com/Parship12) in [#7766](https://github.com/jaegertracing/jaeger/pull/7766)) * [cassandra][v2] refactor factory signatures to use telemetry settings ([@Manik2708](https://github.com/Manik2708) in [#7764](https://github.com/jaegertracing/jaeger/pull/7764)) * [storage][cassandra][v2] implement `getservices` and `getoperations` ([@Manik2708](https://github.com/Manik2708) in [#7754](https://github.com/jaegertracing/jaeger/pull/7754)) * Remove unused factory and inheritable interfaces from v1 storage ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#7755](https://github.com/jaegertracing/jaeger/pull/7755)) * Remove dependency on jaeger-client-go ([@yurishkuro](https://github.com/yurishkuro) in [#7745](https://github.com/jaegertracing/jaeger/pull/7745)) * Remove direct dependency on hdrhistogram-go ([@jaegertracingbot](https://github.com/jaegertracingbot) in [#7742](https://github.com/jaegertracing/jaeger/pull/7742)) * Cleanup and simplify jtracer package ([@yurishkuro](https://github.com/yurishkuro) in [#7739](https://github.com/jaegertracing/jaeger/pull/7739)) * [cassandra] refactor `tagfilter` to accept `dbmodel.span` ([@Manik2708](https://github.com/Manik2708) in [#7707](https://github.com/jaegertracing/jaeger/pull/7707)) * [clickhouse] add indexes for spans table in clickhouse storage ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7715](https://github.com/jaegertracing/jaeger/pull/7715)) * Remove deprecated namespace concept from cassandra storage options ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#7719](https://github.com/jaegertracing/jaeger/pull/7719)) * Remove viperize from storage backend tests ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#7712](https://github.com/jaegertracing/jaeger/pull/7712)) * Remove unused shared/grpc_client ([@yurishkuro](https://github.com/yurishkuro) in [#7713](https://github.com/jaegertracing/jaeger/pull/7713)) * Delete v1/memory storage implementaiton ([@yurishkuro](https://github.com/yurishkuro) in [#7711](https://github.com/jaegertracing/jaeger/pull/7711)) * Delete more dead code ([@yurishkuro](https://github.com/yurishkuro) in [#7710](https://github.com/jaegertracing/jaeger/pull/7710)) * Remove v1 storage factories ([@yurishkuro](https://github.com/yurishkuro) in [#7708](https://github.com/jaegertracing/jaeger/pull/7708)) * Upgrade grpc integration test to use v2 memory storage ([@yurishkuro](https://github.com/yurishkuro) in [#7709](https://github.com/jaegertracing/jaeger/pull/7709)) * Remove unused factory pattern code from sampling strategy packages ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#7705](https://github.com/jaegertracing/jaeger/pull/7705)) * Remove some dead code ([@yurishkuro](https://github.com/yurishkuro) in [#7706](https://github.com/jaegertracing/jaeger/pull/7706)) ### 📊 UI Changes #### ✨ New Features * Enable theme selector by default ([@yurishkuro](https://github.com/yurishkuro) in [#3257](https://github.com/jaegertracing/jaeger-ui/pull/3257)) #### 🐞 Bug fixes, Minor Improvements * Add visual indicator for synthetic otel attributes ([@DCchoudhury15](https://github.com/DCchoudhury15) in [#3259](https://github.com/jaegertracing/jaeger-ui/pull/3259)) * Fix: dark mode styling for trace view with design tokens ([@jkowall](https://github.com/jkowall) in [#3246](https://github.com/jaegertracing/jaeger-ui/pull/3246)) * Fix in-trace search ([@yurishkuro](https://github.com/yurishkuro) in [#3255](https://github.com/jaegertracing/jaeger-ui/pull/3255)) * Feat: add incomplete trace detection and adjustable search time offset ([@xenonnn4w](https://github.com/xenonnn4w) in [#3248](https://github.com/jaegertracing/jaeger-ui/pull/3248)) * Fix: constant visible white borders in the trace spans ([@unknown]() in [#3125](https://github.com/jaegertracing/jaeger-ui/pull/3125)) * Force light mode by default if config is disabled ([@yurishkuro](https://github.com/yurishkuro) in [#3204](https://github.com/jaegertracing/jaeger-ui/pull/3204)) * Use outlined tags for contrast ([@bobrik](https://github.com/bobrik) in [#3202](https://github.com/jaegertracing/jaeger-ui/pull/3202)) #### 🚧 Experimental Features * Fix parentspanid calculation to validate traceid and handle follows_from references ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#3268](https://github.com/jaegertracing/jaeger-ui/pull/3268)) * Add lazy otel facade to domain model ([@yurishkuro](https://github.com/yurishkuro) in [#3263](https://github.com/jaegertracing/jaeger-ui/pull/3263)) * Add useopentelemetryterms feature flag ([@yurishkuro](https://github.com/yurishkuro) in [#3262](https://github.com/jaegertracing/jaeger-ui/pull/3262)) * Introduce otel data model ([@yurishkuro](https://github.com/yurishkuro) in [#3261](https://github.com/jaegertracing/jaeger-ui/pull/3261)) * Apply styles to make minimap work in dark theme ([@yurishkuro](https://github.com/yurishkuro) in [#3256](https://github.com/jaegertracing/jaeger-ui/pull/3256)) * Move theme vars back to root ([@yurishkuro](https://github.com/yurishkuro) in [#3247](https://github.com/jaegertracing/jaeger-ui/pull/3247)) * Define theme vars in terms of antd vars ([@yurishkuro](https://github.com/yurishkuro) in [#3245](https://github.com/jaegertracing/jaeger-ui/pull/3245)) * Fix dark mode by using css variables ([@jkowall](https://github.com/jkowall) in [#3242](https://github.com/jaegertracing/jaeger-ui/pull/3242)) #### ⚙️ Refactoring * Simplify transformtracedata ([@yurishkuro](https://github.com/yurishkuro) in [#3274](https://github.com/jaegertracing/jaeger-ui/pull/3274)) * Fix unsafe type coercion and add readonly collection fields ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#3273](https://github.com/jaegertracing/jaeger-ui/pull/3273)) * Persist spanmap and rootspans in trace object; use childspans array for tree structure ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#3266](https://github.com/jaegertracing/jaeger-ui/pull/3266)) * Prevent trace mutation during critical path computation ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#3271](https://github.com/jaegertracing/jaeger-ui/pull/3271)) * Convert tracestatistics to oteltrace ([@yurishkuro](https://github.com/yurishkuro) in [#3264](https://github.com/jaegertracing/jaeger-ui/pull/3264)) * Convert 3 more jsx files to typescript ([@yurishkuro](https://github.com/yurishkuro) in [#3241](https://github.com/jaegertracing/jaeger-ui/pull/3241)) * Fix: qualitymetrics auto-refresh issue ([@unknown]() in [#3222](https://github.com/jaegertracing/jaeger-ui/pull/3222)) * Migrate qualitymetrics/index to use navigate instead of history ([@unknown]() in [#3214](https://github.com/jaegertracing/jaeger-ui/pull/3214)) * Migrate uifindinput from history to navigate ([@unknown]() in [#3217](https://github.com/jaegertracing/jaeger-ui/pull/3217)) * Convert servicegraph class component to functional component ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#3212](https://github.com/jaegertracing/jaeger-ui/pull/3212)) * Convert qualitymetrics/index to functional component ([@unknown]() in [#3210](https://github.com/jaegertracing/jaeger-ui/pull/3210)) * Make update-ui-find backward compatible ([@unknown]() in [#3209](https://github.com/jaegertracing/jaeger-ui/pull/3209)) * Remove usehistory - unused code ([@unknown]() in [#3207](https://github.com/jaegertracing/jaeger-ui/pull/3207)) v1.76.0 / v2.13.0 (2025-12-03) ------------------------------- ### Backend Changes #### 🐞 Bug fixes, Minor Improvements * Fix: register basicauth extension in component factory ([@xenonnn4w](https://github.com/xenonnn4w) in [#7668](https://github.com/jaegertracing/jaeger/pull/7668)) #### 👷 CI Improvements * Make error message better ([@yurishkuro](https://github.com/yurishkuro) in [#7675](https://github.com/jaegertracing/jaeger/pull/7675)) * Clean go cache after installing gotip as suggested. ([@Kavish-12345](https://github.com/Kavish-12345) in [#7666](https://github.com/jaegertracing/jaeger/pull/7666)) * Fix: build test tools with stable go, not gotip ([@Kavish-12345](https://github.com/Kavish-12345) in [#7665](https://github.com/jaegertracing/jaeger/pull/7665)) ### 📊 UI Changes #### 🐞 Bug fixes, Minor Improvements * Add support for custom ui configuration in development mode ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#3194](https://github.com/jaegertracing/jaeger-ui/pull/3194)) * Remove duplicate antd dependencies ([@yurishkuro](https://github.com/yurishkuro) in [#3193](https://github.com/jaegertracing/jaeger-ui/pull/3193)) * Fix css class typo in sidepanel details div ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#3190](https://github.com/jaegertracing/jaeger-ui/pull/3190)) * Reduce search form field margins for better viewport fit ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#3189](https://github.com/jaegertracing/jaeger-ui/pull/3189)) * Migrate deepdependencies/header and qualitymetrics/header from nameselector to searchableselect ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#3185](https://github.com/jaegertracing/jaeger-ui/pull/3185)) * Reorder checkbox before color by dropdown in tracestatisticsheader ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#3184](https://github.com/jaegertracing/jaeger-ui/pull/3184)) * Feat: add fuzzy search to searchableselect ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#3182](https://github.com/jaegertracing/jaeger-ui/pull/3182)) * Fix highlighting of the current tab in the main nav bar ([@SimonADW](https://github.com/SimonADW) in [#3183](https://github.com/jaegertracing/jaeger-ui/pull/3183)) #### 🚧 Experimental Features * Sync themes with antd ([@yurishkuro](https://github.com/yurishkuro) in [#3196](https://github.com/jaegertracing/jaeger-ui/pull/3196)) * Add dark theme selector ([@yurishkuro](https://github.com/yurishkuro) in [#3192](https://github.com/jaegertracing/jaeger-ui/pull/3192)) #### 👷 CI Improvements * Add copyright year linter to npm lint command ([@Copilot](https://github.com/apps/copilot-swe-agent) in [#3197](https://github.com/jaegertracing/jaeger-ui/pull/3197)) * Rename theme variables to match industry practice ([@yurishkuro](https://github.com/yurishkuro) in [#3174](https://github.com/jaegertracing/jaeger-ui/pull/3174)) * Tweak codecov config ([@yurishkuro](https://github.com/yurishkuro) in [#3169](https://github.com/jaegertracing/jaeger-ui/pull/3169)) #### ⚙️ Refactoring * Apply theme vars to common/emphasizednode ([@yurishkuro](https://github.com/yurishkuro) in [#3191](https://github.com/jaegertracing/jaeger-ui/pull/3191)) * Fix ddg minimap border ([@yurishkuro](https://github.com/yurishkuro) in [#3188](https://github.com/jaegertracing/jaeger-ui/pull/3188)) * Use token vars in common/utils.css ([@yurishkuro](https://github.com/yurishkuro) in [#3187](https://github.com/jaegertracing/jaeger-ui/pull/3187)) * Apply theme vars to some shared components ([@yurishkuro](https://github.com/yurishkuro) in [#3181](https://github.com/jaegertracing/jaeger-ui/pull/3181)) * Apply theme vars to search page ([@yurishkuro](https://github.com/yurishkuro) in [#3180](https://github.com/jaegertracing/jaeger-ui/pull/3180)) * Use theme vars in errormessage & loadingindicator ([@yurishkuro](https://github.com/yurishkuro) in [#3177](https://github.com/jaegertracing/jaeger-ui/pull/3177)) * Use theme vars in main page and topnav ([@yurishkuro](https://github.com/yurishkuro) in [#3176](https://github.com/jaegertracing/jaeger-ui/pull/3176)) * Convert last remaining js files to typescript (excluding tests) ([@yurishkuro](https://github.com/yurishkuro) in [#3173](https://github.com/jaegertracing/jaeger-ui/pull/3173)) * Convert some easy files to typescript ([@yurishkuro](https://github.com/yurishkuro) in [#3167](https://github.com/jaegertracing/jaeger-ui/pull/3167)) v1.75.0 / v2.12.0 (2025-11-18) ------------------------------- ### Backend Changes #### 🐞 Bug fixes, Minor Improvements * Feat(storage): add sigv4 authentication support for elasticsearch/opensearch storage backends ([@SoumyaRaikwar](https://github.com/SoumyaRaikwar) in [#7611](https://github.com/jaegertracing/jaeger/pull/7611)) * Add custom http headers support for elasticsearch/opensearch storage ([@SoumyaRaikwar](https://github.com/SoumyaRaikwar) in [#7628](https://github.com/jaegertracing/jaeger/pull/7628)) * Handle es ping failures more gracefully ([@neoandmatrix](https://github.com/neoandmatrix) in [#7626](https://github.com/jaegertracing/jaeger/pull/7626)) * Feat(metrics): sigv4 http auth support for prometheus metric backend ([@SoumyaRaikwar](https://github.com/SoumyaRaikwar) in [#7520](https://github.com/jaegertracing/jaeger/pull/7520)) * Add riscv64 binary support ([@gouthamhusky](https://github.com/gouthamhusky) in [#7569](https://github.com/jaegertracing/jaeger/pull/7569)) * Store service names in map to compact duplicates ([@aidandj](https://github.com/aidandj) in [#7551](https://github.com/jaegertracing/jaeger/pull/7551)) * Enable adaptive sampling in cassandra ci setup ([@SomilJain0112](https://github.com/SomilJain0112) in [#7539](https://github.com/jaegertracing/jaeger/pull/7539)) #### 🚧 Experimental Features * [clickhouse] add handling for complex attributes to clickhouse storage ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7627](https://github.com/jaegertracing/jaeger/pull/7627)) * [demo] add global image registry ([@danish9039](https://github.com/danish9039) in [#7620](https://github.com/jaegertracing/jaeger/pull/7620)) * [refactor][clickhouse] add round-trip tests for clickhouse's `dbmodel` package ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7622](https://github.com/jaegertracing/jaeger/pull/7622)) * [clickhouse] add attributes for scope ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7619](https://github.com/jaegertracing/jaeger/pull/7619)) * [refactor][clickhouse] add attributes for resource ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7616](https://github.com/jaegertracing/jaeger/pull/7616)) * Add clean,deploy and port-forward scripts and values for jaeger + opensearch + otel demo ([@danish9039](https://github.com/danish9039) in [#7516](https://github.com/jaegertracing/jaeger/pull/7516)) * [clickhouse][refactor] group attributes into structs ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7603](https://github.com/jaegertracing/jaeger/pull/7603)) * [clickhouse][refactor] remove indirection in database model ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7602](https://github.com/jaegertracing/jaeger/pull/7602)) * [clickhouse] append link in writer ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7601](https://github.com/jaegertracing/jaeger/pull/7601)) * [clickhouse] remove unused function ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7600](https://github.com/jaegertracing/jaeger/pull/7600)) * [clickhouse] append event in writer ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7558](https://github.com/jaegertracing/jaeger/pull/7558)) * Used fully qualified names for images ([@danish9039](https://github.com/danish9039) in [#7553](https://github.com/jaegertracing/jaeger/pull/7553)) * [clickhouse] add span attributes to writer ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7541](https://github.com/jaegertracing/jaeger/pull/7541)) * [clickhouse] integrate clickhouse into storage extension ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7524](https://github.com/jaegertracing/jaeger/pull/7524)) #### 👷 CI Improvements * Enable range-val-address linter ([@neoandmatrix](https://github.com/neoandmatrix) in [#7593](https://github.com/jaegertracing/jaeger/pull/7593)) * Enable switch linter ([@neoandmatrix](https://github.com/neoandmatrix) in [#7573](https://github.com/jaegertracing/jaeger/pull/7573)) * Skip delve for riscv64 arch ([@gouthamhusky](https://github.com/gouthamhusky) in [#7571](https://github.com/jaegertracing/jaeger/pull/7571)) * Enable lint rule: import-alias-naming ([@alkak95](https://github.com/alkak95) in [#7565](https://github.com/jaegertracing/jaeger/pull/7565)) * Fix bug in make lint ([@SomilJain0112](https://github.com/SomilJain0112) in [#7563](https://github.com/jaegertracing/jaeger/pull/7563)) * Do not run metrics diff workflow except on prs ([@yurishkuro](https://github.com/yurishkuro) in [#7554](https://github.com/jaegertracing/jaeger/pull/7554)) * Define dockerhub_username env var ([@yurishkuro](https://github.com/yurishkuro) in [#7538](https://github.com/jaegertracing/jaeger/pull/7538)) * Fix: resolve docker hub authentication issues in upload-docker-readme.sh ([@SomilJain0112](https://github.com/SomilJain0112) in [#7536](https://github.com/jaegertracing/jaeger/pull/7536)) #### ⚙️ Refactoring * [refactor]: use the built-in max to simplify the code ([@zhetaicheleba](https://github.com/zhetaicheleba) in [#7624](https://github.com/jaegertracing/jaeger/pull/7624)) * Speed up es tests ([@yurishkuro](https://github.com/yurishkuro) in [#7606](https://github.com/jaegertracing/jaeger/pull/7606)) * [refactor]: replace split in loops with more efficient splitseq ([@pennylees](https://github.com/pennylees) in [#7588](https://github.com/jaegertracing/jaeger/pull/7588)) ### 📊 UI Changes #### 🐞 Bug fixes, Minor Improvements * Fix: clicking dots and ddg button ([@Parship12](https://github.com/Parship12) in [#3149](https://github.com/jaegertracing/jaeger-ui/pull/3149)) * Fix in-trace span search ([@Parship12](https://github.com/Parship12) in [#3132](https://github.com/jaegertracing/jaeger-ui/pull/3132)) * Fix: trace id search input on the search page ([@Parship12](https://github.com/Parship12) in [#3124](https://github.com/jaegertracing/jaeger-ui/pull/3124)) #### ⚙️ Refactoring * Convert tracepage {spanbarrow, spantreeoffset, opnode} to functional ([@JeevaRamanathan](https://github.com/JeevaRamanathan) in [#3136](https://github.com/jaegertracing/jaeger-ui/pull/3136)) * Convert searchresults/index to functional component ([@Parship12](https://github.com/Parship12) in [#3138](https://github.com/jaegertracing/jaeger-ui/pull/3138)) * Remove history from tracediff component ([@Parship12](https://github.com/Parship12) in [#3135](https://github.com/jaegertracing/jaeger-ui/pull/3135)) * Remove history instances from traces.tsx ([@Parship12](https://github.com/Parship12) in [#3110](https://github.com/jaegertracing/jaeger-ui/pull/3110)) * Convert tracepage {timelinecollapser} to functional component ([@JeevaRamanathan](https://github.com/JeevaRamanathan) in [#3108](https://github.com/jaegertracing/jaeger-ui/pull/3108)) v1.74.0 / v2.11.0 (2025-10-01) ------------------------------- ### Backend Changes #### 🐞 Bug fixes, Minor Improvements * Make enabletracing param work correctly in jaeger-v2 query extension ([@Frapschen](https://github.com/Frapschen) in [#7226](https://github.com/jaegertracing/jaeger/pull/7226)) #### 🚧 Experimental Features * [clickhouse] implement factory with minimal configuration ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7518](https://github.com/jaegertracing/jaeger/pull/7518)) * [clickhouse] implement writer for clickhouse storage ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7514](https://github.com/jaegertracing/jaeger/pull/7514)) * [clickhouse] add attributes for event in clickhouse storage ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7512](https://github.com/jaegertracing/jaeger/pull/7512)) * [clickhouse] add column for storing complex attributes ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7510](https://github.com/jaegertracing/jaeger/pull/7510)) * [clickhouse] add attributes to span table for clickhouse storage ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7503](https://github.com/jaegertracing/jaeger/pull/7503)) #### ⚙️ Refactoring * Move clickhouse queries to sql files with embed directive ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7523](https://github.com/jaegertracing/jaeger/pull/7523)) * Use maps.copy for cleaner map handling ([@quantpoet](https://github.com/quantpoet) in [#7513](https://github.com/jaegertracing/jaeger/pull/7513)) ### 📊 UI Changes #### 🐞 Bug fixes, Minor Improvements * Replace dependency react-window ([@Parship999](https://github.com/Parship999) in [#3070](https://github.com/jaegertracing/jaeger-ui/pull/3070)) * Fix the flaky test in tracepage/index.test.js ([@Parship999](https://github.com/Parship999) in [#3089](https://github.com/jaegertracing/jaeger-ui/pull/3089)) * Fix top bar tab order ([@mdwyer6](https://github.com/mdwyer6) in [#3067](https://github.com/jaegertracing/jaeger-ui/pull/3067)) * Expand the logs automatically ([@Parship999](https://github.com/Parship999) in [#3054](https://github.com/jaegertracing/jaeger-ui/pull/3054)) #### ⚙️ Refactoring * Convert tracediff component from class to functional component ([@Parship999](https://github.com/Parship999) in [#3099](https://github.com/jaegertracing/jaeger-ui/pull/3099)) * Remove the history instance from the app component ([@Parship999](https://github.com/Parship999) in [#3100](https://github.com/jaegertracing/jaeger-ui/pull/3100)) * Update to modern jsx transform ([@Parship999](https://github.com/Parship999) in [#3097](https://github.com/jaegertracing/jaeger-ui/pull/3097)) * Fix some eslint warnings ([@Parship999](https://github.com/Parship999) in [#3096](https://github.com/jaegertracing/jaeger-ui/pull/3096)) * Convert servicesview/index to functional component ([@Parship999](https://github.com/Parship999) in [#3004](https://github.com/jaegertracing/jaeger-ui/pull/3004)) * Convert filteredlist/index.tsx from class to functional component ([@Parship999](https://github.com/Parship999) in [#3083](https://github.com/jaegertracing/jaeger-ui/pull/3083)) * Fix some lint warnings ([@Parship999](https://github.com/Parship999) in [#3090](https://github.com/jaegertracing/jaeger-ui/pull/3090)) * Convert searchresults/diffselection to functional component and improved testcases ([@JeevaRamanathan](https://github.com/JeevaRamanathan) in [#3076](https://github.com/jaegertracing/jaeger-ui/pull/3076)) * Convert tracediff/tracediffheader {cohorttable, tracediffheader} to functional component ([@JeevaRamanathan](https://github.com/JeevaRamanathan) in [#3082](https://github.com/jaegertracing/jaeger-ui/pull/3082)) * Convert seachresults{resultitem, resultitemtitle} to functional components ([@JeevaRamanathan](https://github.com/JeevaRamanathan) in [#3071](https://github.com/jaegertracing/jaeger-ui/pull/3071)) * Tighten tracearchive type to more strictly enforce correct state ([@tklever](https://github.com/tklever) in [#623](https://github.com/jaegertracing/jaeger-ui/pull/623)) v1.73.0 / v2.10.0 (2025-09-02) ------------------------------- ### Backend Changes #### 🐞 Bug fixes, Minor Improvements * Chore(jaeger-tracegen): upgrade tracegen docker compose to jaeger-v2 ([@lekaf974](https://github.com/lekaf974) in [#7481](https://github.com/jaegertracing/jaeger/pull/7481)) * Fix extra `_total` suffix in metrics ([@pipiland2612](https://github.com/pipiland2612) in [#7476](https://github.com/jaegertracing/jaeger/pull/7476)) #### 🚧 Experimental Features * Add timeout to helm commands in jaeger demo deployment ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#7488](https://github.com/jaegertracing/jaeger/pull/7488)) * Separate scripts for deployment upgrade and clean install of jaeger demo deployment and enable g-tracking in jaeger ui ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#7440](https://github.com/jaegertracing/jaeger/pull/7440)) * Redirect to demo documentation in jaeger demo deployment ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#7429](https://github.com/jaegertracing/jaeger/pull/7429)) * Multiple minor changes in jaeger demo deployment in oke ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#7427](https://github.com/jaegertracing/jaeger/pull/7427)) * Change example hotrod version in jaeger demo version ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#7425](https://github.com/jaegertracing/jaeger/pull/7425)) #### 👷 CI Improvements * Validate jaeger demo configurations in ci workflow ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#7464](https://github.com/jaegertracing/jaeger/pull/7464)) * [ci] sanitize transient metric labels before comparison ([@wololowarrior](https://github.com/wololowarrior) in [#7482](https://github.com/jaegertracing/jaeger/pull/7482)) * Download pre-built go tip from grafana/gotip repo ([@yurishkuro](https://github.com/yurishkuro) in [#7447](https://github.com/jaegertracing/jaeger/pull/7447)) * [ci] improve summary comment ([@pipiland2612](https://github.com/pipiland2612) in [#7462](https://github.com/jaegertracing/jaeger/pull/7462)) * [ci] improve on metrics comment ([@pipiland2612](https://github.com/pipiland2612) in [#7449](https://github.com/jaegertracing/jaeger/pull/7449)) * [ci] add upload pr_number artifacts action to ci-e2e-all.yml ([@pipiland2612](https://github.com/pipiland2612) in [#7448](https://github.com/jaegertracing/jaeger/pull/7448)) * [ci] add new post comment workflow ([@pipiland2612](https://github.com/pipiland2612) in [#7414](https://github.com/jaegertracing/jaeger/pull/7414)) * Use latest go version when building gotip ([@yurishkuro](https://github.com/yurishkuro) in [#7445](https://github.com/jaegertracing/jaeger/pull/7445)) #### ⚙️ Refactoring * Chore: enable badlock from go-critic ([@mmorel-35](https://github.com/mmorel-35) in [#7437](https://github.com/jaegertracing/jaeger/pull/7437)) * Chore: enable rangevalcopy from go-critic ([@mmorel-35](https://github.com/mmorel-35) in [#7438](https://github.com/jaegertracing/jaeger/pull/7438)) * Chore: enable more rules from go-critic ([@mmorel-35](https://github.com/mmorel-35) in [#7434](https://github.com/jaegertracing/jaeger/pull/7434)) * Chore: enable several rules from go-critic ([@mmorel-35](https://github.com/mmorel-35) in [#7430](https://github.com/jaegertracing/jaeger/pull/7430)) * Chore: enable several rules from staticcheck by default ([@mmorel-35](https://github.com/mmorel-35) in [#7431](https://github.com/jaegertracing/jaeger/pull/7431)) ### 📊 UI Changes #### ✨ New Features * Upgrade project to react 19 ([@vishvamsinh28](https://github.com/vishvamsinh28) in [#3040](https://github.com/jaegertracing/jaeger-ui/pull/3040)) #### 🐞 Bug fixes, Minor Improvements * Make the scrollbar always visible in lookback dropdown ([@Parship999](https://github.com/Parship999) in [#3048](https://github.com/jaegertracing/jaeger-ui/pull/3048)) * Add click to copy for trace id ([@Darshit42](https://github.com/Darshit42) in [#2997](https://github.com/jaegertracing/jaeger-ui/pull/2997)) * Improve performance on trace statistics page after value for sub-group is selected ([@DamianMaslanka5](https://github.com/DamianMaslanka5) in [#2843](https://github.com/jaegertracing/jaeger-ui/pull/2843)) * Highlight active mode button in tracegraph ([@Saquib45](https://github.com/Saquib45) in [#3034](https://github.com/jaegertracing/jaeger-ui/pull/3034)) #### ⚙️ Refactoring * Fix typescript ref typing errors across react components ([@vishvamsinh28](https://github.com/vishvamsinh28) in [#3042](https://github.com/jaegertracing/jaeger-ui/pull/3042)) * Convert `VerticalResizer.tsx` from class component to functional component ([@Parship999](https://github.com/Parship999) in [#2951](https://github.com/jaegertracing/jaeger-ui/pull/2951)) v1.72.0 / v2.9.0 (2025-08-03) ------------------------------- ### Backend Changes #### ✨ New Features * Implement custom rangequery interface to support elasticsearch v9 ([@shuraih775](https://github.com/shuraih775) in [#7358](https://github.com/jaegertracing/jaeger/pull/7358)) * Add opensearch 3.x support ([@Parship999](https://github.com/Parship999) in [#7356](https://github.com/jaegertracing/jaeger/pull/7356)) * Ingress service for jaeger demo deployment ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#7308](https://github.com/jaegertracing/jaeger/pull/7308)) #### 🐞 Bug fixes, Minor Improvements * Add `/deep-dependencies` endpoint ([@sujalshah-bit](https://github.com/sujalshah-bit) in [#7399](https://github.com/jaegertracing/jaeger/pull/7399)) * Add api key authentication support for elasticsearch storage ([@danish9039](https://github.com/danish9039) in [#7402](https://github.com/jaegertracing/jaeger/pull/7402)) * Fix kafka tls configuration with plaintext authentication ([@Parship999](https://github.com/Parship999) in [#7395](https://github.com/jaegertracing/jaeger/pull/7395)) * Add alerts for jaeger 2.x ([@danish9039](https://github.com/danish9039) in [#6854](https://github.com/jaegertracing/jaeger/pull/6854)) * Add missing mapstructure tag for tls in promcfg/config.go ([@pipiland2612](https://github.com/pipiland2612) in [#7367](https://github.com/jaegertracing/jaeger/pull/7367)) * Add bearer token reloading and reuse in multiple storage backends ([@danish9039](https://github.com/danish9039) in [#7360](https://github.com/jaegertracing/jaeger/pull/7360)) * Fixed invalid string type issue for array-valued tags ([@Parship999](https://github.com/Parship999) in [#7350](https://github.com/jaegertracing/jaeger/pull/7350)) * Enable stale bot ([@Parship999](https://github.com/Parship999) in [#7355](https://github.com/jaegertracing/jaeger/pull/7355)) * Enable automated closing of stale pull requests and issues ([@Parship999](https://github.com/Parship999) in [#7347](https://github.com/jaegertracing/jaeger/pull/7347)) * Fix codeql security alert: remove sensitive file paths from log messages ([@danish9039](https://github.com/danish9039) in [#7345](https://github.com/jaegertracing/jaeger/pull/7345)) * Decouple from otel collector semconv package ([@danish9039](https://github.com/danish9039) in [#7318](https://github.com/jaegertracing/jaeger/pull/7318)) * Expose jaeger 4318 otlp http port in grafana integration example ([@gokulvootla](https://github.com/gokulvootla) in [#7325](https://github.com/jaegertracing/jaeger/pull/7325)) * Add ttl to badger sample config ([@yurishkuro](https://github.com/yurishkuro) in [#7319](https://github.com/jaegertracing/jaeger/pull/7319)) * [hotrod] load jquery from cdn to allow hotrod to work with a basepath ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#7321](https://github.com/jaegertracing/jaeger/pull/7321)) * [refactor] improved gethttproundtripper ([@danish9039](https://github.com/danish9039) in [#7313](https://github.com/jaegertracing/jaeger/pull/7313)) * Correct command in docker-compose/monitor/readme.md ([@pipiland2612](https://github.com/pipiland2612) in [#7309](https://github.com/jaegertracing/jaeger/pull/7309)) #### 🚧 Experimental Features * Added tls/ssl certification and automation for jaeger demo deployment ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#7419](https://github.com/jaegertracing/jaeger/pull/7419)) * Automate jaeger demo deployment to oke using github actions ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#7334](https://github.com/jaegertracing/jaeger/pull/7334)) * Add required helm repositories for jaeger demo deployment ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#7403](https://github.com/jaegertracing/jaeger/pull/7403)) * Add metrics_storage to config-elasticsearch/opensearch ([@pipiland2612](https://github.com/pipiland2612) in [#7390](https://github.com/jaegertracing/jaeger/pull/7390)) * Change basepath and remove unused yaml for jaeger demo deployment ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#7374](https://github.com/jaegertracing/jaeger/pull/7374)) * Add readiness and liveness probe paths to demo deployment for improved health checks ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#7371](https://github.com/jaegertracing/jaeger/pull/7371)) * [spm] add optimisation by time range ([@pipiland2612](https://github.com/pipiland2612) in [#7322](https://github.com/jaegertracing/jaeger/pull/7322)) * Serve hotrod ui and grafana from separate basepaths in jaeger demo ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#7328](https://github.com/jaegertracing/jaeger/pull/7328)) * Add support for elasticsearch v9 index template creation by reusing v8 template ([@shuraih775](https://github.com/shuraih775) in [#7320](https://github.com/jaegertracing/jaeger/pull/7320)) * [spm] add opensearch option ([@pipiland2612](https://github.com/pipiland2612) in [#7304](https://github.com/jaegertracing/jaeger/pull/7304)) * [spm] bug fix for metricstore/elasticsearch/processor.go ([@pipiland2612](https://github.com/pipiland2612) in [#7303](https://github.com/jaegertracing/jaeger/pull/7303)) * Add jaeger demo monitoring setup with deployment script ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#7300](https://github.com/jaegertracing/jaeger/pull/7300)) * [spm] geterrorrates implementation ([@pipiland2612](https://github.com/pipiland2612) in [#7298](https://github.com/jaegertracing/jaeger/pull/7298)) * [spm] getlatencies implementation ([@pipiland2612](https://github.com/pipiland2612) in [#7290](https://github.com/jaegertracing/jaeger/pull/7290)) * Add load generator for jaeger demo to generate trace data from hotrod service ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#7296](https://github.com/jaegertracing/jaeger/pull/7296)) * Refactor metricstore/elasticsearch/reader.go ([@pipiland2612](https://github.com/pipiland2612) in [#7295](https://github.com/jaegertracing/jaeger/pull/7295)) #### 👷 CI Improvements * [ci] add metrics summary action ([@pipiland2612](https://github.com/pipiland2612) in [#7376](https://github.com/jaegertracing/jaeger/pull/7376)) * [spm e2e] add e2e test for new spm (using elasticsearch as metrics_backend) ([@pipiland2612](https://github.com/pipiland2612) in [#7307](https://github.com/jaegertracing/jaeger/pull/7307)) #### ⚙️ Refactoring * Refactor basic authentication to http transport layer ([@danish9039](https://github.com/danish9039) in [#7388](https://github.com/jaegertracing/jaeger/pull/7388)) * Fix enforce-switch-style linting errors ([@Andrei-hub11](https://github.com/Andrei-hub11) in [#7387](https://github.com/jaegertracing/jaeger/pull/7387)) * Remove pool.go/pool.stop method (deadcode) ([@Parship999](https://github.com/Parship999) in [#7373](https://github.com/jaegertracing/jaeger/pull/7373)) * Removing dead code from thrift/jaeger ([@wololowarrior](https://github.com/wololowarrior) in [#7365](https://github.com/jaegertracing/jaeger/pull/7365)) * Remove sanitizer/service_name_sanitizer (dead code) ([@Parship999](https://github.com/Parship999) in [#7366](https://github.com/jaegertracing/jaeger/pull/7366)) * Remove deadcode jptrace/utf8 ([@Parship999](https://github.com/Parship999) in [#7369](https://github.com/jaegertracing/jaeger/pull/7369)) * Removal of all-in-one/setupcontext package (dead codes) ([@Parship999](https://github.com/Parship999) in [#7359](https://github.com/jaegertracing/jaeger/pull/7359)) * Remove of sanitizer/utf8_sanitizer (dead code) ([@Parship999](https://github.com/Parship999) in [#7363](https://github.com/jaegertracing/jaeger/pull/7363)) * Removal of sanitizer/cache package (dead code) ([@Parship999](https://github.com/Parship999) in [#7357](https://github.com/jaegertracing/jaeger/pull/7357)) * [refactor] used otel optional type for union auth struct ([@danish9039](https://github.com/danish9039) in [#7316](https://github.com/jaegertracing/jaeger/pull/7316)) * [refactor] move bearertoken under auth/ ([@danish9039](https://github.com/danish9039) in [#7312](https://github.com/jaegertracing/jaeger/pull/7312)) * [badger] give responsibility of creating v2 factory to storage backend ([@Manik2708](https://github.com/Manik2708) in [#7299](https://github.com/jaegertracing/jaeger/pull/7299)) * Refactor metricstore/es/reader.go by introducing querybuilder ([@pipiland2612](https://github.com/pipiland2612) in [#7297](https://github.com/jaegertracing/jaeger/pull/7297)) v1.71.0 / v2.8.0 (2025-07-03) ------------------------------- ### Backend Changes #### ⛔ Breaking Changes * [es] materialize span.kind and span.status tags ([@pipiland2612](https://github.com/pipiland2612) in [#7272](https://github.com/jaegertracing/jaeger/pull/7272)) * Make jaeger.es.disablelegacyid feature stable ([@yurishkuro](https://github.com/yurishkuro) in [#7267](https://github.com/jaegertracing/jaeger/pull/7267)) #### ✨ New Features * [v2] switch memory backend to storage api v2 implementation ([@Manik2708](https://github.com/Manik2708) in [#7157](https://github.com/jaegertracing/jaeger/pull/7157)) #### 🐞 Bug fixes, Minor Improvements * Fix panic when reading malformed warning attribute ([@yurishkuro](https://github.com/yurishkuro) in [#7293](https://github.com/jaegertracing/jaeger/pull/7293)) * [fix] prevent panic when sanitizing read-only traces with multiple exporters ([@victornguen](https://github.com/victornguen) in [#7245](https://github.com/jaegertracing/jaeger/pull/7245)) * Update elasticsearch to use olivere/elastic/v7 ([@pipiland2612](https://github.com/pipiland2612) in [#7244](https://github.com/jaegertracing/jaeger/pull/7244)) * Repoint docker compose files to use cr.jaegertracing.io ([@jkowall](https://github.com/jkowall) in [#7240](https://github.com/jaegertracing/jaeger/pull/7240)) * [es/v2] add metrics decorator for trace reader ([@Manik2708](https://github.com/Manik2708) in [#7201](https://github.com/jaegertracing/jaeger/pull/7201)) #### 🚧 Experimental Features * [spm] getcallrate implementation ([@pipiland2612](https://github.com/pipiland2612) in [#7229](https://github.com/jaegertracing/jaeger/pull/7229)) * [cassandra] give responsibility of creating v2 factory to storage backend ([@Manik2708](https://github.com/Manik2708) in [#7228](https://github.com/jaegertracing/jaeger/pull/7228)) * Jaeger demo on kubernetes ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#7262](https://github.com/jaegertracing/jaeger/pull/7262)) * [clickhouse] implement gettraces for clickhouse storage ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7207](https://github.com/jaegertracing/jaeger/pull/7207)) * [spm] and esclient to es/reader.go and refactor test ([@pipiland2612](https://github.com/pipiland2612) in [#7216](https://github.com/jaegertracing/jaeger/pull/7216)) * Add skeleton implementation for es as metricstorage for spm ([@pipiland2612](https://github.com/pipiland2612) in [#7209](https://github.com/jaegertracing/jaeger/pull/7209)) ### 📊 UI Changes #### ⚙️ Refactoring * Convert `opsgraph.tsx` from class component to functional component ([@Parship999](https://github.com/Parship999) in [#2914](https://github.com/jaegertracing/jaeger-ui/pull/2914)) * Convert `regiondemo.tsx` from class component to functional component ([@Parship999](https://github.com/Parship999) in [#2910](https://github.com/jaegertracing/jaeger-ui/pull/2910)) * Convert `dividerdemo.tsx` from class component to functional component ([@Parship999](https://github.com/Parship999) in [#2909](https://github.com/jaegertracing/jaeger-ui/pull/2909)) * Convert `draggablemanagerdemo.tsx` from class component to functional component ([@Parship999](https://github.com/Parship999) in [#2908](https://github.com/jaegertracing/jaeger-ui/pull/2908)) * Convert `nameselector.tsx` from class component to functional component ([@Parship999](https://github.com/Parship999) in [#2889](https://github.com/jaegertracing/jaeger-ui/pull/2889)) * Convert `copyicon.tsx` from class component to functional component ([@Parship999](https://github.com/Parship999) in [#2887](https://github.com/jaegertracing/jaeger-ui/pull/2887)) v1.70.0 / v2.7.0 (2025-06-10) ------------------------------- ### Backend Changes #### ✨ New Features * [feat] use v2 es/os storage in jaeger-v2 ([@Manik2708](https://github.com/Manik2708) in [#7151](https://github.com/jaegertracing/jaeger/pull/7151)) #### 🐞 Bug fixes, Minor Improvements * Feat: add option to disable elasticsearch health check ([@timonegk](https://github.com/timonegk) in [#7212](https://github.com/jaegertracing/jaeger/pull/7212)) * Fix(elasticsearch): respect explicitly configured replicas=0 in index… ([@masihkhatibzadeh99](https://github.com/masihkhatibzadeh99) in [#7160](https://github.com/jaegertracing/jaeger/pull/7160)) * Add sanitizers for negative span duration ([@iypetrov](https://github.com/iypetrov) in [#7122](https://github.com/jaegertracing/jaeger/pull/7122)) * [fix] fix prometheus label value is not valid utf 8 cause api timeout ([@iypetrov](https://github.com/iypetrov) in [#7128](https://github.com/jaegertracing/jaeger/pull/7128)) * Add retries to ilm client ([@iypetrov](https://github.com/iypetrov) in [#7120](https://github.com/jaegertracing/jaeger/pull/7120)) * Add retry configuration to storage exporter ([@kumarlokesh](https://github.com/kumarlokesh) in [#7132](https://github.com/jaegertracing/jaeger/pull/7132)) * [fix] restore es metrics ([@AnmolxSingh](https://github.com/AnmolxSingh) in [#7006](https://github.com/jaegertracing/jaeger/pull/7006)) * [fix] propagate environment variables to binary from integration tests ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7112](https://github.com/jaegertracing/jaeger/pull/7112)) #### 🚧 Experimental Features * [refactor] rework clickhouse schema structure ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7181](https://github.com/jaegertracing/jaeger/pull/7181)) * [clickhouse] implement getoperations for trace reader in clickhouse storage ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7180](https://github.com/jaegertracing/jaeger/pull/7180)) * [clickhouse] implement getservices for trace reader in clickhouse storage ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7159](https://github.com/jaegertracing/jaeger/pull/7159)) * [v2] implement `getdependencies` for memory backend ([@Manik2708](https://github.com/Manik2708) in [#7154](https://github.com/jaegertracing/jaeger/pull/7154)) * [v2] implement `gettraces` for memory backend ([@Manik2708](https://github.com/Manik2708) in [#7152](https://github.com/jaegertracing/jaeger/pull/7152)) * [v2] implement `findtraceids` for memory backend ([@Manik2708](https://github.com/Manik2708) in [#7143](https://github.com/jaegertracing/jaeger/pull/7143)) * Add description for docker-compose-elasticsearch.yml ([@pipiland2612](https://github.com/pipiland2612) in [#7146](https://github.com/jaegertracing/jaeger/pull/7146)) * Add e2e test for docker-compose-elasticsearch.yml file ([@pipiland2612](https://github.com/pipiland2612) in [#7145](https://github.com/jaegertracing/jaeger/pull/7145)) * Add docker-compose-elasticsearch.yml and its sample configuration ([@pipiland2612](https://github.com/pipiland2612) in [#7144](https://github.com/jaegertracing/jaeger/pull/7144)) * [v2] implement `findtraces` for memory backend ([@Manik2708](https://github.com/Manik2708) in [#7062](https://github.com/jaegertracing/jaeger/pull/7062)) #### ⚙️ Refactoring * Relocate the docker directory from the root directory to under scripts/build ([@ris-tlp](https://github.com/ris-tlp) in [#7189](https://github.com/jaegertracing/jaeger/pull/7189)) * [refactor] move sanitizer ([@yurishkuro](https://github.com/yurishkuro) in [#7158](https://github.com/jaegertracing/jaeger/pull/7158)) * [es][v2] refactor the factory of v1 to make it reusable for v2 ([@Manik2708](https://github.com/Manik2708) in [#7086](https://github.com/jaegertracing/jaeger/pull/7086)) * [refactor] allow storage cleaner to be overridden via environment variable ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7114](https://github.com/jaegertracing/jaeger/pull/7114)) * [refactor] remove archive storage from grpc config ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7113](https://github.com/jaegertracing/jaeger/pull/7113)) * [grpc] allow remote storage endpoint to be set via environment variable ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7111](https://github.com/jaegertracing/jaeger/pull/7111)) * [refactor] use proto files from `jaeger-idl` for remote storage api ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7104](https://github.com/jaegertracing/jaeger/pull/7104)) ### 📊 UI Changes #### 🐞 Bug fixes, Minor Improvements * Fix react fragment key issues in multiple components ([@Parship999](https://github.com/Parship999) in [#2823](https://github.com/jaegertracing/jaeger-ui/pull/2823)) * Move tracediff header chevron icon ([@Parship999](https://github.com/Parship999) in [#2845](https://github.com/jaegertracing/jaeger-ui/pull/2845)) * Feat: filter logs based on the selected time range ([@tejas-raskar](https://github.com/tejas-raskar) in [#2844](https://github.com/jaegertracing/jaeger-ui/pull/2844)) * Enhance tracediff ui components ([@Parship999](https://github.com/Parship999) in [#2806](https://github.com/jaegertracing/jaeger-ui/pull/2806)) * Rewrite computeselftime to improve performance on trace statistics page ([@DamianMaslanka5](https://github.com/DamianMaslanka5) in [#2767](https://github.com/jaegertracing/jaeger-ui/pull/2767)) * Fix array return pattern in `labeledlist` component ([@Parship999](https://github.com/Parship999) in [#2812](https://github.com/jaegertracing/jaeger-ui/pull/2812)) * Allow json logs to occupy entire available width ([@tejas-raskar](https://github.com/tejas-raskar) in [#2814](https://github.com/jaegertracing/jaeger-ui/pull/2814)) * Feat: convert monitoratmemptystate to a functional component ([@vishvamsinh28](https://github.com/vishvamsinh28) in [#2790](https://github.com/jaegertracing/jaeger-ui/pull/2790)) * Replace deprecated `overlayclassname` with `classnames.root` ([@abhayporwals](https://github.com/abhayporwals) in [#2772](https://github.com/jaegertracing/jaeger-ui/pull/2772)) * [fix]: reduce default minimum allowed zoom ([@hari45678](https://github.com/hari45678) in [#2775](https://github.com/jaegertracing/jaeger-ui/pull/2775)) * Fix dependencygraph dag extra render ([@mdwyer6](https://github.com/mdwyer6) in [#2749](https://github.com/jaegertracing/jaeger-ui/pull/2749)) #### ⚙️ Refactoring * Convert qualitymetrics components to functional components ([@Parship999](https://github.com/Parship999) in [#2856](https://github.com/jaegertracing/jaeger-ui/pull/2856)) * Refactor: spandetailrow to functional component ([@tejas-raskar](https://github.com/tejas-raskar) in [#2827](https://github.com/jaegertracing/jaeger-ui/pull/2827)) * Migrate tracetimelineviewerimpl to a functional component ([@tejas-raskar](https://github.com/tejas-raskar) in [#2816](https://github.com/jaegertracing/jaeger-ui/pull/2816)) * Refactor canvasspangraph to functional component and improve test coverage ([@vishvamsinh28](https://github.com/vishvamsinh28) in [#2824](https://github.com/jaegertracing/jaeger-ui/pull/2824)) v1.69.0 / v2.6.0 (2025-05-08) ------------------------------- ### Backend Changes #### ⛔ Breaking Changes * Feat(elasticsearch): Add flag to enable gzip compression by default ([@timonegk](https://github.com/timonegk) in [#7072](https://github.com/jaegertracing/jaeger/pull/7072)) * Only Remote Storage API v2 is supported in Jaeger v2 ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6969](https://github.com/jaegertracing/jaeger/pull/6969)) #### ✨ New Features * Add filterprocessor ([@yurishkuro](https://github.com/yurishkuro) in [#7094](https://github.com/jaegertracing/jaeger/pull/7094)) #### 🐞 Bug fixes, Minor Improvements * Upgrade reverse-proxy example to jaeger-v2 ([@yurishkuro](https://github.com/yurishkuro) in [#7076](https://github.com/jaegertracing/jaeger/pull/7076)) * Add pprof extension ([@denysvitali](https://github.com/denysvitali) in [#7073](https://github.com/jaegertracing/jaeger/pull/7073)) * [es][v1] change the db tag value from `string` to `any` type ([@Manik2708](https://github.com/Manik2708) in [#6998](https://github.com/jaegertracing/jaeger/pull/6998)) * Remove outdated info related to jaeger exporter ([@DamianMaslanka5](https://github.com/DamianMaslanka5) in [#6987](https://github.com/jaegertracing/jaeger/pull/6987)) * [bug] fix the version module path in ldflags ([@developer-guy](https://github.com/developer-guy) in [#6990](https://github.com/jaegertracing/jaeger/pull/6990)) #### 🚧 Experimental Features * [es][v2] implement `getdependenies` and `writedependencies` ([@Manik2708](https://github.com/Manik2708) in [#7085](https://github.com/jaegertracing/jaeger/pull/7085)) * [clickhouse] convert otel traces model to native format ([@zhengkezhou1](https://github.com/zhengkezhou1) in [#6935](https://github.com/jaegertracing/jaeger/pull/6935)) * [es] make `nestedtags` and `elevatedtags` distinction at `corespanreader` level ([@Manik2708](https://github.com/Manik2708) in [#7067](https://github.com/jaegertracing/jaeger/pull/7067)) * [es][v2] move `coredependencystore` and `dbmodel` from v1 to v2 ([@Manik2708](https://github.com/Manik2708) in [#7079](https://github.com/jaegertracing/jaeger/pull/7079)) * [grpc][v2] use standard otlp receiver for grpc storage write path ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7065](https://github.com/jaegertracing/jaeger/pull/7065)) * [es][v2] implement `findtraces` for es/os for v2 ([@Manik2708](https://github.com/Manik2708) in [#7021](https://github.com/jaegertracing/jaeger/pull/7021)) * [refactor] remove `jaeger_query` extension from remote storage backend config ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7059](https://github.com/jaegertracing/jaeger/pull/7059)) * [v2][remote-storage] implement remote storage extension ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7043](https://github.com/jaegertracing/jaeger/pull/7043)) * [es][v2] implement `gettraces` for es/os ([@Manik2708](https://github.com/Manik2708) in [#7054](https://github.com/jaegertracing/jaeger/pull/7054)) * [v2] implement `getoperations` and `getservices` for memory backend ([@Manik2708](https://github.com/Manik2708) in [#7053](https://github.com/jaegertracing/jaeger/pull/7053)) * [v2] implement `findtraceids` for es/os ([@Manik2708](https://github.com/Manik2708) in [#7035](https://github.com/jaegertracing/jaeger/pull/7035)) * [v2] implement `writetraces` for memory backend ([@Manik2708](https://github.com/Manik2708) in [#7027](https://github.com/jaegertracing/jaeger/pull/7027)) * [es] refactor `dependencystore` to make it reusable for v2 apis ([@Manik2708](https://github.com/Manik2708) in [#7044](https://github.com/jaegertracing/jaeger/pull/7044)) * [grpc][v2] use v2 grpc factory in storage extension ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6969](https://github.com/jaegertracing/jaeger/pull/6969)) * [grpc][v2] register grpc v2 handler in remote-storage server ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7037](https://github.com/jaegertracing/jaeger/pull/7037)) * [es][v2] implement `getoperations` and `getservices` for v2 ([@Manik2708](https://github.com/Manik2708) in [#7025](https://github.com/jaegertracing/jaeger/pull/7025)) * [es][v2] implement `writetraces` for v2 ([@Manik2708](https://github.com/Manik2708) in [#7020](https://github.com/jaegertracing/jaeger/pull/7020)) * [grpc][v2] implement `getdependencies` in grpc v2 server ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7016](https://github.com/jaegertracing/jaeger/pull/7016)) * [grpc][v2] implement otlp exporter api in grpc v2 handler ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7012](https://github.com/jaegertracing/jaeger/pull/7012)) * [es][v2] change the db tag value from `string` to `any` type ([@Manik2708](https://github.com/Manik2708) in [#6994](https://github.com/jaegertracing/jaeger/pull/6994)) * [grpc][v2] implement findtraceids in grpc v2 handler ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7003](https://github.com/jaegertracing/jaeger/pull/7003)) * [grpc][v2] implement `findtraces` in grpc v2 handler ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6992](https://github.com/jaegertracing/jaeger/pull/6992)) * [grpc][v2] implement `gettraces` in grpc v2 handler ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6985](https://github.com/jaegertracing/jaeger/pull/6985)) * [grpc][v2] implement getservices in grpc v2 handler ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6984](https://github.com/jaegertracing/jaeger/pull/6984)) * [grpc][v2] implement `getservices` in grpc v2 handler ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6980](https://github.com/jaegertracing/jaeger/pull/6980)) #### 👷 CI Improvements * Do not run binary size check on push to main ([@yurishkuro](https://github.com/yurishkuro) in [#7096](https://github.com/jaegertracing/jaeger/pull/7096)) * Check that version number is corectly embedded in the binary ([@yurishkuro](https://github.com/yurishkuro) in [#7092](https://github.com/jaegertracing/jaeger/pull/7092)) * Update module github.com/vektra/mockery/v2 to v3 ([@AnmolxSingh](https://github.com/AnmolxSingh) in [#7051](https://github.com/jaegertracing/jaeger/pull/7051)) * [fix] add query integration test to workflows file ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7056](https://github.com/jaegertracing/jaeger/pull/7056)) * Enable mockery/with-expecter ([@yurishkuro](https://github.com/yurishkuro) in [#7046](https://github.com/jaegertracing/jaeger/pull/7046)) * Fix paths in mockery config ([@yurishkuro](https://github.com/yurishkuro) in [#7045](https://github.com/jaegertracing/jaeger/pull/7045)) * Fix flakiness in runindexcleanertest by filtering jaeger indices ([@0xShubhamSolanki](https://github.com/0xShubhamSolanki) in [#7004](https://github.com/jaegertracing/jaeger/pull/7004)) * Add e2e integration test for query service ([@pipiland2612](https://github.com/pipiland2612) in [#6966](https://github.com/jaegertracing/jaeger/pull/6966)) * #5608 improve spm e2e test with test for error rate ([@pipiland2612](https://github.com/pipiland2612) in [#6991](https://github.com/jaegertracing/jaeger/pull/6991)) #### ⚙️ Refactoring * [es] make `nestedtags` and `fieldtags` distinction at `corespanwriter` level ([@Manik2708](https://github.com/Manik2708) in [#6946](https://github.com/jaegertracing/jaeger/pull/6946)) * [refactor] change remote storage server to accept v2 factories ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7024](https://github.com/jaegertracing/jaeger/pull/7024)) * [refactor] consolidate v1/v2 writer factory adapter functionality ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7022](https://github.com/jaegertracing/jaeger/pull/7022)) * [refactor] consolidate v1/v2 reader factory adapter functionality ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7019](https://github.com/jaegertracing/jaeger/pull/7019)) ### 📊 UI Changes #### 🐞 Bug fixes, Minor Improvements * Reduce load time of trace page by deferring critical path tooltip ([@DamianMaslanka5](https://github.com/DamianMaslanka5) in [#2718](https://github.com/jaegertracing/jaeger-ui/pull/2718)) * Migrate copyicon tests ([@nojaf](https://github.com/nojaf) in [#2727](https://github.com/jaegertracing/jaeger-ui/pull/2727)) * [fix]: make reset icon in sdg more intuitive ([@hari45678](https://github.com/hari45678) in [#2723](https://github.com/jaegertracing/jaeger-ui/pull/2723)) * Migrate from enzyme to @testing-library/react in keyboardshortshelp ([@nojaf](https://github.com/nojaf) in [#2725](https://github.com/jaegertracing/jaeger-ui/pull/2725)) * Improve performance of trace statistics page when grouping by tag ([@DamianMaslanka5](https://github.com/DamianMaslanka5) in [#2724](https://github.com/jaegertracing/jaeger-ui/pull/2724)) * Improve performance of expanding and collapsing spans ([@DamianMaslanka5](https://github.com/DamianMaslanka5) in [#2722](https://github.com/jaegertracing/jaeger-ui/pull/2722)) * Improve performance of trace statistics ([@DamianMaslanka5](https://github.com/DamianMaslanka5) in [#2721](https://github.com/jaegertracing/jaeger-ui/pull/2721)) * [feat]: add context menu on node to dag ([@hari45678](https://github.com/hari45678) in [#2719](https://github.com/jaegertracing/jaeger-ui/pull/2719)) * Fix grouping on trace statistics page for tags ([@DamianMaslanka5](https://github.com/DamianMaslanka5) in [#2717](https://github.com/jaegertracing/jaeger-ui/pull/2717)) * Improve performance when expanding/collapsing span details ([@DamianMaslanka5](https://github.com/DamianMaslanka5) in [#2716](https://github.com/jaegertracing/jaeger-ui/pull/2716)) #### 👷 CI Improvements * Add ability to use typescript in tests ([@DamianMaslanka5](https://github.com/DamianMaslanka5) in [#2731](https://github.com/jaegertracing/jaeger-ui/pull/2731)) #### ⚙️ Refactoring * [es] make `nestedtags` and `fieldtags` distinction at `corespanwriter` level ([@Manik2708](https://github.com/Manik2708) in [#6946](https://github.com/jaegertracing/jaeger/pull/6946)) * [refactor] change remote storage server to accept v2 factories ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7024](https://github.com/jaegertracing/jaeger/pull/7024)) * [refactor] consolidate v1/v2 writer factory adapter functionality ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7022](https://github.com/jaegertracing/jaeger/pull/7022)) * [refactor] consolidate v1/v2 reader factory adapter functionality ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#7019](https://github.com/jaegertracing/jaeger/pull/7019)) v1.68.0 / v2.5.0 (2025-04-05) ------------------------------- ### Backend Changes #### ⛔ Breaking Changes * Remove sampling.strategies.bugfix-5270 flag and mark feature stable ([@yurishkuro](https://github.com/yurishkuro) in [#6872](https://github.com/jaegertracing/jaeger/pull/6872)) #### 🐞 Bug fixes, Minor Improvements * Minor fixes to release checklist generator ([@albertteoh](https://github.com/albertteoh) in [#6976](https://github.com/jaegertracing/jaeger/pull/6976)) * Support configuring prometheus.extra_query_parameters via cli ([@andreasgerstmayr](https://github.com/andreasgerstmayr) in [#6931](https://github.com/jaegertracing/jaeger/pull/6931)) * Cleanup legacy models ([@yurishkuro](https://github.com/yurishkuro) in [#6875](https://github.com/jaegertracing/jaeger/pull/6875)) * 🪦 remove agent code ([@yurishkuro](https://github.com/yurishkuro) in [#6868](https://github.com/jaegertracing/jaeger/pull/6868)) * [es] refactor `findtraces` and `gettrace` of spanreader to make them reusable for v2 apis ([@Manik2708](https://github.com/Manik2708) in [#6845](https://github.com/jaegertracing/jaeger/pull/6845)) * [es] add feature to stop legacy trace ids handling in spanreader ([@Manik2708](https://github.com/Manik2708) in [#6848](https://github.com/jaegertracing/jaeger/pull/6848)) * Feat: move pkg/testutils to internal/testutils ([@jinjiaKarl](https://github.com/jinjiaKarl) in [#6840](https://github.com/jaegertracing/jaeger/pull/6840)) * [fix] allow es-index-cleaner to delete indices based on current time ([@Asatyam](https://github.com/Asatyam) in [#6790](https://github.com/jaegertracing/jaeger/pull/6790)) * [chore] remove gogoproto annotations from `trace_storage.proto` and `dependency_storage.proto` ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6819](https://github.com/jaegertracing/jaeger/pull/6819)) #### 🚧 Experimental Features * [es][v2] add snapshot tests for spans conversion ([@Manik2708](https://github.com/Manik2708) in [#6970](https://github.com/jaegertracing/jaeger/pull/6970)) * [grpc][v2] implement grpc v2 factory ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6968](https://github.com/jaegertracing/jaeger/pull/6968)) * [grpc][v2] implement `findtraces` call in grpc reader for remote storage api v2 ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6962](https://github.com/jaegertracing/jaeger/pull/6962)) * [grpc][v2] implement `gettraces` call in grpc reader for remote storage api v2 ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6857](https://github.com/jaegertracing/jaeger/pull/6857)) * [es][v2] refactor `from_dbmodel` and `to_dbmodel` to accept and return db spans ([@Manik2708](https://github.com/Manik2708) in [#6934](https://github.com/jaegertracing/jaeger/pull/6934)) * [grpc][v2] implement v2 grpc dependency reader ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6933](https://github.com/jaegertracing/jaeger/pull/6933)) * [es][v2] copy jaeger<->otlp translator from otel contrib ([@Manik2708](https://github.com/Manik2708) in [#6923](https://github.com/jaegertracing/jaeger/pull/6923)) * [grpc][v2] implement v2 grpc trace writer ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6919](https://github.com/jaegertracing/jaeger/pull/6919)) * [grpc][v2] implement `findtraceids` call in grpc reader for remote storage api v2 ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6858](https://github.com/jaegertracing/jaeger/pull/6858)) * [grpc][v2] implement `getoperations` call in grpc reader for remote storage api v2 ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6843](https://github.com/jaegertracing/jaeger/pull/6843)) * [grpc][v2] implement `getservices` call in grpc reader for remote storage api v2 ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6829](https://github.com/jaegertracing/jaeger/pull/6829)) * [refactor] return chunk of traces from remote storage api v2 ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6830](https://github.com/jaegertracing/jaeger/pull/6830)) #### 👷 CI Improvements * [all-in-one] avoid multi-arch builds in merge queue (#6880) ([@sAchin-680](https://github.com/sAchin-680) in [#6882](https://github.com/jaegertracing/jaeger/pull/6882)) * Instruct renovate to pin github action hashes ([@yurishkuro](https://github.com/yurishkuro) in [#6860](https://github.com/jaegertracing/jaeger/pull/6860)) #### ⚙️ Refactoring * Move model/json/model.go to internal/uimodel/converter/v1 ([@pipiland2612](https://github.com/pipiland2612) in [#6973](https://github.com/jaegertracing/jaeger/pull/6973)) * Add usetesting linter and fix lint issues (#6892) ([@anurag-rajawat](https://github.com/anurag-rajawat) in [#6972](https://github.com/jaegertracing/jaeger/pull/6972)) * Delete empty pkg package ([@pipiland2612](https://github.com/pipiland2612) in [#6967](https://github.com/jaegertracing/jaeger/pull/6967)) * Move pkg/otelsemconv to internal/telemetry/otelsemconv ([@pipiland2612](https://github.com/pipiland2612) in [#6961](https://github.com/jaegertracing/jaeger/pull/6961)) * Move pkg/cassandra to internal/storage/cassandra ([@pipiland2612](https://github.com/pipiland2612) in [#6960](https://github.com/jaegertracing/jaeger/pull/6960)) * Move pkg/adjuster to cmd/query/app/querysvc/internal/adjuster ([@pipiland2612](https://github.com/pipiland2612) in [#6956](https://github.com/jaegertracing/jaeger/pull/6956)) * Remove package pkg/netutils ([@pipiland2612](https://github.com/pipiland2612) in [#6955](https://github.com/jaegertracing/jaeger/pull/6955)) * [refactor] remove `traceschunk` type and stream otlp traces directly ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6954](https://github.com/jaegertracing/jaeger/pull/6954)) * [es] remove pointer signatures from `fromdbmodel` and `todbmodel` ([@Manik2708](https://github.com/Manik2708) in [#6942](https://github.com/jaegertracing/jaeger/pull/6942)) * Move proto-gen to internal ([@yurishkuro](https://github.com/yurishkuro) in [#6941](https://github.com/jaegertracing/jaeger/pull/6941)) * Move pkg/es to internal/storage/elasticsearch ([@danish9039](https://github.com/danish9039) in [#6937](https://github.com/jaegertracing/jaeger/pull/6937)) * Move pkg/distributedlock to internal/distributedlock ([@danish9039](https://github.com/danish9039) in [#6903](https://github.com/jaegertracing/jaeger/pull/6903)) * Move pkg/httpmetrics to internal/httpmetrics ([@danish9039](https://github.com/danish9039) in [#6905](https://github.com/jaegertracing/jaeger/pull/6905)) * Move pkg/{gogocodec,httpfs,bearertoken,boundqueue} to internal ([@sAchin-680](https://github.com/sAchin-680) in [#6896](https://github.com/jaegertracing/jaeger/pull/6896)) * Move pkg/metrics to internal/metrics ([@danish9039](https://github.com/danish9039) in [#6901](https://github.com/jaegertracing/jaeger/pull/6901)) * Move pkg/kafka to internal/kafka ([@danish9039](https://github.com/danish9039) in [#6908](https://github.com/jaegertracing/jaeger/pull/6908)) * Move pkg/prometheus to internal/config/promcfg ([@danish9039](https://github.com/danish9039) in [#6911](https://github.com/jaegertracing/jaeger/pull/6911)) * Move model/proto to internal/proto ([@danish9039](https://github.com/danish9039) in [#6918](https://github.com/jaegertracing/jaeger/pull/6918)) * [es] move db model out of `v1/elasticsearch/internal/spanstore/internal` ([@Manik2708](https://github.com/Manik2708) in [#6894](https://github.com/jaegertracing/jaeger/pull/6894)) * Move pkg/version to internal/version ([@danish9039](https://github.com/danish9039) in [#6913](https://github.com/jaegertracing/jaeger/pull/6913)) * Move model/converter to internal/converter ([@danish9039](https://github.com/danish9039) in [#6917](https://github.com/jaegertracing/jaeger/pull/6917)) * Move pkg/gzipfs to internal/gzipfs ([@sAchin-680](https://github.com/sAchin-680) in [#6897](https://github.com/jaegertracing/jaeger/pull/6897)) * Move pkg/jtracer to internal/jtracer ([@danish9039](https://github.com/danish9039) in [#6907](https://github.com/jaegertracing/jaeger/pull/6907)) * Move pkg/telemetry to internal/telemetry ([@danish9039](https://github.com/danish9039) in [#6912](https://github.com/jaegertracing/jaeger/pull/6912)) * Move pkg/fswatcher to internal/fswatcher ([@sAchin-680](https://github.com/sAchin-680) in [#6895](https://github.com/jaegertracing/jaeger/pull/6895)) * [es] separate the `corespanwriter` from `spanwriter` ([@Manik2708](https://github.com/Manik2708) in [#6883](https://github.com/jaegertracing/jaeger/pull/6883)) * Move pkg/config to internal/config ([@gentcod](https://github.com/gentcod) in [#6884](https://github.com/jaegertracing/jaeger/pull/6884)) * Move pkg/healthcheck to internal/healthcheck ([@danish9039](https://github.com/danish9039) in [#6888](https://github.com/jaegertracing/jaeger/pull/6888)) * Moved pkg/hostname to internal/hostname ([@danish9039](https://github.com/danish9039) in [#6886](https://github.com/jaegertracing/jaeger/pull/6886)) * Move pkg/recoveryhandler to internal/recoveryhandler ([@danish9039](https://github.com/danish9039) in [#6887](https://github.com/jaegertracing/jaeger/pull/6887)) * Move pkg/tenancy to internal/tenancy ([@danish9039](https://github.com/danish9039) in [#6889](https://github.com/jaegertracing/jaeger/pull/6889)) * Move pkg/normalizer to collector ([@danish9039](https://github.com/danish9039) in [#6877](https://github.com/jaegertracing/jaeger/pull/6877)) * Replace the use of model/converter/thrift/zipkin ([@shuraih775](https://github.com/shuraih775) in [#6879](https://github.com/jaegertracing/jaeger/pull/6879)) * [es] remove pointer signatures from `corespanreader` ([@Manik2708](https://github.com/Manik2708) in [#6874](https://github.com/jaegertracing/jaeger/pull/6874)) * [refactor] move interface to remove cmd/agent dependency ([@yurishkuro](https://github.com/yurishkuro) in [#6863](https://github.com/jaegertracing/jaeger/pull/6863)) * [agent] refactor udp server ([@yurishkuro](https://github.com/yurishkuro) in [#6852](https://github.com/jaegertracing/jaeger/pull/6852)) * [agent] remove unnecessary server interface ([@yurishkuro](https://github.com/yurishkuro) in [#6851](https://github.com/jaegertracing/jaeger/pull/6851)) * [es] refactor the `findtraceids` of spanreader to make them reusable for v2 apis ([@Manik2708](https://github.com/Manik2708) in [#6831](https://github.com/jaegertracing/jaeger/pull/6831)) * [es] refactor the `getoperations` and `getservices` of spanreader to make them reusable for v2 apis ([@Manik2708](https://github.com/Manik2708) in [#6828](https://github.com/jaegertracing/jaeger/pull/6828)) ### 📊 UI Changes #### ✨ New Features * Feat: change dag styling and add search functionality ([@hari45678](https://github.com/hari45678) in [#2710](https://github.com/jaegertracing/jaeger-ui/pull/2710)) #### 🐞 Bug fixes, Minor Improvements * Add sample graph data when in dev mode ([@hari45678](https://github.com/hari45678) in [#2698](https://github.com/jaegertracing/jaeger-ui/pull/2698)) * Add depth and layout controls and sfdp layout to dag view ([@hari45678](https://github.com/hari45678) in [#2691](https://github.com/jaegertracing/jaeger-ui/pull/2691)) * Add sfdp engine in @jaegertracing/plexus ([@hari45678](https://github.com/hari45678) in [#2690](https://github.com/jaegertracing/jaeger-ui/pull/2690)) * Add handling or error for invalid json formats and tests ([@rohitlohar45](https://github.com/rohitlohar45) in [#2689](https://github.com/jaegertracing/jaeger-ui/pull/2689)) v1.67.0 / v2.4.0 (2025-03-07) ------------------------------- ### Backend Changes #### ⛔ Breaking Changes * [query] drop support for shared grpc/http query server ports ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6695](https://github.com/jaegertracing/jaeger/pull/6695)) #### 🐞 Bug fixes, Minor Improvements * [es] refactor the es spanwriter to make it reusable for v2 apis ([@Manik2708](https://github.com/Manik2708) in [#6796](https://github.com/jaegertracing/jaeger/pull/6796)) * [refactor] move internal `tracesdata` type to package `jptrace` ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6809](https://github.com/jaegertracing/jaeger/pull/6809)) * Use empty slices instead of nil ([@zhengkezhou1](https://github.com/zhengkezhou1) in [#6799](https://github.com/jaegertracing/jaeger/pull/6799)) * [refactor] refactor `jptrace/attributes_tests.go` for readability ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6786](https://github.com/jaegertracing/jaeger/pull/6786)) * [refactor] converge v2 api with v2 remote storage api ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6784](https://github.com/jaegertracing/jaeger/pull/6784)) * Feat: enable configuration of hostnames for hotrod services ([@w-h-a](https://github.com/w-h-a) in [#6782](https://github.com/jaegertracing/jaeger/pull/6782)) * [refactor] change `tracequeryparams` to accept typed attributes ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6780](https://github.com/jaegertracing/jaeger/pull/6780)) * [refactor] decouple `tracequeryparams` from `query` in integration tests ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6779](https://github.com/jaegertracing/jaeger/pull/6779)) * [refactor] inline proto definiton of `keyvalue` from otel ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6775](https://github.com/jaegertracing/jaeger/pull/6775)) * [refactor] return start and end timestamps from findtraceids in v2 remote storage api ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6772](https://github.com/jaegertracing/jaeger/pull/6772)) * [refactor] return start and end timestamps from `findtraceids` in v2 api ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6770](https://github.com/jaegertracing/jaeger/pull/6770)) * Revert "add 'features' command to print available feature gates" ([@yurishkuro](https://github.com/yurishkuro) in [#6771](https://github.com/jaegertracing/jaeger/pull/6771)) * [remote-storage][v2] add complete idl for trace storage ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6737](https://github.com/jaegertracing/jaeger/pull/6737)) * [remote-storage][v2] add idl for dependency storage ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6738](https://github.com/jaegertracing/jaeger/pull/6738)) * [remote-storage][v2] add proto definition for `getservices` and `getoperations` rpc ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6736](https://github.com/jaegertracing/jaeger/pull/6736)) * Fix /qualitymetrics to return data in expected format ([@yurishkuro](https://github.com/yurishkuro) in [#6733](https://github.com/jaegertracing/jaeger/pull/6733)) * [remote-storage][v2] add proto definition for `gettraces` rpc ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6730](https://github.com/jaegertracing/jaeger/pull/6730)) * [bug][storage] make es-rollover idempotent by checking if the index or alias already exist ([@Manik2708](https://github.com/Manik2708) in [#6638](https://github.com/jaegertracing/jaeger/pull/6638)) * [refactor] use plain loops with iterators ([@yurishkuro](https://github.com/yurishkuro) in [#6722](https://github.com/jaegertracing/jaeger/pull/6722)) * Use stdlib iterators ([@yurishkuro](https://github.com/yurishkuro) in [#6714](https://github.com/jaegertracing/jaeger/pull/6714)) * Create a /quality-metrics endpoint ([@ADI-ROXX](https://github.com/ADI-ROXX) in [#6608](https://github.com/jaegertracing/jaeger/pull/6608)) * Move pkg/cache to internal ([@won-js](https://github.com/won-js) in [#6720](https://github.com/jaegertracing/jaeger/pull/6720)) * [storage] change storage extension to hold v2 factories ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6699](https://github.com/jaegertracing/jaeger/pull/6699)) * Fix go alpine version to 1.24.0 ([@yurishkuro](https://github.com/yurishkuro) in [#6713](https://github.com/jaegertracing/jaeger/pull/6713)) * [refactor] conditionally implement interfaces in v1adapter factory ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6710](https://github.com/jaegertracing/jaeger/pull/6710)) * [fix] revert changes to tracereader adapter ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6705](https://github.com/jaegertracing/jaeger/pull/6705)) * [refactor] conditionally implement interfaces in `v1adapter` ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6701](https://github.com/jaegertracing/jaeger/pull/6701)) * [refactor] use `gettracestorefactory` instead of `getstoragefactory` ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6696](https://github.com/jaegertracing/jaeger/pull/6696)) * [storage] add helper to storage extension for retrieving purger ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6694](https://github.com/jaegertracing/jaeger/pull/6694)) * Import nop receiver/exporter and add a sample query service config ([@danish9039](https://github.com/danish9039) in [#6687](https://github.com/jaegertracing/jaeger/pull/6687)) * [storage] add helper to storage extension for retrieving sampling store factory ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6689](https://github.com/jaegertracing/jaeger/pull/6689)) #### 👷 CI Improvements * [idl check] fetch tags ([@yurishkuro](https://github.com/yurishkuro) in [#6758](https://github.com/jaegertracing/jaeger/pull/6758)) * [test]: check for jaeger-idl version mismatch ([@ary82](https://github.com/ary82) in [#6753](https://github.com/jaegertracing/jaeger/pull/6753)) * Allow dependency-review workflow to run from merge queue ([@yurishkuro](https://github.com/yurishkuro) in [#6729](https://github.com/jaegertracing/jaeger/pull/6729)) * Do not run dco-check from merge queue ([@yurishkuro](https://github.com/yurishkuro) in [#6727](https://github.com/jaegertracing/jaeger/pull/6727)) * Do not run label check from merge queue ([@yurishkuro](https://github.com/yurishkuro) in [#6726](https://github.com/jaegertracing/jaeger/pull/6726)) * Allow ci workflows to run from merge queue ([@danish9039](https://github.com/danish9039) in [#6719](https://github.com/jaegertracing/jaeger/pull/6719)) ### 📊 UI Changes #### 🐞 Bug fixes, Minor Improvements * Replace react-vis with recharts ([@hari45678](https://github.com/hari45678) in [#2679](https://github.com/jaegertracing/jaeger-ui/pull/2679)) * Add config option to allow displaying full traceid ([@avinpy-255](https://github.com/avinpy-255) in [#2536](https://github.com/jaegertracing/jaeger-ui/pull/2536)) v1.66.0 / v2.3.0 (2025-02-03) ------------------------------- #### ⛔ Breaking Changes * [refactor] remove archive reader and writer from remote storage grpc handler ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6611](https://github.com/jaegertracing/jaeger/pull/6611)) * Delete grpc metricsqueryservice, metricsquery.proto and related code ([@yurishkuro](https://github.com/yurishkuro) in [#6616](https://github.com/jaegertracing/jaeger/pull/6616)) * [storage] remove distinction between primary and `archive` storage interfaces ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6567](https://github.com/jaegertracing/jaeger/pull/6567)) * [v2][query] create archive reader/writer using regular factory methods ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6519](https://github.com/jaegertracing/jaeger/pull/6519)) #### 🐞 Bug fixes, Minor Improvements * [fix] replace deprecated address field in service::telemetry ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6679](https://github.com/jaegertracing/jaeger/pull/6679)) * [fix] change metrics port in kafka ingester config to avoid conflict with collector ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6678](https://github.com/jaegertracing/jaeger/pull/6678)) * Update elasticsearch article link ([@timyip3](https://github.com/timyip3) in [#6662](https://github.com/jaegertracing/jaeger/pull/6662)) * [chore] move scylladb implementation to `docker-compose` ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6652](https://github.com/jaegertracing/jaeger/pull/6652)) * [fix] refactor archive storage initialization and remove error log ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6636](https://github.com/jaegertracing/jaeger/pull/6636)) * Update import paths for jaeger thrift files to use jaeger-idl ([@Nabil-Salah](https://github.com/Nabil-Salah) in [#6635](https://github.com/jaegertracing/jaeger/pull/6635)) * [v2][query] apply "max clock skew adjust" setting ([@dnaka91](https://github.com/dnaka91) in [#6566](https://github.com/jaegertracing/jaeger/pull/6566)) * Alias samping.thrift and clean thrift files ([@Nabil-Salah](https://github.com/Nabil-Salah) in [#6630](https://github.com/jaegertracing/jaeger/pull/6630)) * Fix(hotrod): include ca certificates for hotrod dockerfile ([@prashant-shahi](https://github.com/prashant-shahi) in [#6627](https://github.com/jaegertracing/jaeger/pull/6627)) * Replace all imports of jaeger/thrift-gen/* with jaeger-idl/thrift-gen/* ([@danish9039](https://github.com/danish9039) in [#6621](https://github.com/jaegertracing/jaeger/pull/6621)) * Redefine thrift-gen types as aliases to jaeger-idl ([@danish9039](https://github.com/danish9039) in [#6619](https://github.com/jaegertracing/jaeger/pull/6619)) * Add 'features' command to print available feature gates ([@ADI-ROXX](https://github.com/ADI-ROXX) in [#6542](https://github.com/jaegertracing/jaeger/pull/6542)) * Replace jaeger_image_tag with jaeger_version ([@ADI-ROXX](https://github.com/ADI-ROXX) in [#6614](https://github.com/jaegertracing/jaeger/pull/6614)) * Use jeager-idl/proto-gen/api_v2 ([@Nabil-Salah](https://github.com/Nabil-Salah) in [#6609](https://github.com/jaegertracing/jaeger/pull/6609)) * Additional model/ cleanup ([@yurishkuro](https://github.com/yurishkuro) in [#6610](https://github.com/jaegertracing/jaeger/pull/6610)) * Return 400 instead of 500 on incorrect otlp payload ([@ADI-ROXX](https://github.com/ADI-ROXX) in [#6599](https://github.com/jaegertracing/jaeger/pull/6599)) * Replace model imports with jaeger-idl ([@Nabil-Salah](https://github.com/Nabil-Salah) in [#6595](https://github.com/jaegertracing/jaeger/pull/6595)) * Redefine model/ and api_v2/ types as aliases to jaeger-idl/ types ([@Nabil-Salah](https://github.com/Nabil-Salah) in [#6602](https://github.com/jaegertracing/jaeger/pull/6602)) * Add example of es/os server_urls to configs ([@yurishkuro](https://github.com/yurishkuro) in [#6601](https://github.com/jaegertracing/jaeger/pull/6601)) * Sanitize cassandra version before use it ([@rubenvp8510](https://github.com/rubenvp8510) in [#6596](https://github.com/jaegertracing/jaeger/pull/6596)) * Feat: add esmapping-generator into jaeger binary ([@Rohanraj123](https://github.com/Rohanraj123) in [#6327](https://github.com/jaegertracing/jaeger/pull/6327)) * Add replication parameter to cassandra schema script ([@asimchoudhary](https://github.com/asimchoudhary) in [#6582](https://github.com/jaegertracing/jaeger/pull/6582)) * Exclude idl/ as a source of go code ([@yurishkuro](https://github.com/yurishkuro) in [#6591](https://github.com/jaegertracing/jaeger/pull/6591)) * Change model.tootelxxxid() to accept id argument ([@yurishkuro](https://github.com/yurishkuro) in [#6589](https://github.com/jaegertracing/jaeger/pull/6589)) * [refactor][storage][badger]refactored the prefilling of cache to reader ([@Manik2708](https://github.com/Manik2708) in [#6575](https://github.com/jaegertracing/jaeger/pull/6575)) * Move span.getsamplerparams out of model/ into sampling/aggregator ([@Nabil-Salah](https://github.com/Nabil-Salah) in [#6583](https://github.com/jaegertracing/jaeger/pull/6583)) * Remove logger parameter in adaptive/aggregator.go ([@Nabil-Salah](https://github.com/Nabil-Salah) in [#6586](https://github.com/jaegertracing/jaeger/pull/6586)) * Separate model parts into more independent pieces ([@yurishkuro](https://github.com/yurishkuro) in [#6581](https://github.com/jaegertracing/jaeger/pull/6581)) * [storage]generate mocks for dependency writer of v2 ([@Manik2708](https://github.com/Manik2708) in [#6576](https://github.com/jaegertracing/jaeger/pull/6576)) * [chore] remove unused method from grpc handler ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6580](https://github.com/jaegertracing/jaeger/pull/6580)) * Document usage of feature gates for breaking changes ([@yurishkuro](https://github.com/yurishkuro) in [#6568](https://github.com/jaegertracing/jaeger/pull/6568)) * [refactor] move sampling strategy providers to internal/sampling/samplingstrategy ([@ary82](https://github.com/ary82) in [#6561](https://github.com/jaegertracing/jaeger/pull/6561)) * [v2][storage] implement reverse adapter to translate v2 writer to v1 ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6555](https://github.com/jaegertracing/jaeger/pull/6555)) * [refactor] move sampling strategy interfaces to internal/sampling/strategy ([@ary82](https://github.com/ary82) in [#6547](https://github.com/jaegertracing/jaeger/pull/6547)) * Switch v1 receivers to use v2 write path ([@yurishkuro](https://github.com/yurishkuro) in [#6532](https://github.com/jaegertracing/jaeger/pull/6532)) * [refactor] move plugin/sampling/leaderelection to internal/leaderelection ([@ary82](https://github.com/ary82) in [#6546](https://github.com/jaegertracing/jaeger/pull/6546)) * [refactor] move sampling http handler to internal/sampling/http ([@ary82](https://github.com/ary82) in [#6545](https://github.com/jaegertracing/jaeger/pull/6545)) * [storage] remove dependency on archive flag in es reader ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6490](https://github.com/jaegertracing/jaeger/pull/6490)) * [refactor] move sampling grpc handler to internal/sampling/grpc ([@ary82](https://github.com/ary82) in [#6540](https://github.com/jaegertracing/jaeger/pull/6540)) * Correct references in cmd readme.md ([@jyoungs](https://github.com/jyoungs) in [#6539](https://github.com/jaegertracing/jaeger/pull/6539)) * Use jaeger-v2 by default in hotrod and monitor examples ([@zzzk1](https://github.com/zzzk1) in [#6523](https://github.com/jaegertracing/jaeger/pull/6523)) * Pass context through span processors ([@yurishkuro](https://github.com/yurishkuro) in [#6534](https://github.com/jaegertracing/jaeger/pull/6534)) #### 👷 CI Improvements * Upgrade storage integration test: use v2 archive readerwriter ([@ekefan](https://github.com/ekefan) in [#6489](https://github.com/jaegertracing/jaeger/pull/6489)) * [chore][tests] clean up integration tests to remove archive reader / writer ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6625](https://github.com/jaegertracing/jaeger/pull/6625)) * Bump jaeger-idl ([@yurishkuro](https://github.com/yurishkuro) in [#6569](https://github.com/jaegertracing/jaeger/pull/6569)) * [storage]upgraded integration tests to use dependency writer of storage_v2 ([@Manik2708](https://github.com/Manik2708) in [#6559](https://github.com/jaegertracing/jaeger/pull/6559)) * [ci] fix binary-size-check workflow ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#6552](https://github.com/jaegertracing/jaeger/pull/6552)) * [ci] scrape and verify metrics at the end of e2e tests ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#6330](https://github.com/jaegertracing/jaeger/pull/6330)) * [ci] add workflow to guard against increases in the binary size ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#6529](https://github.com/jaegertracing/jaeger/pull/6529)) ### 📊 UI Changes #### 🐞 Bug fixes, Minor Improvements * Remove defaultprops from minimap.tsx ([@ADI-ROXX](https://github.com/ADI-ROXX) in [#2615](https://github.com/jaegertracing/jaeger-ui/pull/2615)) * Remove defaultprops from scatterplot.jsx ([@ADI-ROXX](https://github.com/ADI-ROXX) in [#2618](https://github.com/jaegertracing/jaeger-ui/pull/2618)) * Migrate empasizednode from class based to function based component ([@AdiIsHappy](https://github.com/AdiIsHappy) in [#2638](https://github.com/jaegertracing/jaeger-ui/pull/2638)) * Remove defaultprops from accordiantext.tsx ([@ADI-ROXX](https://github.com/ADI-ROXX) in [#2612](https://github.com/jaegertracing/jaeger-ui/pull/2612)) * Remove defaultprops from ticks.tsx ([@ADI-ROXX](https://github.com/ADI-ROXX) in [#2617](https://github.com/jaegertracing/jaeger-ui/pull/2617)) * Remove defaultprops from timelinerow.tsx ([@ADI-ROXX](https://github.com/ADI-ROXX) in [#2616](https://github.com/jaegertracing/jaeger-ui/pull/2616)) * Remove defaultprops from traceheader.jsx ([@ADI-ROXX](https://github.com/ADI-ROXX) in [#2620](https://github.com/jaegertracing/jaeger-ui/pull/2620)) * Remove defaultprops from accordianlogs.tsx ([@ADI-ROXX](https://github.com/ADI-ROXX) in [#2613](https://github.com/jaegertracing/jaeger-ui/pull/2613)) * Remove defaultprops fromt breakabletext.tsx ([@ADI-ROXX](https://github.com/ADI-ROXX) in [#2611](https://github.com/jaegertracing/jaeger-ui/pull/2611)) * Remove defaultprops from errormessage & newwindowicon ([@ADI-ROXX](https://github.com/ADI-ROXX) in [#2609](https://github.com/jaegertracing/jaeger-ui/pull/2609)) * [loadingindicator]: replace defaultprops with destructuring ([@its-me-abhishek](https://github.com/its-me-abhishek) in [#2601](https://github.com/jaegertracing/jaeger-ui/pull/2601)) * [fix]: disable submit button on invalid minduration or maxduration ([@hari45678](https://github.com/hari45678) in [#2600](https://github.com/jaegertracing/jaeger-ui/pull/2600)) * [deps]: remove dependency on redux-form ([@hari45678](https://github.com/hari45678) in [#2593](https://github.com/jaegertracing/jaeger-ui/pull/2593)) * [fix]: remove redux-form dependency from sort selector ([@hari45678](https://github.com/hari45678) in [#2569](https://github.com/jaegertracing/jaeger-ui/pull/2569)) * [revert]: revert redux and react-redux dependency upgrades ([@yurishkuro](https://github.com/yurishkuro) in [#2577](https://github.com/jaegertracing/jaeger-ui/pull/2577)) * Fix: deep clone trace data for consistency ([@Zen-cronic](https://github.com/Zen-cronic) in [#2571](https://github.com/jaegertracing/jaeger-ui/pull/2571)) * [fix]: remove redux-form dependency from monitor page ([@hari45678](https://github.com/hari45678) in [#2562](https://github.com/jaegertracing/jaeger-ui/pull/2562)) * Fix tracediff graph pan and zoom issue ([@anshgoyalevil](https://github.com/anshgoyalevil) in [#2566](https://github.com/jaegertracing/jaeger-ui/pull/2566)) #### 👷 CI Improvements * Remove unused matrix from codeql workflow ([@yurishkuro](https://github.com/yurishkuro) in [#2635](https://github.com/jaegertracing/jaeger-ui/pull/2635)) * Rename dco->dco check ([@yurishkuro](https://github.com/yurishkuro) in [#2633](https://github.com/jaegertracing/jaeger-ui/pull/2633)) * Add fake dco check for merge queue events ([@yurishkuro](https://github.com/yurishkuro) in [#2632](https://github.com/jaegertracing/jaeger-ui/pull/2632)) * Don't run label check in merge queue ([@yurishkuro](https://github.com/yurishkuro) in [#2631](https://github.com/jaegertracing/jaeger-ui/pull/2631)) * Don't run codeql from merge queue ([@yurishkuro](https://github.com/yurishkuro) in [#2630](https://github.com/jaegertracing/jaeger-ui/pull/2630)) * Enable workflows to run in merge queue ([@yurishkuro](https://github.com/yurishkuro) in [#2629](https://github.com/jaegertracing/jaeger-ui/pull/2629)) * [ci] fix cache resolution and syntax in check binary size workflow ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#2591](https://github.com/jaegertracing/jaeger-ui/pull/2591)) * [ci]: add workflow to guard against growing bundle size ([@hari45678](https://github.com/hari45678) in [#2586](https://github.com/jaegertracing/jaeger-ui/pull/2586)) v1.65.0 / v2.2.0 (2025-01-08) ------------------------------- ### Backend Changes #### ⛔ Breaking Changes * [sampling] inherit default per-operation strategies ([@yurishkuro](https://github.com/yurishkuro) in [#6441](https://github.com/jaegertracing/jaeger/pull/6441)) * [query] enable trace adjusters in api_v2 and api_v3 handlers ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6423](https://github.com/jaegertracing/jaeger/pull/6423)) #### ✨ New Features * [feat] add time window for gettrace in span store interface ([@rim99](https://github.com/rim99) in [#6242](https://github.com/jaegertracing/jaeger/pull/6242)) #### 🐞 Bug fixes, Minor Improvements * Return errors from span processor creation ([@yurishkuro](https://github.com/yurishkuro) in [#6488](https://github.com/jaegertracing/jaeger/pull/6488)) * Change collector's queue to use generics ([@yurishkuro](https://github.com/yurishkuro) in [#6486](https://github.com/jaegertracing/jaeger/pull/6486)) * Refactor collector pipeline to allow v1/v2 data model ([@yurishkuro](https://github.com/yurishkuro) in [#6484](https://github.com/jaegertracing/jaeger/pull/6484)) * [v2][storage] implement reverse adapter to translate v2 storage api to v1 ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6485](https://github.com/jaegertracing/jaeger/pull/6485)) * [refractor] remove dependency on tlscfg.options ([@Saumya40-codes](https://github.com/Saumya40-codes) in [#6478](https://github.com/jaegertracing/jaeger/pull/6478)) * [query] update v1 query service to check for adapter at construction ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6482](https://github.com/jaegertracing/jaeger/pull/6482)) * [api_v3][query] change api_v3 http handler to use v2 query service ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6459](https://github.com/jaegertracing/jaeger/pull/6459)) * [api_v3][query] change api_v3 grpc handler to use query service v2 ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6452](https://github.com/jaegertracing/jaeger/pull/6452)) * [v2][storage] create v2 query service to operate on otlp data model ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6343](https://github.com/jaegertracing/jaeger/pull/6343)) * Support sampling file reload interval ([@yurishkuro](https://github.com/yurishkuro) in [#6440](https://github.com/jaegertracing/jaeger/pull/6440)) * [jptrace] add spaniter helper function ([@yurishkuro](https://github.com/yurishkuro) in [#6407](https://github.com/jaegertracing/jaeger/pull/6407)) * [refactor][query] propagate `rawtraces` flag to query service ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6438](https://github.com/jaegertracing/jaeger/pull/6438)) * [v1][adjuster] change v1 adjuster interface to not return error and modify trace in place ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6426](https://github.com/jaegertracing/jaeger/pull/6426)) * [chore] move es/spanstore/dbmodel to internal directory ([@zzzk1](https://github.com/zzzk1) in [#6428](https://github.com/jaegertracing/jaeger/pull/6428)) * [refactor] move model<->otlp translation from `jptrace` to `v1adapter` ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6414](https://github.com/jaegertracing/jaeger/pull/6414)) * Enable udp ports in all-in-one ([@yurishkuro](https://github.com/yurishkuro) in [#6413](https://github.com/jaegertracing/jaeger/pull/6413)) * [refactor] introduce helper for creating map of spans ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6406](https://github.com/jaegertracing/jaeger/pull/6406)) * [fix] fix incorrect usage of iter package in aggregator ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6403](https://github.com/jaegertracing/jaeger/pull/6403)) * [v2][query] implement helper to buffer sequence of traces ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6401](https://github.com/jaegertracing/jaeger/pull/6401)) * [v2][adjuster] implement model to otlp translator with post processing ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6397](https://github.com/jaegertracing/jaeger/pull/6397)) * [v2][adjuster] implement function to get standard adjusters to operate on otlp format ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6396](https://github.com/jaegertracing/jaeger/pull/6396)) * [v2][adjuster] implement otlp to model translator with post processing ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6394](https://github.com/jaegertracing/jaeger/pull/6394)) * [v2][adjuster] implement adjuster for correct timestamps for clockskew ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6392](https://github.com/jaegertracing/jaeger/pull/6392)) * [v2][adjuster] implement adjuster for deduplicating spans ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6391](https://github.com/jaegertracing/jaeger/pull/6391)) * Add optional time window for gettrace & searchtrace of http_handler ([@rim99](https://github.com/rim99) in [#6159](https://github.com/jaegertracing/jaeger/pull/6159)) * [v2][adjuster] implement adjuster for sorting attributes and events ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6389](https://github.com/jaegertracing/jaeger/pull/6389)) * Support extra custom query parameters in requests to prometheus backend ([@akstron](https://github.com/akstron) in [#6360](https://github.com/jaegertracing/jaeger/pull/6360)) * [v2][adjuster] remove error return from adjuster interface ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6383](https://github.com/jaegertracing/jaeger/pull/6383)) * [fix][query] filter out tracing for access to static ui assets ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6374](https://github.com/jaegertracing/jaeger/pull/6374)) * [v2][adjuster] implement span id uniquifier adjuster to operate on otlp data model ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6367](https://github.com/jaegertracing/jaeger/pull/6367)) * [api_v3] add time window for gettrace http_gateway ([@rim99](https://github.com/rim99) in [#6372](https://github.com/jaegertracing/jaeger/pull/6372)) * [v2][adjuster] add warning to span links adjuster ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6381](https://github.com/jaegertracing/jaeger/pull/6381)) * Feat: add time window for gettrace of anonymizer ([@rim99](https://github.com/rim99) in [#6368](https://github.com/jaegertracing/jaeger/pull/6368)) * [v2][adjuster] rework adjuster interface and refactor adjusters to return implemented struct ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6362](https://github.com/jaegertracing/jaeger/pull/6362)) * [v2][adjuster] implement otel attribute adjuster to operate on otlp data model ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6358](https://github.com/jaegertracing/jaeger/pull/6358)) * Respond correctly to stream send error ([@yurishkuro](https://github.com/yurishkuro) in [#6357](https://github.com/jaegertracing/jaeger/pull/6357)) * [v2][adjuster] implement ip attribute adjuster to operate on otlp data model ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6355](https://github.com/jaegertracing/jaeger/pull/6355)) * Remove tls loading and replace with otel configtls ([@yurishkuro](https://github.com/yurishkuro) in [#6345](https://github.com/jaegertracing/jaeger/pull/6345)) * [jaeger][v2] implement span links adjuster to operate on otlp data model ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6354](https://github.com/jaegertracing/jaeger/pull/6354)) * [remote-storage] use otel helper instead of tlscfg ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#6351](https://github.com/jaegertracing/jaeger/pull/6351)) * Add go leak check for badgerstore, grpc and memstore e2e tests ([@Manik2708](https://github.com/Manik2708) in [#6347](https://github.com/jaegertracing/jaeger/pull/6347)) * [v2][query] add interface for adjuster to operate on otlp data format ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6346](https://github.com/jaegertracing/jaeger/pull/6346)) #### 🚧 Experimental Features * Change storage_v2 gettrace to gettraces plural ([@yurishkuro](https://github.com/yurishkuro) in [#6361](https://github.com/jaegertracing/jaeger/pull/6361)) * Change storage v2 api to use streaming ([@yurishkuro](https://github.com/yurishkuro) in [#6359](https://github.com/jaegertracing/jaeger/pull/6359)) #### 👷 CI Improvements * Upgrade storage integration tests: `dependencyreader` to v2 ([@zzzk1](https://github.com/zzzk1) in [#6477](https://github.com/jaegertracing/jaeger/pull/6477)) * Move remaining util scripts ([@danish9039](https://github.com/danish9039) in [#6472](https://github.com/jaegertracing/jaeger/pull/6472)) * Move lint scripts to scripts/lint ([@danish9039](https://github.com/danish9039) in [#6449](https://github.com/jaegertracing/jaeger/pull/6449)) * Move util scripts to scripts/util ([@danish9039](https://github.com/danish9039) in [#6463](https://github.com/jaegertracing/jaeger/pull/6463)) * Upgrade storage integration test: use `tracewriter` ([@ekefan](https://github.com/ekefan) in [#6437](https://github.com/jaegertracing/jaeger/pull/6437)) * Move e2e scripts to scripts/e2e ([@danish9039](https://github.com/danish9039) in [#6448](https://github.com/jaegertracing/jaeger/pull/6448)) * Move build scripts under scripts/build/ ([@danish9039](https://github.com/danish9039) in [#6446](https://github.com/jaegertracing/jaeger/pull/6446)) * Replace apiv2 with apiv3 client in e2e tests ([@yurishkuro](https://github.com/yurishkuro) in [#6424](https://github.com/jaegertracing/jaeger/pull/6424)) * Do not test with kafka 2.x ([@yurishkuro](https://github.com/yurishkuro) in [#6427](https://github.com/jaegertracing/jaeger/pull/6427)) * Upgrade storage integration test to v2 trace reader ([@ekefan](https://github.com/ekefan) in [#6388](https://github.com/jaegertracing/jaeger/pull/6388)) * Enhance kafka integration tests to support multiple kafka versions ([@zzzk1](https://github.com/zzzk1) in [#6400](https://github.com/jaegertracing/jaeger/pull/6400)) * [fix] fix test expectations for translator to avoid depending on order ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6404](https://github.com/jaegertracing/jaeger/pull/6404)) ### 📊 UI Changes #### 🐞 Bug fixes, Minor Improvements * [clean-up]: remove deprecated plexus/directedgraph ([@hari45678](https://github.com/hari45678) in [#2548](https://github.com/jaegertracing/jaeger-ui/pull/2548)) * [fix]: make plexus demo work again ([@hari45678](https://github.com/hari45678) in [#2538](https://github.com/jaegertracing/jaeger-ui/pull/2538)) * Upgrade from raven-js to sentry/browser ([@avinpy-255](https://github.com/avinpy-255) in [#2509](https://github.com/jaegertracing/jaeger-ui/pull/2509)) v1.64.0 / v2.1.0 (2024-12-06) ------------------------------- ### Backend Changes #### ⛔ Breaking Changes * [metrics][storage] move metrics reader decorator to metrics storage factory ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6287](https://github.com/jaegertracing/jaeger/pull/6287)) * [v2][storage] move span reader decorator to storage factories ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6280](https://github.com/jaegertracing/jaeger/pull/6280)) #### ✨ New Features * [v2][storage] implement read path for v2 storage interface ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6170](https://github.com/jaegertracing/jaeger/pull/6170)) * Create cassandra db schema on session initialization ([@akstron](https://github.com/akstron) in [#5922](https://github.com/jaegertracing/jaeger/pull/5922)) #### 🐞 Bug fixes, Minor Improvements * Fix password in integration test ([@akstron](https://github.com/akstron) in [#6284](https://github.com/jaegertracing/jaeger/pull/6284)) * [cassandra] change compaction window default to 2hrs ([@yurishkuro](https://github.com/yurishkuro) in [#6282](https://github.com/jaegertracing/jaeger/pull/6282)) * Improve telemetry.settings ([@yurishkuro](https://github.com/yurishkuro) in [#6275](https://github.com/jaegertracing/jaeger/pull/6275)) * [kafka] otel helper instead of tlscfg package ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#6270](https://github.com/jaegertracing/jaeger/pull/6270)) * [refactor] fix package misspelling: telemetery->telemetry ([@yurishkuro](https://github.com/yurishkuro) in [#6269](https://github.com/jaegertracing/jaeger/pull/6269)) * [prometheus] use otel helper instead of tlscfg package ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#6266](https://github.com/jaegertracing/jaeger/pull/6266)) * [fix] use metrics decorator around metricstorage ([@yurishkuro](https://github.com/yurishkuro) in [#6262](https://github.com/jaegertracing/jaeger/pull/6262)) * Use real metrics factory instead of nullfactory ([@yurishkuro](https://github.com/yurishkuro) in [#6261](https://github.com/jaegertracing/jaeger/pull/6261)) * [v2] use only version number for buildinfo ([@yurishkuro](https://github.com/yurishkuro) in [#6260](https://github.com/jaegertracing/jaeger/pull/6260)) * [refactor] move spm v2 config to cmd/jaeger/ with all other configs ([@yurishkuro](https://github.com/yurishkuro) in [#6256](https://github.com/jaegertracing/jaeger/pull/6256)) * [es-index-cleaner] use otel helper instead of tlscfg ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#6259](https://github.com/jaegertracing/jaeger/pull/6259)) * [api_v2] change time fields in archivetracerequest to non-nullable ([@rim99](https://github.com/rim99) in [#6251](https://github.com/jaegertracing/jaeger/pull/6251)) * [es-rollover] use otel helpers for tls config instead of tlscfg ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#6238](https://github.com/jaegertracing/jaeger/pull/6238)) * Enable usestdlibvars linter ([@mmorel-35](https://github.com/mmorel-35) in [#6249](https://github.com/jaegertracing/jaeger/pull/6249)) * [storage_v1] add time window to gettracerequest ([@rim99](https://github.com/rim99) in [#6244](https://github.com/jaegertracing/jaeger/pull/6244)) * [fix][query] fix misconfiguration in tls settings from using otel http helper ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6239](https://github.com/jaegertracing/jaeger/pull/6239)) * Auto-generate gogo annotations for api_v3 ([@yurishkuro](https://github.com/yurishkuro) in [#6233](https://github.com/jaegertracing/jaeger/pull/6233)) * Use confighttp in expvar extension ([@yurishkuro](https://github.com/yurishkuro) in [#6227](https://github.com/jaegertracing/jaeger/pull/6227)) * Parameterize listen host and override when in container ([@yurishkuro](https://github.com/yurishkuro) in [#6231](https://github.com/jaegertracing/jaeger/pull/6231)) * Remove 0.0.0.0 overrides in hotrod ci ([@yurishkuro](https://github.com/yurishkuro) in [#6226](https://github.com/jaegertracing/jaeger/pull/6226)) * [storage][v2] add reader adapter that just exposes the underlying v1 reader ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6221](https://github.com/jaegertracing/jaeger/pull/6221)) * Change start/end time in gettrace request to not be pointers ([@yurishkuro](https://github.com/yurishkuro) in [#6218](https://github.com/jaegertracing/jaeger/pull/6218)) * Pass real meterprovider to components ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#6173](https://github.com/jaegertracing/jaeger/pull/6173)) * [v2] update versions in readme ([@yurishkuro](https://github.com/yurishkuro) in [#6206](https://github.com/jaegertracing/jaeger/pull/6206)) * Fix: testcreatecollectorproxy unit test failing on go-tip ([@Saumya40-codes](https://github.com/Saumya40-codes) in [#6204](https://github.com/jaegertracing/jaeger/pull/6204)) * Respect environment variables when creating internal tracer ([@akstron](https://github.com/akstron) in [#6179](https://github.com/jaegertracing/jaeger/pull/6179)) #### 🚧 Experimental Features * [v2]add script for metrics markdown table ([@vvs-personalstash](https://github.com/vvs-personalstash) in [#5941](https://github.com/jaegertracing/jaeger/pull/5941)) #### 👷 CI Improvements * Allow using different container runtime ([@rim99](https://github.com/rim99) in [#6247](https://github.com/jaegertracing/jaeger/pull/6247)) * K8s integration test for hotrod ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#6155](https://github.com/jaegertracing/jaeger/pull/6155)) * Pass username/password to cassandra docker-compose health check ([@akstron](https://github.com/akstron) in [#6214](https://github.com/jaegertracing/jaeger/pull/6214)) * [fix][ci] change the prometheus healthcheck endpoint ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6217](https://github.com/jaegertracing/jaeger/pull/6217)) ### 📊 UI Changes #### 🐞 Bug fixes, Minor Improvements * Add new formatting function "add" ([@drewcorlin1](https://github.com/drewcorlin1) in [#2507](https://github.com/jaegertracing/jaeger-ui/pull/2507)) * Add pad_start link formatting function #2505 ([@drewcorlin1](https://github.com/drewcorlin1) in [#2504](https://github.com/jaegertracing/jaeger-ui/pull/2504)) * Allow formatting link parameter values as iso date #2487 ([@drewcorlin1](https://github.com/drewcorlin1) in [#2501](https://github.com/jaegertracing/jaeger-ui/pull/2501)) v1.63.0 / v2.0.0 (2024-11-10) ------------------------------- Jaeger v2 is here! 🎉 🎉 🎉 ### Backend Changes #### ⛔ Breaking Changes * Remove jaeger-agent from distributions ([@yurishkuro](https://github.com/yurishkuro) in [#6081](https://github.com/jaegertracing/jaeger/pull/6081)) #### 🐞 Bug fixes, Minor Improvements * Fix possible null pointer deference ([@vaidikcode](https://github.com/vaidikcode) in [#6184](https://github.com/jaegertracing/jaeger/pull/6184)) * Chore: enable all rules of perfsprint linter ([@mmorel-35](https://github.com/mmorel-35) in [#6164](https://github.com/jaegertracing/jaeger/pull/6164)) * Chore: enable err-error and errorf rules from perfsprint linter ([@mmorel-35](https://github.com/mmorel-35) in [#6160](https://github.com/jaegertracing/jaeger/pull/6160)) * [query] move trace handler to server level ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6147](https://github.com/jaegertracing/jaeger/pull/6147)) * [fix][query] remove bifurcation for grpc query server ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6145](https://github.com/jaegertracing/jaeger/pull/6145)) * [jaeger-v2] add hotrod integration test for jaeger-v2 ([@Saumya40-codes](https://github.com/Saumya40-codes) in [#6138](https://github.com/jaegertracing/jaeger/pull/6138)) * [query] use otel's helpers for http server ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6121](https://github.com/jaegertracing/jaeger/pull/6121)) * Use grpc interceptors instead of explicit context wrappers ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#6133](https://github.com/jaegertracing/jaeger/pull/6133)) * Fix command in v2 example ([@haoqixu](https://github.com/haoqixu) in [#6134](https://github.com/jaegertracing/jaeger/pull/6134)) * Fix span deduplication via correct ordering of adjusters ([@cdanis](https://github.com/cdanis) in [#6116](https://github.com/jaegertracing/jaeger/pull/6116)) * Move all query service http handlers into one function ([@yurishkuro](https://github.com/yurishkuro) in [#6128](https://github.com/jaegertracing/jaeger/pull/6128)) * [fix][grpc] disable tracing in grpc storage writer clients ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6125](https://github.com/jaegertracing/jaeger/pull/6125)) * Feat: automatically publish readme to docker hub ([@inosmeet](https://github.com/inosmeet) in [#6118](https://github.com/jaegertracing/jaeger/pull/6118)) * Use grpc interceptors for bearer token ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#6063](https://github.com/jaegertracing/jaeger/pull/6063)) * [fix][query] correct query server legacy condition ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6120](https://github.com/jaegertracing/jaeger/pull/6120)) * [query] use otel's helpers for grpc server ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6055](https://github.com/jaegertracing/jaeger/pull/6055)) * Enable lint rule: import-shadowing ([@inosmeet](https://github.com/inosmeet) in [#6102](https://github.com/jaegertracing/jaeger/pull/6102)) * [refractor] switch to enums for es mappings ([@Saumya40-codes](https://github.com/Saumya40-codes) in [#6091](https://github.com/jaegertracing/jaeger/pull/6091)) * Fix rebuild-ui.sh script ([@andreasgerstmayr](https://github.com/andreasgerstmayr) in [#6098](https://github.com/jaegertracing/jaeger/pull/6098)) * Use otel component host instead of no op host for prod code ([@chahatsagarmain](https://github.com/chahatsagarmain) in [#6085](https://github.com/jaegertracing/jaeger/pull/6085)) * [cassandra] prevent fallback to old schema for operation names table in case of db issues ([@arunvelsriram](https://github.com/arunvelsriram) in [#6061](https://github.com/jaegertracing/jaeger/pull/6061)) #### 🚧 Experimental Features * Add otlp json support for kafka e2e integration tests ([@joeyyy09](https://github.com/joeyyy09) in [#5935](https://github.com/jaegertracing/jaeger/pull/5935)) * [v2] add es config comments ([@yurishkuro](https://github.com/yurishkuro) in [#6110](https://github.com/jaegertracing/jaeger/pull/6110)) * [chore][docs] add documentation to elasticsearch configuration ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6103](https://github.com/jaegertracing/jaeger/pull/6103)) * [jaeger-v2] refactor elasticsearch/opensearch configurations to have more logical groupings ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6090](https://github.com/jaegertracing/jaeger/pull/6090)) * [jaeger-v2] implement utf-8 sanitizer for otlp ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6078](https://github.com/jaegertracing/jaeger/pull/6078)) * [jaeger-v2] migrate elasticsearch/opensearch to use otel's tls configuration ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6079](https://github.com/jaegertracing/jaeger/pull/6079)) * [jaeger-v2] enable queueing configuration in storage exporter ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6080](https://github.com/jaegertracing/jaeger/pull/6080)) * [jaeger-v2] implement empty service name sanitizer for otlp ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6077](https://github.com/jaegertracing/jaeger/pull/6077)) * [jaeger-v2] refactor elasticsearch/opensearch storage configurations ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6060](https://github.com/jaegertracing/jaeger/pull/6060)) #### 👷 CI Improvements * [v2] use health check in grpc e2e test ([@yurishkuro](https://github.com/yurishkuro) in [#6113](https://github.com/jaegertracing/jaeger/pull/6113)) * Update node.js github action to use npm lockfile, switch to latest jaeger ui ([@andreasgerstmayr](https://github.com/andreasgerstmayr) in [#6074](https://github.com/jaegertracing/jaeger/pull/6074)) ### 📊 UI Changes #### 🐞 Bug fixes, Minor Improvements * Migrate from yarn v1 to npm ([@andreasgerstmayr](https://github.com/andreasgerstmayr) in [#2462](https://github.com/jaegertracing/jaeger-ui/pull/2462)) #### 👷 CI Improvements * Run s390x build on push to main only ([@andreasgerstmayr](https://github.com/andreasgerstmayr) in [#2481](https://github.com/jaegertracing/jaeger-ui/pull/2481)) 1.62.0 / 2.0.0-rc2 (2024-10-06) ------------------------------- ### Backend Changes #### ⛔ Breaking Changes * [query] change http and tls server configurations to use otel configurations ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6023](https://github.com/jaegertracing/jaeger/pull/6023)) * [fix][spm]: change default metrics namespace to match new default in spanmetricsconnector ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6007](https://github.com/jaegertracing/jaeger/pull/6007)) #### 🐞 Bug fixes, Minor Improvements * [grpc storage]: propagate tenant to grpc backend ([@frzifus](https://github.com/frzifus) in [#6030](https://github.com/jaegertracing/jaeger/pull/6030)) * [feat] deduplicate spans based on their hashcode ([@cdanis](https://github.com/cdanis) in [#6009](https://github.com/jaegertracing/jaeger/pull/6009)) #### 🚧 Experimental Features * [jaeger-v2] consolidate v1 and v2 configurations for grpc storage ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6042](https://github.com/jaegertracing/jaeger/pull/6042)) * [jaeger-v2] use environment variables in kafka config ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6028](https://github.com/jaegertracing/jaeger/pull/6028)) * [jaeger-v2] align cassandra storage config with otel ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#5949](https://github.com/jaegertracing/jaeger/pull/5949)) * [jaeger-v2] refactor configuration for query service ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#5998](https://github.com/jaegertracing/jaeger/pull/5998)) * [v2] add temporary expvar extension ([@yurishkuro](https://github.com/yurishkuro) in [#5986](https://github.com/jaegertracing/jaeger/pull/5986)) #### 👷 CI Improvements * [ci] disable fail fast behaviour for ci workflows ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#6052](https://github.com/jaegertracing/jaeger/pull/6052)) * Testifylint: enable go-require ([@mmorel-35](https://github.com/mmorel-35) in [#5983](https://github.com/jaegertracing/jaeger/pull/5983)) * Fix regex for publishing v2 image ([@yurishkuro](https://github.com/yurishkuro) in [#5988](https://github.com/jaegertracing/jaeger/pull/5988)) ### 📊 UI Changes #### 🐞 Bug fixes, Minor Improvements * Support uploads of .jsonl files ([@Saumya40-codes](https://github.com/Saumya40-codes) in [#2461](https://github.com/jaegertracing/jaeger-ui/pull/2461)) 1.61.0 / 2.0.0-rc1 (2024-09-14) ------------------------------- ### Backend Changes This release contains an official pre-release candidate of Jaeger v2, as binary and Docker image `jaeger`. #### ⛔ Breaking Changes * Remove support for cassandra 3.x and add cassandra 5.x ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#5962](https://github.com/jaegertracing/jaeger/pull/5962)) #### 🐞 Bug fixes, Minor Improvements * Fix: the 'tagtype' in es jaeger-span mapping tags.properties should be 'type' ([@chinaran](https://github.com/chinaran) in [#5980](https://github.com/jaegertracing/jaeger/pull/5980)) * Add readme for adaptive sampling ([@yurishkuro](https://github.com/yurishkuro) in [#5955](https://github.com/jaegertracing/jaeger/pull/5955)) * [adaptive sampling] clean-up after previous refactoring ([@yurishkuro](https://github.com/yurishkuro) in [#5954](https://github.com/jaegertracing/jaeger/pull/5954)) * [adaptive processor] remove redundant function ([@yurishkuro](https://github.com/yurishkuro) in [#5953](https://github.com/jaegertracing/jaeger/pull/5953)) * [jaeger-v2] consolidate options and namespaceconfig for badger storage ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#5937](https://github.com/jaegertracing/jaeger/pull/5937)) * Remove unused "namespace" field from badger config ([@yurishkuro](https://github.com/yurishkuro) in [#5929](https://github.com/jaegertracing/jaeger/pull/5929)) * Simplify bundling of ui assets ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#5917](https://github.com/jaegertracing/jaeger/pull/5917)) * Clean up grpc storage config ([@yurishkuro](https://github.com/yurishkuro) in [#5877](https://github.com/jaegertracing/jaeger/pull/5877)) * Add script to replace apache headers with spdx ([@thecaffeinedev](https://github.com/thecaffeinedev) in [#5808](https://github.com/jaegertracing/jaeger/pull/5808)) * Add copyright/license headers to script files ([@Zen-cronic](https://github.com/Zen-cronic) in [#5829](https://github.com/jaegertracing/jaeger/pull/5829)) * Clearer output from lint scripts ([@yurishkuro](https://github.com/yurishkuro) in [#5820](https://github.com/jaegertracing/jaeger/pull/5820)) #### 🚧 Experimental Features * [jaeger-v2] add validation and comments to badger storage config ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#5927](https://github.com/jaegertracing/jaeger/pull/5927)) * [jaeger-v2] add validation and comments to memory storage config ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#5925](https://github.com/jaegertracing/jaeger/pull/5925)) * Support tail based sampling processor from otel collector extension ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#5878](https://github.com/jaegertracing/jaeger/pull/5878)) * [v2] configure health check extension for all configs ([@Wise-Wizard](https://github.com/Wise-Wizard) in [#5861](https://github.com/jaegertracing/jaeger/pull/5861)) * [v2] add legacy formats into e2e kafka integration tests ([@joeyyy09](https://github.com/joeyyy09) in [#5824](https://github.com/jaegertracing/jaeger/pull/5824)) * [v2] configure healthcheck extension ([@Wise-Wizard](https://github.com/Wise-Wizard) in [#5831](https://github.com/jaegertracing/jaeger/pull/5831)) * Added _total suffix to otel counter metrics. ([@Wise-Wizard](https://github.com/Wise-Wizard) in [#5810](https://github.com/jaegertracing/jaeger/pull/5810)) #### 👷 CI Improvements * Release v2 cleanup 3 ([@yurishkuro](https://github.com/yurishkuro) in [#5984](https://github.com/jaegertracing/jaeger/pull/5984)) * Replace loopvar linter ([@anishbista60](https://github.com/anishbista60) in [#5976](https://github.com/jaegertracing/jaeger/pull/5976)) * Stop using v1 and v1.x tags for docker images ([@yurishkuro](https://github.com/yurishkuro) in [#5956](https://github.com/jaegertracing/jaeger/pull/5956)) * V2 repease prep ([@yurishkuro](https://github.com/yurishkuro) in [#5932](https://github.com/jaegertracing/jaeger/pull/5932)) * Normalize build-binaries targets ([@yurishkuro](https://github.com/yurishkuro) in [#5924](https://github.com/jaegertracing/jaeger/pull/5924)) * Fix integration test log dumping for storage backends ([@mahadzaryab1](https://github.com/mahadzaryab1) in [#5915](https://github.com/jaegertracing/jaeger/pull/5915)) * Add jaeger-v2 binary as new release artifact ([@renovate-bot](https://github.com/renovate-bot) in [#5893](https://github.com/jaegertracing/jaeger/pull/5893)) * [ci] add support for v2 tags during build ([@yurishkuro](https://github.com/yurishkuro) in [#5890](https://github.com/jaegertracing/jaeger/pull/5890)) * Add hardcoded db password and username to cassandra integration test ([@Ali-Alnosairi](https://github.com/Ali-Alnosairi) in [#5805](https://github.com/jaegertracing/jaeger/pull/5805)) * Define contents permissions on "dependabot validate" workflow ([@mmorel-35](https://github.com/mmorel-35) in [#5874](https://github.com/jaegertracing/jaeger/pull/5874)) * [fix] print kafka logs on test failure ([@joeyyy09](https://github.com/joeyyy09) in [#5873](https://github.com/jaegertracing/jaeger/pull/5873)) * Pin github actions dependencies ([@harshitasao](https://github.com/harshitasao) in [#5860](https://github.com/jaegertracing/jaeger/pull/5860)) * Add go.mod for docker debug image ([@hellspawn679](https://github.com/hellspawn679) in [#5852](https://github.com/jaegertracing/jaeger/pull/5852)) * Enable lint rule: redefines-builtin-id ([@ZXYxc](https://github.com/ZXYxc) in [#5791](https://github.com/jaegertracing/jaeger/pull/5791)) * Require manual go version updates for patch versions ([@wasup-yash](https://github.com/wasup-yash) in [#5848](https://github.com/jaegertracing/jaeger/pull/5848)) * Clean up obselete 'version' tag from docker-compose files ([@vvs-personalstash](https://github.com/vvs-personalstash) in [#5826](https://github.com/jaegertracing/jaeger/pull/5826)) * Update expected codecov flags count to 19 ([@yurishkuro](https://github.com/yurishkuro) in [#5811](https://github.com/jaegertracing/jaeger/pull/5811)) ### 📊 UI Changes Dependencies upgrades only. 1.60.0 / 2.0.0-rc0 (2024-08-06) ------------------------------- ### Backend Changes #### ⛔ Breaking Changes * Completely remove "grpc-plugin" as storage type ([@yurishkuro](https://github.com/yurishkuro) in [#5741](https://github.com/jaegertracing/jaeger/pull/5741)) #### 🐞 Bug fixes, Minor Improvements * Do not use image tag without version ([@yurishkuro](https://github.com/yurishkuro) in [#5783](https://github.com/jaegertracing/jaeger/pull/5783)) * Only attach :latest tag to versioned images from main ([@yurishkuro](https://github.com/yurishkuro) in [#5781](https://github.com/jaegertracing/jaeger/pull/5781)) * Add references to jaeger v2 ([@yurishkuro](https://github.com/yurishkuro) in [#5779](https://github.com/jaegertracing/jaeger/pull/5779)) * Ensure hotrod image is published at the end of e2e test ([@yurishkuro](https://github.com/yurishkuro) in [#5764](https://github.com/jaegertracing/jaeger/pull/5764)) * [bug] [hotrod] delay env var mapping until logger is initialized ([@yurishkuro](https://github.com/yurishkuro) in [#5760](https://github.com/jaegertracing/jaeger/pull/5760)) * Make otlp receiver listen on all ips again ([@yurishkuro](https://github.com/yurishkuro) in [#5739](https://github.com/jaegertracing/jaeger/pull/5739)) * [hotrod] fix connectivity in docker compose ([@yurishkuro](https://github.com/yurishkuro) in [#5734](https://github.com/jaegertracing/jaeger/pull/5734)) #### 🚧 Experimental Features * [v2] enable remote sampling extension and include in e2e tests ([@yurishkuro](https://github.com/yurishkuro) in [#5802](https://github.com/jaegertracing/jaeger/pull/5802)) * Ensure similar naming for storage write metrics ([@Wise-Wizard](https://github.com/Wise-Wizard) in [#5798](https://github.com/jaegertracing/jaeger/pull/5798)) * [v2] ensure similar naming for query service metrics ([@Wise-Wizard](https://github.com/Wise-Wizard) in [#5785](https://github.com/jaegertracing/jaeger/pull/5785)) * Configure otel collector to observe internal telemetry ([@Wise-Wizard](https://github.com/Wise-Wizard) in [#5752](https://github.com/jaegertracing/jaeger/pull/5752)) * Add kafka exporter and receiver configuration ([@joeyyy09](https://github.com/joeyyy09) in [#5703](https://github.com/jaegertracing/jaeger/pull/5703)) * Enable spm in jaeger v2 ([@FlamingSaint](https://github.com/FlamingSaint) in [#5681](https://github.com/jaegertracing/jaeger/pull/5681)) * [jaeger-v2] add `remotesampling` extension ([@Pushkarm029](https://github.com/Pushkarm029) in [#5389](https://github.com/jaegertracing/jaeger/pull/5389)) * Created telset for remote-storage component ([@Wise-Wizard](https://github.com/Wise-Wizard) in [#5731](https://github.com/jaegertracing/jaeger/pull/5731)) #### 👷 CI Improvements * Unpin codeql actions ([@yurishkuro](https://github.com/yurishkuro) in [#5787](https://github.com/jaegertracing/jaeger/pull/5787)) * Skip building hotrod for all platforms for pull requests ([@Manoramsharma](https://github.com/Manoramsharma) in [#5765](https://github.com/jaegertracing/jaeger/pull/5765)) * Add a threshold for expected zero values in the spm script ([@FlamingSaint](https://github.com/FlamingSaint) in [#5753](https://github.com/jaegertracing/jaeger/pull/5753)) * [v2] add e2e test with memory store ([@yurishkuro](https://github.com/yurishkuro) in [#5751](https://github.com/jaegertracing/jaeger/pull/5751)) * Rationalize naming of gha workflow files ([@yurishkuro](https://github.com/yurishkuro) in [#5750](https://github.com/jaegertracing/jaeger/pull/5750)) ### 📊 UI Changes #### 🐞 Bug fixes, Minor Improvements * Allow uploading json-per-line otlp data ([@BenzeneAlcohol](https://github.com/BenzeneAlcohol) in [#2380](https://github.com/jaegertracing/jaeger-ui/pull/2380)) 1.59.0 (2024-07-10) ------------------- ### Backend Changes #### ⛔ Breaking Changes * The OTEL Collector upgrade brought in a change where OTLP receivers started listening on `localhost` instead of `0.0.0.0` as before. As a result, when running in container environment the endpoints are likely unreachable from other containers (Issue [#5737](https://github.com/jaegertracing/jaeger/issues/5737)). The fix will be available in the next release. Meanwhile, the workaround is to instruct Jaeger to listen on `0.0.0.0`, as in [this fix](https://github.com/jaegertracing/jaeger/pull/5734/files#diff-299f817cc4ab077ddb763f1e6a023d9d042d714e2fd3736cc40af3f218d44f1eR15): ``` - COLLECTOR_OTLP_GRPC_HOST_PORT=0.0.0.0:4317 - COLLECTOR_OTLP_HTTP_HOST_PORT=0.0.0.0:4318 ``` * Update opentelemetry-go to v1.28.0 and refactor references to semantic conventions ([@renovate-bot](https://github.com/renovate-bot) in [#5698](https://github.com/jaegertracing/jaeger/pull/5698)) #### ✨ New Features * Run jaeger-es-index-cleaner and jaeger-es-rollover locally ([@hellspawn679](https://github.com/hellspawn679) in [#5714](https://github.com/jaegertracing/jaeger/pull/5714)) * [tracegen] allow use of adaptive sampling ([@yurishkuro](https://github.com/yurishkuro) in [#5718](https://github.com/jaegertracing/jaeger/pull/5718)) * [v2] add v1 factory converter to v2 storage factory ([@james-ryans](https://github.com/james-ryans) in [#5497](https://github.com/jaegertracing/jaeger/pull/5497)) * Upgrade badger v3->badger v4 ([@hellspawn679](https://github.com/hellspawn679) in [#5619](https://github.com/jaegertracing/jaeger/pull/5619)) #### 🐞 Bug fixes, Minor Improvements * Cleanup the prometheus config ([@FlamingSaint](https://github.com/FlamingSaint) in [#5720](https://github.com/jaegertracing/jaeger/pull/5720)) * Upgrade microsim to v0.4.1 ([@FlamingSaint](https://github.com/FlamingSaint) in [#5702](https://github.com/jaegertracing/jaeger/pull/5702)) * Add all mocks to mockery config file and regenerate ([@danish9039](https://github.com/danish9039) in [#5626](https://github.com/jaegertracing/jaeger/pull/5626)) * Add better logging options ([@yurishkuro](https://github.com/yurishkuro) in [#5675](https://github.com/jaegertracing/jaeger/pull/5675)) * Restore "operation" name in the metrics response ([@yurishkuro](https://github.com/yurishkuro) in [#5673](https://github.com/jaegertracing/jaeger/pull/5673)) * Add flag for custom authenticators in cassandra storage ([@hellspawn679](https://github.com/hellspawn679) in [#5628](https://github.com/jaegertracing/jaeger/pull/5628)) * Rename strategy store to sampling strategy provider ([@yurishkuro](https://github.com/yurishkuro) in [#5634](https://github.com/jaegertracing/jaeger/pull/5634)) * [query] avoid errors when closing shared listener ([@vermaaatul07](https://github.com/vermaaatul07) in [#5559](https://github.com/jaegertracing/jaeger/pull/5559)) * Bump github.com/golangci/golangci-lint from 1.55.2 to 1.59.1 and fix linter errors ([@FlamingSaint](https://github.com/FlamingSaint) in [#5579](https://github.com/jaegertracing/jaeger/pull/5579)) * Fix binary path in package-deploy.sh ([@yurishkuro](https://github.com/yurishkuro) in [#5561](https://github.com/jaegertracing/jaeger/pull/5561)) #### 🚧 Experimental Features * Implement telemetry struct for v1 components initialization ([@Wise-Wizard](https://github.com/Wise-Wizard) in [#5695](https://github.com/jaegertracing/jaeger/pull/5695)) * Support default configs for storage backends ([@yurishkuro](https://github.com/yurishkuro) in [#5691](https://github.com/jaegertracing/jaeger/pull/5691)) * Simplify configs organization ([@yurishkuro](https://github.com/yurishkuro) in [#5690](https://github.com/jaegertracing/jaeger/pull/5690)) * Create metrics.factory adapter for otel metrics ([@Wise-Wizard](https://github.com/Wise-Wizard) in [#5661](https://github.com/jaegertracing/jaeger/pull/5661)) #### 👷 CI Improvements * Apply 'latest' tag to latest published snapshot images ([@yurishkuro](https://github.com/yurishkuro) in [#5724](https://github.com/jaegertracing/jaeger/pull/5724)) * [bug] use correct argument as jaeger-version ([@yurishkuro](https://github.com/yurishkuro) in [#5716](https://github.com/jaegertracing/jaeger/pull/5716)) * Add spm integration tests ([@hellspawn679](https://github.com/hellspawn679) in [#5640](https://github.com/jaegertracing/jaeger/pull/5640)) * Add spm build to ci ([@yurishkuro](https://github.com/yurishkuro) in [#5663](https://github.com/jaegertracing/jaeger/pull/5663)) * Remove unnecessary .nocover files ([@yurishkuro](https://github.com/yurishkuro) in [#5642](https://github.com/jaegertracing/jaeger/pull/5642)) * Add tests for anonymizer/app/query. ([@shanukun](https://github.com/shanukun) in [#5638](https://github.com/jaegertracing/jaeger/pull/5638)) * Add alternate way to install gotip ([@EraKin575](https://github.com/EraKin575) in [#5618](https://github.com/jaegertracing/jaeger/pull/5618)) * Add semver to dependencies ([@danish9039](https://github.com/danish9039) in [#5590](https://github.com/jaegertracing/jaeger/pull/5590)) * Create config file for mockery instead of using explicit cli flags in the makefile ([@jesslourenco](https://github.com/jesslourenco) in [#5623](https://github.com/jaegertracing/jaeger/pull/5623)) * Update renovate bot to not apply patches to e2e test dependencies ([@DustyMMiller](https://github.com/DustyMMiller) in [#5622](https://github.com/jaegertracing/jaeger/pull/5622)) * Require renovate bot to run go mod tidy ([@yurishkuro](https://github.com/yurishkuro) in [#5612](https://github.com/jaegertracing/jaeger/pull/5612)) * Fix new warnings from the linter upgrade ([@WaterLemons2k](https://github.com/WaterLemons2k) in [#5589](https://github.com/jaegertracing/jaeger/pull/5589)) * [ci] validate that generated mocks are up to date ([@yurishkuro](https://github.com/yurishkuro) in [#5568](https://github.com/jaegertracing/jaeger/pull/5568)) ### 📊 UI Changes #### 🐞 Bug fixes, Minor Improvements * Add escaped example to tag search help popup ([@yurishkuro](https://github.com/yurishkuro) in [#2354](https://github.com/jaegertracing/jaeger-ui/pull/2354)) 1.58.1 (2024-06-22) ------------------- ### Backend Changes #### 🐞 Bug fixes, Minor Improvements * SPM: Restore "operation" name in the metrics response ([@yurishkuro](https://github.com/yurishkuro) in [#5673](https://github.com/jaegertracing/jaeger/pull/5673)) 1.58.0 (2024-06-11) ------------------- ### Backend Changes #### ⛔ Breaking Changes * Remove support for otel spanmetrics processor ([@varshith257](https://github.com/varshith257) in [#5539](https://github.com/jaegertracing/jaeger/pull/5539)) * Remove support for expvar-backed metrics ([@joeyyy09](https://github.com/joeyyy09) in [#5437](https://github.com/jaegertracing/jaeger/pull/5437)) * Remove support for elasticsearch v5/v6 ([@FlamingSaint](https://github.com/FlamingSaint) in [#5448](https://github.com/jaegertracing/jaeger/pull/5448)) * Remove support for gRPC-Plugin ([@h4shk4t](https://github.com/h4shk4t) in [#5388](https://github.com/jaegertracing/jaeger/pull/5388)) #### 🐞 Bug fixes, Minor Improvements * Set desired providers/converters instead of relying on defaults ([@TylerHelmuth](https://github.com/TylerHelmuth) in [#5543](https://github.com/jaegertracing/jaeger/pull/5543)) * Add elasticsearch helper binaries to release ([@FlamingSaint](https://github.com/FlamingSaint) in [#5501](https://github.com/jaegertracing/jaeger/pull/5501)) * Replace internal metrics.factory usage with direct calls to expvar ([@prakrit55](https://github.com/prakrit55) in [#5496](https://github.com/jaegertracing/jaeger/pull/5496)) * [refactor] move root span handler into aggregator ([@Pushkarm029](https://github.com/Pushkarm029) in [#5478](https://github.com/jaegertracing/jaeger/pull/5478)) * Refactor adaptive sampling aggregator & strategy store ([@Pushkarm029](https://github.com/Pushkarm029) in [#5441](https://github.com/jaegertracing/jaeger/pull/5441)) * [remote-storage] add healthcheck to grpc server ([@james-ryans](https://github.com/james-ryans) in [#5461](https://github.com/jaegertracing/jaeger/pull/5461)) * Fix alpine image to 3.19.0 ([@prakrit55](https://github.com/prakrit55) in [#5454](https://github.com/jaegertracing/jaeger/pull/5454)) * Replace grpc-plugin storage type name with just grpc ([@yurishkuro](https://github.com/yurishkuro) in [#5442](https://github.com/jaegertracing/jaeger/pull/5442)) * [grpc-storage] use grpc.newclient ([@yurishkuro](https://github.com/yurishkuro) in [#5393](https://github.com/jaegertracing/jaeger/pull/5393)) * Replace public initfromoptions with private configurefromoptions ([@yurishkuro](https://github.com/yurishkuro) in [#5417](https://github.com/jaegertracing/jaeger/pull/5417)) * [jaeger-v2] fix e2e storage integration v0.99.0 otel col failing ([@james-ryans](https://github.com/james-ryans) in [#5419](https://github.com/jaegertracing/jaeger/pull/5419)) * Add purge method for cassandra ([@akagami-harsh](https://github.com/akagami-harsh) in [#5414](https://github.com/jaegertracing/jaeger/pull/5414)) * [v2] add diagrams to the docs ([@yurishkuro](https://github.com/yurishkuro) in [#5412](https://github.com/jaegertracing/jaeger/pull/5412)) * [es] remove unused indexcache ([@yurishkuro](https://github.com/yurishkuro) in [#5408](https://github.com/jaegertracing/jaeger/pull/5408)) #### 🚧 Experimental Features * Create new grpc storage configuration to align with otel ([@akagami-harsh](https://github.com/akagami-harsh) in [#5331](https://github.com/jaegertracing/jaeger/pull/5331)) #### 👷 CI Improvements * Add workflow to validate dependabot config ([@yurishkuro](https://github.com/yurishkuro) in [#5556](https://github.com/jaegertracing/jaeger/pull/5556)) * Create separate directories for docker compose files ([@FlamingSaint](https://github.com/FlamingSaint) in [#5554](https://github.com/jaegertracing/jaeger/pull/5554)) * [ci] use m.x version names in workflows ([@hellspawn679](https://github.com/hellspawn679) in [#5546](https://github.com/jaegertracing/jaeger/pull/5546)) * Enable lint rule: unused-parameter ([@FlamingSaint](https://github.com/FlamingSaint) in [#5534](https://github.com/jaegertracing/jaeger/pull/5534)) * Add a new workflow for testing the release workflow ([@yurishkuro](https://github.com/yurishkuro) in [#5532](https://github.com/jaegertracing/jaeger/pull/5532)) * Enable lint rules: struct-tag & unexported-return ([@FlamingSaint](https://github.com/FlamingSaint) in [#5533](https://github.com/jaegertracing/jaeger/pull/5533)) * Enable lint rules: early-return & indent-error-flow ([@FlamingSaint](https://github.com/FlamingSaint) in [#5526](https://github.com/jaegertracing/jaeger/pull/5526)) * Enable lint rule: exported ([@FlamingSaint](https://github.com/FlamingSaint) in [#5525](https://github.com/jaegertracing/jaeger/pull/5525)) * Enable lint rules: confusing-results & receiver-naming ([@FlamingSaint](https://github.com/FlamingSaint) in [#5524](https://github.com/jaegertracing/jaeger/pull/5524)) * Add manual dco check using python script dco-check ([@yurishkuro](https://github.com/yurishkuro) in [#5528](https://github.com/jaegertracing/jaeger/pull/5528)) * Use docker compose for cassandra integration tests ([@hellspawn679](https://github.com/hellspawn679) in [#5520](https://github.com/jaegertracing/jaeger/pull/5520)) * Enable lint rule: modifies-value-receiver ([@FlamingSaint](https://github.com/FlamingSaint) in [#5517](https://github.com/jaegertracing/jaeger/pull/5517)) * Enable lint rule: unused-receiver ([@FlamingSaint](https://github.com/FlamingSaint) in [#5521](https://github.com/jaegertracing/jaeger/pull/5521)) * Enable lint rule: dot-imports ([@FlamingSaint](https://github.com/FlamingSaint) in [#5513](https://github.com/jaegertracing/jaeger/pull/5513)) * Enable lint rules: bare-return & empty-lines ([@FlamingSaint](https://github.com/FlamingSaint) in [#5512](https://github.com/jaegertracing/jaeger/pull/5512)) * Manage 3rd party tools via dedicated go.mod ([@yurishkuro](https://github.com/yurishkuro) in [#5509](https://github.com/jaegertracing/jaeger/pull/5509)) * Enable lint rule: use-any ([@FlamingSaint](https://github.com/FlamingSaint) in [#5510](https://github.com/jaegertracing/jaeger/pull/5510)) * Enable lint rule: unexported-naming ([@FlamingSaint](https://github.com/FlamingSaint) in [#5511](https://github.com/jaegertracing/jaeger/pull/5511)) * Add nolintlint linter ([@yurishkuro](https://github.com/yurishkuro) in [#5508](https://github.com/jaegertracing/jaeger/pull/5508)) * Use docker compose for kafka integration tests ([@hellspawn679](https://github.com/hellspawn679) in [#5500](https://github.com/jaegertracing/jaeger/pull/5500)) * Use docker compose for elasticsearch/opensearch integration tests ([@hellspawn679](https://github.com/hellspawn679) in [#5490](https://github.com/jaegertracing/jaeger/pull/5490)) * Split v1 and v2 es/os integration tests ([@yurishkuro](https://github.com/yurishkuro) in [#5487](https://github.com/jaegertracing/jaeger/pull/5487)) * Remove args and depict the image in from directive ([@prakrit55](https://github.com/prakrit55) in [#5465](https://github.com/jaegertracing/jaeger/pull/5465)) * [v2] remove retries and increase timeout for e2e tests ([@james-ryans](https://github.com/james-ryans) in [#5460](https://github.com/jaegertracing/jaeger/pull/5460)) * Restore code coverage threshold back to 95% ([@varshith257](https://github.com/varshith257) in [#5457](https://github.com/jaegertracing/jaeger/pull/5457)) * [v2] add logging to read/write spans in e2e tests ([@james-ryans](https://github.com/james-ryans) in [#5456](https://github.com/jaegertracing/jaeger/pull/5456)) * Remove elasticsearch v5/v6 from tests ([@FlamingSaint](https://github.com/FlamingSaint) in [#5451](https://github.com/jaegertracing/jaeger/pull/5451)) * [v2] replace e2e span_reader grpc.dialcontext with newclient ([@james-ryans](https://github.com/james-ryans) in [#5443](https://github.com/jaegertracing/jaeger/pull/5443)) * Stop running integration tests for elasticsearch v5/v6 ([@yurishkuro](https://github.com/yurishkuro) in [#5440](https://github.com/jaegertracing/jaeger/pull/5440)) * [v2] remove temporary skipbinaryattrs flag from e2e tests ([@james-ryans](https://github.com/james-ryans) in [#5436](https://github.com/jaegertracing/jaeger/pull/5436)) * [v2] dump storage docker logs in github ci if e2e test failed ([@james-ryans](https://github.com/james-ryans) in [#5433](https://github.com/jaegertracing/jaeger/pull/5433)) * [v2] change e2e jaeger-v2 binary log output to temp file ([@james-ryans](https://github.com/james-ryans) in [#5431](https://github.com/jaegertracing/jaeger/pull/5431)) * [bug] fix syntax for invoking upload-codecov action ([@yurishkuro](https://github.com/yurishkuro) in [#5416](https://github.com/jaegertracing/jaeger/pull/5416)) * Use helper action to retry codecov uploads ([@yurishkuro](https://github.com/yurishkuro) in [#5411](https://github.com/jaegertracing/jaeger/pull/5411)) * Only build docker images for crossdock tests for linux/amd64 ([@varshith257](https://github.com/varshith257) in [#5410](https://github.com/jaegertracing/jaeger/pull/5410)) ### 📊 UI Changes #### 🐞 Bug fixes, Minor Improvements * Document how to debug unit tests in vscode ([@RISHIKESHk07](https://github.com/RISHIKESHk07) in [#2297](https://github.com/jaegertracing/jaeger-ui/pull/2297)) #### 👷 CI Improvements * Github actions added to block prs from fork/main branch ([@varshith257](https://github.com/varshith257) in [#2296](https://github.com/jaegertracing/jaeger-ui/pull/2296)) 1.57.0 (2024-05-01) ------------------- ### Backend Changes #### 🐞 Bug fixes, Minor Improvements * [jaeger-v2] define an internal interface of storage v2 spanstore ([@james-ryans](https://github.com/james-ryans) in [#5399](https://github.com/jaegertracing/jaeger/pull/5399)) * Combine jaeger ui release notes with jaeger backend ([@albertteoh](https://github.com/albertteoh) in [#5405](https://github.com/jaegertracing/jaeger/pull/5405)) * [agent] use grpc.newclient ([@yurishkuro](https://github.com/yurishkuro) in [#5392](https://github.com/jaegertracing/jaeger/pull/5392)) * [sampling] fix merging of per-operation strategies into service strategies without them ([@kuujis](https://github.com/kuujis) in [#5277](https://github.com/jaegertracing/jaeger/pull/5277)) * Create sampling templates when creating sampling store ([@JaeguKim](https://github.com/JaeguKim) in [#5349](https://github.com/jaegertracing/jaeger/pull/5349)) * [kafka-consumer] set the rackid in consumer config ([@sappusaketh](https://github.com/sappusaketh) in [#5374](https://github.com/jaegertracing/jaeger/pull/5374)) * Adding best practices badge to readme.md ([@jkowall](https://github.com/jkowall) in [#5369](https://github.com/jaegertracing/jaeger/pull/5369)) #### 👷 CI Improvements * Moving global write permissions down into the ci jobs ([@jkowall](https://github.com/jkowall) in [#5370](https://github.com/jaegertracing/jaeger/pull/5370)) ### 📊 UI Changes #### 🐞 Bug fixes, Minor Improvements * Improve trace page title with data and unique emoji (fixes #2256) ([@nox](https://github.com/nox) in [#2275](https://github.com/jaegertracing/jaeger-ui/pull/2275)) * Require node version 20+ ([@Baalekshan](https://github.com/Baalekshan) in [#2274](https://github.com/jaegertracing/jaeger-ui/pull/2274)) 1.56.0 (2024-04-02) ------------------- ### Backend Changes #### ⛔ Breaking Changes * Fix hotrod instructions ([@yurishkuro](https://github.com/yurishkuro) in [#5273](https://github.com/jaegertracing/jaeger/pull/5273)) #### 🐞 Bug fixes, Minor Improvements * Refactor healthcheck signalling between server and service ([@WillSewell](https://github.com/WillSewell) in [#5308](https://github.com/jaegertracing/jaeger/pull/5308)) * Docs: badger file permission as non-root service ([@tico88612](https://github.com/tico88612) in [#5282](https://github.com/jaegertracing/jaeger/pull/5282)) * [kafka-consumer] add support for setting fetch message max bytes ([@sappusaketh](https://github.com/sappusaketh) in [#5283](https://github.com/jaegertracing/jaeger/pull/5283)) * [chore] remove repetitive words ([@tgolang](https://github.com/tgolang) in [#5265](https://github.com/jaegertracing/jaeger/pull/5265)) * Fix zipkin spanformat ([@fyuan1316](https://github.com/fyuan1316) in [#5261](https://github.com/jaegertracing/jaeger/pull/5261)) * [kafka-producer] support setting max message size ([@sappusaketh](https://github.com/sappusaketh) in [#5263](https://github.com/jaegertracing/jaeger/pull/5263)) #### 🚧 Experimental Features * [jaeger-v2] add support for opensearch ([@akagami-harsh](https://github.com/akagami-harsh) in [#5257](https://github.com/jaegertracing/jaeger/pull/5257)) * [jaeger-v2] add support for cassandra ([@Pushkarm029](https://github.com/Pushkarm029) in [#5253](https://github.com/jaegertracing/jaeger/pull/5253)) #### 👷 CI Improvements * Allow go-leak linter to fail ci ([@yurishkuro](https://github.com/yurishkuro) in [#5316](https://github.com/jaegertracing/jaeger/pull/5316)) * [jaeger-v2] add grpc storage backend integration test ([@james-ryans](https://github.com/james-ryans) in [#5259](https://github.com/jaegertracing/jaeger/pull/5259)) * Github actions added to block prs from fork/main branch ([@varshith257](https://github.com/varshith257) in [#5272](https://github.com/jaegertracing/jaeger/pull/5272)) ### 📊 UI Changes * UI pinned to version [1.40.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1400-2024-04-02). 1.55.0 (2024-03-04) ------------------- ### Backend Changes #### ✨ New Features: * Support uploading traces to UI in OpenTelemetry format (OTLP/JSON) ([@NavinShrinivas](https://github.com/NavinShrinivas) in [#5155](https://github.com/jaegertracing/jaeger/pull/5155)) * Add Elasticsearch storage support for adaptive sampling ([@Pushkarm029](https://github.com/Pushkarm029) in [#5158](https://github.com/jaegertracing/jaeger/pull/5158)) #### 🐞 Bug fixes, Minor Improvements: * Add the `print-config` subcommand ([@gmafrac](https://github.com/gmafrac) in [#5200](https://github.com/jaegertracing/jaeger/pull/5200)) * Return more detailed errors from ES storage ([@yurishkuro](https://github.com/yurishkuro) in [#5209](https://github.com/jaegertracing/jaeger/pull/5209)) * Bump go version ([@yurishkuro](https://github.com/yurishkuro) in [#5180](https://github.com/jaegertracing/jaeger/pull/5180)) #### 🚧 Experimental Features: * [jaeger-v2] Add support for gRPC storarge ([@james-ryans](https://github.com/james-ryans) in [#5228](https://github.com/jaegertracing/jaeger/pull/5228)) * [jaeger-v2] Add support for Elasticsearch ([@akagami-harsh](https://github.com/akagami-harsh) in [#5152](https://github.com/jaegertracing/jaeger/pull/5152)) ### 📊 UI Changes * UI pinned to version [1.39.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1390-2024-03-04). 1.54.0 (2024-02-06) ------------------- ### Backend Changes #### ⛔ Breaking Changes: * Remove remnants of internal otlp types ([@yurishkuro](https://github.com/yurishkuro) in [#5107](https://github.com/jaegertracing/jaeger/pull/5107)) * Use official otlp types in api_v3 and avoid triple-marshaling ([@yurishkuro](https://github.com/yurishkuro) in [#5098](https://github.com/jaegertracing/jaeger/pull/5098)) #### ✨ New Features: * [jaeger-v2] add support for badger ([@akagami-harsh](https://github.com/akagami-harsh) in [#5112](https://github.com/jaegertracing/jaeger/pull/5112)) #### 🐞 Bug fixes, Minor Improvements: * [jaeger-v2] streamline storage initialization ([@yurishkuro](https://github.com/yurishkuro) in [#5171](https://github.com/jaegertracing/jaeger/pull/5171)) * Replace security self-assesment with one from cncf/tag-security ([@jkowall](https://github.com/jkowall) in [#5142](https://github.com/jaegertracing/jaeger/pull/5142)) * Avoid changing a correct order of span references ([@sherwoodwang](https://github.com/sherwoodwang) in [#5121](https://github.com/jaegertracing/jaeger/pull/5121)) #### 👷 CI Improvements: * Remove test summary reports ([@albertteoh](https://github.com/albertteoh) in [#5126](https://github.com/jaegertracing/jaeger/pull/5126)) ### UI Changes * UI pinned to version [1.38.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1380-2024-02-06). 1.53.0 (2024-01-08) ------------------- ### Backend Changes #### ⛔ Breaking Changes: * 💤 swap zipkin server for zipkin receiver from otel collector contrib ([@yurishkuro](https://github.com/yurishkuro) in [#5045](https://github.com/jaegertracing/jaeger/pull/5045)) * Make all-in-one metric names match metrics from standalone components ([@yurishkuro](https://github.com/yurishkuro) in [#5008](https://github.com/jaegertracing/jaeger/pull/5008)) #### 🐞 Bug fixes, Minor Improvements: * Upgrade thrift compiler to v0.19 and regenerate types ([@yurishkuro](https://github.com/yurishkuro) in [#5080](https://github.com/jaegertracing/jaeger/pull/5080)) * Add gogo/protobuf to opentelemetry otlp data model ([@yurishkuro](https://github.com/yurishkuro) in [#5067](https://github.com/jaegertracing/jaeger/pull/5067)) * Remove grpc-gateway dependency ([@yurishkuro](https://github.com/yurishkuro) in [#5060](https://github.com/jaegertracing/jaeger/pull/5060)) * Add manual implementation of apiv3 http endpoints ([@yurishkuro](https://github.com/yurishkuro) in [#5054](https://github.com/jaegertracing/jaeger/pull/5054)) * Allow specifying version for hotrod docker-compose ([@yurishkuro](https://github.com/yurishkuro) in [#5011](https://github.com/jaegertracing/jaeger/pull/5011)) #### 👷 CI Improvements: * Publish go tip test report ([@albertteoh](https://github.com/albertteoh) in [#5082](https://github.com/jaegertracing/jaeger/pull/5082)) * Upload test report ([@albertteoh](https://github.com/albertteoh) in [#5035](https://github.com/jaegertracing/jaeger/pull/5035)) * Separate test report collection from the main target ([@yurishkuro](https://github.com/yurishkuro) in [#5061](https://github.com/jaegertracing/jaeger/pull/5061)) * Bugfix: set pipefail when running unit tests to preserve exit code ([@yurishkuro](https://github.com/yurishkuro) in [#5057](https://github.com/jaegertracing/jaeger/pull/5057)) * Regenerate thrift types and enable thrift check ([@yurishkuro](https://github.com/yurishkuro) in [#5039](https://github.com/jaegertracing/jaeger/pull/5039)) * Regenerate hotrod proto ([@yurishkuro](https://github.com/yurishkuro) in [#5040](https://github.com/jaegertracing/jaeger/pull/5040)) * Fix permission failed on checks-run ([@albertteoh](https://github.com/albertteoh) in [#5041](https://github.com/jaegertracing/jaeger/pull/5041)) * Refactor protobuf types generation ([@yurishkuro](https://github.com/yurishkuro) in [#5037](https://github.com/jaegertracing/jaeger/pull/5037)) * Publish test report ([@albertteoh](https://github.com/albertteoh) in [#5030](https://github.com/jaegertracing/jaeger/pull/5030)) * Ci: simplify check-label workflow ([@EshaanAgg](https://github.com/EshaanAgg) in [#5033](https://github.com/jaegertracing/jaeger/pull/5033)) * Fix goroutine leaks in several packages ([@yurishkuro](https://github.com/yurishkuro) in [#5026](https://github.com/jaegertracing/jaeger/pull/5026)) * Add goleak check in more tests that do not fail ([@akagami-harsh](https://github.com/akagami-harsh) in [#5025](https://github.com/jaegertracing/jaeger/pull/5025)) * Ci: add retry logic in the install go tip github action ([@akagami-harsh](https://github.com/akagami-harsh) in [#5022](https://github.com/jaegertracing/jaeger/pull/5022)) * Move go tip installation into sub-action ([@yurishkuro](https://github.com/yurishkuro) in [#5020](https://github.com/jaegertracing/jaeger/pull/5020)) * Add goleak check to packages with empty tests ([@yurishkuro](https://github.com/yurishkuro) in [#5017](https://github.com/jaegertracing/jaeger/pull/5017)) * Add goleak check to cmd/agent/app/configmanager ([@yurishkuro](https://github.com/yurishkuro) in [#5015](https://github.com/jaegertracing/jaeger/pull/5015)) * Feature: add goleak to check goroutine leak in tests ([@akagami-harsh](https://github.com/akagami-harsh) in [#5010](https://github.com/jaegertracing/jaeger/pull/5010)) * Remove custom gocache location ([@yurishkuro](https://github.com/yurishkuro) in [#4995](https://github.com/jaegertracing/jaeger/pull/4995)) 1.52.0 (2023-12-05) ------------------- ### Backend Changes #### ✨ New Features: * Support Elasticsearch 8.x ([@pmuls99](https://github.com/pmuls99) in [#4829](https://github.com/jaegertracing/jaeger/pull/4829)) * Make ArchiveTrace button auto-configurable ([@thecoons](https://github.com/thecoons) in [#4913](https://github.com/jaegertracing/jaeger/pull/4913)) #### 🐞 Bug fixes, Minor Improvements: * [SPM] differentiate null from no error data ([@albertteoh](https://github.com/albertteoh) in [#4985](https://github.com/jaegertracing/jaeger/pull/4985)) * Fix example/grafana-integration ([@angristan](https://github.com/angristan) in [#4980](https://github.com/jaegertracing/jaeger/pull/4980)) * Fix (badger): add missing SamplingStoreFactory.CreateLock method ([@slayer321](https://github.com/slayer321) in [#4966](https://github.com/jaegertracing/jaeger/pull/4966)) * Normalize metric names due to breaking change ([@albertteoh](https://github.com/albertteoh) in [#4957](https://github.com/jaegertracing/jaeger/pull/4957)) * [kafka-consumer] add topic name as a tag to offset manager metrics ([@abliqo](https://github.com/abliqo) in [#4951](https://github.com/jaegertracing/jaeger/pull/4951)) * Make UI placeholder more descriptive ([@yurishkuro](https://github.com/yurishkuro) in [#4937](https://github.com/jaegertracing/jaeger/pull/4937)) * Remove google.golang.org/protobuf dependency from model & storage apis ([@akagami-harsh](https://github.com/akagami-harsh) in [#4917](https://github.com/jaegertracing/jaeger/pull/4917)) * Read OTEL env vars for resource attributes ([@yurishkuro](https://github.com/yurishkuro) in [#4932](https://github.com/jaegertracing/jaeger/pull/4932)) #### 🚧 Experimental Features: * Exp: rename jaeger-v2 binary to just jaeger ([@yurishkuro](https://github.com/yurishkuro) in [#4918](https://github.com/jaegertracing/jaeger/pull/4918)) #### 👷 CI Improvements: * [CI]: improve kafka integration test self-sufficiency ([@RipulHandoo](https://github.com/RipulHandoo) in [#4989](https://github.com/jaegertracing/jaeger/pull/4989)) * Separate all-in-one integration tests for v1 and v2 ([@yurishkuro](https://github.com/yurishkuro) in [#4968](https://github.com/jaegertracing/jaeger/pull/4968)) * Collect code coverage from integration tests and upload to codecov ([@yurishkuro](https://github.com/yurishkuro) in [#4964](https://github.com/jaegertracing/jaeger/pull/4964)) * [CI/ES] use default template priorities ([@yurishkuro](https://github.com/yurishkuro) in [#4962](https://github.com/jaegertracing/jaeger/pull/4962)) * Unleash dependabot on docker files and add dependency review workflow ([@step-security-bot](https://github.com/step-security-bot) in [#4945](https://github.com/jaegertracing/jaeger/pull/4945)) * Split unit-test workflow into tests and lint ([@MeenuyD](https://github.com/MeenuyD) in [#4933](https://github.com/jaegertracing/jaeger/pull/4933)) * [CI]: harden github actions ([@step-security-bot](https://github.com/step-security-bot) in [#4923](https://github.com/jaegertracing/jaeger/pull/4923)) * [CI]: build jaeger v2 image on main branch runs ([@yurishkuro](https://github.com/yurishkuro) in [#4920](https://github.com/jaegertracing/jaeger/pull/4920)) * Exp: publish jaeger v2 image ([@yurishkuro](https://github.com/yurishkuro) in [#4919](https://github.com/jaegertracing/jaeger/pull/4919)) * [CI]: set default to fix 'unbound variable' error on main ([@yurishkuro](https://github.com/yurishkuro) in [#4916](https://github.com/jaegertracing/jaeger/pull/4916)) * [CI]: test jaeger-v2 as all-in-one in ci ([@yurishkuro](https://github.com/yurishkuro) in [#4890](https://github.com/jaegertracing/jaeger/pull/4890)) * Fix release script broken by recent linting cleanup ([@yurishkuro](https://github.com/yurishkuro) in [#4915](https://github.com/jaegertracing/jaeger/pull/4915)) ### UI Changes * UI pinned to version [1.36.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1360-2023-12-05). 1.51.0 (2023-11-04) ------------------- ### Backend Changes #### ✨ New Features: * Feat: add sampling store support to badger ([@slayer321](https://github.com/slayer321) in [#4834](https://github.com/jaegertracing/jaeger/pull/4834)) * Feat: add span adjuster that moves some otel resource attributes to span.process ([@james-ryans](https://github.com/james-ryans) in [#4844](https://github.com/jaegertracing/jaeger/pull/4844)) * Add product/file version in windows executables ([@ResamVi](https://github.com/ResamVi) in [#4811](https://github.com/jaegertracing/jaeger/pull/4811)) #### 🐞 Bug fixes, Minor Improvements: * Fix dependency policy and add to security-insights.yml ([@jkowall](https://github.com/jkowall) in [#4907](https://github.com/jaegertracing/jaeger/pull/4907)) * Add reload interval to otel server certificates ([@james-ryans](https://github.com/james-ryans) in [#4898](https://github.com/jaegertracing/jaeger/pull/4898)) * Feat: add blackhole storage, for benchmarking ([@yurishkuro](https://github.com/yurishkuro) in [#4896](https://github.com/jaegertracing/jaeger/pull/4896)) * Add otel resource detector to jaeger components ([@james-ryans](https://github.com/james-ryans) in [#4864](https://github.com/jaegertracing/jaeger/pull/4864)) * Fix batchprocessor to set correct span format flags ([@k0zl](https://github.com/k0zl) in [#4796](https://github.com/jaegertracing/jaeger/pull/4796)) * Expose collector ports in docker images ([@arunvelsriram](https://github.com/arunvelsriram) in [#4810](https://github.com/jaegertracing/jaeger/pull/4810)) #### 🚧 Experimental Features: * Exp(jaeger-v2): simplify all-in-one configuration ([@yurishkuro](https://github.com/yurishkuro) in [#4875](https://github.com/jaegertracing/jaeger/pull/4875)) * Exp: support primary and archive storage ([@yurishkuro](https://github.com/yurishkuro) in [#4873](https://github.com/jaegertracing/jaeger/pull/4873)) * Feat(jaeger-v2): create default config for all-in-one ([@yurishkuro](https://github.com/yurishkuro) in [#4842](https://github.com/jaegertracing/jaeger/pull/4842)) #### 👷 CI Improvements: * Ci: split the install-tools into test/build groups ([@MeenuyD](https://github.com/MeenuyD) in [#4878](https://github.com/jaegertracing/jaeger/pull/4878)) * Simplify binary building in makefile ([@yurishkuro](https://github.com/yurishkuro) in [#4885](https://github.com/jaegertracing/jaeger/pull/4885)) * Ci: pass variable instead of calling make build-xxx-debug ([@yurishkuro](https://github.com/yurishkuro) in [#4883](https://github.com/jaegertracing/jaeger/pull/4883)) * Simplify makefile ([@yurishkuro](https://github.com/yurishkuro) in [#4882](https://github.com/jaegertracing/jaeger/pull/4882)) * Test: add more linters ([@yurishkuro](https://github.com/yurishkuro) in [#4881](https://github.com/jaegertracing/jaeger/pull/4881)) * Ci: enable linting of code in examples/ ([@yurishkuro](https://github.com/yurishkuro) in [#4880](https://github.com/jaegertracing/jaeger/pull/4880)) * Ci: keep the ui asset's .gz file timestamps the same as the original file ([@yurishkuro](https://github.com/yurishkuro) in [#4879](https://github.com/jaegertracing/jaeger/pull/4879)) * Add first pass at the security-insights.yml ([@jkowall](https://github.com/jkowall) in [#4872](https://github.com/jaegertracing/jaeger/pull/4872)) * Create scorecard.yml for ossf implementation ([@jkowall](https://github.com/jkowall) in [#4870](https://github.com/jaegertracing/jaeger/pull/4870)) * Add ci validation of shell scripts using shellcheck ([@akagami-harsh](https://github.com/akagami-harsh) in [#4826](https://github.com/jaegertracing/jaeger/pull/4826)) * Chore: add dynamic loading bar functionality to release-notes.py ([@anshgoyalevil](https://github.com/anshgoyalevil) in [#4857](https://github.com/jaegertracing/jaeger/pull/4857)) * Ci: add the label-check workflow to verify changelog labels on each pr ([@anshgoyalevil](https://github.com/anshgoyalevil) in [#4847](https://github.com/jaegertracing/jaeger/pull/4847)) * Ci(hotrod): print hotrod container logs in case of test failure ([@yurishkuro](https://github.com/yurishkuro) in [#4845](https://github.com/jaegertracing/jaeger/pull/4845)) * Ci: drop -v from ci unit tests to make failures easier to see ([@yurishkuro](https://github.com/yurishkuro) in [#4839](https://github.com/jaegertracing/jaeger/pull/4839)) * Use commit hash as image label when building & integration-testing ([@yurishkuro](https://github.com/yurishkuro) in [#4824](https://github.com/jaegertracing/jaeger/pull/4824)) * Clean-up some linter warnings in build scripts ([@yurishkuro](https://github.com/yurishkuro) in [#4823](https://github.com/jaegertracing/jaeger/pull/4823)) * Fix build-all-in-one-image script ([@albertteoh](https://github.com/albertteoh) in [#4819](https://github.com/jaegertracing/jaeger/pull/4819)) * [ci-release] improve release workflow for manual runs ([@yurishkuro](https://github.com/yurishkuro) in [#4818](https://github.com/jaegertracing/jaeger/pull/4818)) * Add --force to docker commands ([@albertteoh](https://github.com/albertteoh) in [#4820](https://github.com/jaegertracing/jaeger/pull/4820)) * Use setup-node.js for publish release ([@albertteoh](https://github.com/albertteoh) in [#4816](https://github.com/jaegertracing/jaeger/pull/4816)) * Clean up ci scripts and prune docker images between builds ([@yurishkuro](https://github.com/yurishkuro) in [#4815](https://github.com/jaegertracing/jaeger/pull/4815)) * Clean-up & fortify ci-release ([@yurishkuro](https://github.com/yurishkuro) in [#4813](https://github.com/jaegertracing/jaeger/pull/4813)) ### UI Changes * UI pinned to version [1.35.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1350-2023-11-02). 1.50.0 (2023-10-06) ------------------- ### Backend Changes #### ⛔ Breaking Changes * [sampling] Remove support for SAMPLING_TYPE env var and 'static' value ([@yurishkuro](https://github.com/yurishkuro) in [#4735](https://github.com/jaegertracing/jaeger/pull/4735)) * Use non-root user in built containers ([@nikzayn](https://github.com/nikzayn) in [#4783](https://github.com/jaegertracing/jaeger/pull/4783)) - this change may cause issues with existing installations using Badger storage, because the existing files would be owned by a different user and would not be writeable after Jaeger upgrade. The workaround is to manually chown the files to the new user (uid=10001). #### New Features * Add cassandra schema compaction window configuration ([@sameersecond](https://github.com/sameersecond) in [#4767](https://github.com/jaegertracing/jaeger/pull/4767)) * Add jaeger-v2 single binary based on otel collector ([@yurishkuro](https://github.com/yurishkuro) in [#4766](https://github.com/jaegertracing/jaeger/pull/4766)) * [kafka-consumer] Consumer metrics should have a tag with topic name ([@abliqo](https://github.com/abliqo) in [#4778](https://github.com/jaegertracing/jaeger/pull/4778)) * Support http proxy env variables ([@pavolloffay](https://github.com/pavolloffay) in [#4769](https://github.com/jaegertracing/jaeger/pull/4769)) * Support reloading es client's password from file ([@haanhvu](https://github.com/haanhvu) in [#4342](https://github.com/jaegertracing/jaeger/pull/4342)) #### Bug fixes, Minor Improvements * Fix jaegerqueryreqsfailing alert rule missing 'operation' in query ([@james-ryans](https://github.com/james-ryans) in [#4797](https://github.com/jaegertracing/jaeger/pull/4797)) * Add e2e test for sampling storage ([@slayer321](https://github.com/slayer321) in [#4772](https://github.com/jaegertracing/jaeger/pull/4772)) * [tests] Simplify cassandra e2e test cleanup ([@yurishkuro](https://github.com/yurishkuro) in [#4794](https://github.com/jaegertracing/jaeger/pull/4794)) * [tests] Fix failing e2e test for cassandra storage ([@slayer321](https://github.com/slayer321) in [#4776](https://github.com/jaegertracing/jaeger/pull/4776)) * Remove unneeded references to opentracing ([@yurishkuro](https://github.com/yurishkuro) in [#4790](https://github.com/jaegertracing/jaeger/pull/4790)) * Use non-root user in built containers ([@nikzayn](https://github.com/nikzayn) in [#4783](https://github.com/jaegertracing/jaeger/pull/4783)) * Run all integration tests against cassandra ([@yurishkuro](https://github.com/yurishkuro) in [#4773](https://github.com/jaegertracing/jaeger/pull/4773)) * [hotrod] Log driver locations as json to demo respective ui capability ([@yurishkuro](https://github.com/yurishkuro) in [#4765](https://github.com/jaegertracing/jaeger/pull/4765)) * Replace python script with tracegen ([@albertteoh](https://github.com/albertteoh) in [#4753](https://github.com/jaegertracing/jaeger/pull/4753)) * [fix] Close elasticsearch client properly ([@Lauquik](https://github.com/Lauquik) in [#4754](https://github.com/jaegertracing/jaeger/pull/4754)) * Add deprecation warning to jaeger-agent ([@yurishkuro](https://github.com/yurishkuro) in [#4749](https://github.com/jaegertracing/jaeger/pull/4749)) * Deprecate grpc-storage-plugin sidecar model ([@yurishkuro](https://github.com/yurishkuro) in [#4744](https://github.com/jaegertracing/jaeger/pull/4744)) * Upgrade query api v3 to official opentelemetry format ([@yurishkuro](https://github.com/yurishkuro) in [#4742](https://github.com/jaegertracing/jaeger/pull/4742)) * [SPM] Deprecate support for spanmetrics processor naming convention ([@yurishkuro](https://github.com/yurishkuro) in [#4741](https://github.com/jaegertracing/jaeger/pull/4741)) * Deprecate expvar metrics backend ([@yurishkuro](https://github.com/yurishkuro) in [#4740](https://github.com/jaegertracing/jaeger/pull/4740)) * Fix flaky testgetroundtripper* tests ([@albertteoh](https://github.com/albertteoh) in [#4738](https://github.com/jaegertracing/jaeger/pull/4738)) ### UI Changes * UI pinned to version [1.34.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1340-2023-10-04). 1.49.0 (2023-09-07) ------------------- ### Backend Changes #### ⛔ Breaking Changes * [hotrod] Make OTLP the default exporter in HotROD ([@yurishkuro](https://github.com/yurishkuro) in [#4698](https://github.com/jaegertracing/jaeger/pull/4698)) * [SPM] Support spanmetrics connector by default ([@albertteoh](https://github.com/albertteoh) in [#4704](https://github.com/jaegertracing/jaeger/pull/4704)) * [tracegen] Stop supporting -trace-exporter=jaeger ([@yurishkuro](https://github.com/yurishkuro) in [#4717](https://github.com/jaegertracing/jaeger/pull/4717)) * [hotrod] Stop supporting -otel-exporter=jaeger ([@yurishkuro](https://github.com/yurishkuro) in [#4719](https://github.com/jaegertracing/jaeger/pull/4719)) * [hotrod] Metrics endpoints moved from route service (:8083) to frontend service (:8080) ([@yurishkuro](https://github.com/yurishkuro) in [#4720](https://github.com/jaegertracing/jaeger/pull/4720)) #### Bug fixes, Minor Improvements * Allow disabling brearer token override from request in metrics store ([@pavolloffay](https://github.com/pavolloffay) in [#4726](https://github.com/jaegertracing/jaeger/pull/4726)) * Add the enable tracing opt-in flag ([@albertteoh](https://github.com/albertteoh) in [#4685](https://github.com/jaegertracing/jaeger/pull/4685)) * [tracegen] Add build info during compilation ([@yurishkuro](https://github.com/yurishkuro) in [#4727](https://github.com/jaegertracing/jaeger/pull/4727)) * Log version/build info on startup ([@yurishkuro](https://github.com/yurishkuro) in [#4723](https://github.com/jaegertracing/jaeger/pull/4723)) * [zipkin] Replace zipkin exporter from jaeger sdk with otel zipkin exp ([@afzal442](https://github.com/afzal442) in [#4674](https://github.com/jaegertracing/jaeger/pull/4674)) ### UI Changes * UI pinned to version [1.33.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1330-2023-08-06). 1.48.0 (2023-08-15) ------------------- ### Backend Changes #### Bug fixes, Minor Improvements * [fix] Disable tracing of OTLP Receiver ([@yurishkuro](https://github.com/yurishkuro) in [#4662](https://github.com/jaegertracing/jaeger/pull/4662)) * [hotrod/observer_test] Switch to OpenTelemetry ([@afzal442](https://github.com/afzal442) in [#4635](https://github.com/jaegertracing/jaeger/pull/4635)) * [memstore-plugin]Switch to OpenTelemetry SDK ([@afzal442](https://github.com/afzal442) in [#4643](https://github.com/jaegertracing/jaeger/pull/4643)) * [tracegen] Allow to control cardinality of attribute keys ([@yurishkuro](https://github.com/yurishkuro) in [#4634](https://github.com/jaegertracing/jaeger/pull/4634)) * Replace OT const wth OTEL trace.span for zipkin comp ([@afzal442](https://github.com/afzal442) in [#4625](https://github.com/jaegertracing/jaeger/pull/4625)) * Replace OpenTracing instrumentation with OpenTelemetry in grpc storage plugin ([@afzal442](https://github.com/afzal442) in [#4611](https://github.com/jaegertracing/jaeger/pull/4611)) * Replace OT trace with `otel trace` spans type to span model ([@afzal442](https://github.com/afzal442) in [#4614](https://github.com/jaegertracing/jaeger/pull/4614)) * Replace cassandra-spanstore tracing instrumentation with`OTEL` ([@afzal442](https://github.com/afzal442) in [#4599](https://github.com/jaegertracing/jaeger/pull/4599)) * Replace es-spanstore tracing instrumentation with OpenTelemetry ([@afzal442](https://github.com/afzal442) in [#4596](https://github.com/jaegertracing/jaeger/pull/4596)) * Replace metricsstore/reader tracing instrumentation with OpenTelemetry ([@afzal442](https://github.com/afzal442) in [#4595](https://github.com/jaegertracing/jaeger/pull/4595)) * Replace Jaeger SDK with OTEL SDK + OT Bridge ([@afzal442](https://github.com/afzal442) in [#4574](https://github.com/jaegertracing/jaeger/pull/4574)) * [kafka-consumer] Ingester should use topic name from actual Kafka consumer instead of configuration ([@abliqo](https://github.com/abliqo) in [#4593](https://github.com/jaegertracing/jaeger/pull/4593)) * Enable CORS settings on OTLP HTTP endpoint ([@pmuls99](https://github.com/pmuls99) in [#4586](https://github.com/jaegertracing/jaeger/pull/4586)) * [hotrod] Return trace ID via traceresponse header ([@yurishkuro](https://github.com/yurishkuro) in [#4584](https://github.com/jaegertracing/jaeger/pull/4584)) * [hotrod] Remove most references to OpenTracing ([@yurishkuro](https://github.com/yurishkuro) in [#4585](https://github.com/jaegertracing/jaeger/pull/4585)) * [hotrod] Validate user input to avoid security warnings from code scanning ([@yurishkuro](https://github.com/yurishkuro) in [#4583](https://github.com/jaegertracing/jaeger/pull/4583)) * [hotrod] Upgrade HotROD to use OpenTelemetry instrumentation ([@afzal442](https://github.com/afzal442) in [#4548](https://github.com/jaegertracing/jaeger/pull/4548)) * [kafka-consumer] Use wait group to ensure goroutine is finished before returning from Close ([@kennyaz](https://github.com/kennyaz) in [#4582](https://github.com/jaegertracing/jaeger/pull/4582)) * [tracegen] Enable BlockOnQueueFull in OTel SDK to avoid dropped spans ([@haanhvu](https://github.com/haanhvu) in [#4578](https://github.com/jaegertracing/jaeger/pull/4578)) * [hotrod] Handle both OT and OTEL baggage ([@yurishkuro](https://github.com/yurishkuro) in [#4572](https://github.com/jaegertracing/jaeger/pull/4572)) ### UI Changes * UI pinned to version [1.32.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1320-2023-08-14). 1.47.0 (2023-07-05) ------------------- ### Backend Changes #### ⛔ Breaking Changes * [SPM] Due to a breaking change in OpenTelemetry's prometheus exporter ([details](https://github.com/open-telemetry/opentelemetry-collector-contrib/releases/tag/v0.80.0)) metric names will no longer be normalized by default, meaning that the expected metric names would be `calls` and `duration_*`. Backwards compatibility with older OpenTelemetry Collector versions can be achieved through the following flags: * `prometheus.query.normalize-calls`: If true, normalizes the "calls" metric name. e.g. "calls_total". * `prometheus.query.normalize-duration`: If true, normalizes the "duration" metric name to include the duration units. e.g. "duration_milliseconds_bucket". #### New Features * [Cassandra] Add Configuration.Close() to ensure TLS cert watcher is closed ([@kennyaz](https://github.com/kennyaz) in [#4515](https://github.com/jaegertracing/jaeger/pull/4515)) * Add *.kerberos.disable-fast-negotiation option to Kafka consumer ([@pmuls99](https://github.com/pmuls99) in [#4520](https://github.com/jaegertracing/jaeger/pull/4520)) * Support Prometheus normalization for specific metrics related to OpenTelemetry compatibility ([@albertteoh](https://github.com/albertteoh) in [#4555](https://github.com/jaegertracing/jaeger/pull/4555)) #### Bug fixes, Minor Improvements * Add readme for memstore plugin ([@yurishkuro](https://github.com/yurishkuro) in [283bdd9](https://github.com/jaegertracing/jaeger/commit/283bdd93cbb4a467842625d8eb320722fcb83494)) * Pass a wrapper instead of `opentracing.Tracer` to ease migration to OTEL in the future [part 1] ([@afzalbin64](https://github.com/afzalbin64) in [#4529](https://github.com/jaegertracing/jaeger/pull/4529)) * [hotROD] Add OTEL instrumentation to customer svc ([@afzal442](https://github.com/afzal442) in [#4559](https://github.com/jaegertracing/jaeger/pull/4559)) * [hotROD] Replace gRPC instrumentation with OTEL ([@afzal442](https://github.com/afzal442) in [#4558](https://github.com/jaegertracing/jaeger/pull/4558)) * [hotROD]: Upgrade `redis` service to use native OTEL instrumentation ([@afzal442](https://github.com/afzal442) in [#4533](https://github.com/jaegertracing/jaeger/pull/4533)) * [hotROD] Fix OTEL logging in HotRod example ([@albertteoh](https://github.com/albertteoh) in [#4556](https://github.com/jaegertracing/jaeger/pull/4556)) * [hotrod] Reduce span exporter's batch timeout to let the spans be exported sooner ([@GLVSKiriti](https://github.com/GLVSKiriti) in [#4518](https://github.com/jaegertracing/jaeger/pull/4518)) * [tracegen] Add options to generate more spans and attributes for additional use cases ([@yurishkuro](https://github.com/yurishkuro) in [#4535](https://github.com/jaegertracing/jaeger/pull/4535)) * Build improvement to rebuild jaeger-ui if the tree does not match any tag ([@bobrik](https://github.com/bobrik) in [#4553](https://github.com/jaegertracing/jaeger/pull/4553)) * [Test] Fixed a race condition causing unit test failure by changing the logging ([@yurishkuro](https://github.com/yurishkuro) in [#4546](https://github.com/jaegertracing/jaeger/pull/4546)) resolves [#4497](https://github.com/jaegertracing/jaeger/issues/4497) ### UI Changes * UI pinned to version [1.31.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1310-2023-07-05). 1.46.0 (2023-06-05) ------------------- ### Backend Changes #### ⛔ Breaking Changes OTLP receiver is now enabled by default ([#4494](https://github.com/jaegertracing/jaeger/pull/4494)). This change follows the Jaeger's strategic direction of aligning closely with the OpenTelemetry project. This may cause port conflicts if `jaeger-collector` is depoyed in host network namespace. The original `--collector.otlp.enabled` option is still available and MUST be set to `false` if OTLP receiver is not desired. #### New Features * Make OTLP receiver enabled by default ([@yurishkuro](https://github.com/yurishkuro) in [#4494](https://github.com/jaegertracing/jaeger/pull/4494)) * [SPM] Add support for OpenTelemetry SpanMetrics Connector ([@albertteoh](https://github.com/albertteoh) in [#4452](https://github.com/jaegertracing/jaeger/pull/4452)). See [Migration README](https://github.com/jaegertracing/jaeger/blob/main/docker-compose/monitor/README.md#migrating). #### Bug fixes, Minor Improvements * Log processor error in Kafka consumer ([@pavolloffay](https://github.com/pavolloffay) in [#4399](https://github.com/jaegertracing/jaeger/pull/4399)) * [bug] Remove TerminateAfter from Elasticsearch/Opensearch query resulting in incomplete span count/list ([@Jakob3xD](https://github.com/Jakob3xD) in [#4336](https://github.com/jaegertracing/jaeger/pull/4336)) * [agent] Use RawConn.Control to get fd instead of Fd() to prevent deadlock on shutdown ([@ChenX1993](https://github.com/ChenX1993) in [#4449](https://github.com/jaegertracing/jaeger/pull/4449)) * [SPM] Fix docker compose command ([@tqi-raurora](https://github.com/tqi-raurora) in [#4444](https://github.com/jaegertracing/jaeger/pull/4444)) #### Maintenance * [test] Fix flaky test - TestSpanProcessorWithOnDroppedSpanOption ([@yurishkuro](https://github.com/yurishkuro) in [#4489](https://github.com/jaegertracing/jaeger/pull/4489)) * [ci] Skip debug builds when not making a release ([@psk001](https://github.com/psk001) in [#4496](https://github.com/jaegertracing/jaeger/pull/4496)) * Fix some function comments ([@cuishuang](https://github.com/cuishuang) in [#4410](https://github.com/jaegertracing/jaeger/pull/4410)) * Increase dependabot open-pull-requests-limit=10 ([@yurishkuro](https://github.com/yurishkuro) in [04548fc](https://github.com/jaegertracing/jaeger/commit/04548fc339689f970da2de36b964fd3abfca41c2)) * Add jkowall as release manger for July ([@jkowall](https://github.com/jkowall) in [#4446](https://github.com/jaegertracing/jaeger/pull/4446)) * Fix versions in release schedule ([@yurishkuro](https://github.com/yurishkuro) in [8a9d13a](https://github.com/jaegertracing/jaeger/commit/8a9d13a31b477707cec73a9b7bf6242b27cec0ea)) ### UI Changes * UI pinned to version [1.30.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1300-2023-06-05). 1.45.0 (2023-05-03) ------------------- ### Backend Changes #### Bug fixes, Minor Improvements * Add HTTP post port mapping to docker command ([@albertteoh](https://github.com/albertteoh) in [#4407](https://github.com/jaegertracing/jaeger/pull/4407)) * Simplify ES config and factory ([@yurishkuro](https://github.com/yurishkuro) in [#4396](https://github.com/jaegertracing/jaeger/pull/4396)) * Add otlp-grpc for tracegen's trace-exporter ([@boysusu](https://github.com/boysusu) in [#4374](https://github.com/jaegertracing/jaeger/pull/4374)) * Allow follows-from reference as a parent span id ([@kubarydz](https://github.com/kubarydz) in [#4382](https://github.com/jaegertracing/jaeger/pull/4382)) * Expose drop span hook as an option in Collector SpanProcessor ([@ChenX1993](https://github.com/ChenX1993) in [#4387](https://github.com/jaegertracing/jaeger/pull/4387)) ### UI Changes * UI pinned to version [1.29.1](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1291-2023-05-03). 1.44.0 (2023-04-10) ------------------- ### Backend Changes #### Bug fixes, Minor Improvements * Store span warnings as tags in Cassandra ([@utsavoza](https://github.com/utsavoza) in [#4313](https://github.com/jaegertracing/jaeger/pull/4313)) * Add Keep-Alive flag for Zipkin HTTP server ([@topjung3](https://github.com/topjung3) in [#4366](https://github.com/jaegertracing/jaeger/pull/4366)) * Log access to static assets; remove favicon test ([@yurishkuro](https://github.com/yurishkuro) in [#4302](https://github.com/jaegertracing/jaeger/pull/4302)) ### UI Changes * UI pinned to version [1.29.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1290-2023-04-10). 1.43.0 (2023-03-15) ------------------- ### Backend Changes #### New Features * Support secure OTLP exporter config for hotrod ([@graphaelli](https://github.com/graphaelli) in [#4231](https://github.com/jaegertracing/jaeger/pull/4231)) * Jaeger YugabyteDB(YCQL) support ([@HarshDaryani896](https://github.com/HarshDaryani896) in [#4220](https://github.com/jaegertracing/jaeger/pull/4220)) #### Bug fixes, Minor Improvements * Replace pkg/multierror with standard errors.Join ([@ClementRepo](https://github.com/ClementRepo) in [#4293](https://github.com/jaegertracing/jaeger/pull/4293)) * Remove pkg/multicloser ([@yurishkuro](https://github.com/yurishkuro) in [#4291](https://github.com/jaegertracing/jaeger/pull/4291)) * Refactor build linux artifacts only for PR ([@Eileen-Yu](https://github.com/Eileen-Yu) in [#4286](https://github.com/jaegertracing/jaeger/pull/4286)) * Speed-up CI by using published UI artifacts ([@shubbham1219](https://github.com/shubbham1219) in [#4251](https://github.com/jaegertracing/jaeger/pull/4251)) * Update Go version to 1.20 ([@SaarthakMaini](https://github.com/SaarthakMaini) in [#4206](https://github.com/jaegertracing/jaeger/pull/4206)) * Use http.MethodGet instead of "GET" ([@my-git9](https://github.com/my-git9) in [#4248](https://github.com/jaegertracing/jaeger/pull/4248)) * Updating all-in-one path ([@bigfleet](https://github.com/bigfleet) in [#4234](https://github.com/jaegertracing/jaeger/pull/4234)) * Migrate the use of fsnotify to fswatcher in cert_watcher.go ([@haanhvu](https://github.com/haanhvu) in [#4232](https://github.com/jaegertracing/jaeger/pull/4232)) * Restore baggage support in HotROD 🚗 ([@yurishkuro](https://github.com/yurishkuro) in [#4225](https://github.com/jaegertracing/jaeger/pull/4225)) ### UI Changes * UI pinned to version [1.28.1](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1281-2023-03-15). 1.42.0 (2023-02-05) ------------------- ### Backend Changes #### ⛔ Breaking Changes * HotROD 🚗 application is switched from Jaeger SDK to OpenTelemetry SDK ([@yurishkuro](https://github.com/yurishkuro) in [#4187](https://github.com/jaegertracing/jaeger/pull/4187)). Some environment variables previously accepted are no longer recognized. See PR for details. * Map old env vars from Jaeger SDK to OTel SDK vars ([@yurishkuro](https://github.com/yurishkuro) in [#4200](https://github.com/jaegertracing/jaeger/pull/4200)) * Use patched version of github.com/opentracing-contrib/go-grpc in HotROD ([@yurishkuro](https://github.com/yurishkuro) in [#4210](https://github.com/jaegertracing/jaeger/pull/4210)) * `tracegen` utility is switched from Jaeger SDK to OpenTelemetry SDK ([@yurishkuro](https://github.com/yurishkuro) in [#4189](https://github.com/jaegertracing/jaeger/pull/4189)). Some environment variables previously accepted are no longer recognized. See PR for details. #### New Features * Add CLI flags for controlling HTTP server timeouts ([@yurishkuro](https://github.com/yurishkuro) in [#4167](https://github.com/jaegertracing/jaeger/pull/4167)) * Watch directories for certificate hot-reload ([@tsaarni](https://github.com/tsaarni) in [#4159](https://github.com/jaegertracing/jaeger/pull/4159)) * Support tenant header propagation in query service and grpc-plugin ([@pavolloffay](https://github.com/pavolloffay) in [#4151](https://github.com/jaegertracing/jaeger/pull/4151)) #### Bug fixes, Minor Improvements * Replace Thrift-gen with Proto-gen types for sampling strategies ([@yurishkuro](https://github.com/yurishkuro) in [#4181](https://github.com/jaegertracing/jaeger/pull/4181)) * Use Protobuf-based JSON output for sampling strategies ([@yurishkuro](https://github.com/yurishkuro) in [#4173](https://github.com/jaegertracing/jaeger/pull/4173)) * [tests]: Use `t.Setenv` to set env vars in tests ([@Juneezee](https://github.com/Juneezee) in [#4169](https://github.com/jaegertracing/jaeger/pull/4169)) * ci(lint): bump golangci-lint to v1.50.1 ([@mmorel-35](https://github.com/mmorel-35) in [#4195](https://github.com/jaegertracing/jaeger/pull/4195)) * Convert token propagation integration test to plain unit test ([@yurishkuro](https://github.com/yurishkuro) in [#4162](https://github.com/jaegertracing/jaeger/pull/4162)) * Refactor scripts/es-integration-test.sh ([@yurishkuro](https://github.com/yurishkuro) in [#4161](https://github.com/jaegertracing/jaeger/pull/4161)) * Fix "index out of range" when receiving a dual client/server Zipkin span ([@yurishkuro](https://github.com/yurishkuro) in [#4160](https://github.com/jaegertracing/jaeger/pull/4160)) ### UI Changes * No changes. 1.41.0 (2023-01-04) ------------------- ### Backend Changes #### Bug fixes, Minor Improvements * Remove global platform arg in cassandra schema dockerfile ([@jkandasa](https://github.com/jkandasa) in [#4123](https://github.com/jaegertracing/jaeger/pull/4123)) * Add multi arch support to cassandra-schema container ([@jkandasa](https://github.com/jkandasa) in [#4122](https://github.com/jaegertracing/jaeger/pull/4122)) ### UI * No changes. 1.40.0 (2022-12-07) ------------------- ### Backend Changes #### New Features * Release signing ([@jkowall](https://github.com/jkowall) in [#4033](https://github.com/jaegertracing/jaeger/pull/4033)) #### Bug fixes, Minor Improvements * Fix cassandra schema scripts to be able to run from a remote node ([@yurishkuro](https://github.com/yurishkuro) in [#4087](https://github.com/jaegertracing/jaeger/pull/4087)) * Catch panic from Prometheus client on invalid label strings ([@yurishkuro](https://github.com/yurishkuro) in [#4051](https://github.com/jaegertracing/jaeger/pull/4051)) * Span tags of type int64 may lose precision ([@shubbham1215](https://github.com/shubbham1215) in [#4034](https://github.com/jaegertracing/jaeger/pull/4034)) ### UI * UI pinned to version [1.27.3](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1273-2022-12-07). 1.39.0 (2022-11-01) ------------------- ### Backend Changes #### New Features * Add support for OpenSearch 2.x ([@gaurav-05](https://github.com/gaurav-05) in [#3966](https://github.com/jaegertracing/jaeger/pull/3966)) #### Bug fixes, Minor Improvements * Pin SBOM action to commit ([@yurishkuro](https://github.com/yurishkuro) in [bb49249](https://github.com/jaegertracing/jaeger/commit/bb492490594c9d9321ed9242862ac2a8864ff771)) * Remove auth requirement on monitor demo ([@joe-elliott](https://github.com/joe-elliott) in [#4005](https://github.com/jaegertracing/jaeger/pull/4005)) * Increase sleep time when waiting for ES/OS backend ([@yurishkuro](https://github.com/yurishkuro) in [b9805f7](https://github.com/jaegertracing/jaeger/commit/b9805f7bc075224cfab37abab9df24ca51f38683)) * Fix CVE-2022-32149 for gotlang.org/x/text package ([@mehta-ankit](https://github.com/mehta-ankit) in [#3992](https://github.com/jaegertracing/jaeger/pull/3992)) * Expose otel configured thrift http port ([@albertteoh](https://github.com/albertteoh) in [#3986](https://github.com/jaegertracing/jaeger/pull/3986)) * Adding anchore for SBOM signing during release ([@jkowall](https://github.com/jkowall) in [#3987](https://github.com/jaegertracing/jaeger/pull/3987)) * Bump sarama to 1.33.0 ([@pavolloffay](https://github.com/pavolloffay) in [#3983](https://github.com/jaegertracing/jaeger/pull/3983)) * Add note on jaeger grpc storage compliance ([@arajkumar](https://github.com/arajkumar) in [#3985](https://github.com/jaegertracing/jaeger/pull/3985)) * Added link to FOSSA and Artifact Hub to README ([@jkowall](https://github.com/jkowall) in [#3964](https://github.com/jaegertracing/jaeger/pull/3964)) * Add grafana container to monitor docker-compose ([@albertteoh](https://github.com/albertteoh) in [#3955](https://github.com/jaegertracing/jaeger/pull/3955)) * Expose storage integration helpers as go pkg ([@arajkumar](https://github.com/arajkumar) in [#3944](https://github.com/jaegertracing/jaeger/pull/3944)) ### UI * UI pinned to version [1.27.2](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1272-2022-11-01). 1.38.1 (2022-10-04) ------------------- ### Backend Changes #### Bug fixes, Minor Improvements * Bump github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger ([@dependabot[bot]](https://github.com/apps/dependabot) in [#3939](https://github.com/jaegertracing/jaeger/pull/3939)) * Bump github.com/apache/thrift from 0.16.0 to 0.17.0 ([@dependabot[bot]](https://github.com/apps/dependabot) in [#3936](https://github.com/jaegertracing/jaeger/pull/3936)) * Bump github.com/hashicorp/go-hclog from 1.2.2 to 1.3.1 ([@dependabot[bot]](https://github.com/apps/dependabot) in [#3934](https://github.com/jaegertracing/jaeger/pull/3934)) * Bump go.opentelemetry.io/otel from 1.9.0 to 1.10.0 ([@dependabot[bot]](https://github.com/apps/dependabot) in [#3932](https://github.com/jaegertracing/jaeger/pull/3932)) * Bump github.com/hashicorp/go-plugin from 1.4.4 to 1.4.5 ([@dependabot[bot]](https://github.com/apps/dependabot) in [#3930](https://github.com/jaegertracing/jaeger/pull/3930)) * Bump github.com/spf13/viper from 1.12.0 to 1.13.0 ([@dependabot[bot]](https://github.com/apps/dependabot) in [#3931](https://github.com/jaegertracing/jaeger/pull/3931)) * Bump OTEL dependencies => v0.60.0 and grpc => v1.49.0 ([@yurishkuro](https://github.com/yurishkuro) in [#3928](https://github.com/jaegertracing/jaeger/pull/3928)) * Bump golang.org/x/net to 2022-09-26 ([@yurishkuro](https://github.com/yurishkuro) in [#3927](https://github.com/jaegertracing/jaeger/pull/3927)) * Bump codecov/codecov-action from 3.1.0 to 3.1.1 ([@dependabot[bot]](https://github.com/apps/dependabot) in [#3917](https://github.com/jaegertracing/jaeger/pull/3917)) ### UI Changes * UI pinned to version [1.27.1](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1271-2022-10-04) to bump dependencies. 1.38.0 (2022-09-16) ------------------- ### Backend Changes #### Bug fixes, Minor Improvements * fix: jaeger-agent sampling endpoint returns backwards incompatible JSON ([@vprithvi](https://github.com/vprithvi) in [#3897](https://github.com/jaegertracing/jaeger/pull/3897)) * fix: streaming span writer is not working in grpc based remote storage plugin ([@arajkumar](https://github.com/arajkumar) in [#3887](https://github.com/jaegertracing/jaeger/pull/3887)) * Fix race condition when adding collector tags ([@yurishkuro](https://github.com/yurishkuro) in [#3886](https://github.com/jaegertracing/jaeger/pull/3886)) * Change build info date to commit timestamp ([@TripleDogDare](https://github.com/TripleDogDare) in [#3876](https://github.com/jaegertracing/jaeger/pull/3876)) * Add 🚗 ([@yurishkuro](https://github.com/yurishkuro) in [55a8ca9](https://github.com/jaegertracing/jaeger/commit/55a8ca97e3772579b395ffbe4b937a4f5993b008)) * Add AdditionalDialOptions to ConnBuilder ([@vprithvi](https://github.com/vprithvi) in [#3865](https://github.com/jaegertracing/jaeger/pull/3865)) * Add sample docker-compose configuration using Kafka ([@yurishkuro](https://github.com/yurishkuro) in [7006e9f](https://github.com/jaegertracing/jaeger/commit/7006e9fe50c8467ad6b84f2072a3cf136bfbe4ec)) ### UI Changes * UI pinned to version [1.27.0 - see the changelog](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1270-2022-09-15). 1.37.0 (2022-08-03) ------------------- ### Backend Changes * Add remote-storage service ([@yurishkuro](https://github.com/yurishkuro) in [#3836](https://github.com/jaegertracing/jaeger/pull/3836)) #### Bug fixes, Minor Improvements * Fix ingester panic when span.process=nil ([@locmai](https://github.com/locmai) in [#3819](https://github.com/jaegertracing/jaeger/pull/3819)) * Added windows zip file generation ([@adhithyasrinivasan](https://github.com/adhithyasrinivasan) in [#3817](https://github.com/jaegertracing/jaeger/pull/3817)) * Refactor gRPC storage plugin for better composability ([@yurishkuro](https://github.com/yurishkuro) in [#3833](https://github.com/jaegertracing/jaeger/pull/3833)) ### UI Changes * UI pinned to version [1.26.0 - see the changelog](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1260-2022-08-03). 1.36.0 (2022-07-05) ------------------- ### Backend Changes #### New Features * Add flag to enable span size metrics reporting ([@ymtdzzz](https://github.com/ymtdzzz) in [#3782](https://github.com/jaegertracing/jaeger/pull/3782)) * Span size metrics are enabled via the `--collector.enable-span-size-metrics` flag (even if `--collector.queue-size-memory` is disabled). * Add multi-tenancy support ([@esnible](https://github.com/esnible) in [#3688](https://github.com/jaegertracing/jaeger/pull/3688)) * Enabled when `--multi_tenancy.enabled=true` is passed to the collector. * The header carrying the tenants can be specified via the `--multi_tenancy.header` flag, which defaults to `x-tenant`. * The list of allowed tenants can be set via the `--multi_tenancy.tenants` flag, which defaults to an unrestricted list of tenants. #### Bug fixes, Minor Improvements * Introduce ParentReference adjuster ([@bobrik](https://github.com/bobrik) in [#3786](https://github.com/jaegertracing/jaeger/pull/3786)) * Ignore the problem of self-reported spans when multi-tenant enabled ([@esnible](https://github.com/esnible) in [#3787](https://github.com/jaegertracing/jaeger/pull/3787)) * Copy expvar metrics implementation from jaeger-lib ([@yurishkuro](https://github.com/yurishkuro) in [#3772](https://github.com/jaegertracing/jaeger/pull/3772)) * Copy Prometheus metrics implementation from jaeger-lib ([@yurishkuro](https://github.com/yurishkuro) in [#3771](https://github.com/jaegertracing/jaeger/pull/3771)) * Copy metrics API from jaeger-lib ([@yurishkuro](https://github.com/yurishkuro) in [#3767](https://github.com/jaegertracing/jaeger/pull/3767)) * Use file move instead of overwriting content ([@yurishkuro](https://github.com/yurishkuro) in [#3726](https://github.com/jaegertracing/jaeger/pull/3726)) * Refactor tenancy checking from gRPC to gRPC batch consumer ([@esnible](https://github.com/esnible) in [#3718](https://github.com/jaegertracing/jaeger/pull/3718)) * Fix ineffectual `--skip-dependencies` flag in es-rollover ([@frzifus](https://github.com/frzifus) in [#3724](https://github.com/jaegertracing/jaeger/pull/3724)) * Fix custom gogo codec to allow OTLP data ([@yurishkuro](https://github.com/yurishkuro) in [#3719](https://github.com/jaegertracing/jaeger/pull/3719)) ### UI Changes * UI pinned to version [1.25.0 - see the changelog](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1250-2022-07-03). 1.35.2 (2022-06-15) ------------------- ### Backend Changes #### Bug fixes, Minor Improvements * fix: dependency flag not recognized ([@frzifus](https://github.com/frzifus) in [#3724](https://github.com/jaegertracing/jaeger/pull/3724)) 1.35.1 (2022-06-01) ------------------- ### Backend Changes #### Bug fixes, Minor Improvements - Fix custom gogo codec to allow OTLP data ([@yurishkuro](https://github.com/yurishkuro) in [#3719](https://github.com/jaegertracing/jaeger/pull/3719)) 1.35.0 (2022-06-01) ------------------- ### Backend Changes #### New Features - Introduce OTLP receiver configuration flags ([@yurishkuro](https://github.com/yurishkuro) in [#3710](https://github.com/jaegertracing/jaeger/pull/3710)) - Define Health Server for GRPC servers ([@mmorel-35](https://github.com/mmorel-35) in [#3712](https://github.com/jaegertracing/jaeger/pull/3712)) - Add OTLP receiver to collector ([@yurishkuro](https://github.com/yurishkuro) in [#3701](https://github.com/jaegertracing/jaeger/pull/3701)) - Add flag to enable/disable dependencies on rollover ([@rubenvp8510](https://github.com/rubenvp8510) in [#3705](https://github.com/jaegertracing/jaeger/pull/3705)) - Add TLS configuration for Admin Server ([@mmorel-35](https://github.com/mmorel-35) in [#3679](https://github.com/jaegertracing/jaeger/pull/3679)) - Add TLS configuration for Zipkin ([@mmorel-35](https://github.com/mmorel-35) in [#3676](https://github.com/jaegertracing/jaeger/pull/3676)) #### Bug fixes, Minor Improvements - Fix for WithTenant() lost orig context ([@esnible](https://github.com/esnible) in [#3685](https://github.com/jaegertracing/jaeger/pull/3685)) - Add entries to env command for new storage types ([@yurishkuro](https://github.com/yurishkuro) in [#3678](https://github.com/jaegertracing/jaeger/pull/3678)) - Fix Prometheus factory signature ([@yurishkuro](https://github.com/yurishkuro) in [#3681](https://github.com/jaegertracing/jaeger/pull/3681)) ### UI Changes * UI pinned to version [1.24.0 - see the changelog](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1240-2022-06-01). 1.34.0 (2022-05-11) ------------------- ### Backend Changes #### New Features * Add kubernetes example for hotrod app ([@highb](https://github.com/highb) in [#3645](https://github.com/jaegertracing/jaeger/pull/3645)) * Support writing via gRPC stream in storage plugin ([@vuuihc](https://github.com/vuuihc) in [#3640](https://github.com/jaegertracing/jaeger/pull/3640)) * Instrument MetricsReader with metrics ([@albertteoh](https://github.com/albertteoh) in [#3667](https://github.com/jaegertracing/jaeger/pull/3667)) #### Bug fixes, Minor Improvements * Sanitize spans with null process or empty service name ([@yurishkuro](https://github.com/yurishkuro) in [#3631](https://github.com/jaegertracing/jaeger/pull/3631)) * Flow tenant through processors as a string ([@esnible](https://github.com/esnible) in [#3661](https://github.com/jaegertracing/jaeger/pull/3661)) * Fix es.log-level behaviour ([@albertteoh](https://github.com/albertteoh) in [#3664](https://github.com/jaegertracing/jaeger/pull/3664)) * Mention remote gRPC storage API ([@yurishkuro](https://github.com/yurishkuro) in [cb6853d](https://github.com/jaegertracing/jaeger/commit/cb6853d4eea1ab5430f5e8db6b603cd7de5a16c3)) * Perform log.fatal if TLS flags are used when tls.enabled=false #2893 ([@clock21am](https://github.com/clock21am) in [#3030](https://github.com/jaegertracing/jaeger/pull/3030)) * Upgrade to Go 1.18 ([@yurishkuro](https://github.com/yurishkuro) in [#3624](https://github.com/jaegertracing/jaeger/pull/3624)) ### UI Changes * UI pinned to version 1.23.0. The changelog is available here [v1.23.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1230-2022-05-10). 1.33.0 (2022-04-11) ------------------- #### New Features * Add SAMPLING_STORAGE_TYPE environment variable ([@joe-elliott](https://github.com/joe-elliott) in [#3573](https://github.com/jaegertracing/jaeger/pull/3573)) * Support min/max TLS version in TLS config ([@Ashmita152](https://github.com/Ashmita152) in [#3567](https://github.com/jaegertracing/jaeger/pull/3567)) * Add support for ciphersuites in tls config ([@Ashmita152](https://github.com/Ashmita152) in [#3564](https://github.com/jaegertracing/jaeger/pull/3564)) #### Bug fixes, Minor Improvements * Fix: exit on grpc plugin crash ([@johanneswuerbach](https://github.com/johanneswuerbach) in [#3604](https://github.com/jaegertracing/jaeger/pull/3604)) * Bump go.opentelemetry.io/collector/model from 0.47.0 to 0.48.0 ([@dependabot[bot]](https://github.com/apps/dependabot) in [#3608](https://github.com/jaegertracing/jaeger/pull/3608)) * Fix format string for go1.18 ([@bobrik](https://github.com/bobrik) in [#3596](https://github.com/jaegertracing/jaeger/pull/3596)) * Fix favicon returning 500 inside container ([@Ashmita152](https://github.com/Ashmita152) in [#3569](https://github.com/jaegertracing/jaeger/pull/3569)) * Elasticsearch: Do not create index templates if ILM is enabled. ([@rbizos](https://github.com/rbizos) in [#3610](https://github.com/jaegertracing/jaeger/pull/3610)) ### UI Changes * UI pinned to version 1.22.0. The changelog is available here [v1.22.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1220-2022-04-11). 1.32.0 (2022-03-06) ------------------- ### Backend Changes #### New Features * Enable gRPC reflection service on collector/query ([@yurishkuro](https://github.com/yurishkuro) in [#3526](https://github.com/jaegertracing/jaeger/pull/3526)) #### Bug fixes, Minor Improvements * Fix query for latency metrics ([@Ashmita152](https://github.com/Ashmita152) in [#3559](https://github.com/jaegertracing/jaeger/pull/3559)) * Fix integration tests containing spans in the future ([@johanneswuerbach](https://github.com/johanneswuerbach) in [#3538](https://github.com/jaegertracing/jaeger/pull/3538)) * Add system diagram using mermaid markdown ([@yurishkuro](https://github.com/yurishkuro) in [#3529](https://github.com/jaegertracing/jaeger/pull/3529)) * Fix indexDateLayout for elasticsearch dependencies #3523 ([@ilyamor](https://github.com/ilyamor) in [#3524](https://github.com/jaegertracing/jaeger/pull/3524)) * Fix builds due to upstream OTEL proto path change ([@albertteoh](https://github.com/albertteoh) in [#3525](https://github.com/jaegertracing/jaeger/pull/3525)) ### UI Changes * UI pinned to version 1.21.0. The changelog is available here [v1.21.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1210-2022-03-06). 1.31.0 (2022-02-04) ------------------- ### Backend Changes #### Bug fixes, Minor Improvements * Bump Go compiler in CI to 1.17.6 ([@yurishkuro](https://github.com/yurishkuro) in [#3516](https://github.com/jaegertracing/jaeger/pull/3516)) * Add support for ES index aliases / rollover to the dependency store (Resolves #2143) ([@frittentheke](https://github.com/frittentheke) in [#2144](https://github.com/jaegertracing/jaeger/pull/2144)) * Use existing functions from xdg-go/scram pkg ([@yurishkuro](https://github.com/yurishkuro) in [#3481](https://github.com/jaegertracing/jaeger/pull/3481)) ### UI Changes * UI pinned to version 1.20.1. The changelog is available here [v1.20.1](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1201-2022-02-04). 1.30.0 (2022-01-11) ------------------- ### Backend Changes #### New Features * Add remote gRPC option for storage plugin ([@cevian](https://github.com/cevian) in [#3383](https://github.com/jaegertracing/jaeger/pull/3383)) * Build binaries for darwin/arm64 ([@jhchabran](https://github.com/jhchabran) in [#3431](https://github.com/jaegertracing/jaeger/pull/3431)) * Add MaxConnectionAge[Grace] to collector's gRPC server ([@jpkrohling](https://github.com/jpkrohling) in [#3422](https://github.com/jaegertracing/jaeger/pull/3422)) #### Bug fixes, Minor Improvements * Fix prefixed index rollover ([@albertteoh](https://github.com/albertteoh) in [#3457](https://github.com/jaegertracing/jaeger/pull/3457)) * Log problems communicating with Elasticsearch ([@esnible](https://github.com/esnible) in [#3451](https://github.com/jaegertracing/jaeger/pull/3451)) ### UI Changes * UI pinned to version 1.20.0. The changelog is available here [v1.20.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1200-jan-11-2022) 1.29.0 (2021-12-01) ------------------- ### Backend Changes #### ⛔ Breaking Changes * Remove deprecated `--badger.truncate` CLI flag ([@yurishkuro](https://github.com/yurishkuro) in [#3410](https://github.com/jaegertracing/jaeger/pull/3410)) #### New Features * Expose rackID option in ingester ([@shyimo](https://github.com/shyimo) in [#3395](https://github.com/jaegertracing/jaeger/pull/3395)) #### Bug fixes, Minor Improvements * Fix debug image builds by installing `build-base` to enable GCC ([@yurishkuro](https://github.com/yurishkuro) in [#3400](https://github.com/jaegertracing/jaeger/pull/3400)) * Limit URL size in Elasticsearch index delete request ([@jkandasa](https://github.com/jkandasa) in [#3375](https://github.com/jaegertracing/jaeger/pull/3375)) ### UI Changes * UI pinned to version 1.19.0. The changelog is available here [v1.19.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1190-dec-1-2021) 1.28.0 (2021-11-06) ------------------- ### Backend Changes * Add auth token propagation for metrics reader ([@albertteoh](https://github.com/albertteoh) in [#3341](https://github.com/jaegertracing/jaeger/pull/3341)) #### New Features * Add in-memory storage support for adaptive sampling ([@lonewolf3739](https://github.com/lonewolf3739) in [#3335](https://github.com/jaegertracing/jaeger/pull/3335)) #### Bug fixes, Minor Improvements * Do not throw error on empty indices in Elasticsach rollover lookback ([@jkandasa](https://github.com/jkandasa) in [#3369](https://github.com/jaegertracing/jaeger/pull/3369)) * Treat input throughput data as immutable ([@rbroggi](https://github.com/rbroggi) in [#3360](https://github.com/jaegertracing/jaeger/pull/3360)) * Remove dependencies on unused tools, install tools explicitly instead of via go.mod ([@rbroggi](https://github.com/rbroggi) in [#3355](https://github.com/jaegertracing/jaeger/pull/3355)) * Update mockery to version 2 and adapt to `install-tools` approach ([@rbroggi](https://github.com/rbroggi) in [#3358](https://github.com/jaegertracing/jaeger/pull/3358)) * Control lightweight storage integration tests via build tags ([@rbroggi](https://github.com/rbroggi) in [#3346](https://github.com/jaegertracing/jaeger/pull/3346)) * Remove package `integration` from coverage reports ([@rbroggi](https://github.com/rbroggi) in [#3357](https://github.com/jaegertracing/jaeger/pull/3357)) * Remove outdated reference to cover.sh ([@rbroggi](https://github.com/rbroggi) in [#3348](https://github.com/jaegertracing/jaeger/pull/3348)) * Update monitoring mixin ([@jpkrohling](https://github.com/jpkrohling) in [#3331](https://github.com/jaegertracing/jaeger/pull/3331)) * Update Jaeger chart link ([@isbang](https://github.com/isbang) in [#3328](https://github.com/jaegertracing/jaeger/pull/3328)) * Fix args order in strings.Contains in es-rollover ([@pavolloffay](https://github.com/pavolloffay) in [#3324](https://github.com/jaegertracing/jaeger/pull/3324)) * Use `(TB).TempDir` instead of non-portable `/mnt/*` in Badger ([@pavolloffay](https://github.com/pavolloffay) in [#3325](https://github.com/jaegertracing/jaeger/pull/3325)) * Fix `peer.service` retrieval from Zipkin's `MESSAGE_ADDR` annotation ([@Git-Jiro](https://github.com/Git-Jiro) in [#3312](https://github.com/jaegertracing/jaeger/pull/3312)) ### UI Changes * UI pinned to version 1.18.0. The changelog is available here [v1.18.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1180-nov-6-2021) 1.27.0 (2021-10-06) ------------------- ### Backend Changes * Migrate elasticsearch rollover to go ([#3242](https://github.com/jaegertracing/jaeger/pull/3242), [@rubenvp8510](https://github.com/rubenvp8510)) * Add 'opensearch' as a supported value for SPAN_STORAGE_TYPE ([#3255](https://github.com/jaegertracing/jaeger/pull/3255), [@yurishkuro](https://github.com/yurishkuro)) #### New Features * Add support for adaptive sampling with a Cassandra backend. ([#2966](https://github.com/jaegertracing/jaeger/pull/2966), [@joe-elliott](https://github.com/joe-elliott), [@Ashmita152](https://github.com/Ashmita152)) #### Bug fixes, Minor Improvements * Support graceful shutdown in grpc plugin ([#3249](https://github.com/jaegertracing/jaeger/pull/3249), [@slon](https://github.com/slon)) * Enable gzip compression for collector grpc endpoint. ([#3236](https://github.com/jaegertracing/jaeger/pull/3236), [@slon](https://github.com/slon)) * Use UTC in es-index-cleaner ([#3261](https://github.com/jaegertracing/jaeger/pull/3261), [@pavolloffay](https://github.com/pavolloffay)) * Upgrade to alpine-3.14 ([#3304](https://github.com/jaegertracing/jaeger/pull/3304), [@nontw](https://github.com/nontw)) * refactor: move from io/ioutil to io and os package ([#3294](https://github.com/jaegertracing/jaeger/pull/3294), [@Juneezee](https://github.com/Juneezee)) * Changed sampling type env var and updated collector help text ([#3302](https://github.com/jaegertracing/jaeger/pull/3302), [@joe-elliott](https://github.com/joe-elliott)) * Close #3270: Prevent rollover lookback from passing the Unix epoch ([#3299](https://github.com/jaegertracing/jaeger/pull/3299), [@ctreatma](https://github.com/ctreatma)) * Fixing otel configuration in docker compose ([#3286](https://github.com/jaegertracing/jaeger/pull/3286), [@Ashmita152](https://github.com/Ashmita152)) * Added ability to pass config file to grpc plugin in integration tests ([#3253](https://github.com/jaegertracing/jaeger/pull/3253), [@EinKrebs](https://github.com/EinKrebs)) ### UI Changes * UI pinned to version 1.17.0. The changelog is available here [v1.17.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1170-oct-6-2021) 1.26.0 (2021-09-06) ------------------- ### Backend Changes #### New Features * Add cassandra v4 support ([@Ashmita152](https://github.com/Ashmita152) in [#3225](https://github.com/jaegertracing/jaeger/pull/3225)). * **Note**: Users running on existing schema version [v3](https://github.com/jaegertracing/jaeger/blob/1d563f964da4b472a6f7e4cfdb85fe1167c30129/plugin/storage/cassandra/schema/v003.cql.tmpl) without issues, need not run an upgrade to [v4](https://github.com/jaegertracing/jaeger/blob/1d563f964da4b472a6f7e4cfdb85fe1167c30129/plugin/storage/cassandra/schema/v004.cql.tmpl). The new schema simply updates the table metadata, primarily removing `dclocal_read_repair_chance` deprecated table option, [which automatically gets ignored/removed when upgrading to Cassandra 4.0](https://issues.apache.org/jira/browse/CASSANDRA-13910). * Add es-index-cleaner golang implementation ([@pavolloffay](https://github.com/pavolloffay) in [#3204](https://github.com/jaegertracing/jaeger/pull/3204)) * Add CLI Option for gRPC Max Receive Message Size ([@js8080](https://github.com/js8080) in [#3214](https://github.com/jaegertracing/jaeger/pull/3214) and [#3192](https://github.com/jaegertracing/jaeger/pull/3192)) * Automatically detect OpenSearch version ([@pavolloffay](https://github.com/pavolloffay) in [#3198](https://github.com/jaegertracing/jaeger/pull/3198)) * Add SendGetBodyAs on elasticsearch ([@NatMarchand](https://github.com/NatMarchand) in [#3193](https://github.com/jaegertracing/jaeger/pull/3193)) * Set lookback in ES rollover to distant past ([@pavolloffay](https://github.com/pavolloffay) in [#3169](https://github.com/jaegertracing/jaeger/pull/3169)) #### Bug fixes, Minor Improvements * Check for invalid --agent.tags ([@esnible](https://github.com/esnible) in [#3246](https://github.com/jaegertracing/jaeger/pull/3246)) * Replace old linters with golangci-lint ([@albertteoh](https://github.com/albertteoh) in [#3237](https://github.com/jaegertracing/jaeger/pull/3237)) * Fix panic on empty findTraces query ([@akuzni2](https://github.com/akuzni2) in [#3232](https://github.com/jaegertracing/jaeger/pull/3232)) * Upgrade to Go v1.17 ([@Ashmita152](https://github.com/Ashmita152) in [#3220](https://github.com/jaegertracing/jaeger/pull/3220)) * Add docker buildx make target ([@pavolloffay](https://github.com/pavolloffay) in [#3213](https://github.com/jaegertracing/jaeger/pull/3213)) * Fix the name for elasticsearch integration tests ([@Ashmita152](https://github.com/Ashmita152) in [#3208](https://github.com/jaegertracing/jaeger/pull/3208)) * Upgrade ES images in integration tests ([@pavolloffay](https://github.com/pavolloffay) in [#3185](https://github.com/jaegertracing/jaeger/pull/3185)) ### UI Changes * UI pinned to version 1.16.0 - https://github.com/jaegertracing/jaeger-ui/releases/tag/v1.16.0 1.25.0 (2021-08-04) ------------------- #### New Features * Add query service with OTLP ([#3086](https://github.com/jaegertracing/jaeger/pull/3086), [@pavolloffay](https://github.com/pavolloffay)) * Add ppc64le support on multiarch docker images ([#3160](https://github.com/jaegertracing/jaeger/pull/3160), [@krishvoor](https://github.com/krishvoor)) #### Bug fixes, Minor Improvements * Fix base path in grpc gateway for api_v3 ([#3139](https://github.com/jaegertracing/jaeger/pull/3139), [@pavolloffay](https://github.com/pavolloffay)) * Add /api prefix for /v3 API ([#3178](https://github.com/jaegertracing/jaeger/pull/3178), [@pavolloffay](https://github.com/pavolloffay)) * Define `http.Server.ErrorLog` to forward logs to Zap ([#3157](https://github.com/jaegertracing/jaeger/pull/3157), [@yurishkuro](https://github.com/yurishkuro)) * Add ATM dev environment docker-compose and API doc ([#3171](https://github.com/jaegertracing/jaeger/pull/3171), [@albertteoh](https://github.com/albertteoh)) * Log the source of sampling strategies ([#3166](https://github.com/jaegertracing/jaeger/pull/3166), [@yurishkuro](https://github.com/yurishkuro)) * Pin elasticsearch-py to older version without elastic.co product check ([#3180](https://github.com/jaegertracing/jaeger/pull/3180), [@pavolloffay](https://github.com/pavolloffay)) 1.24.0 (2021-07-07) ------------------- ### Backend Changes #### ⛔ Breaking Changes * Upgrade Badger from v1.6.2 to v3.2103.0 ([#3096](https://github.com/jaegertracing/jaeger/pull/3096), [@Ashmita152](https://github.com/Ashmita152)): * Deprecated `--badger.truncate` flag. * All badger related expvar prefix has changed from `badger` to `badger_v3`. #### New Features * Add docker images for linux/arm64 ([#3124](https://github.com/jaegertracing/jaeger/pull/3124), [@GaruGaru](https://github.com/GaruGaru)) * Add s390x support on multiarch docker images ([#2948](https://github.com/jaegertracing/jaeger/pull/2948), [@kun-lu20](https://github.com/kun-lu20)) * Add TLS support for Prometheus reader ([#3096](https://github.com/jaegertracing/jaeger/pull/3096), [@albertteoh](https://github.com/albertteoh)) ##### [Monitor tab for service health metrics](https://github.com/jaegertracing/jaeger/issues/2954) * Add HTTP handler for metrics querying [#3095](https://github.com/jaegertracing/jaeger/pull/3095), [@albertteoh](https://github.com/albertteoh)) * Add MetricsQueryService grcp handler [#3091](https://github.com/jaegertracing/jaeger/pull/3091), [@albertteoh](https://github.com/albertteoh)) * Hook up MetricsQueryService to main funcs ([#3079](https://github.com/jaegertracing/jaeger/pull/3079), [@albertteoh](https://github.com/albertteoh)) * Add metrics query capability to query service ([#3061](https://github.com/jaegertracing/jaeger/pull/3061), [@albertteoh](https://github.com/albertteoh)) #### Bug fixes, Minor Improvements * Add build info metrics to Jaeger components ([#3087](https://github.com/jaegertracing/jaeger/pull/3087), [@Ashmita152](https://github.com/Ashmita152)) * Upgrade gRPC to 1.38.x ([#3096](https://github.com/jaegertracing/jaeger/pull/3096), [@pavolloffay](https://github.com/pavolloffay)) 1.23.0 (2021-06-04) ------------------- ### Backend Changes #### New Features #### ⛔ Breaking Changes * Remove unused `--es-archive.max-span-age` flag ([#2865](https://github.com/jaegertracing/jaeger/pull/2865), [@albertteoh](https://github.com/albertteoh)): #### New Features * Inject trace context to grpc metadata ([#2870](https://github.com/jaegertracing/jaeger/pull/2870), [@lujiajing1126](https://github.com/lujiajing1126)) * Passing default sampling strategies file as environment variable ([#3027](https://github.com/jaegertracing/jaeger/pull/3027), [@Ashmita152](https://github.com/Ashmita152)) * [es] Add index rollover mode that can choose day and hour ([#2965](https://github.com/jaegertracing/jaeger/pull/2965), [@WalkerWang731](https://github.com/WalkerWang731)) * Add a TIMEOUT environment variable for es rollover ([#2938](https://github.com/jaegertracing/jaeger/pull/2938), [@ediezh](https://github.com/ediezh)) * Allow the ILM policy name to be configurable ([#2971](https://github.com/jaegertracing/jaeger/pull/2971), [@jrRibeiro](https://github.com/jrRibeiro)) * [es] Add remote read clusters option for cross-cluster querying ([#2874](https://github.com/jaegertracing/jaeger/pull/2874), [@dgrizzanti](https://github.com/dgrizzanti)) * Enable logging in ES client ([#2862](https://github.com/jaegertracing/jaeger/pull/2862), [@albertteoh](https://github.com/albertteoh)) #### Bug fixes, Minor Improvements * Fix jaeger-agent reproducible memory leak ([#3050](https://github.com/jaegertracing/jaeger/pull/3050), [@jpkrohling](https://github.com/jpkrohling)) * Changed Range Query to use startTimeMillis date field instead of startTime field ([#2980](https://github.com/jaegertracing/jaeger/pull/2980), [@Sreevani871](https://github.com/Sreevani871)) * Verify FindTraces() received a query ([#2979](https://github.com/jaegertracing/jaeger/pull/2979), [@esnible](https://github.com/esnible)) * Set Content-Type in healthcheck's http response ([#2926](https://github.com/jaegertracing/jaeger/pull/2926), [@logeable](https://github.com/logeable)) * Add jaeger-query HTTP handler diagnostic logging ([#2906](https://github.com/jaegertracing/jaeger/pull/2906), [@albertteoh](https://github.com/albertteoh)) * Fix es-archive namespace default values ([#2865](https://github.com/jaegertracing/jaeger/pull/2865), [@albertteoh](https://github.com/albertteoh)) 1.22.0 (2021-02-23) ------------------- ### Backend Changes #### ⛔ Breaking Changes * Remove deprecated TLS flags ([#2790](https://github.com/jaegertracing/jaeger/issues/2790), [@albertteoh](https://github.com/albertteoh)): * `--cassandra.tls` is replaced by `--cassandra.tls.enabled` * `--cassandra-archive.tls` is replaced by `--cassandra-archive.tls.enabled` * `--collector.grpc.tls` is replaced by `--collector.grpc.tls.enabled` * `--collector.grpc.tls.client.ca` is replaced by `--collector.grpc.tls.client-ca` * `--es.tls` is replaced by `--es.tls.enabled` * `--es-archive.tls` is replaced by `--es-archive.tls.enabled` * `--kafka.consumer.tls` is replaced by `--kafka.consumer.tls.enabled` * `--kafka.producer.tls` is replaced by `--kafka.producer.tls.enabled` * `--reporter.grpc.tls` is replaced by `--reporter.grpc.tls.enabled` * Remove deprecated flags of Query Server `--query.port` and `--query.host-port`, please use dedicated HTTP `--query.http-server.host-port` (defaults to `:16686`) and gRPC `--query.grpc-server.host-port` (defaults to `:16685`) host-ports flags instead ([#2772](https://github.com/jaegertracing/jaeger/pull/2772), [@rjs211](https://github.com/rjs211)) * By default, if no flags are set, the query server starts on the dedicated ports. To use common port for gRPC and HTTP endpoints, the host-port flags have to be explicitly set * Remove deprecated CLI flags ([#2751](https://github.com/jaegertracing/jaeger/issues/2751), [@LostLaser](https://github.com/LostLaser)): * `--collector.http-port` is replaced by `--collector.http-server.host-port` * `--collector.grpc-port` is replaced by `--collector.grpc-server.host-port` * `--collector.zipkin.http-port` is replaced by `--collector.zipkin.host-port` * Remove deprecated flags `--health-check-http-port` & `--admin-http-port`, please use `--admin.http.host-port` ([#2752](https://github.com/jaegertracing/jaeger/pull/2752), [@pradeepnnv](https://github.com/pradeepnnv)) * Remove deprecated flag `--es.max-num-spans`, please use `--es.max-doc-count` ([#2482](https://github.com/jaegertracing/jaeger/pull/2482), [@BernardTolosajr](https://github.com/BernardTolosajr)) * Remove deprecated flag `--jaeger.tags`, please use `--agent.tags` ([#2753](https://github.com/jaegertracing/jaeger/pull/2753), [@yurishkuro](https://github.com/yurishkuro)) * Remove deprecated Cassandra flags ([#2789](https://github.com/jaegertracing/jaeger/pull/2789), [@albertteoh](https://github.com/albertteoh)): * `--cassandra.enable-dependencies-v2` - Jaeger will automatically detect the version of the dependencies table * `--cassandra.tls.verify-host` - please use `--cassandra.tls.skip-host-verify` instead * Remove incorrectly scoped downsample flags from the query service ([#2782](https://github.com/jaegertracing/jaeger/pull/2782), [@joe-elliott](https://github.com/joe-elliott)) * `--downsampling.hashsalt` removed from jaeger-query * `--downsampling.ratio` removed from jaeger-query #### New Features * Add TLS Support for gRPC and HTTP endpoints of the Query and Collector servers ([#2337](https://github.com/jaegertracing/jaeger/pull/2337), [#2772](https://github.com/jaegertracing/jaeger/pull/2772), [#2798](https://github.com/jaegertracing/jaeger/pull/2798), [@rjs211](https://github.com/rjs211)) * If TLS in enabled on either or both of gRPC or HTTP endpoints, the gRPC host-port and the HTTP host-port have to be different * If TLS is disabled on both endpoints, common HTTP and gRPC host-port can be explicitly set using the following host-port flags respectively: * Query: `--query.http-server.host-port` and `--query.grpc-server.host-port` * Collector: `--collector.http-server.host-port` and `--collector.grpc-server.host-port` * Add support for Kafka SASL/PLAIN authentication via SCRAM-SHA-256 or SCRAM-SHA-512 mechanism ([#2724](https://github.com/jaegertracing/jaeger/pull/2724), [@WalkerWang731](https://github.com/WalkerWang731)) * [agent] Add metrics to show connections status between agent and collectors ([#2657](https://github.com/jaegertracing/jaeger/pull/2657), [@WalkerWang731](https://github.com/WalkerWang731)) * Add plaintext as a supported kafka auth option ([#2721](https://github.com/jaegertracing/jaeger/pull/2721), [@pdepaepe](https://github.com/pdepaepe)) * Add ability to use JS file for UI configuration (#123 from jaeger-ui) ([#2707](https://github.com/jaegertracing/jaeger/pull/2707), [@th3M1ke](https://github.com/th3M1ke)) * Support Elasticsearch ILM for managing jaeger indices ([#2796](https://github.com/jaegertracing/jaeger/pull/2796), [@bhiravabhatla](https://github.com/bhiravabhatla)) * Push official images to quay.io, in addition to Docker Hub ([#2783](https://github.com/jaegertracing/jaeger/pull/2783), [@Ashmita152](https://github.com/Ashmita152)) * Add status command ([#2684](https://github.com/jaegertracing/jaeger/pull/2684), [@sniperking1234](https://github.com/sniperking1234)) * Usage: ```bash $ ./cmd/collector/collector-darwin-amd64 status {"status":"Server available","upSince":"2021-02-19T17:57:12.671902+11:00","uptime":"25.241233383s"} ``` * Support configurable date separator for Elasticsearch index names ([#2637](https://github.com/jaegertracing/jaeger/pull/2637), [@sniperking1234](https://github.com/sniperking1234)) #### Bug fixes, Minor Improvements * Use workaround for windows x509.SystemCertPool() ([#2756](https://github.com/jaegertracing/jaeger/pull/2756), [@Ashmita152](https://github.com/Ashmita152)) * Guard against mal-formed payloads received by the agent, potentially causing high memory utilization ([#2780](https://github.com/jaegertracing/jaeger/pull/2780), [@jpkrohling](https://github.com/jpkrohling)) * Expose cache TTL for ES span writer index+service ([#2737](https://github.com/jaegertracing/jaeger/pull/2737), [@necrolyte2](https://github.com/necrolyte2)) * Copy spans from memory store ([#2720](https://github.com/jaegertracing/jaeger/pull/2720), [@bobrik](https://github.com/bobrik)) * [pkg/queue] Add `StartConsumersWithFactory` function ([#2714](https://github.com/jaegertracing/jaeger/pull/2714), [@mx-psi](https://github.com/mx-psi)) * Fix potential cross-site scripting issue ([#2697](https://github.com/jaegertracing/jaeger/pull/2697), [@yurishkuro](https://github.com/yurishkuro)) * Updated gRPC Storage Plugin README with example ([#2687](https://github.com/jaegertracing/jaeger/pull/2687), [@js8080](https://github.com/js8080)) * Deduplicate collector tags ([#2658](https://github.com/jaegertracing/jaeger/pull/2658), [@Betula-L](https://github.com/Betula-L)) * Add latency metrics on collector HTTP endpoints ([#2664](https://github.com/jaegertracing/jaeger/pull/2664), [@dimitarvdimitrov](https://github.com/dimitarvdimitrov)) * Fix collector panic due to sarama sdk ([#2654](https://github.com/jaegertracing/jaeger/pull/2654), [@Betula-L](https://github.com/Betula-L)) * Handle collector Start error ([#2647](https://github.com/jaegertracing/jaeger/pull/2647), [@albertteoh](https://github.com/albertteoh)) * [anonymizer] Save trace in UI format ([#2629](https://github.com/jaegertracing/jaeger/pull/2629), [@yurishkuro](https://github.com/yurishkuro)) ### UI Changes * UI pinned to version 1.13.0. The changelog is available here [v1.13.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1130-february-20-2021) 1.21.0 (2020-11-13) ------------------- ### Backend Changes #### New Features * Add trace anonymizer utility ([#2621](https://github.com/jaegertracing/jaeger/pull/2621), [#2585](https://github.com/jaegertracing/jaeger/pull/2585), [@Ashmita152](https://github.com/Ashmita152)) * Add URL option for sampling strategies file ([#2519](https://github.com/jaegertracing/jaeger/pull/2519), [@goku321](https://github.com/goku321)) * Expose tunning options via expvar ([#2496](https://github.com/jaegertracing/jaeger/pull/2496), [@dstdfx](https://github.com/dstdfx)) * Support more encodings for Kafka in OTel Ingester ([#2580](https://github.com/jaegertracing/jaeger/pull/2580), [@XSAM](https://github.com/XSAM)) * Create debug docker images for jaeger backends ([#2545](https://github.com/jaegertracing/jaeger/pull/2545), [@Ashmita152](https://github.com/Ashmita152)) * Display backend & UI versions in Jaeger UI * Inject version info into index.html ([#2547](https://github.com/jaegertracing/jaeger/pull/2547), [@yurishkuro](https://github.com/yurishkuro)) * Added jaeger ui version to about menu ([#606](https://github.com/jaegertracing/jaeger-ui/pull/606), [@alanisaac](https://github.com/alanisaac)) #### Bug fixes, Minor Improvements * Update x/text to v0.3.4 ([#2625](https://github.com/jaegertracing/jaeger/pull/2625), [@objectiser](https://github.com/objectiser)) * Update CodeQL to latest best practices ([#2615](https://github.com/jaegertracing/jaeger/pull/2615), [@jhutchings1](https://github.com/jhutchings1)) * Bump opentelemetry-collector to v0.14.0 ([#2617](https://github.com/jaegertracing/jaeger/pull/2617), [@Vemmy124](https://github.com/Vemmy124)) * Bump Badger to v1.6.2 ([#2613](https://github.com/jaegertracing/jaeger/pull/2613), [@Ackar](https://github.com/Ackar)) * Fix sarama consumer deadlock ([#2587](https://github.com/jaegertracing/jaeger/pull/2587), [@albertteoh](https://github.com/albertteoh)) * Avoid deadlock if Stop is called before Serve ([#2608](https://github.com/jaegertracing/jaeger/pull/2608), [@chlunde](https://github.com/chlunde)) * Return buffers to pool on network errors or queue overflow ([#2609](https://github.com/jaegertracing/jaeger/pull/2609), [@chlunde](https://github.com/chlunde)) * Clarify deadlock panic message ([#2605](https://github.com/jaegertracing/jaeger/pull/2605), [@yurishkuro](https://github.com/yurishkuro)) * fix: don't create tags w/ empty name for internal zipkin spans ([#2596](https://github.com/jaegertracing/jaeger/pull/2596), [@mzahor](https://github.com/mzahor)) * TBufferedServer: Avoid channel close/send race on Stop ([#2583](https://github.com/jaegertracing/jaeger/pull/2583), [@chlunde](https://github.com/chlunde)) * Bumped OpenTelemetry Collector to v0.12.0 ([#2562](https://github.com/jaegertracing/jaeger/pull/2562), [@jpkrohling](https://github.com/jpkrohling)) * Disable Zipkin server if port/address is not configured ([#2554](https://github.com/jaegertracing/jaeger/pull/2554), [@yurishkuro](https://github.com/yurishkuro)) * [hotrod] Add links to traces ([#2536](https://github.com/jaegertracing/jaeger/pull/2536), [@yurishkuro](https://github.com/yurishkuro)) * OTel Cassandra/Elasticsearch Exporter queue defaults ([#2533](https://github.com/jaegertracing/jaeger/pull/2533), [@joe-elliott](https://github.com/joe-elliott)) * [otel] Update jaeger-lib to v2.4.0 ([#2538](https://github.com/jaegertracing/jaeger/pull/2538), [@dstdfx](https://github.com/dstdfx)) * Remove unnecessary ServiceName index seek if tags query is available ([#2535](https://github.com/jaegertracing/jaeger/pull/2535), [@burmanm](https://github.com/burmanm)) * Update static UI assets path in contrib doc ([#2548](https://github.com/jaegertracing/jaeger/pull/2548), [@albertteoh](https://github.com/albertteoh)) ### UI Changes * UI pinned to version 1.12.0. The changelog is available here [v1.12.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1120-november-14-2020) 1.20.0 (2020-09-29) ------------------- ### Backend Changes #### ⛔ Breaking Changes * Configurable ES doc count ([#2453](https://github.com/jaegertracing/jaeger/pull/2453), [@albertteoh](https://github.com/albertteoh)) The `--es.max-num-spans` flag has been deprecated in favour of `--es.max-doc-count`. `--es.max-num-spans` is marked for removal in v1.21.0 as indicated in the flag description. If both `--es.max-num-spans` and `--es.max-doc-count` are set, the lesser of the two will be used. The use of `--es.max-doc-count` (which defaults to 10,000) will limit the results from all Elasticsearch queries by the configured value, limiting counts for Jaeger UI: * Services * Operations * Dependencies (edges in a dependency graph) * Span fetch size for a trace * The default value for the flag `query.max-clock-skew-adjustment` has changed to `0s`, meaning that the clock skew adjustment is now disabled by default. See [#1459](https://github.com/jaegertracing/jaeger/issues/1459). #### New Features * Grpc plugin archive storage support ([#2317](https://github.com/jaegertracing/jaeger/pull/2317), [@m8rge](https://github.com/m8rge)) * Separate Ports for GRPC and HTTP requests in Query Server ([#2387](https://github.com/jaegertracing/jaeger/pull/2387), [@rjs211](https://github.com/rjs211)) * Configurable ES doc count ([#2453](https://github.com/jaegertracing/jaeger/pull/2453), [@albertteoh](https://github.com/albertteoh)) * Add storage metrics to OTEL, metrics by span service name ([#2431](https://github.com/jaegertracing/jaeger/pull/2431), [@pavolloffay](https://github.com/pavolloffay)) #### Bug fixes, Minor Improvements * Increase coverage on otel/app/defaultconfig and otel/app/defaultcomponents ([#2515](https://github.com/jaegertracing/jaeger/pull/2515), [@joe-elliott](https://github.com/joe-elliott)) * Use OTEL Kafka Exporter/Receiver Instead of Jaeger Core ([#2494](https://github.com/jaegertracing/jaeger/pull/2494), [@joe-elliott](https://github.com/joe-elliott)) * Fix OTEL kafka receiver/ingester panic ([#2512](https://github.com/jaegertracing/jaeger/pull/2512), [@pavolloffay](https://github.com/pavolloffay)) * Disable clock-skew-adjustment by default. ([#2513](https://github.com/jaegertracing/jaeger/pull/2513), [@jpkrohling](https://github.com/jpkrohling)) * Fix ES OTEL status code ([#2501](https://github.com/jaegertracing/jaeger/pull/2501), [@pavolloffay](https://github.com/pavolloffay)) * OTel: Factored out Config Factory ([#2495](https://github.com/jaegertracing/jaeger/pull/2495), [@joe-elliott](https://github.com/joe-elliott)) * Fix failing ServerInUseHostPort test on MacOS ([#2477](https://github.com/jaegertracing/jaeger/pull/2477), [@albertteoh](https://github.com/albertteoh)) * Fix unmarshalling in OTEL badger ([#2488](https://github.com/jaegertracing/jaeger/pull/2488), [@pavolloffay](https://github.com/pavolloffay)) * Improve UI placeholder message ([#2487](https://github.com/jaegertracing/jaeger/pull/2487), [@yurishkuro](https://github.com/yurishkuro)) * Translate OTEL instrumentation library to ES DB model ([#2484](https://github.com/jaegertracing/jaeger/pull/2484), [@pavolloffay](https://github.com/pavolloffay)) * Add partial retry capability to OTEL ES exporter. ([#2456](https://github.com/jaegertracing/jaeger/pull/2456), [@pavolloffay](https://github.com/pavolloffay)) * Log deprecation warning only when deprecated flags are set ([#2479](https://github.com/jaegertracing/jaeger/pull/2479), [@pavolloffay](https://github.com/pavolloffay)) * Clean-up Badger's trace-not-found check ([#2481](https://github.com/jaegertracing/jaeger/pull/2481), [@yurishkuro](https://github.com/yurishkuro)) * Run the jaeger-agent as a non-root user by default ([#2466](https://github.com/jaegertracing/jaeger/pull/2466), [@chgl](https://github.com/chgl)) * Regenerate certificates to use SANs instead of Common Name ([#2461](https://github.com/jaegertracing/jaeger/pull/2461), [@albertteoh](https://github.com/albertteoh)) * Support custom port in cassandra schema creation ([#2472](https://github.com/jaegertracing/jaeger/pull/2472), [@MarianZoll](https://github.com/MarianZoll)) * Consolidated OTel ES IndexNameProviders ([#2458](https://github.com/jaegertracing/jaeger/pull/2458), [@joe-elliott](https://github.com/joe-elliott)) * Add positive confirmation that Agent made a connection to Collector (… ([#2423](https://github.com/jaegertracing/jaeger/pull/2423), [@BernardTolosajr](https://github.com/BernardTolosajr)) * Propagate TraceNotFound error from grpc storage plugins ([#2455](https://github.com/jaegertracing/jaeger/pull/2455), [@joe-elliott](https://github.com/joe-elliott)) * Use new ES reader implementation in OTEL ([#2441](https://github.com/jaegertracing/jaeger/pull/2441), [@pavolloffay](https://github.com/pavolloffay)) * Updated grpc-go to v1.29.1 ([#2445](https://github.com/jaegertracing/jaeger/pull/2445), [@jpkrohling](https://github.com/jpkrohling)) * Remove olivere elastic client from OTEL ([#2448](https://github.com/jaegertracing/jaeger/pull/2448), [@pavolloffay](https://github.com/pavolloffay)) * Use queue retry per exporter ([#2444](https://github.com/jaegertracing/jaeger/pull/2444), [@pavolloffay](https://github.com/pavolloffay)) * Add context.Context to WriteSpan ([#2436](https://github.com/jaegertracing/jaeger/pull/2436), [@yurishkuro](https://github.com/yurishkuro)) * Fix mutex unlock in storage exporters ([#2442](https://github.com/jaegertracing/jaeger/pull/2442), [@pavolloffay](https://github.com/pavolloffay)) * Add Grafana integration example ([#2408](https://github.com/jaegertracing/jaeger/pull/2408), [@fktkrt](https://github.com/fktkrt)) * Fix TLS flags settings in jaeger OTEL receiver ([#2438](https://github.com/jaegertracing/jaeger/pull/2438), [@pavolloffay](https://github.com/pavolloffay)) * Add context to dependencies endpoint ([#2434](https://github.com/jaegertracing/jaeger/pull/2434), [@yoave23](https://github.com/yoave23)) * Fix error equals ([#2429](https://github.com/jaegertracing/jaeger/pull/2429), [@albertteoh](https://github.com/albertteoh)) ### UI Changes * UI pinned to version 1.11.0. The changelog is available here [v1.11.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1110-september-28-2020) 1.19.2 (2020-08-26) ------------------- Upgrade to a working UI version before React refactoring. 1.19.1 (2020-08-26) ------------------- Revert UI back to 1.9 due to a bug https://github.com/jaegertracing/jaeger-ui/issues/628 1.19.0 (2020-08-26) ------------------- ### Known Issues The pull request [#2297](https://github.com/jaegertracing/jaeger/pull/2297) aimed to add TLS support for the gRPC Query server but the flag registration is missing, so that this feature can't be used at the moment. A fix is planned for the next Jaeger version (1.20). ### Backend Changes #### New Features * Reload TLS certificates on change ([#2389](https://github.com/jaegertracing/jaeger/pull/2389), [@pavolloffay](https://github.com/pavolloffay)) * Add --kafka.producer.batch-min-messages collector flag ([#2371](https://github.com/jaegertracing/jaeger/pull/2371), [@prymitive](https://github.com/prymitive)) * Make UDP socket buffer size configurable ([#2336](https://github.com/jaegertracing/jaeger/pull/2336), [@kbarukhov](https://github.com/kbarukhov)) * Enable batch and queued retry processors by default ([#2330](https://github.com/jaegertracing/jaeger/pull/2330), [@pavolloffay](https://github.com/pavolloffay)) * Add trace anonymizer prototype ([#2328](https://github.com/jaegertracing/jaeger/pull/2328), [@yurishkuro](https://github.com/yurishkuro)) * Add native OTEL ES exporter ([#2295](https://github.com/jaegertracing/jaeger/pull/2295), [@pavolloffay](https://github.com/pavolloffay)) * Define busy error type in processor ([#2314](https://github.com/jaegertracing/jaeger/pull/2314), [@pavolloffay](https://github.com/pavolloffay)) * Use gRPC instead of tchannel in hotrod ([#2307](https://github.com/jaegertracing/jaeger/pull/2307), [@pavolloffay](https://github.com/pavolloffay)) * TLS support for gRPC Query server ([#2297](https://github.com/jaegertracing/jaeger/pull/2297), [@jan25](https://github.com/jan25)) #### Bug fixes, Minor Improvements * Check missing server URL in ES OTEL client ([#2411](https://github.com/jaegertracing/jaeger/pull/2411), [@pavolloffay](https://github.com/pavolloffay)) * Fix Elasticsearch version in ES OTEL writer ([#2409](https://github.com/jaegertracing/jaeger/pull/2409), [@pavolloffay](https://github.com/pavolloffay)) * Fix go vet warnings on Go 1.15 ([#2401](https://github.com/jaegertracing/jaeger/pull/2401), [@prymitive](https://github.com/prymitive)) * Add new Elasticsearch reader implementation ([#2364](https://github.com/jaegertracing/jaeger/pull/2364), [@pavolloffay](https://github.com/pavolloffay)) * Only add the collector port if it was not explicitly set ([#2396](https://github.com/jaegertracing/jaeger/pull/2396), [@joe-elliott](https://github.com/joe-elliott)) * Fix panic in collector when Zipkin server is shutdown ([#2392](https://github.com/jaegertracing/jaeger/pull/2392), [@Sreevani871](https://github.com/Sreevani871)) * Update validity of TLS certificates to 3650 days ([#2390](https://github.com/jaegertracing/jaeger/pull/2390), [@rjs211](https://github.com/rjs211)) * Added span and trace id to hotrod logs ([#2384](https://github.com/jaegertracing/jaeger/pull/2384), [@joe-elliott](https://github.com/joe-elliott)) * Jaeger agent logs "0" whenever sampling strategies are requested ([#2382](https://github.com/jaegertracing/jaeger/pull/2382), [@jpkrohling](https://github.com/jpkrohling)) * Fix shutdown order for collector components ([#2381](https://github.com/jaegertracing/jaeger/pull/2381), [@Sreevani871](https://github.com/Sreevani871)) * Serve jquery-3.1.1.min.js locally ([#2378](https://github.com/jaegertracing/jaeger/pull/2378), [@chaseSpace](https://github.com/chaseSpace)) * Use a single shared set of CA, client & server keys/certs for testing ([#2343](https://github.com/jaegertracing/jaeger/pull/2343), [@rjs211](https://github.com/rjs211)) * fix null pointer in jaeger-spark-dependencies ([#2327](https://github.com/jaegertracing/jaeger/pull/2327), [@moolen](https://github.com/moolen)) * Rename ARCH to TARGETARCH for multi platform build by docker buildx ([#2320](https://github.com/jaegertracing/jaeger/pull/2320), [@morlay](https://github.com/morlay)) * Mask passwords when written as json ([#2302](https://github.com/jaegertracing/jaeger/pull/2302), [@objectiser](https://github.com/objectiser)) ### UI Changes * UI pinned to version 1.10.0. The changelog is available here [v1.10.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v1100-august-25-2020) 1.18.1 (2020-06-19) ------------------ ### Backend Changes #### Security Fixes * CVE-2020-10750: jaegertracing/jaeger: credentials leaked to container logs ([@chlunde](https://github.com/chlunde)) #### ⛔ Breaking Changes #### New Features * Add ppc64le support ([#2293](https://github.com/jaegertracing/jaeger/pull/2293), [@Siddhesh-Ghadi](https://github.com/Siddhesh-Ghadi)) * Expose option to enable TLS when sniffing an Elasticsearch Cluster ([#2263](https://github.com/jaegertracing/jaeger/pull/2263), [@jennynilsen](https://github.com/jennynilsen)) * Enable OTEL receiver by default ([#2279](https://github.com/jaegertracing/jaeger/pull/2279), [@pavolloffay](https://github.com/pavolloffay)) * Add Badger OTEL exporter ([#2269](https://github.com/jaegertracing/jaeger/pull/2269), [@pavolloffay](https://github.com/pavolloffay)) * Add all-in-one OTEL component ([#2262](https://github.com/jaegertracing/jaeger/pull/2262), [@pavolloffay](https://github.com/pavolloffay)) * Add arm64 support for binaries and docker images ([#2176](https://github.com/jaegertracing/jaeger/pull/2176), [@MrXinWang](https://github.com/MrXinWang)) * Add Zipkin OTEL receiver ([#2247](https://github.com/jaegertracing/jaeger/pull/2247), [@pavolloffay](https://github.com/pavolloffay)) #### Bug fixes, Minor Improvements * Remove experimental flag from rollover ([#2290](https://github.com/jaegertracing/jaeger/pull/2290), [@pavolloffay](https://github.com/pavolloffay)) * Move ES dependencies index mapping to JSON template file ([#2285](https://github.com/jaegertracing/jaeger/pull/2285), [@pavolloffay](https://github.com/pavolloffay)) * Bump go-plugin to 1.3 ([#2261](https://github.com/jaegertracing/jaeger/pull/2261), [@yurishkuro](https://github.com/yurishkuro)) * Ignore chmod events on UI config watcher. ([#2254](https://github.com/jaegertracing/jaeger/pull/2254), [@rubenvp8510](https://github.com/rubenvp8510)) * Normalize CLI flags to use host:port addresses ([#2212](https://github.com/jaegertracing/jaeger/pull/2212), [@ohdearaugustin](https://github.com/ohdearaugustin)) * Add kafka receiver flags to ingester ([#2251](https://github.com/jaegertracing/jaeger/pull/2251), [@pavolloffay](https://github.com/pavolloffay)) * Configure Jaeger receiver and exporter by flags ([#2241](https://github.com/jaegertracing/jaeger/pull/2241), [@pavolloffay](https://github.com/pavolloffay)) ### UI Changes 1.18.0 (2020-05-14) ------------------ ### Backend Changes #### ⛔ Breaking Changes * Remove Tchannel between agent and collector ([#2115](https://github.com/jaegertracing/jaeger/pull/2115), [#2112](https://github.com/jaegertracing/jaeger/pull/2112), [@pavolloffay](https://github.com/pavolloffay)) Remove `Tchannel` port (14267) from collector and `Tchannel` reporter from agent. These flags were removed from agent: ``` --collector.host-port --reporter.tchannel.discovery.conn-check-timeout --reporter.tchannel.discovery.min-peers --reporter.tchannel.host-port --reporter.tchannel.report-timeout ``` These flags were removed from collector: ``` --collector.port ``` * Normalize CLI flags to use host:port addresses ([#1827](https://github.com/jaegertracing/jaeger/pull/1827), [@annanay25](https://github.com/annanay25)) Flags previously accepting listen addresses in any other format have been deprecated: * `collector.port` is superseded by `collector.tchan-server.host-port` * `collector.http-port` is superseded by `collector.http-server.host-port` * `collector.grpc-port` is superseded by `collector.grpc-server.host-port` * `collector.zipkin.http-port` is superseded by `collector.zipkin.host-port` * `admin-http-port` is superseded by `admin.http.host-port` #### New Features * Add grpc storage plugin OTEL exporter ([#2229](https://github.com/jaegertracing/jaeger/pull/2229), [@pavolloffay](https://github.com/pavolloffay)) * Add OTEL ingester component ([#2225](https://github.com/jaegertracing/jaeger/pull/2225), [@pavolloffay](https://github.com/pavolloffay)) * Add Kafka OTEL receiver/ingester ([#2221](https://github.com/jaegertracing/jaeger/pull/2221), [@pavolloffay](https://github.com/pavolloffay)) * Create Jaeger OTEL agent component ([#2216](https://github.com/jaegertracing/jaeger/pull/2216), [@pavolloffay](https://github.com/pavolloffay)) * Merge hardcoded/default configuration with OTEL config file ([#2211](https://github.com/jaegertracing/jaeger/pull/2211), [@pavolloffay](https://github.com/pavolloffay)) * Support periodic refresh of sampling strategies ([#2188](https://github.com/jaegertracing/jaeger/pull/2188), [@defool](https://github.com/defool)) * Add Elasticsearch OTEL exporter ([#2140](https://github.com/jaegertracing/jaeger/pull/2140), [@pavolloffay](https://github.com/pavolloffay)) * Add Cassandra OTEL exporter ([#2139](https://github.com/jaegertracing/jaeger/pull/2139), [@pavolloffay](https://github.com/pavolloffay)) * Add Kafka OTEL storage exporter ([#2135](https://github.com/jaegertracing/jaeger/pull/2135), [@pavolloffay](https://github.com/pavolloffay)) * Clock skew config ([#2119](https://github.com/jaegertracing/jaeger/pull/2119), [@joe-elliott](https://github.com/joe-elliott)) * Introduce OpenTelemetry collector ([#2086](https://github.com/jaegertracing/jaeger/pull/2086), [@pavolloffay](https://github.com/pavolloffay)) * Support regex tags search for Elasticseach backend ([#2049](https://github.com/jaegertracing/jaeger/pull/2049), [@annanay25](https://github.com/annanay25)) #### Bug fixes, Minor Improvements * Do not skip service/operation indexing for firehose spans ([#2242](https://github.com/jaegertracing/jaeger/pull/2242), [@yurishkuro](https://github.com/yurishkuro)) * Add build information to OTEL binaries ([#2237](https://github.com/jaegertracing/jaeger/pull/2237), [@pavolloffay](https://github.com/pavolloffay)) * Enable service default sampling param ([#2230](https://github.com/jaegertracing/jaeger/pull/2230), [@defool](https://github.com/defool)) * Add Jaeger OTEL agent to docker image upload ([#2227](https://github.com/jaegertracing/jaeger/pull/2227), [@ning2008wisc](https://github.com/ning2008wisc)) * Support adding process tags in OTEL via env variable ([#2220](https://github.com/jaegertracing/jaeger/pull/2220), [@pavolloffay](https://github.com/pavolloffay)) * Bump OTEL version and update exporters to use new API ([#2196](https://github.com/jaegertracing/jaeger/pull/2196), [@pavolloffay](https://github.com/pavolloffay)) * Support sampling strategies file flag in OTEL collector ([#2195](https://github.com/jaegertracing/jaeger/pull/2195), [@pavolloffay](https://github.com/pavolloffay)) * Add zipkin receiver to OTEL collector ([#2181](https://github.com/jaegertracing/jaeger/pull/2181), [@pavolloffay](https://github.com/pavolloffay)) * Add Dockerfile for OTEL collector and publish latest tag ([#2167](https://github.com/jaegertracing/jaeger/pull/2167), [@pavolloffay](https://github.com/pavolloffay)) * Run OTEL collector without configuration file ([#2148](https://github.com/jaegertracing/jaeger/pull/2148), [@pavolloffay](https://github.com/pavolloffay)) * Update gocql to support AWS MCS ([#2133](https://github.com/jaegertracing/jaeger/pull/2133), [@johanneswuerbach](https://github.com/johanneswuerbach)) * Return appropriate gRPC errors/codes to indicate request status ([#2132](https://github.com/jaegertracing/jaeger/pull/2132), [@yurishkuro](https://github.com/yurishkuro)) * Remove tchannel port from dockerfile and test ([#2118](https://github.com/jaegertracing/jaeger/pull/2118), [@pavolloffay](https://github.com/pavolloffay)) * Remove tchannel between agent and collector ([#2115](https://github.com/jaegertracing/jaeger/pull/2115), [@pavolloffay](https://github.com/pavolloffay)) * Move all tchannel packages to a single top level package ([#2112](https://github.com/jaegertracing/jaeger/pull/2112), [@pavolloffay](https://github.com/pavolloffay)) ### UI Changes * UI pinned to version 1.9.0. The changelog is available here [v1.9.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v190-may-14-2020) 1.17.1 (2020-03-13) ------------------ #### Bug fixes, Minor Improvements * Fix enable Kafka TLS when TLS auth is specified [#2107](https://github.com/jaegertracing/jaeger/pull/2107), [@pavolloffay](https://github.com/pavoloffay)) * Migrate project to go modules [#2098](https://github.com/jaegertracing/jaeger/pull/2098), [@pavolloffay](https://github.com/pavoloffay)) * Do not skip service/operation indexing for firehose spans [#2090](https://github.com/jaegertracing/jaeger/pull/2090), [@yurishkuro](https://github.com/yurishkuro)) * Close the span writer on main ([#2096](https://github.com/jaegertracing/jaeger/pull/2096), [@jpkrohling](https://github.com/jpkrohling)) * Improved graceful shutdown - Collector ([#2076](https://github.com/jaegertracing/jaeger/pull/2076), [@jpkrohling](https://github.com/jpkrohling)) * Improved graceful shutdown - Agent ([#2031](https://github.com/jaegertracing/jaeger/pull/2031), [@jpkrohling](https://github.com/jpkrohling)) ### UI Changes * UI pinned to version 1.8.0. The changelog is available here [v1.8.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v180-march-12-2020) 1.17.0 (2020-02-24) ------------------ ### Backend Changes #### New Features * [tracegen] Add service name as a command line option ([#2080](https://github.com/jaegertracing/jaeger/pull/2080), [@kevinearls](https://github.com/kevinearls)) * Allow the Configuration of Additional Headers on the Jaeger Query HTTP API ([#2056](https://github.com/jaegertracing/jaeger/pull/2056), [@joe-elliott](https://github.com/joe-elliott)) * Warn about time adjustment in tags ([#2052](https://github.com/jaegertracing/jaeger/pull/2052), [@bobrik](https://github.com/bobrik)) * Add CLI flags for Kafka batching params ([#2047](https://github.com/jaegertracing/jaeger/pull/2047), [@apm-opentt](https://github.com/apm-opentt)) * Added support for dynamic queue sizes ([#1985](https://github.com/jaegertracing/jaeger/pull/1985), [@jpkrohling](https://github.com/jpkrohling)) * [agent] Process data loss stats from clients ([#2010](https://github.com/jaegertracing/jaeger/pull/2010), [@yurishkuro](https://github.com/yurishkuro)) * Add /api/sampling endpoint in collector ([#1990](https://github.com/jaegertracing/jaeger/pull/1990), [@RickyRajinder](https://github.com/RickyRajinder)) * Add basic authentication to Kafka storage ([#1983](https://github.com/jaegertracing/jaeger/pull/1983), [@chandresh-pancholi](https://github.com/chandresh-pancholi)) * Make operation_strategies part also be part of default_strategy ([#1749](https://github.com/jaegertracing/jaeger/pull/1749), [@rutgerbrf](https://github.com/rutgerbrf)) #### Bug fixes, Minor Improvements * Upgrade gRPC to ^1.26 ([#2077](https://github.com/jaegertracing/jaeger/pull/2077), [@yurishkuro](https://github.com/yurishkuro)) * Remove pkg/errors from dependencies ([#2073](https://github.com/jaegertracing/jaeger/pull/2073), [@yurishkuro](https://github.com/yurishkuro)) * Update dependencies, pin grpc<1.27 ([#2072](https://github.com/jaegertracing/jaeger/pull/2072), [@yurishkuro](https://github.com/yurishkuro)) * Refactor collector mains ([#2060](https://github.com/jaegertracing/jaeger/pull/2060), [@jpkrohling](https://github.com/jpkrohling)) * Clarify that "kafka" is not a real storage backend ([#2066](https://github.com/jaegertracing/jaeger/pull/2066), [@yurishkuro](https://github.com/yurishkuro)) * Added -trimpath option to go build ([#2064](https://github.com/jaegertracing/jaeger/pull/2064), [@kadern0](https://github.com/kadern0)) * Use memory size flag to activate dyn queue size feature ([#2059](https://github.com/jaegertracing/jaeger/pull/2059), [@jpkrohling](https://github.com/jpkrohling)) * Add before you push to the queue to prevent race condition on size ([#2044](https://github.com/jaegertracing/jaeger/pull/2044), [@joe-elliott](https://github.com/joe-elliott)) * Count received batches from conforming clients ([#2030](https://github.com/jaegertracing/jaeger/pull/2030), [@yurishkuro](https://github.com/yurishkuro)) * [agent] Do not increment data loss counters on the first client batch ([#2028](https://github.com/jaegertracing/jaeger/pull/2028), [@yurishkuro](https://github.com/yurishkuro)) * Allow raw port numbers for UDP servers ([#2025](https://github.com/jaegertracing/jaeger/pull/2025), [@yurishkuro](https://github.com/yurishkuro)) * Publish tracegen ([#2022](https://github.com/jaegertracing/jaeger/pull/2022), [@jpkrohling](https://github.com/jpkrohling)) * Build binaries for Linux on IBM Z / s390x architecture ([#1982](https://github.com/jaegertracing/jaeger/pull/1982), [@prankkelkar](https://github.com/prankkelkar)) * Admin/Query: Log the real port instead of the provided one to enable the use of port 0 ([#2002](https://github.com/jaegertracing/jaeger/pull/2002), [@ChadiEM](https://github.com/ChadiEM)) * Split agent's HTTP server and handler ([#1996](https://github.com/jaegertracing/jaeger/pull/1996), [@yurishkuro](https://github.com/yurishkuro)) * Clean-up collector handlers builder ([#1991](https://github.com/jaegertracing/jaeger/pull/1991), [@yurishkuro](https://github.com/yurishkuro)) * Added 'resize' operation to BoundedQueue ([#1948](https://github.com/jaegertracing/jaeger/pull/1949), [@jpkrohling](https://github.com/jpkrohling)) * Add common TLS configuration ([#1838](https://github.com/jaegertracing/jaeger/pull/1838), [@backjo](https://github.com/backjo)) ### UI Changes * UI pinned to version 1.7.0. The changelog is available here [v1.7.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v170-february-21-2020) 1.16.0 (2019-12-17) ------------------ ### Backend Changes #### ⛔ Breaking Changes ##### List of service operations can be classified by span kinds ([#1943](https://github.com/jaegertracing/jaeger/pull/1943), [#1942](https://github.com/jaegertracing/jaeger/pull/1942), [#1937](https://github.com/jaegertracing/jaeger/pull/1937), [@guo0693](https://github.com/guo0693)) * Endpoint changes: * Both Http & gRPC servers now take new optional parameter `spanKind` in addition to `service`. When spanKind is absent or empty, operations from all kinds of spans will be returned. * Instead of returning a list of string, both Http & gRPC servers return a list of operation struct. Please update your client code to process the new response. Example response: ``` curl 'http://localhost:6686/api/operations?service=UserService&spanKind=server' | jq { "data": [{ "name": "UserService::getExtendedUser", "spanKind": "server" }, { "name": "UserService::getUserProfile", "spanKind": "server" }], "total": 2, "limit": 0, "offset": 0, "errors": null } ``` * The legacy http endpoint stay untouched: ``` /services/{%s}/operations ``` * Storage plugin changes: * Memory updated to support spanKind on write & read, no migration is required. * [Badger](https://github.com/jaegertracing/jaeger/issues/1922) & [ElasticSearch](https://github.com/jaegertracing/jaeger/issues/1923) to be implemented: For now `spanKind` will be set as empty string during read & write, only `name` will be valid operation name. * Cassandra updated to support spanKind on write & read ([#1937](https://github.com/jaegertracing/jaeger/pull/1937), [@guo0693](https://github.com/guo0693)): If you don't run the migration script, nothing will break, the system will use the old table `operation_names` and set empty `spanKind` in the response. Steps to get the updated functionality: 1. You will need to run the command below on the host where you can use `cqlsh` to connect to Cassandra: ``` KEYSPACE=jaeger_v1 CQL_CMD='cqlsh host 9042 -u test_user -p test_password --request-timeout=3000' bash ./v002tov003.sh ``` The script will create new table `operation_names_v2` and migrate data from the old table. `spanKind` column will be empty for those data. At the end, it will ask you whether you want to drop the old table or not. 2. Restart ingester & query services so that they begin to use the new table ##### Trace and Span IDs are always padded to 32 or 16 hex characters with leading zeros ([#1956](https://github.com/jaegertracing/jaeger/pull/1956), [@yurishkuro](https://github.com/yurishkuro)) Previously, Jaeger backend always rendered trace and span IDs as the shortest possible hex string, e.g. an ID with numeric value 255 would be rendered as a string `ff`. This change makes the IDs to always render as 16 or 32 characters long hex string, e.g. the same id=255 would render as `00000000000000ff`. It mostly affects how UI displays the IDs, the URLs, and the JSON returned from `jaeger-query` service. Motivation: Among randomly generated and uniformly distributed trace IDs, only 1/16th of them start with 0 followed by a significant digit, 1/256th start with two 0s, and so on in decreasing geometric progression. Therefore, trimming the leading 0s is a very modest optimization on the size of the data being transmitted or stored. However, trimming 0s leads to ambiguities when the IDs are used as correlations with other monitoring systems, such as logging, that treat the IDs as opaque strings and cannot establish the equivalence between padded and unpadded IDs. It is also incompatible with W3C Trace Context and Zipkin B3 formats, both of which include all leading 0s, so an application instrumented with OpenTelemetry SDKs may be logging different trace ID strings than application instrumented with Jaeger SDKs (related issue #1657). Overall, the change is backward compatible: * links with non-padded IDs in the UI will still work * data stored in Elasticsearch (where IDs are represented as strings) is still readable However, some custom integration that rely on exact string matches of trace IDs may be broken. ##### Change default rollover conditions to 2 days ([#1963](https://github.com/jaegertracing/jaeger/pull/1963), [@pavolloffay](https://github.com/pavolloffay)) Change default rollover conditions from 7 days to 2 days. Given that by default Jaeger uses daily indices and some organizations do not keep data longer than 7 days the default of 7 days seems unreasonable - it might result in a too big index and running curator would immediately remove the old index. #### New Features * Support collector tags, similar to agent tags ([#1854](https://github.com/jaegertracing/jaeger/pull/1854), [@radekg](https://github.com/radekg)) * Support insecure TLS and only CA cert for Elasticsearch ([#1918](https://github.com/jaegertracing/jaeger/pull/1918), [@pavolloffay](https://github.com/pavolloffay)) * Allow tracer config via env vars ([#1919](https://github.com/jaegertracing/jaeger/pull/1919), [@yurishkuro](https://github.com/yurishkuro)) * Allow turning off tags/logs indexing in Cassandra ([#1915](https://github.com/jaegertracing/jaeger/pull/1915), [@joe-elliott](https://github.com/joe-elliott)) * Blacklisting/Whitelisting tags for Cassandra indexing ([#1904](https://github.com/jaegertracing/jaeger/pull/1904), [@joe-elliott](https://github.com/joe-elliott)) #### Bug fixes, Minor Improvements * Support custom basepath in HotROD ([#1894](https://github.com/jaegertracing/jaeger/pull/1894), [@jan25](https://github.com/jan25)) * Deprecate tchannel reporter flags ([#1978](https://github.com/jaegertracing/jaeger/pull/1978), [@objectiser](https://github.com/objectiser)) * Do not truncate tags in Elasticsearch ([#1970](https://github.com/jaegertracing/jaeger/pull/1970), [@pavolloffay](https://github.com/pavolloffay)) * Export SaveSpan to enable multiplexing ([#1968](https://github.com/jaegertracing/jaeger/pull/1968), [@albertteoh](https://github.com/albertteoh)) * Make rollover init step idempotent ([#1964](https://github.com/jaegertracing/jaeger/pull/1964), [@pavolloffay](https://github.com/pavolloffay)) * Update python urllib3 version required by curator ([#1965](https://github.com/jaegertracing/jaeger/pull/1965), [@pavolloffay](https://github.com/pavolloffay)) * Allow changing max log level for gRPC storage plugins ([#1962](https://github.com/jaegertracing/jaeger/pull/1962), [@yyyogev](https://github.com/yyyogev)) * Fix the bug that operation_name table can not be init more than once ([#1961](https://github.com/jaegertracing/jaeger/pull/1961), [@guo0693](https://github.com/guo0693)) * Improve migration script ([#1946](https://github.com/jaegertracing/jaeger/pull/1946), [@guo0693](https://github.com/guo0693)) * Fix order of the returned results from badger backend. ([#1939](https://github.com/jaegertracing/jaeger/pull/1939), [@burmanm](https://github.com/burmanm)) * Update python pathlib to pathlib2 ([#1930](https://github.com/jaegertracing/jaeger/pull/1930), [@objectiser](https://github.com/objectiser)) * Use proxy env vars if they're configured ([#1910](https://github.com/jaegertracing/jaeger/pull/1910), [@zoidbergwill](https://github.com/zoidbergwill)) ### UI Changes * UI pinned to version 1.6.0. The changelog is available here [v1.6.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v160-december-16-2019) 1.15.1 (2019-11-07) ------------------ ##### Bug fixes, Minor Improvements * Build platform binaries as part of CI ([#1909](https://github.com/jaegertracing/jaeger/pull/1909), [@yurishkuro](https://github.com/yurishkuro)) * Upgrade and fix dependencies ([#1907](https://github.com/jaegertracing/jaeger/pull/1907), [@yurishkuro](https://github.com/yurishkuro)) 1.15.0 (2019-11-07) ------------------ #### Backend Changes ##### ⛔ Breaking Changes * The default value for the Ingester's flag `ingester.deadlockInterval` has been changed to `0` ([#1868](https://github.com/jaegertracing/jaeger/pull/1868), [@jpkrohling](https://github.com/jpkrohling)) With the new default, the ingester won't `panic` if there are no messages for the last minute. To restore the previous behavior, set the flag's value to `1m`. * Mark `--collector.grpc.tls.client.ca` flag as deprecated for jaeger-collector. ([#1840](https://github.com/jaegertracing/jaeger/pull/1840), [@yurishkuro](https://github.com/yurishkuro)) The deprecated flag will still work until being removed, it's recommended to use `--collector.grpc.tls.client-ca` instead. ##### New Features * Support TLS for Kafka ([#1414](https://github.com/jaegertracing/jaeger/pull/1414), [@MichaHoffmann](https://github.com/MichaHoffmann)) * Add ack and compression parameters for Kafka #1359 ([#1712](https://github.com/jaegertracing/jaeger/pull/1712), [@chandresh-pancholi](https://github.com/chandresh-pancholi)) * Propagate the bearer token to the gRPC plugin server ([#1822](https://github.com/jaegertracing/jaeger/pull/1822), [@radekg](https://github.com/radekg)) * Add Truncate and ReadOnly options for badger ([#1842](https://github.com/jaegertracing/jaeger/pull/1842), [@burmanm](https://github.com/burmanm)) ##### Bug fixes, Minor Improvements * Use correct context on ES search methods ([#1850](https://github.com/jaegertracing/jaeger/pull/1850), [@rubenvp8510](https://github.com/rubenvp8510)) * Handling of expected error codes coming from grpc storage plugins #1741 ([#1814](https://github.com/jaegertracing/jaeger/pull/1814), [@chandresh-pancholi](https://github.com/chandresh-pancholi)) * Fix ordering of indexScanKeys after TraceID parsing ([#1809](https://github.com/jaegertracing/jaeger/pull/1809), [@burmanm](https://github.com/burmanm)) * Small memory optimizations in badger write-path ([#1771](https://github.com/jaegertracing/jaeger/pull/1771), [@burmanm](https://github.com/burmanm)) * Set an empty value when a default env var value is missing ([#1777](https://github.com/jaegertracing/jaeger/pull/1777), [@jpkrohling](https://github.com/jpkrohling)) * Decouple storage dependencies and bump Go to 1.13.x ([#1886](https://github.com/jaegertracing/jaeger/pull/1886), [@yurishkuro](https://github.com/yurishkuro)) * Update gopkg.in/yaml.v2 dependency to v2.2.4 ([#1865](https://github.com/jaegertracing/jaeger/pull/1865), [@objectiser](https://github.com/objectiser)) * Upgrade jaeger-client 2.19 and jaeger-lib 2.2 and prom client 1.x ([#1810](https://github.com/jaegertracing/jaeger/pull/1810), [@yurishkuro](https://github.com/yurishkuro)) * Unpin grpc version and use serviceConfig to set the load balancer ([#1786](https://github.com/jaegertracing/jaeger/pull/1786), [@guanw](https://github.com/guanw)) #### UI Changes * UI pinned to version 1.5.0. The changelog is available here [v1.5.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v150-november-4-2019) 1.14.0 (2019-09-02) ------------------ #### Backend Changes ##### ⛔ Breaking Changes * Create ES index templates instead of indices ([#1627](https://github.com/jaegertracing/jaeger/pull/1627), [@pavolloffay](https://github.com/pavolloffay)) This can break existing Elasticsearch deployments if security policies are applied. For instance Jaeger `X-Pack` configuration now requires permission to create index templates - `manage_index_templates`. ##### New Features * Add Elasticsearch version configuration to rollover script ([#1769](https://github.com/jaegertracing/jaeger/pull/1769), [@pavolloffay](https://github.com/pavolloffay)) * Add Elasticsearch version flag ([#1753](https://github.com/jaegertracing/jaeger/pull/1753), [@pavolloffay](https://github.com/pavolloffay)) * Add Elasticsearch 7 support ([#1690](https://github.com/jaegertracing/jaeger/pull/1690), [@gregoryfranklin](https://github.com/gregoryfranklin)) The index mappings in Elasticsearch 7 are not backwards compatible with the older versions. Therefore using Elasticsearch 7 with data created with older version would not work. Elasticsearch 6.8 supports 7.x, 6.x, 5.x compatible mappings. The upgrade has to be done first to ES 6.8, then apply data migration or wait until old daily indices are removed (this requires to start Jaeger with `--es.version=7` to force using ES 7.x mappings for newly created indices). Jaeger by default uses Elasticsearch ping endpoint (`/`) to derive the version which is used for index mappings selection. The version can be overridden by flag `--es.version`. * Support for Zipkin Protobuf spans over HTTP ([#1695](https://github.com/jaegertracing/jaeger/pull/1695), [@jan25](https://github.com/jan25)) * Added support for hot reload of UI config ([#1688](https://github.com/jaegertracing/jaeger/pull/1688), [@jpkrohling](https://github.com/jpkrohling)) * Added base Grafana dashboard and Alert rules ([#1745](https://github.com/jaegertracing/jaeger/pull/1745), [@jpkrohling](https://github.com/jpkrohling)) * Add the jaeger-mixin for monitoring ([#1668](https://github.com/jaegertracing/jaeger/pull/1668), [@gouthamve](https://github.com/gouthamve)) * Added flags for driving cassandra connection compression through config ([#1675](https://github.com/jaegertracing/jaeger/pull/1675), [@sagaranand015](https://github.com/sagaranand015)) * Support index cleaner for rollover indices and add integration tests ([#1689](https://github.com/jaegertracing/jaeger/pull/1689), [@pavolloffay](https://github.com/pavolloffay)) * Add client TLS auth to gRPC reporter ([#1591](https://github.com/jaegertracing/jaeger/pull/1591), [@tcolgate](https://github.com/tcolgate)) * Collector kafka producer protocol version config ([#1658](https://github.com/jaegertracing/jaeger/pull/1658), [@marqc](https://github.com/marqc)) * Configurable kafka protocol version for msg consuming by jaeger ingester ([#1640](https://github.com/jaegertracing/jaeger/pull/1640), [@marqc](https://github.com/marqc)) * Use credentials when describing keyspaces in cassandra schema builder ([#1655](https://github.com/jaegertracing/jaeger/pull/1655), [@MiLk](https://github.com/MiLk)) * Add connect-timeout for Cassandra ([#1647](https://github.com/jaegertracing/jaeger/pull/1647), [@sagaranand015](https://github.com/sagaranand015)) ##### Bug fixes, Minor Improvements * Fix gRPC over cmux and add unit tests ([#1758](https://github.com/jaegertracing/jaeger/pull/1758), [@yurishkuro](https://github.com/yurishkuro)) * Add CA certificates to agent image ([#1764](https://github.com/jaegertracing/jaeger/pull/1764), [@yurishkuro](https://github.com/yurishkuro)) * Fix badger merge-join algorithm to correctly filter indexes ([#1721](https://github.com/jaegertracing/jaeger/pull/1721), [@burmanm](https://github.com/burmanm)) * Change Zipkin CORS origins and headers to comma separated list ([#1556](https://github.com/jaegertracing/jaeger/pull/1556), [@JonasVerhofste](https://github.com/JonasVerhofste)) * Added null guards to 'Process' when processing an incoming span ([#1723](https://github.com/jaegertracing/jaeger/pull/1723), [@jpkrohling](https://github.com/jpkrohling)) * Export expvar metrics of badger to the metricsFactory ([#1704](https://github.com/jaegertracing/jaeger/pull/1704), [@burmanm](https://github.com/burmanm)) * Pass TTL as int, not as float64 ([#1710](https://github.com/jaegertracing/jaeger/pull/1710), [@yurishkuro](https://github.com/yurishkuro)) * Use find by regex for archive index in index cleaner ([#1693](https://github.com/jaegertracing/jaeger/pull/1693), [@pavolloffay](https://github.com/pavolloffay)) * Allow token propagation if token type is not specified ([#1685](https://github.com/jaegertracing/jaeger/pull/1685), [@rubenvp8510](https://github.com/rubenvp8510)) * Fix duplicated spans when querying Elasticsearch ([#1677](https://github.com/jaegertracing/jaeger/pull/1677), [@pavolloffay](https://github.com/pavolloffay)) * Fix the threshold precision issue ([#1665](https://github.com/jaegertracing/jaeger/pull/1665), [@guanw](https://github.com/guanw)) * Badger filter duplicate results from a single indexSeek ([#1649](https://github.com/jaegertracing/jaeger/pull/1649), [@burmanm](https://github.com/burmanm)) * Badger make default dirs work in Windows ([#1653](https://github.com/jaegertracing/jaeger/pull/1653), [@burmanm](https://github.com/burmanm)) #### UI Changes * UI pinned to version 1.4.0. The changelog is available here [v1.4.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v130-june-21-2019) 1.13.1 (2019-06-28) ------------------ #### Backend Changes ##### Bug fixes, Minor Improvements * Change default for bearer-token-propagation to false ([#1642](https://github.com/jaegertracing/jaeger/pull/1642), [@wsoula](https://github.com/wsoula)) #### UI Changes 1.13.0 (2019-06-27) ------------------ #### Backend Changes ##### ⛔ Breaking Changes * The traces related metrics on collector now have a new tag `sampler_type` ([#1576](https://github.com/jaegertracing/jaeger/pull/1576), [@guanw](https://github.com/guanw)) This might break some existing metrics dashboard (if so, users need to update query to aggregate over this new tag). The list of metrics affected: `traces.received`, `traces.rejected`, `traces.saved-by-svc`. * Remove deprecated index prefix separator `:` from Elastic ([#1620](https://github.com/jaegertracing/jaeger/pull/1620), [@pavolloffay](https://github.com/pavolloffay)) In Jaeger 1.9.0 release the Elasticsearch index separator was changed from `:` to `-`. To keep backwards compatibility the query service kept querying indices with `:` separator, however the new indices were created only with `-`. This release of Jaeger removes the query capability for indices containing `:`, therefore it's recommended to keep using older version until indices containing old separator are not queried anymore. ##### New Features * Passthrough OAuth bearer token supplied to Query service through to ES storage ([#1599](https://github.com/jaegertracing/jaeger/pull/1599), [@rubenvp8510](https://github.com/rubenvp8510)) * Kafka kerberos authentication support for collector/ingester ([#1589](https://github.com/jaegertracing/jaeger/pull/1589), [@rubenvp8510](https://github.com/rubenvp8510)) * Allow Cassandra schema builder to use credentials ([#1635](https://github.com/jaegertracing/jaeger/pull/1635), [@PS-EGHornbostel](https://github.com/PS-EGHornbostel)) * Add docs generation command ([#1572](https://github.com/jaegertracing/jaeger/pull/1572), [@pavolloffay](https://github.com/pavolloffay)) ##### Bug fixes, Minor Improvements * Fix data race between `Agent.Run()` and `Agent.Stop()` ([#1625](https://github.com/jaegertracing/jaeger/pull/1625), [@tigrannajaryan](https://github.com/tigrannajaryan)) * Use json number when unmarshalling data from ES ([#1618](https://github.com/jaegertracing/jaeger/pull/1618), [@pavolloffay](https://github.com/pavolloffay)) * Define logs as nested data type ([#1622](https://github.com/jaegertracing/jaeger/pull/1622), [@pavolloffay](https://github.com/pavolloffay)) * Fix archive storage not querying old spans older than maxSpanAge ([#1617](https://github.com/jaegertracing/jaeger/pull/1617), [@pavolloffay](https://github.com/pavolloffay)) * Query service: fix logging errors on SIGINT ([#1601](https://github.com/jaegertracing/jaeger/pull/1601), [@jan25](https://github.com/jan25)) * Direct grpc logs to Zap logger ([#1606](https://github.com/jaegertracing/jaeger/pull/1606), [@yurishkuro](https://github.com/yurishkuro)) * Fix sending status to health check channel in Query service ([#1598](https://github.com/jaegertracing/jaeger/pull/1598), [@jan25](https://github.com/jan25)) * Add tmp-volume to all-in-one image to fix badger storage ([#1571](https://github.com/jaegertracing/jaeger/pull/1571), [@burmanm](https://github.com/burmanm)) * Do not fail es-cleaner if there are no jaeger indices ([#1569](https://github.com/jaegertracing/jaeger/pull/1569), [@pavolloffay](https://github.com/pavolloffay)) * Automatically set `GOMAXPROCS` ([#1560](https://github.com/jaegertracing/jaeger/pull/1560), [@rubenvp8510](https://github.com/rubenvp8510)) * Add CA certs to all-in-one image ([#1554](https://github.com/jaegertracing/jaeger/pull/1554), [@chandresh-pancholi](https://github.com/chandresh-pancholi)) #### UI Changes * UI pinned to version 1.3.0. The changelog is available here [v1.3.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v130-june-21-2019) 1.12.0 (2019-05-16) ------------------ #### Backend Changes ##### ⛔ Breaking Changes - The `kafka` flags were removed in favor of `kafka.producer` and `kafka.consumer` flags ([#1424](https://github.com/jaegertracing/jaeger/pull/1424), [@ledor473](https://github.com/ledor473)) The following flags have been **removed** in the Collector and the Ingester: ``` --kafka.brokers --kafka.encoding --kafka.topic --ingester.brokers --ingester.encoding --ingester.topic --ingester.group-id ``` In the Collector, they are replaced by: ``` --kafka.producer.brokers --kafka.producer.encoding --kafka.producer.topic ``` In the Ingester, they are replaced by: ``` --kafka.consumer.brokers --kafka.consumer.encoding --kafka.consumer.topic --kafka.consumer.group-id ``` * Add Admin port and group all ports in one file ([#1442](https://github.com/jaegertracing/jaeger/pull/1442), [@yurishkuro](https://github.com/yurishkuro)) This change fixes issues [#1428](https://github.com/jaegertracing/jaeger/issues/1428), [#1332](https://github.com/jaegertracing/jaeger/issues/1332) and moves all metrics endpoints from API ports to **admin ports**. It requires re-configuring Prometheus scraping rules. Each Jaeger binary has its own admin port that can be found under `--admin-http-port` command line flag by running the `${binary} help` command. ##### New Features * Add gRPC resolver using external discovery service ([#1498](https://github.com/jaegertracing/jaeger/pull/1498), [@guanw](https://github.com/guanw)) * gRPC storage plugin framework ([#1461](https://github.com/jaegertracing/jaeger/pull/1461), [@chvck](https://github.com/chvck)) * Supports customized kafka client id ([#1507](https://github.com/jaegertracing/jaeger/pull/1507), [@newly12](https://github.com/newly12)) * Support gRPC for query service ([#1307](https://github.com/jaegertracing/jaeger/pull/1307), [@annanay25](https://github.com/annanay25)) * Expose tls.InsecureSkipVerify to es.tls.* CLI flags ([#1473](https://github.com/jaegertracing/jaeger/pull/1473), [@stefanvassilev](https://github.com/stefanvassilev)) * Return info msg for `/health` endpoint ([#1465](https://github.com/jaegertracing/jaeger/pull/1465), [@stefanvassilev](https://github.com/stefanvassilev)) * Add pprof endpoint to admin endpoint ([#1375](https://github.com/jaegertracing/jaeger/pull/1375), [@konradgaluszka](https://github.com/konradgaluszka)) * Add inbound transport as label to collector metrics [#1446](https://github.com/jaegertracing/jaeger/pull/1446) ([guanw](https://github.com/guanw)) * Sorted key/value store `badger` backed storage plugin ([#760](https://github.com/jaegertracing/jaeger/pull/760), [@burmanm](https://github.com/burmanm)) * Add Admin port and group all ports in one file ([#1442](https://github.com/jaegertracing/jaeger/pull/1442), [@yurishkuro](https://github.com/yurishkuro)) * Adds support for agent level tag ([#1396](https://github.com/jaegertracing/jaeger/pull/1396), [@annanay25](https://github.com/annanay25)) * Add a Downsampling writer that drop a percentage of spans ([#1353](https://github.com/jaegertracing/jaeger/pull/1353), [@guanw](https://github.com/guanw)) ##### Bug fixes, Minor Improvements * Sort traces in memory store to return most recent traces ([#1394](https://github.com/jaegertracing/jaeger/pull/1394), [@jacobmarble](https://github.com/jacobmarble)) * Add span format tag for jaeger-collector ([#1493](https://github.com/jaegertracing/jaeger/pull/1493), [@guo0693](https://github.com/guo0693)) * Upgrade gRPC to 1.20.1 ([#1492](https://github.com/jaegertracing/jaeger/pull/1492), [@guanw](https://github.com/guanw)) * Switch from counter to a gauge for partitions held ([#1485](https://github.com/jaegertracing/jaeger/pull/1485), [@bobrik](https://github.com/bobrik)) * Add CORS handling for Zipkin collector service ([#1463](https://github.com/jaegertracing/jaeger/pull/1463), [@JonasVerhofste](https://github.com/JonasVerhofste)) * Check elasticsearch nil response ([#1467](https://github.com/jaegertracing/jaeger/pull/1467), [@YEXINGZHE54](https://github.com/YEXINGZHE54)) * Disable sampling in logger - `zap`([#1460](https://github.com/jaegertracing/jaeger/pull/1460), [@psinghal20](https://github.com/psinghal20)) * New layout for proto definitions and generated files ([#1427](https://github.com/jaegertracing/jaeger/pull/1427), [@annanay25](https://github.com/annanay25)) * Upgrade Go to 1.12.1 ([#1437](https://github.com/jaegertracing/jaeger/pull/1437) ,[@yurishkuro](https://github.com/yurishkuro)) #### UI Changes * UI pinned to version 1.2.0. The changelog is available here [v1.2.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v120-may-14-2019) 1.11.0 (2019-03-07) ------------------ #### Backend Changes ##### ⛔ Breaking Changes - Introduce `kafka.producer` and `kafka.consumer` flags to replace `kafka` flags ([#1360](https://github.com/jaegertracing/jaeger/pull/1360), [@ledor473](https://github.com/ledor473)) The following flags have been deprecated in the Collector and the Ingester: ``` --kafka.brokers --kafka.encoding --kafka.topic ``` In the Collector, they are replaced by: ``` --kafka.producer.brokers --kafka.producer.encoding --kafka.producer.topic ``` In the Ingester, they are replaced by: ``` --kafka.consumer.brokers --kafka.consumer.encoding --kafka.consumer.group-id ``` ##### New Features - Support secure gRPC channel between agent and collector ([#1391](https://github.com/jaegertracing/jaeger/pull/1391), [@ghouscht](https://github.com/ghouscht), [@yurishkuro](https://github.com/yurishkuro)) - Allow to use TLS with ES basic auth ([#1388](https://github.com/jaegertracing/jaeger/pull/1388), [@pavolloffay](https://github.com/pavolloffay)) ##### Bug fixes, Minor Improvements - Make `esRollover.py init` idempotent ([#1407](https://github.com/jaegertracing/jaeger/pull/1407) and [#1408](https://github.com/jaegertracing/jaeger/pull/1408), [@pavolloffay](https://github.com/pavolloffay)) - Allow thrift reporter if grpc hosts are not provided ([#1400](https://github.com/jaegertracing/jaeger/pull/1400), [@pavolloffay](https://github.com/pavolloffay)) - Deprecate colon in index prefix in ES dependency store ([#1386](https://github.com/jaegertracing/jaeger/pull/1386), [@pavolloffay](https://github.com/pavolloffay)) - Make grpc reporter default and add retry ([#1384](https://github.com/jaegertracing/jaeger/pull/1384), [@pavolloffay](https://github.com/pavolloffay)) - Use `CQLSH_HOST` in final call to `cqlsh` ([#1372](https://github.com/jaegertracing/jaeger/pull/1372), [@funny-falcon](https://github.com/funny-falcon)) #### UI Changes * UI pinned to version 1.1.0. The changelog is available here [v1.1.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v110-march-3-2019) 1.10.1 (2019-02-21) ------------------ #### Backend Changes - Discover dependencies table version automatically ([#1364](https://github.com/jaegertracing/jaeger/pull/1364), [@black-adder](https://github.com/black-adder)) ##### Bug fixes, Minor Improvements - Separate query-service functionality from http handler ([#1312](https://github.com/jaegertracing/jaeger/pull/1312), [@annanay25](https://github.com/annanay25)) #### UI Changes 1.10.0 (2019-02-15) ------------------ #### Backend Changes ##### ⛔ Breaking Changes - Remove cassandra SASI indices ([#1328](https://github.com/jaegertracing/jaeger/pull/1328), [@black-adder](https://github.com/black-adder)) Migration Path: 1. Run `plugin/storage/cassandra/schema/migration/v001tov002part1.sh` which will copy dependencies into a csv, update the `dependency UDT`, create a new `dependencies_v2` table, and write dependencies from the csv into the `dependencies_v2` table. 2. Run the collector and query services with the cassandra flag `cassandra.enable-dependencies-v2=true` which will instruct jaeger to write and read to and from the new `dependencies_v2` table. 3. Update [spark job](https://github.com/jaegertracing/spark-dependencies) to write to the new `dependencies_v2` table. The feature will be done in [#58](https://github.com/jaegertracing/spark-dependencies/issues/58). 4. Run `plugin/storage/cassandra/schema/migration/v001tov002part2.sh` which will DELETE the old dependency table and the SASI index. Users who wish to continue to use the v1 table don't have to do anything as the cassandra flag `cassandra.enable-dependencies-v2` will default to false. Users may migrate on their own timeline however new features will be built solely on the `dependencies_v2` table. In the future, we will remove support for v1 completely. - Remove `ErrorBusy`, it essentially duplicates `SpansDropped` ([#1091](https://github.com/jaegertracing/jaeger/pull/1091), [@cstyan](https://github.com/cstyan)) ##### New Features - Support certificates in elasticsearch scripts ([#1339](https://github.com/jaegertracing/jaeger/pull/1399), [@pavolloffay](https://github.com/pavolloffay)) - Add ES Rollover support to main indices ([#1309](https://github.com/jaegertracing/jaeger/pull/1309), [@pavolloffay](https://github.com/pavolloffay)) - Load ES auth token from file ([#1319](https://github.com/jaegertracing/jaeger/pull/1319), [@pavolloffay](https://github.com/pavolloffay)) - Add username/password authentication to ES index cleaner ([#1318](https://github.com/jaegertracing/jaeger/pull/1318), [@gregoryfranklin](https://github.com/gregoryfranklin)) - Add implementation of FindTraceIDs function for Elasticsearch reader ([#1280](https://github.com/jaegertracing/jaeger/pull/1280), [@vlamug](https://github.com/vlamug)) - Support archive traces for ES storage ([#1197](https://github.com/jaegertracing/jaeger/pull/1197), [@pavolloffay](https://github.com/pavolloffay)) ##### Bug fixes, Minor Improvements - Use Zipkin annotations if the timestamp is zero ([#1341](https://github.com/jaegertracing/jaeger/pull/1341), [@geobeau](https://github.com/geobeau)) - Use GRPC round robin balancing even if only one hostname ([#1329](https://github.com/jaegertracing/jaeger/pull/1329), [@benley](https://github.com/benley)) - Tolerate whitespaces in ES servers and kafka brokers ([#1305](https://github.com/jaegertracing/jaeger/pull/1305), [@verma-varsha](https://github.com/verma-varsha)) - Let cassandra servers contain whitespace in config ([#1301](https://github.com/jaegertracing/jaeger/pull/1301), [@karlpokus](https://github.com/karlpokus)) #### UI Changes 1.9.0 (2019-01-21) ------------------ #### Backend Changes ##### ⛔ Breaking Changes - Change Elasticsearch index prefix from `:` to `-` ([#1284](https://github.com/jaegertracing/jaeger/pull/1284), [@pavolloffay](https://github.com/pavolloffay)) Changed index prefix separator from `:` to `-` because Elasticsearch 7 does not allow `:` in index name. Jaeger query still reads from old indices containing `-` as a separator, therefore no configuration or migration changes are required. - Add CLI configurable `es.max-num-spans` while retrieving spans from ES ([#1283](https://github.com/jaegertracing/jaeger/pull/1283), [@annanay25](https://github.com/annanay25)) The default value is set to 10000. Before no limit was applied. - Update to jaeger-lib 2 and latest sha for jaeger-client-go, to pick up refactored metric names ([#1282](https://github.com/jaegertracing/jaeger/pull/1282), [@objectiser](https://github.com/objectiser)) Update to latest version of `jaeger-lib`, which includes a change to the naming of counters exported to prometheus, to follow the convention of using a `_total` suffix, e.g. `jaeger_query_requests` is now `jaeger_query_requests_total`. Jaeger go client metrics, previously under the namespace `jaeger_client_jaeger_` are now under `jaeger_tracer_`. - Add gRPC metrics to agent ([#1180](https://github.com/jaegertracing/jaeger/pull/1180), [@pavolloffay](https://github.com/pavolloffay)) The following metrics: ``` jaeger_agent_tchannel_reporter_batch_size{format="jaeger"} 0 jaeger_agent_tchannel_reporter_batch_size{format="zipkin"} 0 jaeger_agent_tchannel_reporter_batches_failures{format="jaeger"} 0 jaeger_agent_tchannel_reporter_batches_failures{format="zipkin"} 0 jaeger_agent_tchannel_reporter_batches_submitted{format="jaeger"} 0 jaeger_agent_tchannel_reporter_batches_submitted{format="zipkin"} 0 jaeger_agent_tchannel_reporter_spans_failures{format="jaeger"} 0 jaeger_agent_tchannel_reporter_spans_failures{format="zipkin"} 0 jaeger_agent_tchannel_reporter_spans_submitted{format="jaeger"} 0 jaeger_agent_tchannel_reporter_spans_submitted{format="zipkin"} 0 jaeger_agent_collector_proxy{endpoint="baggage",result="err"} 0 jaeger_agent_collector_proxy{endpoint="baggage",result="ok"} 0 jaeger_agent_collector_proxy{endpoint="sampling",result="err"} 0 jaeger_agent_collector_proxy{endpoint="sampling",result="ok"} 0 ``` have been renamed to: ``` jaeger_agent_reporter_batch_size{format="jaeger",protocol="tchannel"} 0 jaeger_agent_reporter_batch_size{format="zipkin",protocol="tchannel"} 0 jaeger_agent_reporter_batches_failures{format="jaeger",protocol="tchannel"} 0 jaeger_agent_reporter_batches_failures{format="zipkin",protocol="tchannel"} 0 jaeger_agent_reporter_batches_submitted{format="jaeger",protocol="tchannel"} 0 jaeger_agent_reporter_batches_submitted{format="zipkin",protocol="tchannel"} 0 jaeger_agent_reporter_spans_failures{format="jaeger",protocol="tchannel"} 0 jaeger_agent_reporter_spans_failures{format="zipkin",protocol="tchannel"} 0 jaeger_agent_reporter_spans_submitted{format="jaeger",protocol="tchannel"} 0 jaeger_agent_reporter_spans_submitted{format="zipkin",protocol="tchannel"} 0 jaeger_agent_collector_proxy{endpoint="baggage",protocol="tchannel",result="err"} 0 jaeger_agent_collector_proxy{endpoint="baggage",protocol="tchannel",result="ok"} 0 jaeger_agent_collector_proxy{endpoint="sampling",protocol="tchannel",result="err"} 0 jaeger_agent_collector_proxy{endpoint="sampling",protocol="tchannel",result="ok"} 0 ``` - Rename tcollector proxy metric in agent ([#1182](https://github.com/jaegertracing/jaeger/pull/1182), [@pavolloffay](https://github.com/pavolloffay)) The following metric: ``` jaeger_http_server_errors{source="tcollector-proxy",status="5xx"} ``` has been renamed to: ``` jaeger_http_server_errors{source="collector-proxy",status="5xx"} ``` ##### New Features - Add tracegen utility for generating traces ([#1245](https://github.com/jaegertracing/jaeger/pull/1245), [@yurishkuro](https://github.com/yurishkuro)) - Use DCAwareRoundRobinPolicy as fallback for TokenAwarePolicy ([#1285](https://github.com/jaegertracing/jaeger/pull/1285), [@vprithvi](https://github.com/vprithvi)) - Add Zipkin Thrift as kafka ingestion format ([#1256](https://github.com/jaegertracing/jaeger/pull/1256), [@geobeau](https://github.com/geobeau)) - Add `FindTraceID` to the spanstore interface ([#1246](https://github.com/jaegertracing/jaeger/pull/1246), [@vprithvi](https://github.com/vprithvi)) - Migrate from glide to dep ([#1240](https://github.com/jaegertracing/jaeger/pull/1240), [@isaachier](https://github.com/isaachier)) - Make tchannel timeout for reporting in agent configurable ([#1034](https://github.com/jaegertracing/jaeger/pull/1034), [@gouthamve](https://github.com/gouthamve)) - Add archive traces to all-in-one ([#1189](https://github.com/jaegertracing/jaeger/pull/1189), [@pavolloffay](https://github.com/pavolloffay)) - Start moving components of adaptive sampling to OSS ([#973](https://github.com/jaegertracing/jaeger/pull/973), [@black-adder](https://github.com/black-adder)) - Add gRPC communication between agent and collector ([#1165](https://github.com/jaegertracing/jaeger/pull/1165), [#1187](https://github.com/jaegertracing/jaeger/pull/1187), [#1181](https://github.com/jaegertracing/jaeger/pull/1181) and [#1180](https://github.com/jaegertracing/jaeger/pull/1180), [@pavolloffay](https://github.com/pavolloffay)) ##### Bug fixes, Minor Improvements - Update exposed ports in ingester dockerfile ([#1289](https://github.com/jaegertracing/jaeger/pull/1289), [@objectiser](https://github.com/objectiser)) - Upgrade Shopify/Sarama for proper handling newest kafka servers 2.x by ingester ([#1248](https://github.com/jaegertracing/jaeger/pull/1248), [@vprithvi](https://github.com/vprithvi)) - Fix sampling strategies overwriting service entry when no sampling type is specified ([#1244](https://github.com/jaegertracing/jaeger/pull/1244), [@objectiser](https://github.com/objectiser)) - Fix dot replacement for int ([#1272](https://github.com/jaegertracing/jaeger/pull/1272), [@pavolloffay](https://github.com/pavolloffay)) - Add C* query to error logs ([#1250](https://github.com/jaegertracing/jaeger/pull/1250), [@vprithvi](https://github.com/vprithvi)) - Add locking around partitionIDToState map accesses ([#1239](https://github.com/jaegertracing/jaeger/pull/1239), [@vprithvi](https://github.com/vprithvi)) - Reorganize config manager packages in agent ([#1198](https://github.com/jaegertracing/jaeger/pull/1198), [@pavolloffay](https://github.com/pavolloffay)) #### UI Changes * UI pinned to version 1.0.0. The changelog is available here [v1.0.0](https://github.com/jaegertracing/jaeger-ui/blob/main/CHANGELOG.md#v100-january-18-2019) 1.8.2 (2018-11-28) ------------------ #### UI Changes ##### New Features - Embedded components (SearchTraces and Tracepage) ([#263](https://github.com/jaegertracing/jaeger/pull/263), [@aljesusg](https://github.com/aljesusg)) ##### Bug fixes, Minor Improvements - Fix link in scatter plot when embed mode ([#283](https://github.com/jaegertracing/jaeger-ui/pull/283), [@aljesusg](https://github.com/aljesusg)) - Fix rendering X axis in TraceResultsScatterPlot - pass milliseconds to moment.js ([#274](https://github.com/jaegertracing/jaeger-ui/pull/274), [@istrel](https://github.com/istrel)) 1.8.1 (2018-11-23) ------------------ #### Backend Changes ##### Bug fixes, Minor Improvements - Make agent timeout for reporting configurable and fix flags overriding ([#1034](https://github.com/jaegertracing/jaeger/pull/1034), [@gouthamve](https://github.com/gouthamve)) - Fix metrics handler registration in agent ([#1178](https://github.com/jaegertracing/jaeger/pull/1178), [@pavolloffay](https://github.com/pavolloffay)) 1.8.0 (2018-11-12) ------------------ #### Backend Changes ##### ⛔ Breaking Changes - Refactor agent configuration ([#1092](https://github.com/jaegertracing/jaeger/pull/1092), [@pavolloffay](https://github.com/pavolloffay)) The following agent flags has been deprecated in order to support multiple reporters: ```bash --collector.host-port --discovery.conn-check-timeout --discovery.min-peers ``` New flags: ```bash --reporter.tchannel.host-port --reporter.tchannel.discovery.conn-check-timeout --reporter.tchannel.discovery.min-peers ``` - Various changes around metrics produced by jaeger-query: Names scoped to the query component, generated for all span readers (not just ES), consolidate query metrics and include result tag ([#1074](https://github.com/jaegertracing/jaeger/pull/1074), [#1075](https://github.com/jaegertracing/jaeger/pull/1075) and [#1096](https://github.com/jaegertracing/jaeger/pull/1096), [@objectiser](https://github.com/objectiser)) For example, sample of metrics produced for `find_traces` operation before: ``` jaeger_find_traces_attempts 1 jaeger_find_traces_errLatency_bucket{le="0.005"} 0 jaeger_find_traces_errors 0 jaeger_find_traces_okLatency_bucket{le="0.005"} 0 jaeger_find_traces_responses_bucket{le="0.005"} 1 jaeger_find_traces_successes 1 ``` And now: ``` jaeger_query_latency_bucket{operation="find_traces",result="err",le="0.005"} 0 jaeger_query_latency_bucket{operation="find_traces",result="ok",le="0.005"} 2 jaeger_query_requests{operation="find_traces",result="err"} 0 jaeger_query_requests{operation="find_traces",result="ok"} 2 jaeger_query_responses_bucket{operation="find_traces",le="0.005"} 2 ``` ##### New Features - Configurable deadlock detector interval for ingester ([#1134](https://github.com/jaegertracing/jaeger/pull/1134), [@marqc](https://github.com/marqc)) - Emit spans for elastic storage backend ([#1128](https://github.com/jaegertracing/jaeger/pull/1128), [@annanay25](https://github.com/annanay25)) - Allow to use TLS certificates for Elasticsearch authentication ([#1139](https://github.com/jaegertracing/jaeger/pull/1139), [@clyang82](https://github.com/clyang82)) - Add ingester metrics, healthcheck and rename Kafka cli flags ([#1094](https://github.com/jaegertracing/jaeger/pull/1094), [@ledor473](https://github.com/ledor473)) - Add a metric for number of partitions held ([#1154](https://github.com/jaegertracing/jaeger/pull/1154), [@vprithvi](https://github.com/vprithvi)) - Log jaeger-collector tchannel port ([#1136](https://github.com/jaegertracing/jaeger/pull/1136), [@mindaugasrukas](https://github.com/mindaugasrukas)) - Support tracer env based initialization in hotrod ([#1115](https://github.com/jaegertracing/jaeger/pull/1115), [@eundoosong](https://github.com/eundoosong)) - Publish ingester as binaries and docker image ([#1086](https://github.com/jaegertracing/jaeger/pull/1086), [@ledor473](https://github.com/ledor473)) - Use Go 1.11 ([#1104](https://github.com/jaegertracing/jaeger/pull/1104), [@isaachier](https://github.com/isaachier)) - Tag images with commit SHA and publish to `-snapshot` repository ([#1082](https://github.com/jaegertracing/jaeger/pull/1082), [@pavolloffay](https://github.com/pavolloffay)) ##### Bug fixes, Minor Improvements - Fix child span context while tracing cassandra queries ([#1131](https://github.com/jaegertracing/jaeger/pull/1131), [@annanay25](https://github.com/annanay25)) - Deadlock detector hack for Kafka driver instability ([#1087](https://github.com/jaegertracing/jaeger/pull/1087), [@vprithvi](https://github.com/vprithvi)) - Fix processor overriding data in a buffer ([#1099](https://github.com/jaegertracing/jaeger/pull/1099), [@pavolloffay](https://github.com/pavolloffay)) #### UI Changes ##### New Features - Span Search - Highlight search results ([#238](https://github.com/jaegertracing/jaeger-ui/pull/238)), [@davit-y](https://github.com/davit-y) - Span Search - Improve search logic ([#237](https://github.com/jaegertracing/jaeger-ui/pull/237)), [@davit-y](https://github.com/davit-y) - Span Search - Add result count, navigation and clear buttons ([#234](https://github.com/jaegertracing/jaeger-ui/pull/234)), [@davit-y](https://github.com/davit-y) ##### Bug Fixes, Minor Improvements - Use correct duration format for scatter plot ([#266](https://github.com/jaegertracing/jaeger-ui/pull/266)), [@tiffon](https://github.com/tiffon)) - Fix collapse all issues ([#264](https://github.com/jaegertracing/jaeger-ui/pull/264)), [@tiffon](https://github.com/tiffon)) - Use a moderately sized canvas for the span graph ([#257](https://github.com/jaegertracing/jaeger-ui/pull/257)), [@tiffon](https://github.com/tiffon)) 1.7.0 (2018-09-19) ------------------ #### UI Changes - Compare two traces ([#228](https://github.com/jaegertracing/jaeger-ui/pull/228), [@tiffon](https://github.com/tiffon)) - Make tags clickable ([#223](https://github.com/jaegertracing/jaeger-ui/pull/223), [@divdavem](https://github.com/divdavem)) - Directed graph as React component ([#224](https://github.com/jaegertracing/jaeger-ui/pull/224), [@tiffon](https://github.com/tiffon)) - Timeline Expand and Collapse Features ([#221](https://github.com/jaegertracing/jaeger-ui/issues/221), [@davit-y](https://github.com/davit-y)) - Integrate Google Analytics into Search Page ([#220](https://github.com/jaegertracing/jaeger-ui/issues/220), [@davit-y](https://github.com/davit-y)) #### Backend Changes ##### ⛔ Breaking Changes - `jaeger-standalone` binary has been renamed to `jaeger-all-in-one`. This change also includes package rename from `standalone` to `all-in-one` ([#1062](https://github.com/jaegertracing/jaeger/pull/1062), [@pavolloffay](https://github.com/pavolloffay)) ##### New Features - (Experimental) Allow storing tags as object fields in Elasticsearch for better Kibana support(([#1018](https://github.com/jaegertracing/jaeger/pull/1018), [@pavolloffay](https://github.com/pavolloffay)) - Enable tracing of Cassandra queries ([#1038](https://github.com/jaegertracing/jaeger/pull/1038), [@yurishkuro](https://github.com/yurishkuro)) - Make Elasticsearch index configurable ([#1009](https://github.com/jaegertracing/jaeger/pull/1009), [@pavolloffay](https://github.com/pavoloffay)) - Add flags to allow changing ports for HotROD services ([#951](https://github.com/jaegertracing/jaeger/pull/951), [@cboornaz17](https://github.com/cboornaz17)) - (Experimental) Kafka ingester ([#952](https://github.com/jaegertracing/jaeger/pull/952), [#942](https://github.com/jaegertracing/jaeger/pull/942), [#944](https://github.com/jaegertracing/jaeger/pull/944), [#940](https://github.com/jaegertracing/jaeger/pull/940), [@davit-y](https://github.com/davit-y) and [@vprithvi](https://github.com/vprithvi))) - Use tags in agent metrics ([#950](https://github.com/jaegertracing/jaeger/pull/950), [@eundoosong](https://github.com/eundoosong)) - Add support for Cassandra reconnect interval ([#934](https://github.com/jaegertracing/jaeger/pull/934), [@nyanshak](https://github.com/nyanshak)) 1.6.0 (2018-07-10) ------------------ #### Backend Changes ##### ⛔ Breaking Changes - The storage implementations no longer write the parentSpanID field to storage (#856). If you are upgrading to this version, **you must upgrade query service first**! - Update Dockerfiles to reference executable via ENTRYPOINT (#815) by Zachary DiCesare (@zdicesare) It is no longer necessary to specify the binary name when passing flags to containers. For example, to execute the `help` command of the collector, instead of ``` $ docker run -it --rm jaegertracing/jaeger-collector /go/bin/collector-linux help ``` run ``` $ docker run -it --rm jaegertracing/jaeger-collector help ``` - Detect HTTP payload format from Content-Type (#916) by Yuri Shkuro (@yurishkuro) When submitting spans in Thrift format to HTTP endpoint `/api/traces`, the `format` argument is no longer required, but the Content-Type header must be set to "application/vnd.apache.thrift.binary". - Change metric tag from "service" to "svc" (#883) by Won Jun Jang (@black-adder) ##### New Features - Add Kafka as a Storage Plugin (#862) by David Yeghshatyan (@davit-y) The collectors can be configured to write spans to Kafka for further data mining. - Package static assets inside the query-service binary (#918) by Yuri Shkuro (@yurishkuro) It is no longer necessary (but still possible) to pass the path to UI static assets to jaeger-query and jaeger-standalone binaries. - Replace domain model with Protobuf/gogo-generated model (#856) by Yuri Shkuro (@yurishkuro) First step towards switching to Protobuf and gRPC. - Include HotROD binary in the distributions (#917) by Yuri Shkuro (@yurishkuro) - Improve HotROD demo (#915) by Yuri Shkuro (@yurishkuro) - Add DisableAutoDiscovery param to cassandra config (#912) by Bill Westlin (@whistlinwilly) - Add connCheckTimeout flag to agent (#911) by Henrique Rodrigues (@Henrod) - Ability to use multiple storage types (#880) by David Yeghshatyan (@davit-y) ##### Minor Improvements - [ES storage] Log number of total and failed requests (#902) by Tomasz Adamski (@tmszdmsk) - [ES storage] Do not log requests on error (#901) by Tomasz Adamski (@tmszdmsk) - [ES storage] Do not exceed ES _id length limit (#905) by Łukasz Harasimowicz (@harnash) and Tomasz Adamski (@tmszdmsk) - Add cassandra index filter (#876) by Won Jun Jang (@black-adder) - Close span writer in standalone (#863) (4 weeks ago) by Pavol Loffay (@pavolloffay) - Log configuration options for memory storage (#852) (6 weeks ago) by Juraci Paixão Kröhling (@jpkrohling) - Update collector metric counters to have a name (#886) by Won Jun Jang (@black-adder) - Add CONTRIBUTING_GUIDELINES.md (#864) by (@PikBot) 1.5.0 (2018-05-28) ------------------ #### Backend Changes - Add bounds to memory storage (#845) by Juraci Paixão Kröhling (@jpkrohling) - Add metric for debug traces (#796) by Won Jun Jang (@black-adder) - Change metrics naming scheme (#776) by Juraci Paixão Kröhling (@jpkrohling) - Remove ParentSpanID from domain model (#831) by Yuri Shkuro (@yurishkuro) - Add ability to adjust static sampling probabilities per operation (#827) by Won Jun Jang (@black-adder) - Support log-level flag on agent (#828) by Won Jun Jang (@black-adder) - Add healthcheck to standalone (#784) by Eundoo Song (@eundoosong) - Do not use KeyValue fields directly and use KeyValues as decorator only (#810) by Yuri Shkuro (@yurishkuro) - Upgrade to go 1.10 (#792) by Prithvi Raj (@vprithvi) - Do not create Cassandra index if it already exists (#782) by Greg Swift (@gregswift) #### UI Changes - None 1.4.1 (2018-04-21) ------------------ #### Backend Changes - Publish binaries for Linux, Darwin, and Windows (#765) - thanks to @grounded042 #### UI Changes ##### New Features - View Trace JSON buttons return formatted JSON (fixes [#199](https://github.com/jaegertracing/jaeger-ui/issues/199)) 1.4.0 (2018-04-20) ------------------ #### Backend Changes ##### New Features - Support traces with >10k spans in Elasticsearch (#668) - thanks to @sramakr ##### Bug Fixes, Minor Improvements - Allow slash '/' in service names (#586) - Log errors from HotROD services (#769) 1.3.0 (2018-03-26) ------------------ #### Backend Changes ##### New Features - Add sampling handler with file-based configuration for agents to query (#720) (#674) - Allow overriding base path for UI/API routes and remove --query.prefix (#748) - Add Dockerfile for hotrod example app (#694) - Publish hotrod image to docker hub (#702) - Dockerize es-index-cleaner script (#741) - Add a flag to control Cassandra consistency level (#700) - Collect metrics from ES bulk service (#688) - Allow zero replicas for Elasticsearch (#754) ##### Bug Fixes, Minor Improvements - Apply namespace when creating Prometheus metrics factory (fix for #732) (#733) - Disable double compression on Prom Handler - fixes #697 (#735) - Use the default metricsFactory if not provided (#739) - Avoid duplicate expvar metrics - fixes #716 (#726) - Make sure different tracers in HotROD process use different random generator seeds (#718) - Test that processes with identical tags are deduped (#708) - When converting microseconds to time.Time ensure UTC timezone (#712) - Add to WaitGroup before the goroutine creation (#711) - Pin testify version to ^1.2.1 (#710) #### UI Changes ##### New Features - Support running Jaeger behind a reverse proxy (fixes [#42](https://github.com/jaegertracing/jaeger-ui/issues/42)) - Track Javascript errors via Google Analytics (fixes [#39](https://github.com/jaegertracing/jaeger-ui/issues/39)) - Add Google Analytics event tracking for actions in trace view ([#191](https://github.com/jaegertracing/jaeger-ui/issues/191)) ##### Bug Fixes, Minor Improvements - Clearly identify traces without a root span (fixes [#190](https://github.com/jaegertracing/jaeger-ui/issues/190)) - Fix [#166](https://github.com/jaegertracing/jaeger-ui/issues/166) JS error on search page after viewing 404 trace #### Documentation Changes 1.2.0 (2018-02-07) ------------------ #### Backend Changes ##### New Features - Use elasticsearch bulk API (#656) - Support archive storage in the query-service (#604) - Introduce storage factory framework and composable CLI (#625) - Make agent host port configurable in hotrod (#663) - Add signal handling to standalone (#657) ##### Bug Fixes, Minor Improvements - Remove the override of GOMAXPROCS (#679) - Use UTC timezone for ES indices (#646) - Fix elasticsearch create index race condition error (#641) #### UI Changes ##### New Features - Use Ant Design instead of Semantic UI (https://github.com/jaegertracing/jaeger-ui/pull/169) - Fix [#164](https://github.com/jaegertracing/jaeger-ui/issues/164) - Use Ant Design instead of Semantic UI - Fix [#165](https://github.com/jaegertracing/jaeger-ui/issues/165) - Search results are shown without a date - Fix [#69](https://github.com/jaegertracing/jaeger-ui/issues/69) - Missing endpoints in jaeger ui dropdown ##### Bug Fixes, Minor Improvements - Fix 2 digit lookback (12h, 24h) parsing (https://github.com/jaegertracing/jaeger-ui/issues/167) 1.1.0 (2018-01-03) ------------------ #### Backend Changes ##### New Features - Add support for retrieving unadjusted/raw traces (#615) - Add CA certificates to collector/query images (#485) - Parse zipkin v2 high trace id (#596) ##### Bug Fixes, Minor Improvements - Skip nil and zero length hits in ElasticSearch storage (#601) - Make Cassandra service_name_index inserts idempotent (#587) - Align atomic int64 to word boundary to fix SIGSEGV (#592) - Add adjuster that removes bad span references (#614) - Set operationNames cache initial capacity to 10000 (#621) #### UI Changes ##### New Features - Change tag search input syntax to logfmt (https://github.com/jaegertracing/jaeger-ui/issues/145) - Make threshold for enabling DAG view configurable (https://github.com/jaegertracing/jaeger-ui/issues/130) - Show better error messages for failed API calls (https://github.com/jaegertracing/jaeger-ui/issues/127) - Add View Option for raw/unadjusted trace (https://github.com/jaegertracing/jaeger-ui/issues/153) - Add timezone tooltip to custom lookback form-field (https://github.com/jaegertracing/jaeger-ui/pull/161) ##### Bug Fixes, Minor Improvements - Use consistent icons for logs expanded/collapsed (https://github.com/jaegertracing/jaeger-ui/issues/86) - Encode service name in API calls to allow '/' (https://github.com/jaegertracing/jaeger-ui/issues/138) - Fix endless trace HTTP requests (https://github.com/jaegertracing/jaeger-ui/issues/128) - Fix JSON view when running in dev mode (https://github.com/jaegertracing/jaeger-ui/issues/139) - Fix trace name resolution (https://github.com/jaegertracing/jaeger-ui/pull/134) - Only JSON.parse JSON strings in tags/logs values (https://github.com/jaegertracing/jaeger-ui/pull/162) 1.0.0 (2017-12-04) ------------------ #### Backend Changes - Support Prometheus metrics as default for all components (#516) - Enable TLS client connections to Cassandra (#555) - Fix issue where Domain to UI model converter double reports references (#579) #### UI Changes - Make dependencies tab configurable (#122) 0.10.0 (2017-11-17) ------------------ #### UI Changes - Verify stored search settings [jaegertracing/jaeger-ui#111](https://github.com/jaegertracing/jaeger-ui/pull/111) - Fix browser back button not working correctly [jaegertracing/jaeger-ui#110](https://github.com/jaegertracing/jaeger-ui/pull/110) - Handle FOLLOWS_FROM ref type [jaegertracing/jaeger-ui#118](https://github.com/jaegertracing/jaeger-ui/pull/118) #### Backend Changes - Allow embedding custom UI config in index.html [#490](https://github.com/jaegertracing/jaeger/pull/490) - Add startTimeMillis field to JSON Spans submitted to ElasticSearch [#491](https://github.com/jaegertracing/jaeger/pull/491) - Introduce version command and handler [#517](https://github.com/jaegertracing/jaeger/pull/517) - Fix ElasticSearch aggregation errors when index is empty [#535](https://github.com/jaegertracing/jaeger/pull/535) - Change package from uber to jaegertracing [#528](https://github.com/jaegertracing/jaeger/pull/528) - Introduce logging level configuration [#514](https://github.com/jaegertracing/jaeger/pull/514) - Support Zipkin v2 json [#518](https://github.com/jaegertracing/jaeger/pull/518) - Add HTTP compression handler [#545](https://github.com/jaegertracing/jaeger/pull/545) 0.9.0 (2017-10-25) ------------------ #### UI Changes - Refactor trace detail [jaegertracing/jaeger-ui#53](https://github.com/jaegertracing/jaeger-ui/pull/53) - Virtualized scrolling for trace detail view [jaegertracing/jaeger-ui#68](https://github.com/jaegertracing/jaeger-ui/pull/68) - Mouseover expands truncated text to full length in left column in trace view [jaegertracing/jaeger-ui#71](https://github.com/jaegertracing/jaeger-ui/pull/71) - Make left column adjustable in trace detail view [jaegertracing/jaeger-ui#74](https://github.com/jaegertracing/jaeger-ui/pull/74) - Fix trace mini-map blurriness when < 60 spans [jaegertracing/jaeger-ui#77](https://github.com/jaegertracing/jaeger-ui/pull/77) - Fix Google Analytics tracking [jaegertracing/jaeger-ui#81](https://github.com/jaegertracing/jaeger-ui/pull/81) - Improve search dropdowns [jaegertracing/jaeger-ui#84](https://github.com/jaegertracing/jaeger-ui/pull/84) - Add keyboard shortcuts and minimap UX [jaegertracing/jaeger-ui#93](https://github.com/jaegertracing/jaeger-ui/pull/93) #### Backend Changes - Add tracing to the query server [#454](https://github.com/uber/jaeger/pull/454) - Support configuration files [#462](https://github.com/uber/jaeger/pull/462) - Add cassandra tag filter [#442](https://github.com/uber/jaeger/pull/442) - Handle ports > 32k in Zipkin JSON [#488](https://github.com/uber/jaeger/pull/488) 0.8.0 (2017-09-24) ------------------ - Convert to Apache 2.0 License 0.7.0 (2017-08-22) ------------------ - Add health check server to collector and query [#280](https://github.com/uber/jaeger/pull/280) - Add/fix sanitizer for Zipkin span start time and duration [#333](https://github.com/uber/jaeger/pull/333) - Support Zipkin json encoding for /api/v1/spans HTTP endpoint [#348](https://github.com/uber/jaeger/pull/348) - Support Zipkin 128bit traceId and ipv6 [#349](https://github.com/uber/jaeger/pull/349) 0.6.0 (2017-08-09) ------------------ - Add viper/cobra configuration support [#245](https://github.com/uber/jaeger/pull/245) [#307](https://github.com/uber/jaeger/pull/307) - Add Zipkin /api/v1/spans endpoint [#282](https://github.com/uber/jaeger/pull/282) - Add basic authenticator to configs for cass ================================================ FILE: CODE_OF_CONDUCT.md ================================================ ## Community Code of Conduct Jaeger follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). Please contact the [Jaeger Maintainers](mailto:cncf-jaeger-maintainers@lists.cncf.io) or the [CNCF Code of Conduct Committee](mailto:conduct@cncf.io) in order to report violations of the Code of Conduct. ================================================ FILE: CONTRIBUTING.md ================================================ # How to Contribute to Jaeger We'd love your help! General contributing guidelines are described in [Contributing Guidelines](./CONTRIBUTING_GUIDELINES.md). Jaeger is [Apache 2.0 licensed](LICENSE) and accepts contributions via GitHub pull requests. This document outlines some of the conventions on development workflow, commit message formatting, contact points and other resources to make it easier to get your contribution accepted. We gratefully welcome improvements to documentation as well as to code. ## Getting Started ### Pre-requisites * Install [Go](https://golang.org/doc/install) and setup GOPATH and add $GOPATH/bin in PATH This library uses Go modules to manage dependencies. If you are running `make test` or other Makefile targets on macOS, please ensure that you have GNU `sed` installed. To install GNU `sed`: ```bash brew install gnu-sed ``` ``` git clone git@github.com:jaegertracing/jaeger.git jaeger cd jaeger ``` Then install dependencies and run the tests: ``` # Adds the jaeger-ui submodule git submodule update --init --recursive # Installs required tools make install-tools # Runs all unit tests: make test ``` ### Contributing Code We accept new changes as pull requests on GitHub. Please make sure the following conditions are met before submitting PRs: 1. Use a named branch in your fork, not the `main` branch, otherwise the CI jobs will fail and we won't be able to merge the PR. 2. All commits in the PR must be signed (verified by the DCO check on GitHub). 3. Before submitting a PR, make sure to run: ``` make fmt # commit all changes from auto-format make lint make test ``` ### Auto-format We are currently using `gofumpt`, which is installed automatically by `make install-tools` as part of `golangci-lint` installation. We recommend configuring your IDE to run `gofumpt` on file saves, e.g. in VSCode: ```json "go.formatTool": "gofumpt", "gopls": { "formatting.gofumpt": true, } ``` ### Running local build with the UI ``` $ go run ./cmd/jaeger --config ./cmd/jaeger/config.yaml ``` #### What does this command do? The Jaeger binary runs with the default configuration file (config.yaml) that includes the UI configuration via the `jaeger_query` extension. The `jaeger-ui` submodule, which was added from the Pre-requisites step above, contains the source code for the UI assets (requires Node.js 24+). The assets must be compiled first with `make build-ui`, which normally downloads them from the latest UI release, but can also build them from source. ## Project Structure These are general guidelines on how to organize source code in this repository. ``` github.com/jaegertracing/jaeger cmd/ - All binaries go here jaeger/ - The main Jaeger binary (v2) that combines collector, query, and ingester anonymizer/ - Utility to anonymize traces from Jaeger query and save to file tracegen/ - Utility to generate a steady flow of simple traces es-index-cleaner/ - Utility to purge old indices from Elasticsearch es-rollover/ - Utility to manage Elastic Search indices esmapping-generator/ - Utility to generate Elasticsearch mapping remote-storage/ - Component to enable sharing single-node storage implementations via Remote Storage API v2 examples/ grafana-integration/ - Demo application combining Jaeger, Grafana, Loki, Prometheus hotrod/ - Demo application demonstrating tracing instrumentation otel-demo/ - Demo application using OpenTelemetry Collector and Jaeger docker-compose/ - Docker-compose recipes to simulate different Jaeger deployments monitor/ - Service Performance Monitoring (SPM) Development/Demo Environment idl/ - (submodule) https://github.com/jaegertracing/jaeger-idl jaeger-ui/ - (submodule) https://github.com/jaegertracing/jaeger-ui internal/ - Internal modules that make up Jaeger storage/ - Trace/Metrics Storage interfaces and implementations metricstore/ - Metrics Storage interface and implementations (e.g. Prometheus, Elasticsearch) v1/ - Trace Storage v1 interfaces and implementations (Cassandra, Elasticsearch, Badger, etc.) v2/ - Trace Storage v2 interfaces and implementations (gRPC, ClickHouse, etc.) monitoring/ - Jaeger monitoring assets (e.g. jaeger-mixin) ports/ - Centralized port definitions scripts/ - Miscellaneous project scripts, e.g. github action and license update script go.mod - Go module file to track dependencies Makefile - Define various recipes to automate build, test, and deployment tasks ``` ## Imports grouping This project follows the following pattern for grouping imports in Go files: - imports from standard library - imports from other projects - imports from `jaeger` project For example: ```go import ( "fmt" "github.com/uber/jaeger-lib/metrics" "go.uber.org/zap" "github.com/jaegertracing/jaeger/cmd/agent/app" "github.com/jaegertracing/jaeger/cmd/collector/app/builder" ) ``` ## Testing guidelines **Policy**: All new functionality must include tests. Bug fixes should include regression tests that would have caught the bug, where feasible. Pull requests without adequate test coverage will not be merged. We strive to maintain as high code coverage as possible. The current repository limit is set at 95%, with some exclusions discussed below. ### Packages with no tests Since `go test` command does not generate code coverage information for packages that have no test files, we have a build step (`make nocover`) that breaks the build when such packages are discovered, with the following error: ``` error: at least one *_test.go file must be in all directories with go files so that they are counted for code coverage. If no tests are possible for a package (e.g. it only defines types), create empty_test.go ``` As the message says, all packages are required to have at least one `*_test.go` file. ### Excluding packages from testing There are conditions that cannot be tested without external dependencies, such as a function that creates a `gocql.Session`, because it requires an active connection to Cassandra database. It is recommended to isolate such functions in a separate package with bare minimum of code and add a file `.nocover` to exclude the package from coverage calculations. The file should contain a comment explaining why it is there, for example: ``` $ cat ./pkg/cassandra/config/.nocover requires connection to Cassandra ``` ## Merging PRs **For maintainers:** before merging a PR make sure the title is descriptive and follows [a good commit message](./CONTRIBUTING_GUIDELINES.md) Merge the PR by using "Squash and merge" option on Github. Avoid creating merge commits. After the merge make sure referenced issues were closed. ## Deprecating CLI Flags * If a flag is deprecated in release N, it can be removed in release N+2 or three months later, whichever is later. * When adding a (deprecated) prefix to the flags, indicate via a deprecation message that the flag could be removed in the future. For example: ``` (deprecated, will be removed after 2020-03-15 or in release v1.19.0, whichever is later) ``` * At the top of the file where the flag name is defined, add a constant and a comment, e.g. ``` // TODO deprecated flag to be removed healthCheckHTTPPortWarning = "(deprecated, will be removed after 2020-03-15 or in release v1.19.0, whichever is later)" ``` * Use that constant as the prefix to the help text, e.g. ``` flagSet.Int(healthCheckHTTPPort, 0, healthCheckHTTPPortWarning+" see --"+adminHTTPHostPort) ``` * When parsing a deprecated flag into config, log a warning with the same deprecation message * Take care of deprecated flags in `initFromViper` functions, do not pass them to business functions. ### Removing Deprecated CLI Flags * Ensure all references to the flag's variables have been removed in code. * Ensure a "Breaking Changes" entry is added in the [CHANGELOG](./CHANGELOG.md) indicating which CLI flag is being removed and which CLI flag should be used in favor of this removed flag. For example: ``` * Remove deprecated flags `--old-flag`, please use `--new-flag` ([#1234](), [@myusername](https://github.com/myusername)) ``` ## Using Feature Gates for Breaking Changes As much as possible, use OTel Collector's [feature gates][feature_gates] to manage breaking changes. For example, consider that we discovered a bug in the existing behavior, such as https://github.com/jaegertracing/jaeger/issues/5270. Simply changing the behavior might be a breaking change, so we implement a new behavior and create an internal config setting that enables or disables it. But how will users ever know and be encouraged to migrate to the new behavior? For that we can create a feature gate (without even creating any additional user-facing configuration), as follows: * Introduce a new feature gate, with the name `jaeger.***`. * If we don't want to change the default behavior right away, we can start the feature in the Alpha state, where it is disabled by default. No breaking changes need to be called out in the changelog. * If we do want to change the default behavior right away, we can start the feature in the Beta state, where it is enabled by default, but the user can still disable it. Call out a breaking change in the changelog. * Two releases later change the gate to Stable, where it is not only enabled by default, but trying to disable it will cause a runtime error. The code for the old behavior should be removed. Call out a breaking change in the changelog. * Two releases later remove the feature gate as unused. Call out a breaking change in the changelog. See https://github.com/jaegertracing/jaeger/pull/6441 for an example of this workflow. [feature_gates]: https://github.com/open-telemetry/opentelemetry-collector/blob/main/featuregate/README.md ================================================ FILE: CONTRIBUTING_GUIDELINES.md ================================================ # How to Contribute to Jaeger We'd love your help! Jaeger is [Apache 2.0 licensed](./LICENSE) and accepts contributions via GitHub pull requests. This document outlines some of the conventions on development workflow, commit message formatting, contact points and other resources to make it easier to get your contribution accepted. We gratefully welcome improvements to documentation as well as to code. Table of Contents: * [Making a Change](#making-a-change) * [AI Usage Policy](#ai-usage-policy) * [Pull Request Limits for New Contributors](#pull-request-limits-for-new-contributors) * [License](#license) * [Certificate of Origin - Sign your work](#certificate-of-origin---sign-your-work) * [Branches](#branches) ## Making a Change ### Open an issue first **Before making any significant changes, please open an issue**. Each issue should describe the following: * Requirement - what kind of business use case are you trying to solve? * Problem - what in Jaeger blocks you from solving the requirement? * Proposal - what changes do you propose to solve the problem or improve the existing situation? * Any open questions to address Discussing your proposed changes ahead of time will make the contribution process smooth for everyone. Once the approach is agreed upon, make your changes and open a pull request (PR). ### Assigning Issues We do not assign issues to contributors. It is almost never the case that multiple people jump on the same issue, and practice showed that occasionally people who ask for an issue to be assigned to them later have a change in priorities and are unable to find time to finish it, which leaves the issue in limbo. So if you have a desire to work on an issue, feel free to mention it in the comment and just submit a PR. ### Creating a pull request If you are new to GitHub's contribution workflow, we recommend the following setup: * Go to the respective Jaeger repo on GitHub and create a fork using the button at the top. Select a destination org where you have write permissions (usually it is your personal "org"). * Clone the fork into your workspace. * (Recommended): register upstream repo as remote * After you clone your forked repo, running below command ```bash git remote -v ``` will show `origin`, e.g. `origin git@github.com:{username}/jaeger.git` * Add `upstream` remote: ```bash git remote add upstream git@github.com:jaegertracing/jaeger.git ``` * Fetch it: ```bash git fetch upstream main ``` * Repoint your main branch: ```bash git branch --set-upstream-to=upstream/main main ``` * With this setup, you will not need to keep your main branch in the fork in sync with the upstream repo. Once you're ready to make changes: * Create a new local branch (DO NOT make changes to `main`, it will cause CI errors). * Commit your changes, making sure **each commit is signed** ([see below](#certificate-of-origin---sign-your-work)): ```bash git commit -s -m "Your commit message" ``` * You do not need to squash the commits, it will happen once the PR is merged into the official repo (but each individual commit must be signed). * When satisfied, push the changes. Git will likely ask for upstream destination, so you push commits like this: ```bash git push --set-upstream origin {branch-name} ``` * After you push, look for the output, it usually contains a URL to create a pull request. * After raising a PR, please refrain from repeatedly merging the upstream main branch into your feature branch unless you're specifically resolving merge conflicts or updating for critical changes. Each merge triggers a reset of CI checks, requiring maintainers to re-approve your PR, which adds unnecessary overhead. Each PR should have: * A descriptive title, known as ["commit message"][good-commit-msg]. In summary: * Limit the title to 50 characters * Capitalize the title * Do not end the title with a period * Use the imperative mood in the title * A description of the problem it is solving. It could be simply a reference to the corresponding issue, e.g. `Resolves #123`. * A summary of changes made to solve the problem. Explain _what_ and _why_ instead of _how_. ## AI Usage Policy ### Goals This policy exists to: - **Keep the effort balanced** – Before AI, contributors did most of the work (writing, testing, understanding). We want to keep it that way. AI should help you, not replace your effort. - **Protect maintainer time** – Large, low-quality AI-generated PRs shift the burden to reviewers. We want to avoid that. - **Ensure understanding** – Contributors should understand and be able to explain every change they submit. - **Keep conversations human** – Code review is a discussion between people, not bots. ### Good use of AI - Using AI to help you understand the code. - Using AI to write drafts of code, tests, or docs. - Using AI to explore ideas or try different approaches. - Saying "AI helped me write this" in your PR description. ### Disallowed use of AI - Copy-pasting AI output without reading or understanding it. - Submitting AI-generated code without testing. - Using AI to reply to review comments – reviewers want to talk to you, not a bot. ### Your responsibility - You own everything you submit, even if AI wrote it. - You must understand your code well enough to explain it. - You must run tests locally before opening or updating a PR. - If AI wrote a big part of your PR, mention that in the PR description. ### Enforcement - PRs that look like low-effort AI slop will be closed. - Repeated violators may be banned from the project. ## Pull Request Limits for New Contributors To ensure high-quality code reviews and long-term codebase stability, we limit the number of simultaneous open PRs for new contributors. ### The Policy Your limit of **simultaneous open PRs** is based on your history with this project: | Merged PRs in this project | Max Simultaneous Open PRs | | :--- | :--- | | **0** (First-time contributor) | **1** | | **1** merged PR | **2** | | **2** merged PRs | **3** | | **3+** merged PRs | **Unlimited** | ### Why We Do This AI tools have dramatically reduced the cost of creating pull requests, but the burden on maintainers for reviewing them remains the same. Large-scale or complex refactors require significant effort to review, and a high volume of PRs from new contributors often leads to: * **Review Bottlenecks:** Quality reviews take time. A flood of PRs prevents us from giving any single PR the attention it deserves. * **Context Fragmentation:** Refactoring legacy code requires deep understanding. We prefer to work with you on one area of the code at a time to ensure the architectural direction is correct. * **Reduced Noise:** This policy helps us distinguish between intentional improvements and automated "bulk" refactors that may introduce subtle regressions. ### How to Proceed If you reach your limit, please focus on addressing feedback and merging your existing PR(s) before opening new ones. PRs opened in excess of these limits may be labeled as **on-hold** or closed to keep our backlog manageable. ### Tips for Success * **Verify AI Output:** If you use AI tools to assist in refactoring, you are responsible for manually verifying every line. We expect contributors to be able to explain the "why" behind every change. * **Small over Large:** Smaller, atomic PRs are much more likely to be merged quickly than large refactors. ## License By contributing your code, you agree to license your contribution under the terms of the [Apache License](./LICENSE). ### Copyright Header If you are adding a new file it should have a header like below. In some languages, e.g. Python, you may need to change the comments to start with `#`. The easiest way is to copy the header from one of the existing source files and make sure the year is current and the copyright says "The Jaeger Authors". ``` // Copyright (c) 2026 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 ``` **Never remove existing copyright headers**. Some files may have other copyright headers, such as: ``` // Copyright (c) 2017 Uber Technologies, Inc. ``` If you are modifying such a file you may add Jaeger copyright on top: ``` // Copyright (c) 2026 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. ``` ## Certificate of Origin - Sign your work By contributing to this project you agree to the [Developer Certificate of Origin](https://developercertificate.org/) (or simply [DCO](./DCO)). This document was created by the Linux Kernel community and is a simple statement that you, as a contributor, have the legal right to make the contribution. The sign-off is a simple line at the end of the explanation for the patch, which certifies that you wrote it or otherwise have the right to pass it on as an open-source patch. The rules are pretty simple: if you can certify the conditions in the [DCO](./DCO), then just add a line to every git commit message: Signed-off-by: Bender Bending Rodriguez using your real name (sorry, no pseudonyms or anonymous contributions.) You can add the sign off when creating the git commit via `git commit -s`. ### Missing sign-offs Note that **every commit in the pull request must be signed**. Jaeger repositories are configured with a [DCO-bot][dco-bot] that will check sign-offs on every commit and block the PR from being merged if some commits are missing sign-offs. If you only have one commit or the latest commit in the PR is missing a sign-off, the simplest way to fix this is to run: ``` git commit --amend -s ``` which will prompt you to edit the commit message while adding a signature. Simply accept the text as is, and push the branch: ``` git push --force ``` If some commit in the middle of your commit history is missing the sign-off, the simplest solution is to squash the commits into one and sign it. For example, suppose that your branch history looks like this: ``` fe43631 - Fix HotROD Docker command 933efb3 - Add files for ingester 214c133 - Rename gas to gosec 0a40309 - Update Makefile build_ui target to lerna structure 7919cd9 - Add support for Cassandra reconnect interval a0dc40e - Fix deploy step 77a0573 - (tag: v1.6.0) Prepare release 1.6.0 ``` Let's assume that the first commit `77a0573` was the commit before you started work on your PR, and commits from `a0dc40e` to `fe43631` are your changes that you want to squash. You can run the soft reset command: ``` git reset --soft 77a0573 ``` It will undo all changes after commit `77a0573` and stage them. You can commit them all at once while adding the signature: ``` git commit -s -m 'your commit message, e.g. the PR title' ``` Then push the branch: ``` git push --force ``` [good-commit-msg]: https://chris.beams.io/posts/git-commit/ [dco-bot]: https://github.com/probot/dco#how-it-works ## Branches Before submitting a PR make sure to create a named branch in your forked repository. Our CI will fail if you submit a PR from the `main` branch. If that happens, just create a new branch and re-submit the PR from that branch. ================================================ FILE: DCO ================================================ Developer Certificate of Origin Version 1.1 Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 660 York Street, Suite 102, San Francisco, CA 94110 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ================================================ FILE: GOVERNANCE.md ================================================ # Jaeger Governance This document defines governance policies for the Jaeger project. ## Maintainers Jaeger Maintainers have write access to the Jaeger GitHub repository https://github.com/jaegertracing/jaeger. They can merge their own patches or patches from others. The current maintainers can be found in [MAINTAINERS](./MAINTAINERS.md). This privilege is granted with some expectation of responsibility: maintainers are people who care about the Jaeger project and want to help it grow and improve. A maintainer is not just someone who can make changes, but someone who has demonstrated his or her ability to collaborate with the team, get the most knowledgeable people to review code, contribute high-quality code, and follow through to fix issues (in code or tests). A maintainer is a contributor to the Jaeger project's success and a citizen helping the project succeed. ## Becoming a Maintainer To become a maintainer you need to demonstrate the following: * commitment to the project * participate in discussions, contributions, code reviews for 3 months or more, * perform code reviews for 10 non-trivial pull requests, * contribute 10 non-trivial pull requests and have them merged into the `main` branch, * ability to write good code, * ability to collaborate with the team, * understanding of how the team works (policies, processes for testing and code review, etc), * understanding of the projects' code base and coding style. A new maintainer must be proposed by an existing maintainer by sending a message to the [jaeger-tracing@googlegroups.com](https://groups.google.com/forum/#!forum/jaeger-tracing) mailing list, or by opening an issue on GitHub, containing the following information: * nominee's first and last name and GitHub user name, * an explanation of why the nominee should be a committer, * a list of links to non-trivial pull requests (top 10) authored by the nominee. Two other maintainers need to second the nomination. If no one objects in 5 working days (U.S.), the nomination is accepted. If anyone objects or wants more information, the maintainers discuss and usually come to a consensus (within the 5 working days). If issues can't be resolved, there's a simple majority vote among current maintainers. ## Maintainer duties Maintainers are required to participate in the project, by joining discussions, submitting and reviewing pull requests, answering user questions, among others. Besides that, we have one concrete activity in which maintainers have to engage from time to time: releasing new versions of Jaeger. This process ideally takes only a couple of hours, but requires coordination on different fronts. Even though the process is well documented, it is not without eventual glitches, so, each release needs a "Release Manager". How it works is described in the [RELEASE.md](RELEASE.md) file. Maintainers are also encouraged to speak about Jaeger at conferences, especially KubeCon+CloudNativeCon which happens twice a year. This event has a "maintainer track", in which maintainers can give an introduction and/or a deep dive about their projects. The Jaeger project has always participated since it became part of the CNCF. ## Changes in Maintainership We do not expect anyone to make a permanent commitment to be a Jaeger maintainer forever. After all, circumstances change, people get new jobs, new interests, and may not be able to continue contributing to the project. At the same time, we need to keep the list of maintainers current in order to have effective governance. People may be removed from the current list of maintainers via one of the following ways: * They can resign * If they stop contributing to the project for a period of 6 months or more * By a 2/3 majority vote by maintainers Former maintainers can be reinstated to full maintainer status through the same process of [Becoming a Maintainer](#becoming-a-maintainer) as first-time nominees. ## Emeritus Maintainers Former maintainers are recognized with an honorary _Emeritus Maintainer_ status, and have their names permanently listed in the [MAINTAINERS](./MAINTAINERS.md#emeritus-maintainers) file as a form of gratitude for their contributions. ## GitHub Project Administration Maintainers will be added to the GitHub @jaegertracing/jaeger-maintainers team, and made a GitHub maintainer of that team. They will be given write permission to the Jaeger GitHub repository https://github.com/jaegertracing/jaeger. ## Changes in Governance All changes in Governance require a 2/3 majority vote by maintainers. ## Other Changes Unless specified above, all other changes to the project require a 2/3 majority vote by maintainers. Additionally, any maintainer may request that any change require a 2/3 majority vote by maintainers. ================================================ FILE: LICENSE ================================================ 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. ================================================ FILE: MAINTAINERS.md ================================================ The current Maintainers Group for the Jaeger Project consists of: | Name | Employer | Responsibilities | | ---- | -------- | ---------------- | | [@albertteoh](https://github.com/albertteoh) | PackSmith | ALL | | [@jkowall](https://github.com/jkowall) | Paessler | ALL | | [@joe-elliott](https://github.com/joe-elliott) | Grafana Labs | ALL | | [@mahadzaryab1](https://github.com/mahadzaryab1) | Bloomberg | ALL | | [@pavolloffay](https://github.com/pavolloffay) | RedHat | ALL | | [@yurishkuro](https://github.com/yurishkuro) | Meta | ALL | This list must be kept in sync with the [CNCF Project Maintainers list](https://github.com/cncf/foundation/blob/master/project-maintainers.csv). See [the project Governance](./GOVERNANCE.md) for how maintainers are selected and replaced. ### Emeritus Maintainers We are grateful to our former maintainers for their contributions to the Jaeger project. * [@black-adder](https://github.com/black-adder) * [@jpkrohling](https://github.com/jpkrohling) * [@objectiser](https://github.com/objectiser) * [@tiffon](https://github.com/tiffon) * [@vprithvi](https://github.com/vprithvi) ### Maintainer Onboarding Upon approval, the following steps should be taken to onboard the new maintainer: * **1. Update Project Documentation** * **`MAINTAINERS.md` File:** Merge the PR to add the new maintainer to the `MAINTAINERS.md` file(s) in the relevant Jaeger repositories. * **2. Grant Permissions** * **GitHub:** Add the new maintainer to the `@jaegertracing/jaeger-maintainers` GitHub team. This grants them write access to the Jaeger repositories. * **CNCF Mailing List:** Add the new maintainer to the `cncf-jaeger-maintainers@lists.cncf.io` mailing list (and any other relevant Jaeger mailing lists). Contact the existing `cncf-jaeger-maintainers` to find out the precise process for adding to the mailing list, it will likely involve getting in touch with the CNCF. * **CNCF Maintainer Registry:** * Create a PR against the `cncf/foundation` repository to add the new maintainer's information to the `project-maintainers.csv` file. The following fields are required: * Reference the PR in the `cncf-jaeger-maintainers` mailing list. * **Signing Keys:** * Jaeger uses a GPG key for encrypted emails sent to the maintainers for security reports along with access to the `maintainers-only` GitHub repository. This key is stored in our 1password repository. * **1Password:** Connect with an existing maintainer to be added to our jaegertracing 1Password team. * **Netlify:** Is where our website is served from, an existing maintainer can add you to our team. * We use two analytics tools, one is **Scarf** and the other is **Google Analytics** for the projects public facing sites and downloads. * **3. Announcement** * Announce the new maintainer to the Jaeger community through the mailing list, blog, or other appropriate channels. ### Maintainer Offboarding The process for removing a maintainer is similar to adding one. A maintainer can step down voluntarily or be removed by a vote of the other maintainers if they are no longer fulfilling their responsibilities or are violating the project's Code of Conduct. A supermajority vote is needed to remove a maintainer. Their access should be revoked from all relevant tools, and the project documentation updated accordingly. ================================================ FILE: Makefile ================================================ # Copyright (c) 2023 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 SHELL := /bin/bash JAEGER_IMPORT_PATH = github.com/jaegertracing/jaeger # PLATFORMS is a list of all supported platforms PLATFORMS="linux/amd64,linux/arm64,linux/s390x,linux/ppc64le,darwin/amd64,darwin/arm64,windows/amd64" LINUX_PLATFORMS=$(shell echo "$(PLATFORMS)" | tr ',' '\n' | grep linux | tr '\n' ',' | sed 's/,$$/\n/') # SRC_ROOT is the top of the source tree. SRC_ROOT := $(shell git rev-parse --show-toplevel) ifeq ($(DEBUG_BINARY),) DISABLE_OPTIMIZATIONS = SUFFIX = TARGET = release else DISABLE_OPTIMIZATIONS = -gcflags="all=-N -l" SUFFIX = -debug TARGET = debug endif # All .go files that are not auto-generated and should be auto-formatted and linted. ALL_SRC = $(shell find . -name '*.go' \ -not -name '_*' \ -not -name '.*' \ -not -name 'mocks*' \ -not -name '*.pb.go' \ -not -path './vendor/*' \ -not -path './idl/*' \ -not -path './internal/tools/*' \ -not -path './scripts/build/docker/debug/*' \ -not -path '*/mocks/*' \ -type f | \ sort) # All .sh or .py or Makefile or .mk files that should be auto-formatted and linted. SCRIPTS_SRC = $(shell find . \( -name '*.sh' -o -name '*.py' -o -name '*.mk' -o -name 'Makefile*' -o -name 'Dockerfile*' \) \ -not -path './.git/*' \ -not -path './vendor/*' \ -not -path './idl/*' \ -not -path './jaeger-ui/*' \ -type f | \ sort) # ALL_PKGS is used with 'nocover' and 'goleak' ALL_PKGS = $(shell echo $(dir $(ALL_SRC)) | tr ' ' '\n' | grep -v '/.*-gen/' | sort -u) GO=go GOOS ?= $(shell $(GO) env GOOS) GOARCH ?= $(shell $(GO) env GOARCH) # go test does not support -race flag on s390x architecture ifeq ($(GOARCH), s390x) RACE= else RACE=-race endif # sed on Mac does not support the same syntax for in-place updates as sed on Linux # When running on MacOS it's best to install gsed and run Makefile with SED=gsed. # We want the actual OS here, not what GOOS may have been set to by recursive make calls. ifeq ($(shell GOOS= $(GO) env GOOS),darwin) SED=gsed else SED=sed endif GOTEST_QUIET=$(GO) test $(RACE) GOTEST=$(GOTEST_QUIET) -v COVEROUT=cover.out GOFMT=gofmt FMT_LOG=.fmt.log IMPORT_LOG=.import.log COLORIZE ?= | $(SED) 's/PASS/✅ PASS/g' | $(SED) 's/FAIL/❌ FAIL/g' | $(SED) 's/SKIP/🔕 SKIP/g' # import other Makefiles after the variables are defined include scripts/makefiles/BuildBinaries.mk include scripts/makefiles/BuildInfo.mk include scripts/makefiles/Docker.mk include scripts/makefiles/IntegrationTests.mk include scripts/makefiles/Protobuf.mk include scripts/makefiles/Tools.mk include scripts/makefiles/Windows.mk .DEFAULT_GOAL := test-and-lint .PHONY: test-and-lint test-and-lint: test fmt lint .PHONY: echo-version echo-version: @echo "$(GIT_CLOSEST_TAG)" .PHONY: echo-platforms echo-platforms: @echo "$(PLATFORMS)" .PHONY: echo-linux-platforms echo-linux-platforms: @echo "$(LINUX_PLATFORMS)" .PHONY: echo-all-pkgs echo-all-pkgs: @echo $(ALL_PKGS) | tr ' ' '\n' | sort .PHONY: echo-all-srcs echo-all-srcs: @echo $(ALL_SRC) | tr ' ' '\n' | sort .PHONY: clean clean: rm -rf cover*.out .cover/ cover.html $(FMT_LOG) $(IMPORT_LOG) \ jaeger-ui/packages/jaeger-ui/build find ./cmd/jaeger/internal/extension/jaegerquery/internal/ui/actual -type f -name '*.gz' -delete GOCACHE=$(GOCACHE) go clean -cache -testcache bash scripts/build/clean-binaries.sh .PHONY: test test: bash -c "set -e; set -o pipefail; $(GOTEST) -tags=memory_storage_integration ./... $(COLORIZE)" .PHONY: cover cover: nocover bash -c "set -e; set -o pipefail; STORAGE=memory $(GOTEST) -timeout 5m -coverprofile $(COVEROUT) ./... | tee test-results.json" go tool cover -html=cover.out -o cover.html .PHONY: nocover nocover: @echo Verifying that all packages have test files to count in coverage @scripts/lint/check-test-files.sh $(ALL_PKGS) .PHONY: fmt fmt: $(GOFUMPT) @echo Running import-order-cleanup on ALL_SRC ... @./scripts/lint/import-order-cleanup.py -o inplace -t $(ALL_SRC) @echo Running gofmt on ALL_SRC ... @$(GOFMT) -e -s -l -w $(ALL_SRC) @echo Running gofumpt on ALL_SRC ... @$(GOFUMPT) -e -l -w $(ALL_SRC) @echo Running updateLicense.py on ALL_SRC ... @./scripts/lint/updateLicense.py $(ALL_SRC) $(SCRIPTS_SRC) .PHONY: lint lint: lint-fmt lint-license lint-imports lint-semconv lint-goversion lint-goleak lint-go .PHONY: lint-license lint-license: @echo Verifying that all files have license headers @./scripts/lint/updateLicense.py $(ALL_SRC) $(SCRIPTS_SRC) > $(FMT_LOG) @[ ! -s "$(FMT_LOG)" ] || (echo "License check failures, run 'make fmt'" | cat - $(FMT_LOG) && false) .PHONY: lint-nocommit lint-nocommit: @if git diff origin/main | grep '@no''commit' ; then \ echo "❌ Cannot merge PR that contains @no""commit string" ; \ GIT_PAGER=cat git diff -G '@no''commit' origin/main ; \ false ; \ else \ echo "✅ Changes do not contain @no""commit string" ; \ fi .PHONY: lint-imports lint-imports: @echo Verifying that all Go files have correctly ordered imports @./scripts/lint/import-order-cleanup.py -o stdout -t $(ALL_SRC) > $(IMPORT_LOG) @[ ! -s "$(IMPORT_LOG)" ] || (echo "Import ordering failures, run 'make fmt'" | cat - $(IMPORT_LOG) && false) .PHONY: lint-fmt lint-fmt: $(GOFUMPT) @echo Verifying that all Go files are formatted with gofmt and gofumpt @rm -f $(FMT_LOG) @$(GOFMT) -d -e -s $(ALL_SRC) > $(FMT_LOG) || true @$(GOFUMPT) -d -e $(ALL_SRC) >> $(FMT_LOG) || true @[ ! -s "$(FMT_LOG)" ] || (echo "Formatting check failed. Please run 'make fmt'" && head -100 $(FMT_LOG) && false) .PHONY: lint-semconv lint-semconv: ./scripts/lint/check-semconv-version.sh .PHONY: lint-goversion lint-goversion: ./scripts/lint/check-go-version.sh .PHONY: lint-goleak lint-goleak: @echo Verifying that all packages with tests have goleak in their TestMain @scripts/lint/check-goleak-files.sh $(ALL_PKGS) .PHONY: lint-go lint-go: $(LINT) $(LINT) -v run .PHONY: govulncheck govulncheck: $(GOVULNCHECK) $(GOVULNCHECK) ./... .PHONY: lint-jaeger-idl-versions lint-jaeger-idl-versions: @echo "checking jaeger-idl version mismatch between git submodule and go.mod dependency" @./scripts/lint/check-jaeger-idl-version.sh .PHONY: run-all-in-one run-all-in-one: build-ui go run ./cmd/all-in-one --log-level debug .PHONY: changelog changelog: @./scripts/release/notes.py --exclude-dependabot --verbose .PHONY: draft-release draft-release: ./scripts/release/draft.py .PHONY: prepare-release prepare-release: @if [ -z "$(VERSION)" ]; then \ echo "Usage: make prepare-release VERSION=x.x.x"; \ echo "Example: make prepare-release VERSION=2.14.0"; \ exit 1; \ fi bash ./scripts/release/prepare.sh $(VERSION) .PHONY: test-ci test-ci: GOTEST := $(GOTEST_QUIET) test-ci: build-examples cover .PHONY: init-submodules init-submodules: git submodule update --init --recursive MOCKERY_FLAGS := --all --disable-version-string .PHONY: generate-mocks generate-mocks: $(MOCKERY) find . -path '*/mocks/*' -name '*.go' -type f -delete $(MOCKERY) | tee .mockery.log .PHONY: certs certs: cd internal/config/tlscfg/testdata && ./gen-certs.sh .PHONY: certs-dryrun certs-dryrun: cd internal/config/tlscfg/testdata && ./gen-certs.sh -d .PHONY: repro-check repro-check: # Check local reproducibility of generated executables. $(MAKE) clean $(MAKE) build-all-platforms # Generate checksum for all executables under ./cmd find cmd -type f -executable -exec shasum -b -a 256 {} \; | sort -k2 | tee sha256sum.combined.txt $(MAKE) clean $(MAKE) build-all-platforms shasum -b -a 256 --strict --check ./sha256sum.combined.txt ================================================ FILE: NOTICE ================================================ Jaeger, Distributed Tracing Platform. Copyright 2015-2019 The Jaeger Project Authors Licensed under Apache License 2.0. See LICENSE for terms. Includes software developed at Uber Technologies, Inc. (https://eng.uber.com/). ================================================ FILE: README.md ================================================ [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://stand-with-ukraine.pp.ua) [![Slack chat][slack-img]](#get-in-touch) [![Unit Tests][ci-img]][ci] [![Coverage Status][cov-img]][cov] [![Project+Community stats][community-badge]][community-stats] [![FOSSA Status][fossa-img]][fossa] [![OpenSSF Scorecard][openssf-img]][openssf] [![OpenSSF Best Practices][openssf-bp-img]][openssf-bp] [![CLOMonitor][clomonitor-img]][clomonitor] [![Artifact Hub][artifacthub-img]][artifacthub] # Jaeger - a Distributed Tracing System 💥💥💥 Jaeger v2 is out! Read the [blog post](https://medium.com/jaegertracing/jaeger-v2-released-09a6033d1b10) and [try it out](https://www.jaegertracing.io/docs/latest/getting-started/). ## Quick Start Get Jaeger running in seconds with Docker: ```bash # Run Jaeger all-in-one (includes UI, collector, query, and in-memory storage) docker run --rm --name jaeger \ -p 16686:16686 \ -p 4317:4317 \ -p 4318:4318 \ jaegertracing/jaeger:latest # Access the UI at http://localhost:16686 # Send traces via OTLP: gRPC on port 4317, HTTP on port 4318 ``` For production deployments and more options, see the [Getting Started Guide](https://www.jaegertracing.io/docs/latest/getting-started/). ## Architecture ```mermaid graph TD SDK["OpenTelemetry SDK"] --> |HTTP or gRPC| COLLECTOR COLLECTOR["Jaeger Collector"] --> STORE[Storage] COLLECTOR --> |gRPC| PLUGIN[Storage Plugin] COLLECTOR --> |gRPC/sampling| SDK PLUGIN --> STORE QUERY[Jaeger Query Service] --> STORE QUERY --> |gRPC| PLUGIN UI[Jaeger UI] --> |HTTP| QUERY subgraph Application Host subgraph User Application SDK end end ``` Jaeger is a distributed tracing platform created by [Uber Technologies](https://eng.uber.com/distributed-tracing/) and donated to [Cloud Native Computing Foundation](https://cncf.io). See Jaeger [documentation][doc] for getting started, operational details, and other information. Jaeger is hosted by the [Cloud Native Computing Foundation](https://cncf.io) (CNCF) as the 7th top-level project, graduated in October 2019. See the CNCF [Jaeger incubation announcement](https://www.cncf.io/blog/2017/09/13/cncf-hosts-jaeger/) and [Jaeger graduation announcement](https://www.cncf.io/announcement/2019/10/31/cloud-native-computing-foundation-announces-jaeger-graduation/). ## Get Involved Jaeger is an open source project with open governance. We welcome contributions from the community, and we would love your help to improve and extend the project. Here are [some ideas](https://www.jaegertracing.io/get-involved/) for how to get involved. Many of them do not even require any coding. ## Version Compatibility Guarantees Since Jaeger uses many components from the [OpenTelemetry Collector](https://github.com/open-telemetry/opentelemetry-collector/) we try to maintain configuration compatibility between Jaeger releases. Occasionally, configuration options in Jaeger (or in Jaeger v1 CLI flags) can be deprecated due to usability improvements, new functionality, or changes in our dependencies. In such situations, developers introducing the deprecation are required to follow [these guidelines](./CONTRIBUTING.md#deprecating-cli-flags). In short, for a deprecated configuration option, you should expect to see the following message in the documentation or release notes: ``` (deprecated, will be removed after yyyy-mm-dd or in release vX.Y.Z, whichever is later) ``` A grace period of at least **3 months** or **two minor version bumps** (whichever is later) from the first release containing the deprecation notice will be provided before the deprecated configuration option _can_ be deleted. For example, consider a scenario where v2.0.0 is released on 01-Sep-2024 containing a deprecation notice for a configuration option. This configuration option will remain in a deprecated state until the later of 01-Dec-2024 or v2.2.0 where it _can_ be removed on or after either of those events. It may remain deprecated for longer than the aforementioned grace period. ## Go Version Compatibility Guarantees The Jaeger project attempts to track the currently supported versions of Go, as [defined by the Go team](https://go.dev/doc/devel/release#policy). Removing support for an unsupported Go version is not considered a breaking change. Starting with the release of Go 1.21, support for Go versions will be updated as follows: 1. Soon after the release of a new Go minor version `N`, updates will be made to the build and tests steps to accommodate the latest Go minor version. 2. Soon after the release of a new Go minor version `N`, support for Go version `N-1` will be removed and version `N` will become the minimum required version. Note: All importable code has been moved to internal packages, so there is no need to maintain backward compatibility with older compilers (previously version `N-1` was used). ## Related Repositories ### Components * [UI](https://github.com/jaegertracing/jaeger-ui) * [Data model](https://github.com/jaegertracing/jaeger-idl) ### Documentation * Published: https://www.jaegertracing.io/docs/ * Source: https://github.com/jaegertracing/documentation ## Building From Source See [CONTRIBUTING](./CONTRIBUTING.md). ## Contributing See [CONTRIBUTING](./CONTRIBUTING.md). Thanks to all the people who already contributed! ### Maintainers Rules for becoming a maintainer are defined in the [GOVERNANCE](./GOVERNANCE.md) document. The official maintainers of the Jaeger project are listed in the [MAINTAINERS](./MAINTAINERS.md) file. Please use `@jaegertracing/jaeger-maintainers` to tag them on issues / PRs. Some repositories under [jaegertracing](https://github.com/jaegertracing) org have additional maintainers. ## Project Status Meetings The Jaeger maintainers and contributors meet regularly on a video call. Everyone is welcome to join, including end users. For meeting details, see https://www.jaegertracing.io/get-in-touch/. ## Roadmap See https://www.jaegertracing.io/docs/roadmap/ ## Get in Touch Have questions, suggestions, bug reports? Reach the project community via these channels: * [Slack chat room `#jaeger`][slack] (need to join [CNCF Slack][slack-join] for the first time) * [`jaeger-tracing` mail group](https://groups.google.com/forum/#!forum/jaeger-tracing) * GitHub [issues](https://github.com/jaegertracing/jaeger/issues) and [discussions](https://github.com/jaegertracing/jaeger/discussions) ## Security Third-party security audits of Jaeger are available in https://github.com/jaegertracing/security-audits. Please see [Issue #1718](https://github.com/jaegertracing/jaeger/issues/1718) for the summary of available security mechanisms in Jaeger. ## Adopters Jaeger as a product consists of multiple components. We want to support different types of users, whether they are only using our instrumentation libraries or full end to end Jaeger installation, whether it runs in production or you use it to troubleshoot issues in development. Please see [ADOPTERS.md](./ADOPTERS.md) for some of the organizations using Jaeger today. If you would like to add your organization to the list, please comment on our [survey issue](https://github.com/jaegertracing/jaeger/issues/207). ## Sponsors The Jaeger project owes its success in open source largely to the Cloud Native Computing Foundation (CNCF), our primary supporter. We deeply appreciate their vital support. Furthermore, we are grateful to Uber for their initial, project-launching donation, and for the continuous contributions of software and infrastructure from 1Password, Codecov.io, Dosu, GitHub, Google Analytics, Netlify, and Oracle Cloud Infrastructure. Thank you for your generous support. ## License Copyright (c) The Jaeger Authors. [Apache 2.0 License](./LICENSE). [doc]: https://jaegertracing.io/docs/ [ci-img]: https://github.com/jaegertracing/jaeger/actions/workflows/ci-unit-tests.yml/badge.svg?branch=main [ci]: https://github.com/jaegertracing/jaeger/actions/workflows/ci-unit-tests.yml?query=branch%3Amain [cov-img]: https://codecov.io/gh/jaegertracing/jaeger/branch/main/graph/badge.svg [cov]: https://codecov.io/gh/jaegertracing/jaeger/branch/main/ [fossa-img]: https://app.fossa.com/api/projects/git%2Bgithub.com%2Fjaegertracing%2Fjaeger.svg?type=shield [fossa]: https://app.fossa.io/projects/git%2Bgithub.com%2Fjaegertracing%2Fjaeger?ref=badge_shield [openssf-img]: https://api.securityscorecards.dev/projects/github.com/jaegertracing/jaeger/badge [openssf]: https://securityscorecards.dev/viewer/?uri=github.com/jaegertracing/jaeger [openssf-bp-img]: https://www.bestpractices.dev/projects/1273/badge [openssf-bp]: https://www.bestpractices.dev/projects/1273 [clomonitor-img]: https://img.shields.io/endpoint?url=https://clomonitor.io/api/projects/cncf/jaeger/badge [clomonitor]: https://clomonitor.io/projects/cncf/jaeger [artifacthub-img]: https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/jaegertracing [artifacthub]: https://artifacthub.io/packages/search?repo=jaegertracing [community-badge]: https://img.shields.io/badge/Project+Community-stats-blue.svg [community-stats]: https://all.devstats.cncf.io/d/54/project-health?orgId=1&var-repogroup_name=Jaeger [hotrod-tutorial]: https://medium.com/jaegertracing/take-jaeger-for-a-hotrod-ride-233cf43e46c2 [slack]: https://cloud-native.slack.com/archives/CGG7NFUJ3 [slack-join]: https://slack.cncf.io [slack-img]: https://img.shields.io/badge/slack-join%20chat%20%E2%86%92-brightgreen?logo=slack ================================================ FILE: RELEASE.md ================================================ # Jaeger Overall Release Process ## ⭐ Start Here: Create Tracking Issue for Release ⭐ Run the following command to create a tracking issue with the full checklist: ```bash bash scripts/release/start.sh ``` This script will: - Automatically determine the next version number (e.g., v2.14.0) - Create a GitHub issue with the complete release checklist - Include the exact automation commands with the correct version numbers Example output: ``` Current version: v2.13.0 New version: v2.14.0 ... Creating issue in jaegertracing/jaeger https://github.com/jaegertracing/jaeger/issues/7757 ``` ## 📝 Release Steps Follow the checklist in the created tracking issue. The high level steps are: 1. Perform UI release according to 2. Perform Backend release (see below) 3. [Publish documentation](https://github.com/jaegertracing/documentation/blob/main/RELEASE.md) for the new version on `jaegertracing.io`. # ⚙️ Jaeger Backend Release Process 1. Create a PR "Prepare release vX.Y.Z" against main or maintenance branch ([example](https://github.com/jaegertracing/jaeger/pull/6826)). * **Automated**: ```bash make prepare-release VERSION=X.Y.Z ``` * Updates CHANGELOG.md (generates content via `make changelog`) * Upgrades jaeger-ui submodule to the corresponding version * Rotates release managers table * Creates PR with label `changelog:skip` * Manual: See [Manual release pull request](https://github.com/jaegertracing/jaeger/blob/main/RELEASE.md#manual-release-pull-request). 2. After the PR is merged, create a release on Github: * **Automated**: ```bash make draft-release ``` * Manual: See [Manual release](https://github.com/jaegertracing/jaeger/blob/main/RELEASE.md#manual-release). 3. Once the release is created, the [Publish Release workflow](https://github.com/jaegertracing/jaeger/actions/workflows/ci-release.yml) will run to build artifacts. * Wait for the workflow to finish. For monitoring and troubleshooting, open the logs of the workflow run from above URL. * Check the images are available on [Docker Hub](https://hub.docker.com/r/jaegertracing/) and binaries are uploaded [to the release](https://github.com/jaegertracing/jaeger/releases). ## Manual release pull request Create a PR "Prepare release vX.Y.Z" against main or maintenance branch ([example](https://github.com/jaegertracing/jaeger/pull/6826)). * Update CHANGELOG.md to include: * A new section with the header `vX.Y.Z (YYYY-MM-DD)` (copy the template at the top) * A curated list of notable changes and links to PRs. Do not simply dump git log, select the changes that affect the users. To obtain the list of all changes run `make changelog`. * The section can be split into sub-section if necessary, e.g. UI Changes, Backend Changes, Bug Fixes, etc. * Then upgrade the submodule versions and finally commit. For example: ``` git submodule init git submodule update pushd jaeger-ui git checkout main git pull git checkout vX.Y.Z # use the new version popd ``` * If there are only dependency bumps, indicate this with "Dependencies upgrades only" ([example](https://github.com/jaegertracing/jaeger-ui/pull/2431/files)). * If there are no changes, indicate this with "No changes" ([example](https://github.com/jaegertracing/jaeger/pull/4131/files)). * Rotate the below release managers table placing yourself at the bottom. The date should be the first Wednesday of the month. * Commit, push and open a PR. * Add label `changelog:skip` to the pull request. ## Manual release Create a release on [GitHub Releases](https://github.com/jaegertracing/jaeger/releases/): * Title "Prepare Release v2.x.x" * Tag `v2.x.x` (note the `v` prefix) and choose appropriate branch (usually `main`) * Copy the new CHANGELOG.md section into the release notes * Extra: GitHub has a button "generate release notes". Those are not formatted as we want, but it has a nice feature of explicitly listing first-time contributors. Before doing the previous step, you can click that button and then remove everything except the New Contributors section. Change the header to `### 👏 New Contributors`, then copy the main changelog above it. [Example](https://github.com/jaegertracing/jaeger/releases/tag/v1.55.0). ## 🔧 Patch Release Sometimes we need to do a patch release, e.g. to fix a newly introduced bug. If the main branch already contains newer changes, it is recommended that a patch release is done from a version branch. Maintenance branches should follow naming convention: `release-major.minor` (e.g.`release-1.8`). 1. Find the commit in `main` for the release you want to patch (e.g., `a49094c2` for v1.34.0). 2. `git checkout ${commit}; git checkout -b ${branch-name}`. The branch name should be in the form `release-major.minor`, e.g., `release-1.34`. Push the branch to the upstream repository. 3. Apply fixes to the branch. The recommended way is to merge the fixes into `main` first and then cherry-pick them into the version branch (e.g., `git cherry-pick c733708c` for the fix going into `v1.34.1`). 4. Follow the regular process for creating a release (except for the Documentation step). * When creating a release on GitHub, pick the version branch when applying the new tag. * Once the release tag is created, the `ci-release` workflow will kick in and deploy the artifacts for the patch release. 5. Do not perform a new release of the documentation since the major.minor is not changing. The one change that may be useful is bumping the `binariesLatest` variable in the `config.toml` file ([example](https://github.com/jaegertracing/documentation/commit/eacb52f332a7e069c254e652a6b4a58ea5a07b32)). ## 👥 Release managers A Release Manager is the person responsible for ensuring that a new version of Jaeger is released. This person will coordinate the required changes, including to the related components such as UI, IDL, and jaeger-lib and will address any problems that might happen during the release, making sure that the documentation above is correct. In order to ensure that knowledge about releasing Jaeger is spread among maintainers, we rotate the role of Release Manager among maintainers. Here are the release managers for future versions with the tentative release dates. The release dates are the first Wednesday of the month, and we might skip a release if not enough changes happened since the previous release. In such case, the next tentative release date is the first Wednesday of the subsequent month. | Version | Release Manager | Tentative release date | |---------|-----------------|---------------------------| | 2.17.0 | @albertteoh | 1 April 2026 | | 2.18.0 | @pavolloffay | 6 May 2026 | | 2.19.0 | @joe-elliott | 3 June 2026 | | 2.20.0 | @yurishkuro | 1 July 2026 | | 2.21.0 | @jkowall | 5 August 2026 | | 2.22.0 | @mahadzaryab1 | 2 September 2026 | ================================================ FILE: SECURITY-INSIGHTS.yml ================================================ header: schema-version: 1.0.0 last-updated: '2026-01-16' last-reviewed: '2026-01-16' expiration-date: '2027-01-16T01:00:00.000Z' project-url: https://github.com/jaegertracing/jaeger/ changelog: https://github.com/jaegertracing/jaeger/blob/main/CHANGELOG.md license: https://github.com/jaegertracing/jaeger/blob/main/LICENSE project-lifecycle: bug-fixes-only: false core-maintainers: - https://github.com/jaegertracing/jaeger/blob/main/README.md#maintainers roadmap: https://www.jaegertracing.io/roadmap/ release-cycle: https://github.com/jaegertracing/jaeger/blob/main/RELEASE.md#release-managers status: active contribution-policy: accepts-pull-requests: true accepts-automated-pull-requests: true contributing-policy: https://github.com/jaegertracing/jaeger/blob/main/CONTRIBUTING.md code-of-conduct: https://github.com/jaegertracing/jaeger/blob/main/CODE_OF_CONDUCT.md documentation: - https://www.jaegertracing.io/docs/ distribution-points: - https://github.com/jaegertracing/jaeger/ - https://hub.docker.com/r/jaegertracing/ - https://quay.io/organization/jaegertracing/ security-artifacts: threat-model: threat-model-created: true evidence-url: - https://github.com/jaegertracing/jaeger/blob/main/THREAT-MODEL.md self-assessment: self-assessment-created: true evidence-url: - https://tag-security.cncf.io/assessments/projects/jaeger/self-assessment/ - https://github.com/cncf/tag-security/blob/main/assessments/projects/jaeger/self-assessment.md security-testing: - tool-type: sca tool-name: Dependabot tool-version: latest integration: ad-hoc: false ci: true before-release: true comment: | Dependabot is enabled for this repo. security-contacts: - type: website value: https://github.com/jaegertracing/jaeger/blob/main/SECURITY.md vulnerability-reporting: accepts-vulnerability-reports: true security-policy: https://github.com/jaegertracing/jaeger/blob/main/SECURITY.md email-contact: jaeger-tracing@googlegroups.com comment: | The first and best way to report a vulnerability is by using private security issues in GitHub or opening an issue on Github. We are also available on the CNCF Slack in the jaeger channel. dependencies: third-party-packages: true dependencies-lists: - https://github.com/jaegertracing/jaeger/blob/main/go.mod sbom: - sbom-file: https://github.com/jaegertracing/jaeger/releases/latest/download/jaeger-SBOM.spdx.json sbom-format: SPDX sbom-url: https://github.com/anchore/sbom-action dependencies-lifecycle: policy-url: https://github.com/jaegertracing/jaeger/blob/main/SECURITY.md#security-patch-policy env-dependencies-policy: policy-url: https://github.com/jaegertracing/jaeger/blob/main/SECURITY.md#dependency-policy ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions The Jaeger project provides community support only for last minor version: bug fixes are released either as part of the next minor version or as an on-demand patch version. Independent of which version is next, all patch versions are cumulative, meaning that they represent the state of our `main` branch at the moment of the release. For instance, if the latest version is 1.19.0, bug fixes are released either as part of 1.20.0 or 1.19.1. Security fixes are given priority and might be enough to cause a new version to be released. ### Security Patch Policy CVEs in Jaeger code will be patched in the newest Jaeger releases. ### Dependency Policy Dependencies are evaluated before being introduced to ensure they: 1) are actively maintained 2) are maintained by trustworthy maintainers 3) are licensed in a way not to impact the Jaeger license based on [the CNCF license allowlist](https://github.com/cncf/foundation/blob/main/allowed-third-party-license-policy.md). These evaluations vary from dependency to dependencies. Dependencies are also scheduled for removal if the project has been deprecated or if the project is no longer maintained. Additionally based on license changes we replace dependencies as necessary. CVEs in dependencies will be patched for all supported versions if the CVE is applicable and is assessed by our dependency scanning automation to be of high or critical severity. Automation generates a new dependabot scan daily and alerts are addressed. ## Reporting a Vulnerability _The following is a copy of the [Report a security issue](https://www.jaegertracing.io/report-security-issue/) page from our website. The website's version has precedence in case of conflicts._ If you find something suspicious and want to report it, we'd really appreciate! ## Ways to report The easiest way to report a vulnerability is through the [Security tab on GitHub](https://github.com/jaegertracing/jaeger/security/advisories). This mechanism allows maintainers to communicate privately with you, and you do not need to encrypt your messages. Alternatively, you can use one of the following public channels to send an **encrypted** message to maintainers: * Chat room on the [#jaeger channel at the CNCF Slack][slack-room] * Send a message to [jaeger-tracing@googlegroups.com][mailing-list] * [Open an issue on GitHub](https://github.com/jaegertracing/jaeger/issues) You can also submit a fix to the issue by forking the affected repository and sending us a pull request. However, we prefer you'd talk to us first, as our repositories are public and we would like to give a heads-up to our users before disclosing vulnerabilities publicly. ## Our PGP key If you choose a public channel to communicate with us, please **encrypt your message** using [our public key](#our-public-key) `ID=C043A4D2B3F2AC31`. It is available in all major key servers and should match the one shown below. If you are new to PGP, you can run the following command to encrypt a file called "message.txt": ```shell # Receive our keys from a key server: gpg --keyserver keyserver.ubuntu.com --recv-keys C043A4D2B3F2AC31 # Alternatively, copy the key below to file C043A4D2B3F2AC31.asc and import it: gpg --import C043A4D2B3F2AC31.asc # Encrypt a "message.txt" file into "message.txt.asc": gpg -ea -r C043A4D2B3F2AC31 message.txt # Send us the resulting "message.txt.asc" ``` ### Our public key ``` -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBFn7N4QBEAC4Vl68Fdcom/U1kb/6zlUeSLh4Vwyr2wvaLd610AUwmrfQC0eh e6vRtt//bYr48gHg1wwnbaQgyg+ZvIfjUa6Olhqi3J1itkagy50pQDWk8nfDdbHO rgR6W3mFxKgIfAiB07oTY6Gzs8vjuO1VA/5p5DOvvXtTQdgWkI93zqIJhupznDOd wPoGF7t6PPTy/hzBJOq9KzX4MgPkOivLAjdSeftzxcvO5oHXEjwAhr5/oaPHvksz J+X8jsBW8J9wSUZWLhJkD5wm1hbcS0MKQAWvM6PpC7RnHmLOJAMBA27ne0qhNzA3 MRkzWpVUzZc/FvauNk+6ohZMW/HcUlGWsSzt3egih9pFCsz2yhXq1891iswfgYGV sRNTDmLNIDk99iDNKZofWDwdOIMJWt0QKSwYbR5mhd7p648RgI+nwyQmX/eX5Eey ECs56j07ZnUUHAm5n+K53SDnaQo40/bfKEGGJMm+72KLisnOhV6G+3y/Wi7SHEhs 9hO9Lin7qZ2EBD/KITlCZf4kkIKMc8srkUlfZSDNTVfoP9JHKMqW6Lf5OyskJdG9 MozUz/Am7QxH8DNZS3UiQubSIX+nuYuc5E03flE9QsFHKqXyYQ49sl8ipLRkV5SI c+gTqCeKcwzrNbJ6+zyt/7mQBwP34oV01z2lvgwvVyj3pgzCUFzuJpep0wARAQAB tDBKYWVnZXIgVHJhY2luZyA8amFlZ2VyLXRyYWNpbmdAZ29vZ2xlZ3JvdXBzLmNv bT6JAk4EEwEIADgWIQT5J7Ll/cozAeoshwfAQ6TSs/KsMQUCWfs3hAIbAwULCQgH AgYVCAkKCwIEFgIDAQIeAQIXgAAKCRDAQ6TSs/KsMa25D/9voNhfY8oldKgniMh/ vzcwiYYM6MFLUanJX2LjNTJ4dXuMYvJtxdfYT/+7PoxyEQfmUj50Ieka+pARyRd4 r7Rrl8eWLrkURcr72TLz+6tPT1R3r+l0e7p20FEL1w5SNcrBMir3ozwWC9K3U48d g0QTD9a3m6oeZ9hqquvsTMfrraVQvx5FdAcfQDSttFuKzfbbacds46I+8Lj4U67O 4v9I6zscC9MJNth1zy3DyZUGVd5qjkzv5r+LoJfOokC5yj6ErBG8l5HKRtoILWVK 3IzlFO/jtHiyLJ7wNPSTQjLnhna5fB/eoPiBCGHASZrVwohaaq5dKVJnoy0TW3Sw KwshWaNA7zbvFol3DZaFh3tcBNRJwh7rQ4zUEu+uY0M1DzRtnE3NjieZcNNH0wwq TbOud0hqpvK9y+xLjsiVhPc1WsTdzafuutFezHILENNDuYaHk1Vwq5FE0wOwWwx9 ah6PDxEgb5P96Zs5FNeT15fiqXKJuyDjLjcML2TUBHBUmhYugVLB9F7TleOwWxL7 /Ny54so0euqht7agTOS1ySebn5xc2yG96dAOjKJSXt2m5hevHBSVYtF8zWAPciDx uEmbjvhgHugDsB9sDu9iQhmgQu7wZ1ihpmcqO725sfW+9aWFHeAf/6dUFoX723Bf PF8iTa4onSKnvb55kFGlGAAczbkCDQRZ+zeEARAAvrm8t1j4N4quJ2H3szXyE+Cs FsHaVRLK+0IXSLwhgso3ol2cxv8GZjrNdGankpR5wvuseFY1JZ6lQOuamqnsN7yo bJxC2g9kUSJcF/cnY+TzIkHxwT492yMgm/FcUrmmWQO0LlcjEpCO8B0UzZU+SqE8 j0cInOnpSLh77HKBJL62Yu9lBQuSUmEjDMfqt7MtQeyHGSdniNE6kESymnElxch7 I0l8FHV/IufWzNbvkBszstMS6O4nL8A09HZMsoqeKhvF+A+mXAZ8xEIGls6P6Hrv PNt6MFJhva3qtu8WPYY5XCYeA3uD0AC9jjKKF2W6K5GS/iJa8OeG3bj5qbDpv790 L8JbtlX2ZnV0xXdbzhZsGdwHMWgzu9cmoJLpKtjmhH79KlbyhF8NDtOUw67LKoep Bdh+lb9htg4EZydfzGxtToD51cais/qqOaaRMTaK/chS7Rr0vIJdCcIztrM7XKRj epzyH2upARG9eZ8et2wIvrT94yIQehXUlzllEGCdeeIblBPVP/2XbpBGm2jk7e5s xuTQFjkJc64WwwCM4vgdJzMGUXdnyJMr46wqGWAWyaQEHNApDHxR0YwlpL4y35U9 bHNyJi0JmxVFgl0pBJ6wkSJUJ48Y0WWLUuHNF0MgAzuTAVgcq8EKjbk0P7Z0eH1H I56eMxIt14U5uqnw8fEAEQEAAYkCNgQYAQgAIBYhBPknsuX9yjMB6iyHB8BDpNKz 8qwxBQJZ+zeEAhsMAAoJEMBDpNKz8qwx514QAIbanXq8DEIk0xN64OT8s+5zZspb 81AV2g8VCur5DI8GQacIQrwfWTqFMt/s11uzMNga6AuhYKENj72Tq0GZHrFPBtD/ qFsbBl2TaWnmnJcsGHDjtxKJMFG9gZJdXsKl7sCWdxkQW5vxtFLdrYKQ1UdBG24Q EHvWaaG1EcNsqb3WNy9h+PYAI/HRS7ntjJdDXNZgb4frJNgZCKCi9tpXS2CvgVpD WeRfIFJtbkemJqMsZGMt52HJJ0bMFeaXjyom/NZtgsOCq1J92trR0AzRthjcmY/6 BevgOrEj8+0aurQ3Qm+IsqPqOyi814yVzOagaZ0dv+rfkomjVWABtoNHkaTyP8h+ dLh5+GUR2MrpW2TtAXh8QKolUS5x764FYHX7VtgYlZnc+qDfMao8KrD1CHMucwjs bysa8gD+jmdegtWFyUvdh+G3EhqW6xldSsixb0enEzzW5utUCvC4xv2tp9GTaUPx M3WJzf3w+c4A19AwyYumWf9J4nHFBhNHCq7Mb5I3PRIgrRCQfR9hyaeDMgd6UuSH yYdeaxVBmZ20N3D39f7tgfE1oZg1SiHVjmBYtlBu6Jji8wwFjsF1WSDZlmmp/VWJ 6GzAJggHtgAod6H/lueqcellXEo2usqZLwDqa9SlglhcMWTqysO4j/1vVQpTstwJ oF+qZY4uEvqFvYo8 =KQzT -----END PGP PUBLIC KEY BLOCK----- ``` [mailing-list]: https://groups.google.com/forum/#!forum/jaeger-tracing [slack-room]: https://cloud-native.slack.com/archives/CGG7NFUJ3 ## Automated Security Scanners We do not accept raw, unanalyzed reports from automated security scanners. Most scanners are generic and produce a high volume of false positives, especially for Go and NPM dependencies. If you use a security scanner: 1) Only report findings that are actually applicable to Jaeger, with an explanation of why. 2) Verify Go dependency CVEs with [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck) run against the source code (not a binary). If the vulnerable code path is not reachable from Jaeger, it is not actionable. 3) Check if we already track it. Jaeger uses Dependabot for automated dependency scanning (see [Dependency Policy](#dependency-policy)). Most dependency CVEs are already tracked. 4) Use the proper reporting process described in [Reporting a Vulnerability](#reporting-a-vulnerability). ## Security Documentation For more detailed security information, see: | Document | Description | |----------|-------------| | [Threat Model](docs/security/threat-model.md) | Threat model and trust boundaries | | [Assurance Case](docs/security/assurance-case.md) | Security assurance case | | [Security Architecture](docs/security/architecture.md) | TLS and cryptographic practices | | [Self-Assessment](docs/security/self-assessment.md) | CNCF TAG Security self-assessment | | [Release Verification](docs/security/verifying-releases.md) | Release signature verification | ================================================ FILE: THREAT-MODEL.md ================================================ # Jaeger Threat Model This is a placeholder for the Jaeger Threat Model. This will be based on [OSSF standards](https://github.com/ossf/security-insights-spec/tree/main/docs/threat-model) and examples of existing threat models. This is a significant chunk of work for Jaeger due to the diversity and complexity of all the supported components in deployment. ================================================ FILE: _To_People_of_Russia.md ================================================ ## 🇷🇺 Русским гражданам В Украине сейчас идет война. Силами РФ наносятся удары по гражданской инфраструктуре в [Харькове][1], [Киеве][2], [Чернигове][3], [Сумах][4], [Ирпене][5] и десятках других городов. Гибнут люди - и гражданское население, и военные, в том числе российские призывники, которых бросили воевать. Чтобы лишить собственный народ доступа к информации, правительство РФ запретило называть войну войной, закрыло независимые СМИ и принимает сейчас ряд диктаторских законов. Эти законы призваны заткнуть рот всем, кто против войны. За обычный призыв к миру сейчас можно получить несколько лет тюрьмы. Не молчите! Молчание - знак вашего согласия с политикой российского правительства. **Вы можете сделать выбор НЕ МОЛЧАТЬ.** --- ## 🇺🇸 To people of Russia There is a war in Ukraine right now. The forces of the Russian Federation are attacking civilian infrastructure in [Kharkiv][1], [Kyiv][2], [Chernihiv][3], [Sumy][4], [Irpin][5] and dozens of other cities. People are dying – both civilians and military servicemen, including Russian conscripts who were thrown into the fighting. In order to deprive its own people of access to information, the government of the Russian Federation has forbidden calling a war a war, shut down independent media and is passing a number of dictatorial laws. These laws are meant to silence all those who are against war. You can be jailed for multiple years for simply calling for peace. Do not be silent! Silence is a sign that you accept the Russian government's policy. **You can choose NOT TO BE SILENT.** [1]: "Kharkiv under attack" [2]: "Kyiv under attack" [3]: "Chernihiv under attack" [4]: "Sumy under attack" [5]: "Irpin under attack" ================================================ FILE: cmd/anonymizer/Dockerfile ================================================ # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 FROM scratch ARG TARGETARCH ARG USER_UID=10001 COPY anonymizer-linux-$TARGETARCH /go/bin/anonymizer-linux ENTRYPOINT ["/go/bin/anonymizer-linux"] USER ${USER_UID} ================================================ FILE: cmd/anonymizer/app/anonymizer/anonymizer.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package anonymizer import ( "context" "encoding/json" "fmt" "hash/fnv" "os" "path/filepath" "sync" "time" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/uimodel" uiconv "github.com/jaegertracing/jaeger/internal/uimodel/converter/v1/json" ) var allowedTags = map[string]bool{ "error": true, "http.method": true, "http.status_code": true, model.SpanKindKey: true, model.SamplerTypeKey: true, model.SamplerParamKey: true, } const PermUserRW = 0o600 // Read-write for owner only // mapping stores the mapping of service/operation names to their one-way hashes, // so that we can do a reverse lookup should the researchers have questions. type mapping struct { Services map[string]string Operations map[string]string // key=[service]:operation } // Anonymizer transforms Jaeger span in the domain model by obfuscating site-specific strings, // like service and operation names, and removes custom tags. It returns obfuscated span in the // Jaeger UI format, to make it easy to visualize traces. // // The mapping from original to obfuscated strings is stored in a file and can be reused between runs. type Anonymizer struct { mappingFile string logger *zap.Logger lock sync.Mutex mapping mapping options Options cancel context.CancelFunc wg sync.WaitGroup } // Options represents the various options with which the anonymizer can be configured. type Options struct { HashStandardTags bool `yaml:"hash_standard_tags" name:"hash_standard_tags"` HashCustomTags bool `yaml:"hash_custom_tags" name:"hash_custom_tags"` HashLogs bool `yaml:"hash_logs" name:"hash_logs"` HashProcess bool `yaml:"hash_process" name:"hash_process"` } // New creates new Anonymizer. The mappingFile stores the mapping from original to // obfuscated strings, in case later investigations require looking at the original traces. func New(mappingFile string, options Options, logger *zap.Logger) *Anonymizer { ctx, cancel := context.WithCancel(context.Background()) a := &Anonymizer{ mappingFile: mappingFile, logger: logger, mapping: mapping{ Services: make(map[string]string), Operations: make(map[string]string), }, options: options, cancel: cancel, } if _, err := os.Stat(filepath.Clean(mappingFile)); err == nil { //nolint:gosec // G703 - CLI tool, path from command-line args dat, err := os.ReadFile(filepath.Clean(mappingFile)) //nolint:gosec // G703 if err != nil { logger.Fatal("Cannot load previous mapping", zap.Error(err)) } if err := json.Unmarshal(dat, &a.mapping); err != nil { logger.Fatal("Cannot unmarshal previous mapping", zap.Error(err)) } } a.wg.Go(func() { ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: a.SaveMapping() case <-ctx.Done(): return } } }) return a } func (a *Anonymizer) Stop() { a.cancel() a.wg.Wait() } // SaveMapping writes the mapping from original to obfuscated strings to a file. // It is called by the anonymizer itself periodically, and should be called at // the end of the extraction run. func (a *Anonymizer) SaveMapping() { a.lock.Lock() defer a.lock.Unlock() dat, err := json.Marshal(a.mapping) if err != nil { a.logger.Error("Failed to marshal mapping file", zap.Error(err)) return } if err := os.WriteFile(filepath.Clean(a.mappingFile), dat, PermUserRW); err != nil { a.logger.Error("Failed to write mapping file", zap.Error(err)) return } a.logger.Sugar().Infof("Saved mapping file %s: %s", a.mappingFile, string(dat)) } func (a *Anonymizer) mapServiceName(service string) string { return a.mapString(service, a.mapping.Services) } func (a *Anonymizer) mapOperationName(service, operation string) string { v := fmt.Sprintf("[%s]:%s", service, operation) return a.mapString(v, a.mapping.Operations) } func (a *Anonymizer) mapString(v string, m map[string]string) string { a.lock.Lock() defer a.lock.Unlock() if s, ok := m[v]; ok { return s } s := hash(v) m[v] = s return s } func hash(value string) string { h := fnv.New64() _, _ = h.Write([]byte(value)) return fmt.Sprintf("%016x", h.Sum64()) } // AnonymizeSpan obfuscates and converts the span. func (a *Anonymizer) AnonymizeSpan(span *model.Span) *uimodel.Span { service := span.Process.ServiceName span.OperationName = a.mapOperationName(service, span.OperationName) outputTags := filterStandardTags(span.Tags) // when true, the allowedTags are hashed and when false they are preserved as it is if a.options.HashStandardTags { outputTags = hashTags(outputTags) } // when true, all tags other than allowedTags are hashed, when false they are dropped if a.options.HashCustomTags { customTags := hashTags(filterCustomTags(span.Tags)) outputTags = append(outputTags, customTags...) } span.Tags = outputTags // when true, logs are hashed, when false, they are dropped if a.options.HashLogs { for _, log := range span.Logs { log.Fields = hashTags(log.Fields) } } else { span.Logs = nil } span.Process.ServiceName = a.mapServiceName(service) // when true, process tags are hashed, when false they are dropped if a.options.HashProcess { span.Process.Tags = hashTags(span.Process.Tags) } else { span.Process.Tags = nil } span.Warnings = nil return uiconv.FromDomainEmbedProcess(span) } // filterStandardTags returns only allowedTags func filterStandardTags(tags []model.KeyValue) []model.KeyValue { out := make([]model.KeyValue, 0, len(tags)) for _, tag := range tags { if !allowedTags[tag.Key] { continue } if tag.Key == "error" { switch tag.VType { case model.BoolType: // allowed case model.StringType: if tag.VStr != "true" && tag.VStr != "false" { tag = model.Bool("error", true) } default: tag = model.Bool("error", true) } } out = append(out, tag) } return out } // filterCustomTags returns all tags other than allowedTags func filterCustomTags(tags []model.KeyValue) []model.KeyValue { out := make([]model.KeyValue, 0, len(tags)) for _, tag := range tags { if !allowedTags[tag.Key] { out = append(out, tag) } } return out } // hashTags converts each tag into corresponding string values // and then find its hash func hashTags(tags []model.KeyValue) []model.KeyValue { out := make([]model.KeyValue, 0, len(tags)) for _, tag := range tags { kv := model.String(hash(tag.Key), hash(tag.AsString())) out = append(out, kv) } return out } ================================================ FILE: cmd/anonymizer/app/anonymizer/anonymizer_test.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package anonymizer import ( "net/http" "os" "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" ) var tags = []model.KeyValue{ model.Bool("error", true), model.String("http.method", http.MethodPost), model.Bool("foobar", true), } var traceID = model.NewTraceID(1, 2) var span1 = &model.Span{ TraceID: traceID, SpanID: model.NewSpanID(1), Process: &model.Process{ ServiceName: "serviceName", Tags: tags, }, OperationName: "operationName", Tags: tags, Logs: []model.Log{ { Timestamp: time.Now(), Fields: []model.KeyValue{ model.String("logKey", "logValue"), }, }, }, Duration: time.Second * 5, StartTime: time.Unix(300, 0), } var span2 = &model.Span{ TraceID: traceID, SpanID: model.NewSpanID(1), Process: &model.Process{ ServiceName: "serviceName", Tags: tags, }, OperationName: "operationName", Tags: tags, Logs: []model.Log{ { Timestamp: time.Now(), Fields: []model.KeyValue{ model.String("logKey", "logValue"), }, }, }, Duration: time.Second * 5, StartTime: time.Unix(300, 0), } func TestNew(t *testing.T) { nopLogger := zap.NewNop() tempDir := t.TempDir() file, err := os.CreateTemp(tempDir, "mapping.json") require.NoError(t, err) defer file.Close() _, err = file.WriteString(` { "services": { "api": "hashed_api" }, "operations": { "[api]:delete": "hashed_api_delete" } } `) require.NoError(t, err) anonymizer := New(file.Name(), Options{}, nopLogger) defer anonymizer.Stop() assert.NotNil(t, anonymizer) } func TestAnonymizer_SaveMapping(t *testing.T) { nopLogger := zap.NewNop() mapping := mapping{ Services: make(map[string]string), Operations: make(map[string]string), } tests := []struct { name string mappingFile string }{ { name: "fail to write mapping file", mappingFile: "", }, { name: "save mapping file successfully", mappingFile: filepath.Join(t.TempDir(), "mapping.json"), }, } for _, tt := range tests { t.Run(tt.name, func(_ *testing.T) { anonymizer := Anonymizer{ logger: nopLogger, mapping: mapping, mappingFile: tt.mappingFile, } anonymizer.SaveMapping() }) } } func TestAnonymizer_FilterStandardTags(t *testing.T) { expected := []model.KeyValue{ model.Bool("error", true), model.String("http.method", http.MethodPost), } actual := filterStandardTags(tags) assert.Equal(t, expected, actual) } func TestAnonymizer_FilterCustomTags(t *testing.T) { expected := []model.KeyValue{ model.Bool("foobar", true), } actual := filterCustomTags(tags) assert.Equal(t, expected, actual) } func TestAnonymizer_Hash(t *testing.T) { data := "foobar" expected := "340d8765a4dda9c2" actual := hash(data) assert.Equal(t, expected, actual) } func TestAnonymizer_AnonymizeSpan_AllTrue(t *testing.T) { anonymizer := &Anonymizer{ mapping: mapping{ Services: make(map[string]string), Operations: make(map[string]string), }, options: Options{ HashStandardTags: true, HashCustomTags: true, HashProcess: true, HashLogs: true, }, } _ = anonymizer.AnonymizeSpan(span1) assert.Len(t, span1.Tags, 3) assert.Len(t, span1.Logs, 1) assert.Len(t, span1.Process.Tags, 3) } func TestAnonymizer_AnonymizeSpan_AllFalse(t *testing.T) { anonymizer := &Anonymizer{ mapping: mapping{ Services: make(map[string]string), Operations: make(map[string]string), }, options: Options{ HashStandardTags: false, HashCustomTags: false, HashProcess: false, HashLogs: false, }, } _ = anonymizer.AnonymizeSpan(span2) assert.Len(t, span2.Tags, 2) assert.Empty(t, span2.Logs) assert.Empty(t, span2.Process.Tags) } func TestAnonymizer_MapString_Present(t *testing.T) { v := "foobar" m := map[string]string{ "foobar": "hashed_foobar", } anonymizer := &Anonymizer{} actual := anonymizer.mapString(v, m) assert.Equal(t, "hashed_foobar", actual) } func TestAnonymizer_MapString_Absent(t *testing.T) { v := "foobar" m := map[string]string{} anonymizer := &Anonymizer{} actual := anonymizer.mapString(v, m) assert.Equal(t, "340d8765a4dda9c2", actual) } func TestAnonymizer_MapServiceName(t *testing.T) { anonymizer := &Anonymizer{ mapping: mapping{ Services: map[string]string{ "api": "hashed_api", }, }, } actual := anonymizer.mapServiceName("api") assert.Equal(t, "hashed_api", actual) } func TestAnonymizer_MapOperationName(t *testing.T) { anonymizer := &Anonymizer{ mapping: mapping{ Services: map[string]string{ "api": "hashed_api", }, Operations: map[string]string{ "[api]:delete": "hashed_api_delete", }, }, } actual := anonymizer.mapOperationName("api", "delete") assert.Equal(t, "hashed_api_delete", actual) } ================================================ FILE: cmd/anonymizer/app/anonymizer/package_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package anonymizer import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: cmd/anonymizer/app/flags.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package app import ( "github.com/spf13/cobra" ) // Options represent configurable parameters for jaeger-anonymizer type Options struct { QueryGRPCHostPort string MaxSpansCount int TraceID string OutputDir string HashStandardTags bool HashCustomTags bool HashLogs bool HashProcess bool StartTime int64 EndTime int64 } const ( queryGRPCHostPortFlag = "query-host-port" outputDirFlag = "output-dir" traceIDFlag = "trace-id" hashStandardTagsFlag = "hash-standard-tags" hashCustomTagsFlag = "hash-custom-tags" hashLogsFlag = "hash-logs" hashProcessFlag = "hash-process" maxSpansCount = "max-spans-count" startTime = "start-time" endTime = "end-time" ) // AddFlags adds flags for anonymizer main program func (o *Options) AddFlags(command *cobra.Command) { command.Flags().StringVar( &o.QueryGRPCHostPort, queryGRPCHostPortFlag, "localhost:16686", "The host:port of the jaeger-query endpoint") command.Flags().StringVar( &o.OutputDir, outputDirFlag, "/tmp", "The directory to store the anonymized trace") command.Flags().StringVar( &o.TraceID, traceIDFlag, "", "The trace-id of trace to anonymize") command.Flags().BoolVar( &o.HashStandardTags, hashStandardTagsFlag, false, "Whether to hash standard tags") command.Flags().BoolVar( &o.HashCustomTags, hashCustomTagsFlag, false, "Whether to hash custom tags") command.Flags().BoolVar( &o.HashLogs, hashLogsFlag, false, "Whether to hash logs") command.Flags().BoolVar( &o.HashProcess, hashProcessFlag, false, "Whether to hash process") command.Flags().IntVar( &o.MaxSpansCount, maxSpansCount, -1, "The maximum number of spans to anonymize") command.Flags().Int64Var( &o.StartTime, startTime, 0, "The start time of time window for searching trace, timestampe in unix nanoseconds") command.Flags().Int64Var( &o.EndTime, endTime, 0, "The end time of time window for searching trace, timestampe in unix nanoseconds") // mark traceid flag as mandatory command.MarkFlagRequired(traceIDFlag) } ================================================ FILE: cmd/anonymizer/app/flags_test.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package app import ( "testing" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestOptionsWithDefaultFlags(t *testing.T) { o := Options{} c := cobra.Command{} o.AddFlags(&c) assert.Equal(t, "localhost:16686", o.QueryGRPCHostPort) assert.Equal(t, "/tmp", o.OutputDir) assert.False(t, o.HashStandardTags) assert.False(t, o.HashCustomTags) assert.False(t, o.HashLogs) assert.False(t, o.HashProcess) assert.Equal(t, -1, o.MaxSpansCount) assert.Equal(t, int64(0), o.StartTime) assert.Equal(t, int64(0), o.EndTime) } func TestOptionsWithFlags(t *testing.T) { o := Options{} c := cobra.Command{} o.AddFlags(&c) c.ParseFlags([]string{ "--query-host-port=192.168.1.10:16686", "--output-dir=/data", "--trace-id=6ef2debb698f2f7c", "--hash-standard-tags", "--hash-custom-tags", "--hash-logs", "--hash-process", "--max-spans-count=100", "--start-time=1", "--end-time=2", }) assert.Equal(t, "192.168.1.10:16686", o.QueryGRPCHostPort) assert.Equal(t, "/data", o.OutputDir) assert.Equal(t, "6ef2debb698f2f7c", o.TraceID) assert.True(t, o.HashStandardTags) assert.True(t, o.HashCustomTags) assert.True(t, o.HashLogs) assert.True(t, o.HashProcess) assert.Equal(t, 100, o.MaxSpansCount) assert.Equal(t, int64(1), o.StartTime) assert.Equal(t, int64(2), o.EndTime) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: cmd/anonymizer/app/query/package_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package query import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: cmd/anonymizer/app/query/query.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package query import ( "context" "errors" "fmt" "io" "strings" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger-idl/proto-gen/api_v2" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" ) // Query represents a jaeger-query's query for trace-id type Query struct { client api_v2.QueryServiceClient conn *grpc.ClientConn } // New creates a Query object func New(addr string) (*Query, error) { conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return nil, fmt.Errorf("failed to connect with the jaeger-query service: %w", err) } return &Query{ client: api_v2.NewQueryServiceClient(conn), conn: conn, }, nil } // unwrapNotFoundErr is a conversion function func unwrapNotFoundErr(err error) error { if s, _ := status.FromError(err); s != nil { if strings.Contains(s.Message(), spanstore.ErrTraceNotFound.Error()) { return spanstore.ErrTraceNotFound } } return err } // QueryTrace queries for a trace and returns all spans inside it func (q *Query) QueryTrace(traceID string, startTime time.Time, endTime time.Time) ([]model.Span, error) { mTraceID, err := model.TraceIDFromString(traceID) if err != nil { return nil, fmt.Errorf("failed to convert the provided trace id: %w", err) } request := api_v2.GetTraceRequest{ TraceID: mTraceID, StartTime: startTime, EndTime: endTime, } stream, err := q.client.GetTrace(context.Background(), &request) if err != nil { return nil, unwrapNotFoundErr(err) } var spans []model.Span for received, err := stream.Recv(); !errors.Is(err, io.EOF); received, err = stream.Recv() { if err != nil { return nil, unwrapNotFoundErr(err) } spans = append(spans, received.Spans...) } return spans, nil } // Close closes the grpc client connection func (q *Query) Close() error { return q.conn.Close() } ================================================ FILE: cmd/anonymizer/app/query/query_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package query import ( "context" "errors" "net" "sync" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger-idl/proto-gen/api_v2" _ "github.com/jaegertracing/jaeger/internal/gogocodec" // force gogo codec registration "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" ) var ( mockInvalidTraceID = "xyz" mockTraceID = model.NewTraceID(0, 123456) mockTraceGRPC = &model.Trace{ Spans: []*model.Span{ { TraceID: mockTraceID, SpanID: model.NewSpanID(1), Process: &model.Process{}, }, { TraceID: mockTraceID, SpanID: model.NewSpanID(2), Process: &model.Process{}, }, }, Warnings: []string{}, } ) var errUninitializedTraceID = status.Error(codes.InvalidArgument, "uninitialized TraceID is not allowed") // testGRPCHandler is a minimal implementation of api_v2.QueryServiceServer // for testing purposes. It only implements GetTrace, using the embedded // UnimplementedQueryServiceServer for other methods. type testGRPCHandler struct { api_v2.UnimplementedQueryServiceServer returnTrace *model.Trace returnError error failDuringRecv bool } // GetTrace implements the gRPC GetTrace method by returning test data directly. func (g *testGRPCHandler) GetTrace(r *api_v2.GetTraceRequest, stream api_v2.QueryService_GetTraceServer) error { if r.TraceID == (model.TraceID{}) { return errUninitializedTraceID } if g.returnError != nil { if errors.Is(g.returnError, spanstore.ErrTraceNotFound) { return status.Errorf(codes.NotFound, "trace not found: %v", g.returnError) } return status.Errorf(codes.Internal, "failed to fetch spans from the backend: %v", g.returnError) } if g.returnTrace == nil { return status.Errorf(codes.NotFound, "trace not found") } if g.failDuringRecv { // Send first chunk then fail chunk := &api_v2.SpansResponseChunk{Spans: []model.Span{*g.returnTrace.Spans[0]}} if err := stream.Send(chunk); err != nil { return err } return status.Errorf(codes.Internal, "failed during recv") } return g.sendSpanChunks(g.returnTrace.Spans, stream.Send) } // sendSpanChunks sends spans in chunks to the client. func (*testGRPCHandler) sendSpanChunks(spans []*model.Span, sendFn func(*api_v2.SpansResponseChunk) error) error { chunk := make([]model.Span, 0, len(spans)) for _, span := range spans { chunk = append(chunk, *span) } return sendFn(&api_v2.SpansResponseChunk{Spans: chunk}) } type mockQueryClient struct { api_v2.QueryServiceClient getTraceErr error } func (m *mockQueryClient) GetTrace(ctx context.Context, in *api_v2.GetTraceRequest, opts ...grpc.CallOption) (api_v2.QueryService_GetTraceClient, error) { if m.getTraceErr != nil { return nil, m.getTraceErr } return m.QueryServiceClient.GetTrace(ctx, in, opts...) } type testServer struct { address net.Addr server *grpc.Server handler *testGRPCHandler } func newTestServer(t *testing.T) *testServer { h := &testGRPCHandler{} server := grpc.NewServer() api_v2.RegisterQueryServiceServer(server, h) lis, err := net.Listen("tcp", ":0") require.NoError(t, err) var started, exited sync.WaitGroup started.Add(1) exited.Go(func() { started.Done() assert.NoError(t, server.Serve(lis)) }) started.Wait() t.Cleanup(func() { server.Stop() exited.Wait() // don't allow test to finish before server exits }) return &testServer{ server: server, address: lis.Addr(), handler: h, } } func TestNew(t *testing.T) { server := newTestServer(t) query, err := New(server.address.String()) require.NoError(t, err) defer query.Close() assert.NotNil(t, query) t.Run("invalid address", func(t *testing.T) { // Try a definitively invalid URI to trigger parser error in NewClient. q, err := New("invalid-scheme://%%") if err != nil { assert.Nil(t, q) } else if q != nil { q.Close() } }) } func TestClose(t *testing.T) { s := newTestServer(t) q, err := New(s.address.String()) require.NoError(t, err) assert.NoError(t, q.Close()) } func TestQueryTrace(t *testing.T) { s := newTestServer(t) q, err := New(s.address.String()) require.NoError(t, err) defer q.Close() t.Run("No error", func(t *testing.T) { startTime := time.Date(1970, time.January, 1, 0, 0, 0, 1000, time.UTC) endTime := time.Date(1970, time.January, 1, 0, 0, 0, 2000, time.UTC) s.handler.returnTrace = mockTraceGRPC s.handler.returnError = nil spans, err := q.QueryTrace(mockTraceID.String(), startTime, endTime) require.NoError(t, err) assert.Len(t, spans, len(mockTraceGRPC.Spans)) }) t.Run("Invalid TraceID", func(t *testing.T) { _, err := q.QueryTrace(mockInvalidTraceID, time.Time{}, time.Time{}) assert.ErrorContains(t, err, "failed to convert the provided trace id") }) t.Run("General error from GetTrace", func(t *testing.T) { s.handler.returnTrace = nil s.handler.returnError = errors.New("random error") spans, err := q.QueryTrace(mockTraceID.String(), time.Time{}, time.Time{}) assert.Nil(t, spans) assert.ErrorContains(t, err, "random error") }) t.Run("Trace not found", func(t *testing.T) { s.handler.returnTrace = nil s.handler.returnError = spanstore.ErrTraceNotFound spans, err := q.QueryTrace(mockTraceID.String(), time.Time{}, time.Time{}) assert.Nil(t, spans) assert.ErrorIs(t, err, spanstore.ErrTraceNotFound) }) t.Run("Error from GetTrace (immediate)", func(t *testing.T) { originalClient := q.client mockClient := &mockQueryClient{ QueryServiceClient: q.client, getTraceErr: errors.New("immediate error"), } q.client = mockClient defer func() { q.client = originalClient }() spans, err := q.QueryTrace(mockTraceID.String(), time.Time{}, time.Time{}) assert.Nil(t, spans) assert.ErrorContains(t, err, "immediate error") }) t.Run("Error from stream.Recv", func(t *testing.T) { s.handler.returnTrace = mockTraceGRPC s.handler.returnError = nil s.handler.failDuringRecv = true defer func() { s.handler.failDuringRecv = false }() spans, err := q.QueryTrace(mockTraceID.String(), time.Time{}, time.Time{}) assert.Nil(t, spans) assert.ErrorContains(t, err, "failed during recv") }) } func TestUnwrapNotFoundErr(t *testing.T) { t.Run("non-gRPC error", func(t *testing.T) { err := errors.New("standard error") assert.Equal(t, err, unwrapNotFoundErr(err)) }) t.Run("gRPC error with trace not found", func(t *testing.T) { err := status.Error(codes.NotFound, "trace not found") assert.Equal(t, spanstore.ErrTraceNotFound, unwrapNotFoundErr(err)) }) t.Run("gRPC error without trace not found", func(t *testing.T) { err := status.Error(codes.Internal, "internal error") assert.Equal(t, err, unwrapNotFoundErr(err)) }) t.Run("nil error", func(t *testing.T) { assert.NoError(t, unwrapNotFoundErr(nil)) }) } ================================================ FILE: cmd/anonymizer/app/uiconv/extractor.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package uiconv import ( "encoding/json" "errors" "fmt" "os" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/uimodel" ) // extractor reads the spans from reader, filters by traceID, and stores as JSON into uiFile. type extractor struct { uiFile *os.File traceID string reader *spanReader logger *zap.Logger } // newExtractor creates extractor. func newExtractor(uiFile string, traceID string, reader *spanReader, logger *zap.Logger) (*extractor, error) { f, err := os.OpenFile(uiFile, os.O_CREATE|os.O_WRONLY, os.ModePerm) if err != nil { return nil, fmt.Errorf("cannot create output file: %w", err) } logger.Sugar().Infof("Writing spans to UI file %s", uiFile) return &extractor{ uiFile: f, traceID: traceID, reader: reader, logger: logger, }, nil } // Run executes the extraction. func (e *extractor) Run() error { e.logger.Info("Parsing captured file for trace", zap.String("trace_id", e.traceID)) var ( spans []uimodel.Span span *uimodel.Span err error ) for span, err = e.reader.NextSpan(); err == nil; span, err = e.reader.NextSpan() { if string(span.TraceID) == e.traceID { spans = append(spans, *span) } } if !errors.Is(err, errNoMoreSpans) { return fmt.Errorf("failed when scanning the file: %w", err) } trace := uimodel.Trace{ TraceID: uimodel.TraceID(e.traceID), Spans: spans, Processes: make(map[uimodel.ProcessID]uimodel.Process), } // (ys) The following is not exactly correct because it does not dedupe the processes, // but I don't think it affects the UI. for i := range spans { span := &spans[i] pid := uimodel.ProcessID(fmt.Sprintf("p%d", i)) trace.Processes[pid] = *span.Process span.Process = nil span.ProcessID = pid } jsonBytes, err := json.Marshal(trace) if err != nil { return fmt.Errorf("failed to marshal UI trace: %w", err) } e.uiFile.WriteString(`{"data": [`) e.uiFile.Write(jsonBytes) e.uiFile.WriteString(`]}`) e.uiFile.Sync() e.uiFile.Close() e.logger.Sugar().Infof("Wrote spans to UI file %s", e.uiFile.Name()) return nil } ================================================ FILE: cmd/anonymizer/app/uiconv/extractor_test.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package uiconv import ( "encoding/json" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" ) type UITrace struct { Data []model.Trace } func TestExtractorTraceSuccess(t *testing.T) { inputFile := "fixtures/trace_success.json" outputFile := "fixtures/trace_success_ui_anonymized.json" defer os.Remove(outputFile) reader, err := newSpanReader(inputFile, zap.NewNop()) require.NoError(t, err) extractor, err := newExtractor( outputFile, "2be38093ead7a083", reader, zap.NewNop(), ) require.NoError(t, err) err = extractor.Run() require.NoError(t, err) var trace UITrace loadJSON(t, outputFile, &trace) for i := range trace.Data { for j := range trace.Data[i].Spans { assert.Equal(t, model.SpanKindKey, trace.Data[i].Spans[j].Tags[0].Key) } } } func TestExtractorTraceOutputFileError(t *testing.T) { inputFile := "fixtures/trace_success.json" outputFile := "fixtures/trace_success_ui_anonymized.json" defer os.Remove(outputFile) reader, err := newSpanReader(inputFile, zap.NewNop()) require.NoError(t, err) err = os.Chmod("fixtures", 0o000) require.NoError(t, err) defer os.Chmod("fixtures", 0o755) _, err = newExtractor( outputFile, "2be38093ead7a083", reader, zap.NewNop(), ) require.ErrorContains(t, err, "cannot create output file") } func TestExtractorTraceScanError(t *testing.T) { inputFile := "fixtures/trace_scan_error.json" outputFile := "fixtures/trace_scan_error_ui_anonymized.json" defer os.Remove(outputFile) reader, err := newSpanReader(inputFile, zap.NewNop()) require.NoError(t, err) extractor, err := newExtractor( outputFile, "2be38093ead7a083", reader, zap.NewNop(), ) require.NoError(t, err) err = extractor.Run() require.ErrorContains(t, err, "failed when scanning the file") } func loadJSON(t *testing.T, fileName string, i any) { b, err := os.ReadFile(fileName) require.NoError(t, err) err = json.Unmarshal(b, i) require.NoError(t, err, "Failed to parse json fixture file %s", fileName) } ================================================ FILE: cmd/anonymizer/app/uiconv/fixtures/trace_empty.json ================================================ ================================================ FILE: cmd/anonymizer/app/uiconv/fixtures/trace_invalid_json.json ================================================ [{"traceID":"2be38093ead7a083","spanID":"7bd66f09ba90ea3d","duration": "invalid"} ] ================================================ FILE: cmd/anonymizer/app/uiconv/fixtures/trace_scan_error.json ================================================ [{"traceID":"2be38093ead7a083","spanID":"7606ddfe69932d34","duration":267037}, ] ================================================ FILE: cmd/anonymizer/app/uiconv/fixtures/trace_success.json ================================================ [{"traceID":"2be38093ead7a083","spanID":"7606ddfe69932d34","flags":1,"operationName":"a071653098f9250d","references":[{"refType":"CHILD_OF","traceID":"2be38093ead7a083","spanID":"492770a15935810f"}],"startTime":1605223981761425,"duration":267037,"tags":[{"key":"span.kind","type":"string","value":"server"}],"logs":[],"process":{"serviceName":"16af988c443cff37","tags":[]},"warnings":null}, {"traceID":"2be38093ead7a083","spanID":"7bd66f09ba90ea3d","flags":1,"operationName":"471418097747d04a","references":[{"refType":"CHILD_OF","traceID":"2be38093ead7a083","spanID":"7606ddfe69932d34"}],"startTime":1605223981965074,"duration":32782,"tags":[{"key":"span.kind","type":"string","value":"client"},{"key":"error","type":"bool","value":"true"}],"logs":[],"process":{"serviceName":"3c220036602f839e","tags":[]},"warnings":null} ] ================================================ FILE: cmd/anonymizer/app/uiconv/fixtures/trace_wrong_format.json ================================================ {"traceID":"2be38093ead7a083","spanID":"7606ddfe69932d34","duration":267037} ================================================ FILE: cmd/anonymizer/app/uiconv/module.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package uiconv import ( "go.uber.org/zap" ) // Config for the extractor. type Config struct { CapturedFile string `yaml:"captured_file"` UIFile string `yaml:"ui_file"` TraceID string `yaml:"trace_id"` } // Extract reads anonymized file, finds spans for a given trace, // and writes out that trace in the UI format. func Extract(config Config, logger *zap.Logger) error { reader, err := newSpanReader(config.CapturedFile, logger) if err != nil { return err } ext, err := newExtractor(config.UIFile, config.TraceID, reader, logger) if err != nil { return err } return ext.Run() } ================================================ FILE: cmd/anonymizer/app/uiconv/module_test.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package uiconv import ( "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" ) func TestModule_TraceSuccess(t *testing.T) { inputFile := "fixtures/trace_success.json" outputFile := "fixtures/trace_success_ui_anonymized.json" defer os.Remove(outputFile) config := Config{ CapturedFile: inputFile, UIFile: outputFile, TraceID: "2be38093ead7a083", } err := Extract(config, zap.NewNop()) require.NoError(t, err) var trace UITrace loadJSON(t, outputFile, &trace) for i := range trace.Data { for j := range trace.Data[i].Spans { assert.Equal(t, model.SpanKindKey, trace.Data[i].Spans[j].Tags[0].Key) } } } func TestModule_TraceNonExistent(t *testing.T) { inputFile := "fixtures/trace_non_existent.json" outputFile := "fixtures/trace_non_existent_ui_anonymized.json" defer os.Remove(outputFile) config := Config{ CapturedFile: inputFile, UIFile: outputFile, TraceID: "2be38093ead7a083", } err := Extract(config, zap.NewNop()) require.ErrorContains(t, err, "cannot open captured file") } func TestModule_TraceOutputFileError(t *testing.T) { inputFile := "fixtures/trace_success.json" outputFile := "fixtures/trace_success_ui_anonymized.json" defer os.Remove(outputFile) config := Config{ CapturedFile: inputFile, UIFile: outputFile, TraceID: "2be38093ead7a083", } err := os.Chmod("fixtures", 0o550) require.NoError(t, err) defer os.Chmod("fixtures", 0o755) err = Extract(config, zap.NewNop()) require.ErrorContains(t, err, "cannot create output file") } ================================================ FILE: cmd/anonymizer/app/uiconv/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package uiconv import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: cmd/anonymizer/app/uiconv/reader.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package uiconv import ( "bufio" "encoding/json" "errors" "fmt" "os" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/uimodel" ) var errNoMoreSpans = errors.New("no more spans") // spanReader loads previously captured spans from a file. type spanReader struct { logger *zap.Logger capturedFile *os.File reader *bufio.Reader spansRead int eofReached bool } // newSpanReader creates a spanReader. func newSpanReader(capturedFile string, logger *zap.Logger) (*spanReader, error) { cf, err := os.OpenFile(capturedFile, os.O_RDONLY, os.ModePerm) if err != nil { return nil, fmt.Errorf("cannot open captured file: %w", err) } logger.Sugar().Infof("Reading captured spans from file %s", capturedFile) return &spanReader{ logger: logger, capturedFile: cf, reader: bufio.NewReader(cf), }, nil } // NextSpan reads the next span from the capture file, or returns errNoMoreSpans. func (r *spanReader) NextSpan() (*uimodel.Span, error) { if r.eofReached { return nil, errNoMoreSpans } if r.spansRead == 0 { b, err := r.reader.ReadByte() if err != nil { r.eofReached = true return nil, fmt.Errorf("cannot read file: %w", err) } if b != '[' { r.eofReached = true return nil, errors.New("file must begin with '['") } } s, err := r.reader.ReadString('\n') if err != nil { r.eofReached = true return nil, fmt.Errorf("cannot read file: %w", err) } if s[len(s)-2] == ',' { // all but last span lines end with ,\n s = s[0 : len(s)-2] } else { r.eofReached = true } var span uimodel.Span err = json.Unmarshal([]byte(s), &span) if err != nil { r.eofReached = true return nil, fmt.Errorf("cannot unmarshal span: %w; %s", err, s) } r.spansRead++ if r.spansRead%1000 == 0 { r.logger.Info("Scan progress", zap.Int("span_count", r.spansRead)) } return &span, nil } ================================================ FILE: cmd/anonymizer/app/uiconv/reader_test.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package uiconv import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" ) func TestReaderTraceSuccess(t *testing.T) { inputFile := "fixtures/trace_success.json" r, err := newSpanReader(inputFile, zap.NewNop()) require.NoError(t, err) s1, err := r.NextSpan() require.NoError(t, err) assert.Equal(t, "a071653098f9250d", s1.OperationName) assert.Equal(t, 1, r.spansRead) assert.False(t, r.eofReached) r.spansRead = 999 s2, err := r.NextSpan() require.NoError(t, err) assert.Equal(t, "471418097747d04a", s2.OperationName) assert.Equal(t, 1000, r.spansRead) assert.True(t, r.eofReached) _, err = r.NextSpan() require.Equal(t, errNoMoreSpans, err) assert.Equal(t, 1000, r.spansRead) assert.True(t, r.eofReached) } func TestReaderTraceNonExistent(t *testing.T) { inputFile := "fixtures/trace_non_existent.json" _, err := newSpanReader(inputFile, zap.NewNop()) require.ErrorContains(t, err, "cannot open captured file") } func TestReaderTraceEmpty(t *testing.T) { inputFile := "fixtures/trace_empty.json" r, err := newSpanReader(inputFile, zap.NewNop()) require.NoError(t, err) _, err = r.NextSpan() require.ErrorContains(t, err, "cannot read file") assert.Equal(t, 0, r.spansRead) assert.True(t, r.eofReached) } func TestReaderTraceWrongFormat(t *testing.T) { inputFile := "fixtures/trace_wrong_format.json" r, err := newSpanReader(inputFile, zap.NewNop()) require.NoError(t, err) _, err = r.NextSpan() require.Equal(t, "file must begin with '['", err.Error()) assert.Equal(t, 0, r.spansRead) assert.True(t, r.eofReached) } func TestReaderTraceInvalidJson(t *testing.T) { inputFile := "fixtures/trace_invalid_json.json" r, err := newSpanReader(inputFile, zap.NewNop()) require.NoError(t, err) _, err = r.NextSpan() require.ErrorContains(t, err, "cannot unmarshal span") assert.Equal(t, 0, r.spansRead) assert.True(t, r.eofReached) } ================================================ FILE: cmd/anonymizer/app/writer/package_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package writer import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: cmd/anonymizer/app/writer/writer.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package writer import ( "bytes" "encoding/json" "errors" "fmt" "os" "sync" "github.com/gogo/protobuf/jsonpb" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/cmd/anonymizer/app/anonymizer" ) var ErrMaxSpansCountReached = errors.New("max spans count reached") // Config contains parameters to NewWriter. type Config struct { MaxSpansCount int `yaml:"max_spans_count" name:"max_spans_count"` CapturedFile string `yaml:"captured_file" name:"captured_file"` AnonymizedFile string `yaml:"anonymized_file" name:"anonymized_file"` MappingFile string `yaml:"mapping_file" name:"mapping_file"` AnonymizerOpts anonymizer.Options `yaml:"anonymizer" name:"anonymizer"` } // Writer is a span Writer that obfuscates the span and writes it to a JSON file. type Writer struct { config Config lock sync.Mutex logger *zap.Logger capturedFile *os.File anonymizedFile *os.File anonymizer *anonymizer.Anonymizer spanCount int } // New creates an Writer func New(config Config, logger *zap.Logger) (*Writer, error) { wd, err := os.Getwd() if err != nil { return nil, err } logger.Sugar().Infof("Current working dir is %s", wd) cf, err := os.OpenFile(config.CapturedFile, os.O_CREATE|os.O_WRONLY, os.ModePerm) if err != nil { return nil, fmt.Errorf("cannot create output file: %w", err) } logger.Sugar().Infof("Writing captured spans to file %s", config.CapturedFile) af, err := os.OpenFile(config.AnonymizedFile, os.O_CREATE|os.O_WRONLY, os.ModePerm) if err != nil { return nil, fmt.Errorf("cannot create output file: %w", err) } logger.Sugar().Infof("Writing anonymized spans to file %s", config.AnonymizedFile) _, err = cf.WriteString("[") if err != nil { return nil, fmt.Errorf("cannot write tp output file: %w", err) } _, err = af.WriteString("[") if err != nil { return nil, fmt.Errorf("cannot write tp output file: %w", err) } options := anonymizer.Options{ HashStandardTags: config.AnonymizerOpts.HashStandardTags, HashCustomTags: config.AnonymizerOpts.HashCustomTags, HashLogs: config.AnonymizerOpts.HashLogs, HashProcess: config.AnonymizerOpts.HashProcess, } return &Writer{ config: config, logger: logger, capturedFile: cf, anonymizedFile: af, anonymizer: anonymizer.New(config.MappingFile, options, logger), }, nil } // WriteSpan anonymized the span and appends it as JSON to w.file. func (w *Writer) WriteSpan(msg *model.Span) error { w.lock.Lock() defer w.lock.Unlock() out := new(bytes.Buffer) if err := new(jsonpb.Marshaler).Marshal(out, msg); err != nil { return err } if w.spanCount > 0 { w.capturedFile.WriteString(",\n") } w.capturedFile.Write(out.Bytes()) w.capturedFile.Sync() span := w.anonymizer.AnonymizeSpan(msg) dat, err := json.Marshal(span) if err != nil { return err } if w.spanCount > 0 { w.anonymizedFile.WriteString(",\n") } if _, err := w.anonymizedFile.Write(dat); err != nil { return err } w.anonymizedFile.Sync() w.spanCount++ if w.spanCount%100 == 0 { w.logger.Info("progress", zap.Int("numSpans", w.spanCount)) } if w.config.MaxSpansCount > 0 && w.spanCount >= w.config.MaxSpansCount { w.logger.Info("Saved enough spans, exiting...") w.Close() return ErrMaxSpansCountReached } return nil } // Close closes the captured and anonymized files. func (w *Writer) Close() { w.capturedFile.WriteString("\n]\n") w.capturedFile.Close() w.anonymizedFile.WriteString("\n]\n") w.anonymizedFile.Close() w.anonymizer.Stop() w.anonymizer.SaveMapping() } ================================================ FILE: cmd/anonymizer/app/writer/writer_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package writer import ( "net/http" "testing" "time" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" ) var tags = []model.KeyValue{ model.Bool("error", true), model.String("http.method", http.MethodPost), model.Bool("foobar", true), } var traceID = model.NewTraceID(1, 2) var span = &model.Span{ TraceID: traceID, SpanID: model.NewSpanID(1), Process: &model.Process{ ServiceName: "serviceName", Tags: tags, }, OperationName: "operationName", Tags: tags, Logs: []model.Log{ { Timestamp: time.Now(), Fields: []model.KeyValue{ model.String("logKey", "logValue"), }, }, }, Duration: time.Second * 5, StartTime: time.Unix(300, 0), } func TestNew(t *testing.T) { nopLogger := zap.NewNop() tempDir := t.TempDir() t.Run("no error", func(t *testing.T) { config := Config{ MaxSpansCount: 10, CapturedFile: tempDir + "/captured.json", AnonymizedFile: tempDir + "/anonymized.json", MappingFile: tempDir + "/mapping.json", } writer, err := New(config, nopLogger) require.NoError(t, err) defer writer.Close() }) t.Run("CapturedFile does not exist", func(t *testing.T) { config := Config{ CapturedFile: tempDir + "/nonexistent_directory/captured.json", AnonymizedFile: tempDir + "/anonymized.json", MappingFile: tempDir + "/mapping.json", } _, err := New(config, nopLogger) require.ErrorContains(t, err, "cannot create output file") }) t.Run("AnonymizedFile does not exist", func(t *testing.T) { config := Config{ CapturedFile: tempDir + "/captured.json", AnonymizedFile: tempDir + "/nonexistent_directory/anonymized.json", MappingFile: tempDir + "/mapping.json", } _, err := New(config, nopLogger) require.ErrorContains(t, err, "cannot create output file") }) } func TestWriter_WriteSpan(t *testing.T) { nopLogger := zap.NewNop() t.Run("write span", func(t *testing.T) { tempDir := t.TempDir() config := Config{ MaxSpansCount: 10, CapturedFile: tempDir + "/captured.json", AnonymizedFile: tempDir + "/anonymized.json", MappingFile: tempDir + "/mapping.json", } writer, err := New(config, nopLogger) require.NoError(t, err) defer writer.Close() for range 9 { err = writer.WriteSpan(span) require.NoError(t, err) } }) t.Run("write span with MaxSpansCount", func(t *testing.T) { tempDir := t.TempDir() config := Config{ MaxSpansCount: 1, CapturedFile: tempDir + "/captured.json", AnonymizedFile: tempDir + "/anonymized.json", MappingFile: tempDir + "/mapping.json", } writer, err := New(config, zap.NewNop()) require.NoError(t, err) defer writer.Close() err = writer.WriteSpan(span) require.ErrorIs(t, err, ErrMaxSpansCountReached) }) } ================================================ FILE: cmd/anonymizer/main.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package main import ( "errors" "fmt" "os" "time" "github.com/spf13/cobra" "go.uber.org/zap" "github.com/jaegertracing/jaeger/cmd/anonymizer/app" "github.com/jaegertracing/jaeger/cmd/anonymizer/app/anonymizer" "github.com/jaegertracing/jaeger/cmd/anonymizer/app/query" "github.com/jaegertracing/jaeger/cmd/anonymizer/app/uiconv" "github.com/jaegertracing/jaeger/cmd/anonymizer/app/writer" "github.com/jaegertracing/jaeger/internal/version" ) var logger, _ = zap.NewDevelopment() func main() { options := app.Options{} command := &cobra.Command{ Use: "jaeger-anonymizer", Short: "Jaeger anonymizer hashes fields of a trace for easy sharing", Long: `Jaeger anonymizer queries Jaeger query for a trace, anonymizes fields, and store in file`, Run: func(_ *cobra.Command, _ /* args */ []string) { prefix := options.OutputDir + "/" + options.TraceID conf := writer.Config{ MaxSpansCount: options.MaxSpansCount, CapturedFile: prefix + ".original.json", AnonymizedFile: prefix + ".anonymized.json", MappingFile: prefix + ".mapping.json", AnonymizerOpts: anonymizer.Options{ HashStandardTags: options.HashStandardTags, HashCustomTags: options.HashCustomTags, HashLogs: options.HashLogs, HashProcess: options.HashProcess, }, } w, err := writer.New(conf, logger) if err != nil { logger.Fatal("error while creating writer object", zap.Error(err)) } query, err := query.New(options.QueryGRPCHostPort) if err != nil { logger.Fatal("error while creating query object", zap.Error(err)) } spans, err := query.QueryTrace( options.TraceID, initTime(options.StartTime), initTime(options.EndTime), ) if err != nil { logger.Fatal("error while querying for trace", zap.Error(err)) } if err := query.Close(); err != nil { logger.Error("Failed to close grpc client connection", zap.Error(err)) } for i := range spans { span := &spans[i] if err := w.WriteSpan(span); err != nil { if errors.Is(err, writer.ErrMaxSpansCountReached) { logger.Info("max spans count reached") os.Exit(0) } logger.Error("error while writing span", zap.Error(err)) } } w.Close() uiCfg := uiconv.Config{ CapturedFile: conf.AnonymizedFile, UIFile: prefix + ".anonymized-ui-trace.json", TraceID: options.TraceID, } if err := uiconv.Extract(uiCfg, logger); err != nil { logger.Fatal("error while extracing UI trace", zap.Error(err)) } logger.Sugar().Infof("Wrote UI-compatible anonymized file to %s", uiCfg.UIFile) }, } options.AddFlags(command) command.AddCommand(version.Command()) if err := command.Execute(); err != nil { fmt.Println(err.Error()) os.Exit(1) } } func initTime(ts int64) time.Time { var t time.Time if ts != 0 { t = time.Unix(0, ts) } return t } ================================================ FILE: cmd/es-index-cleaner/.gitignore ================================================ es-index-cleaner-*-* ================================================ FILE: cmd/es-index-cleaner/Dockerfile ================================================ # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 ARG base_image FROM $base_image AS release ARG TARGETARCH ARG USER_UID=10001 COPY es-index-cleaner-linux-$TARGETARCH /go/bin/es-index-cleaner-linux ENTRYPOINT ["/go/bin/es-index-cleaner-linux"] USER ${USER_UID} ================================================ FILE: cmd/es-index-cleaner/README.md ================================================ # jaeger-es-index-cleaner It is common to only keep observability data for a limited time. However, Elasticsearch does not support expiring of old data via TTL. To help with this task, `jaeger-es-index-cleaner` can be used to purge old Jaeger indices. For example, to delete indices older than 14 days: ``` docker run -it --rm --net=host -e ROLLOVER=true \ jaegertracing/jaeger-es-index-cleaner:latest \ 14 \ http://localhost:9200 ``` Another alternative is to use [Elasticsearch Curator][curator]. [curator]: https://www.elastic.co/guide/en/elasticsearch/client/curator/current/about.html ================================================ FILE: cmd/es-index-cleaner/app/cutoff_time.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package app import ( "time" ) func CalculateDeletionCutoff(currTime time.Time, numOfDays int, relativeIndexEnabled bool) time.Time { year, month, day := currTime.Date() // tomorrow midnight cutoffTime := time.Date(year, month, day, 0, 0, 0, 0, time.UTC).AddDate(0, 0, 1) if relativeIndexEnabled { cutoffTime = currTime } return cutoffTime.Add(-time.Hour * 24 * time.Duration(numOfDays)) } ================================================ FILE: cmd/es-index-cleaner/app/cutoff_time_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package app import ( "testing" "time" "github.com/stretchr/testify/assert" ) func TestCalculateDeletionCutoff(t *testing.T) { time20250309163053 := time.Date(2025, time.March, 9, 16, 30, 53, 0, time.UTC) tests := []struct { name string currTime time.Time numOfDays int relativeIndexEnabled bool expectedCutoff time.Time }{ { name: "get today's midnight", currTime: time20250309163053, numOfDays: 1, relativeIndexEnabled: false, expectedCutoff: time.Date(2025, time.March, 9, 0, 0, 0, 0, time.UTC), }, { name: "get exactly 24 hours before execution time", currTime: time20250309163053, numOfDays: 1, relativeIndexEnabled: true, expectedCutoff: time.Date(2025, time.March, 8, 16, 30, 53, 0, time.UTC), }, { name: "get the current time if numOfDays is 0 and relativeIndexEnabled is true", currTime: time20250309163053, numOfDays: 0, relativeIndexEnabled: true, expectedCutoff: time20250309163053, }, { name: "get tomorrow's midnight if numOfDays is 0 relativeIndexEnabled is False", currTime: time20250309163053, numOfDays: 0, relativeIndexEnabled: false, expectedCutoff: time.Date(2025, time.March, 10, 0, 0, 0, 0, time.UTC), }, { name: "get (numOfDays-1)'s midnight", currTime: time20250309163053, numOfDays: 3, relativeIndexEnabled: false, expectedCutoff: time.Date(2025, time.March, 7, 0, 0, 0, 0, time.UTC), }, { name: "get exactly (24*numOfDays) hours before execution time", currTime: time20250309163053, numOfDays: 3, relativeIndexEnabled: true, expectedCutoff: time.Date(2025, time.March, 6, 16, 30, 53, 0, time.UTC), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { cutoffTime := CalculateDeletionCutoff(time20250309163053, test.numOfDays, test.relativeIndexEnabled) assert.Equal(t, test.expectedCutoff, cutoffTime) }) } } ================================================ FILE: cmd/es-index-cleaner/app/flags.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package app import ( "flag" "github.com/spf13/viper" "go.opentelemetry.io/collector/config/configtls" "go.opentelemetry.io/collector/featuregate" "github.com/jaegertracing/jaeger/internal/config/tlscfg" ) const ( indexPrefix = "index-prefix" archive = "archive" rollover = "rollover" timeout = "timeout" indexDateSeparator = "index-date-separator" username = "es.username" password = "es.password" ) var tlsFlagsCfg = tlscfg.ClientFlagsConfig{Prefix: "es"} // Config holds configuration for index cleaner binary. type Config struct { IndexPrefix string Archive bool Rollover bool MasterNodeTimeoutSeconds int IndexDateSeparator string Username string Password string //nolint:gosec // G117 TLSEnabled bool TLSConfig configtls.ClientConfig } // AddFlags adds flags for TLS to the FlagSet. func (*Config) AddFlags(flags *flag.FlagSet) { flags.String(indexPrefix, "", "Index prefix") flags.Bool(archive, false, "Whether to remove archive indices. It works only for rollover") flags.Bool(rollover, false, "Whether to remove indices created by rollover") flags.Int(timeout, 120, "Number of seconds to wait for master node response") flags.String(indexDateSeparator, "-", "Index date separator") flags.String(username, "", "The username required by storage") flags.String(password, "", "The password required by storage") tlsFlagsCfg.AddFlags(flags) featuregate.GlobalRegistry().RegisterFlags(flags) } // InitFromViper initializes config from viper.Viper. func (c *Config) InitFromViper(v *viper.Viper) error { c.IndexPrefix = v.GetString(indexPrefix) if c.IndexPrefix != "" { c.IndexPrefix += "-" } c.Archive = v.GetBool(archive) c.Rollover = v.GetBool(rollover) c.MasterNodeTimeoutSeconds = v.GetInt(timeout) c.IndexDateSeparator = v.GetString(indexDateSeparator) c.Username = v.GetString(username) c.Password = v.GetString(password) tlsCfg, err := tlsFlagsCfg.InitFromViper(v) if err != nil { return err } c.TLSConfig = tlsCfg return nil } ================================================ FILE: cmd/es-index-cleaner/app/flags_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package app import ( "flag" "testing" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestBindFlags(t *testing.T) { v := viper.New() c := &Config{} command := cobra.Command{} flags := &flag.FlagSet{} c.AddFlags(flags) command.PersistentFlags().AddGoFlagSet(flags) v.BindPFlags(command.PersistentFlags()) err := command.ParseFlags([]string{ "--index-prefix=tenant1", "--rollover=true", "--archive=true", "--timeout=150", "--index-date-separator=@", "--es.username=admin", "--es.password=admin", }) require.NoError(t, err) require.NoError(t, c.InitFromViper(v)) assert.Equal(t, "tenant1-", c.IndexPrefix) assert.True(t, c.Rollover) assert.True(t, c.Archive) assert.Equal(t, 150, c.MasterNodeTimeoutSeconds) assert.Equal(t, "@", c.IndexDateSeparator) assert.Equal(t, "admin", c.Username) assert.Equal(t, "admin", c.Password) } func TestInitFromViper_TLSError(t *testing.T) { v := viper.New() c := &Config{} command := cobra.Command{} flags := &flag.FlagSet{} c.AddFlags(flags) command.PersistentFlags().AddGoFlagSet(flags) v.BindPFlags(command.PersistentFlags()) err := command.ParseFlags([]string{ "--es.tls.ca=/nonexistent/ca.crt", }) require.NoError(t, err) err = c.InitFromViper(v) require.Error(t, err) } ================================================ FILE: cmd/es-index-cleaner/app/index_filter.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package app import ( "fmt" "regexp" "time" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/client" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/filter" ) // IndexFilter holds configuration for index filtering. type IndexFilter struct { // Index prefix. IndexPrefix string // Separator between date fragments. IndexDateSeparator string // Whether to filter archive indices. Archive bool // Whether to filter rollover indices. Rollover bool // Indices created before this date will be deleted. DeleteBeforeThisDate time.Time } // Filter filters indices. func (i *IndexFilter) Filter(indices []client.Index) []client.Index { indices = i.filterByPattern(indices) return filter.ByDate(indices, i.DeleteBeforeThisDate) } func (i *IndexFilter) filterByPattern(indices []client.Index) []client.Index { var reg *regexp.Regexp switch { case i.Archive: // archive works only for rollover reg, _ = regexp.Compile(fmt.Sprintf("^%sjaeger-span-archive-\\d{6}", i.IndexPrefix)) case i.Rollover: reg, _ = regexp.Compile(fmt.Sprintf("^%sjaeger-(span|service|dependencies|sampling)-\\d{6}", i.IndexPrefix)) default: reg, _ = regexp.Compile(fmt.Sprintf("^%sjaeger-(span|service|dependencies|sampling)-\\d{4}%s\\d{2}%s\\d{2}", i.IndexPrefix, i.IndexDateSeparator, i.IndexDateSeparator)) } var filtered []client.Index for _, in := range indices { if reg.MatchString(in.Index) { // index in write alias cannot be removed if in.Aliases[i.IndexPrefix+"jaeger-span-write"] || in.Aliases[i.IndexPrefix+"jaeger-service-write"] || in.Aliases[i.IndexPrefix+"jaeger-span-archive-write"] || in.Aliases[i.IndexPrefix+"jaeger-dependencies-write"] || in.Aliases[i.IndexPrefix+"jaeger-sampling-write"] { continue } filtered = append(filtered, in) } } return filtered } ================================================ FILE: cmd/es-index-cleaner/app/index_filter_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package app import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/client" ) func TestIndexFilter(t *testing.T) { runIndexFilterTest(t, "") } func TestIndexFilterWithPrefix(t *testing.T) { runIndexFilterTest(t, "tenant1-") } func runIndexFilterTest(t *testing.T, prefix string) { time20200807 := time.Date(2020, time.August, 6, 0, 0, 0, 0, time.UTC).AddDate(0, 0, 1) indices := []client.Index{ { Index: prefix + "jaeger-span-2020-08-06", CreationTime: time.Date(2020, time.August, 6, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: prefix + "jaeger-span-2020-08-05", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: prefix + "jaeger-service-2020-08-06", CreationTime: time.Date(2020, time.August, 6, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: prefix + "jaeger-service-2020-08-05", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: prefix + "jaeger-dependencies-2020-08-06", CreationTime: time.Date(2020, time.August, 6, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: prefix + "jaeger-dependencies-2020-08-05", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: prefix + "jaeger-sampling-2020-08-06", CreationTime: time.Date(2020, time.August, 6, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: prefix + "jaeger-sampling-2020-08-05", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: prefix + "jaeger-span-archive", CreationTime: time.Date(2020, time.August, 1, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: prefix + "jaeger-span-000001", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{ prefix + "jaeger-span-read": true, }, }, { Index: prefix + "jaeger-span-000002", CreationTime: time.Date(2020, time.August, 6, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{ prefix + "jaeger-span-read": true, prefix + "jaeger-span-write": true, }, }, { Index: prefix + "jaeger-service-000001", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{ prefix + "jaeger-service-read": true, }, }, { Index: prefix + "jaeger-service-000002", CreationTime: time.Date(2020, time.August, 6, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{ prefix + "jaeger-service-read": true, prefix + "jaeger-service-write": true, }, }, { Index: prefix + "jaeger-span-archive-000001", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{ prefix + "jaeger-span-archive-read": true, }, }, { Index: prefix + "jaeger-span-archive-000002", CreationTime: time.Date(2020, time.August, 6, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{ prefix + "jaeger-span-archive-read": true, prefix + "jaeger-span-archive-write": true, }, }, { Index: "other-jaeger-span-2020-08-05", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: "other-jaeger-service-2020-08-06", CreationTime: time.Date(2020, time.August, 6, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: "other-bar-jaeger-span-000002", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{ "other-jaeger-span-read": true, "other-jaeger-span-write": true, }, }, { Index: "otherfoo-jaeger-span-archive", CreationTime: time.Date(2020, time.August, 1, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: "foo-jaeger-span-archive-000001", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{ "foo-jaeger-span-archive-read": true, }, }, } tests := []struct { name string filter *IndexFilter expected []client.Index }{ { name: "normal indices, remove older than 2 days", filter: &IndexFilter{ IndexPrefix: prefix, IndexDateSeparator: "-", Archive: false, Rollover: false, DeleteBeforeThisDate: time20200807.Add(-time.Hour * 24 * time.Duration(2)), }, }, { name: "normal indices, remove older 1 days", filter: &IndexFilter{ IndexPrefix: prefix, IndexDateSeparator: "-", Archive: false, Rollover: false, DeleteBeforeThisDate: time20200807.Add(-time.Hour * 24 * time.Duration(1)), }, expected: []client.Index{ { Index: prefix + "jaeger-span-2020-08-05", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: prefix + "jaeger-service-2020-08-05", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: prefix + "jaeger-dependencies-2020-08-05", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: prefix + "jaeger-sampling-2020-08-05", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, }, }, { name: "normal indices, remove older 0 days - it should remove all indices", filter: &IndexFilter{ IndexPrefix: prefix, IndexDateSeparator: "-", Archive: false, Rollover: false, DeleteBeforeThisDate: time20200807.Add(-time.Hour * 24 * time.Duration(0)), }, expected: []client.Index{ { Index: prefix + "jaeger-span-2020-08-06", CreationTime: time.Date(2020, time.August, 6, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: prefix + "jaeger-span-2020-08-05", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: prefix + "jaeger-service-2020-08-06", CreationTime: time.Date(2020, time.August, 6, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: prefix + "jaeger-service-2020-08-05", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: prefix + "jaeger-dependencies-2020-08-06", CreationTime: time.Date(2020, time.August, 6, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: prefix + "jaeger-dependencies-2020-08-05", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: prefix + "jaeger-sampling-2020-08-06", CreationTime: time.Date(2020, time.August, 6, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, { Index: prefix + "jaeger-sampling-2020-08-05", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{}, }, }, }, { name: "archive indices, remove older 1 days - archive works only for rollover", filter: &IndexFilter{ IndexPrefix: prefix, IndexDateSeparator: "-", Archive: true, Rollover: false, DeleteBeforeThisDate: time20200807.Add(-time.Hour * 24 * time.Duration(1)), }, expected: []client.Index{ { Index: prefix + "jaeger-span-archive-000001", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{ prefix + "jaeger-span-archive-read": true, }, }, }, }, { name: "rollover indices, remove older 1 days", filter: &IndexFilter{ IndexPrefix: prefix, IndexDateSeparator: "-", Archive: false, Rollover: true, DeleteBeforeThisDate: time20200807.Add(-time.Hour * 24 * time.Duration(1)), }, expected: []client.Index{ { Index: prefix + "jaeger-span-000001", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{ prefix + "jaeger-span-read": true, }, }, { Index: prefix + "jaeger-service-000001", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{ prefix + "jaeger-service-read": true, }, }, }, }, { name: "rollover indices, remove older 0 days, index in write alias cannot be removed", filter: &IndexFilter{ IndexPrefix: prefix, IndexDateSeparator: "-", Archive: false, Rollover: true, DeleteBeforeThisDate: time20200807.Add(-time.Hour * 24 * time.Duration(0)), }, expected: []client.Index{ { Index: prefix + "jaeger-span-000001", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{ prefix + "jaeger-span-read": true, }, }, { Index: prefix + "jaeger-service-000001", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{ prefix + "jaeger-service-read": true, }, }, }, }, { name: "rollover archive indices, remove older 1 days", filter: &IndexFilter{ IndexPrefix: prefix, IndexDateSeparator: "-", Archive: true, Rollover: true, DeleteBeforeThisDate: time20200807.Add(-time.Hour * 24 * time.Duration(1)), }, expected: []client.Index{ { Index: prefix + "jaeger-span-archive-000001", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{ prefix + "jaeger-span-archive-read": true, }, }, }, }, { name: "rollover archive indices, remove older 0 days, index in write alias cannot be removed", filter: &IndexFilter{ IndexPrefix: prefix, IndexDateSeparator: "-", Archive: true, Rollover: true, DeleteBeforeThisDate: time20200807.Add(-time.Hour * 24 * time.Duration(0)), }, expected: []client.Index{ { Index: prefix + "jaeger-span-archive-000001", CreationTime: time.Date(2020, time.August, 5, 15, 0, 0, 0, time.UTC), Aliases: map[string]bool{ prefix + "jaeger-span-archive-read": true, }, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { indices := test.filter.Filter(indices) assert.Equal(t, test.expected, indices) }) } } ================================================ FILE: cmd/es-index-cleaner/app/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package app import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: cmd/es-index-cleaner/main.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "encoding/base64" "errors" "fmt" "log" "net/http" "strconv" "time" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" "go.opentelemetry.io/collector/featuregate" "go.uber.org/zap" "github.com/jaegertracing/jaeger/cmd/es-index-cleaner/app" "github.com/jaegertracing/jaeger/internal/config" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/client" ) var relativeIndexCleaner *featuregate.Gate func init() { relativeIndexCleaner = featuregate.GlobalRegistry().MustRegister( "es.index.relativeTimeIndexDeletion", featuregate.StageAlpha, featuregate.WithRegisterFromVersion("v2.5.0"), featuregate.WithRegisterDescription("Controls whether the indices will be deleted relative to the current time or tomorrow midnight."), featuregate.WithRegisterReferenceURL("https://github.com/jaegertracing/jaeger/issues/6236"), ) } func main() { logger, _ := zap.NewProduction() v := viper.New() cfg := &app.Config{} command := &cobra.Command{ Use: "jaeger-es-index-cleaner NUM_OF_DAYS http://HOSTNAME:PORT", Short: "Jaeger es-index-cleaner removes Jaeger indices", Long: "Jaeger es-index-cleaner removes Jaeger indices", RunE: func(_ *cobra.Command, args []string) error { if len(args) != 2 { return errors.New("wrong number of arguments") } numOfDays, err := strconv.Atoi(args[0]) if err != nil { return fmt.Errorf("could not parse NUM_OF_DAYS argument: %w", err) } if err := cfg.InitFromViper(v); err != nil { return fmt.Errorf("failed to initialize config: %w", err) } ctx := context.Background() tlscfg, err := cfg.TLSConfig.LoadTLSConfig(ctx) if err != nil { return fmt.Errorf("error loading tls config : %w", err) } c := &http.Client{ Timeout: time.Duration(cfg.MasterNodeTimeoutSeconds) * time.Second, Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, TLSClientConfig: tlscfg, }, } i := client.IndicesClient{ Client: client.Client{ Endpoint: args[1], Client: c, BasicAuth: basicAuth(cfg.Username, cfg.Password), }, MasterTimeoutSeconds: cfg.MasterNodeTimeoutSeconds, IgnoreUnavailableIndex: true, } indices, err := i.GetJaegerIndices(cfg.IndexPrefix) if err != nil { return err } deleteIndicesBefore := app.CalculateDeletionCutoff(time.Now().UTC(), numOfDays, relativeIndexCleaner.IsEnabled()) logger.Info("Indices before this date will be deleted", zap.String("date", deleteIndicesBefore.Format(time.RFC3339))) filter := &app.IndexFilter{ IndexPrefix: cfg.IndexPrefix, IndexDateSeparator: cfg.IndexDateSeparator, Archive: cfg.Archive, Rollover: cfg.Rollover, DeleteBeforeThisDate: deleteIndicesBefore, } logger.Info("Queried indices", zap.Any("indices", indices)) indices = filter.Filter(indices) if len(indices) == 0 { logger.Info("No indices to delete") return nil } logger.Info("Deleting indices", zap.Any("indices", indices)) return i.DeleteIndices(indices) }, } config.AddFlags( v, command, cfg.AddFlags, ) command.Flags().AddFlagSet(pflag.CommandLine) if err := command.Execute(); err != nil { log.Fatalln(err) } } func basicAuth(username, password string) string { if username == "" || password == "" { return "" } return base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) } ================================================ FILE: cmd/es-rollover/Dockerfile ================================================ # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 ARG base_image FROM $base_image AS release ARG TARGETARCH ARG USER_UID=10001 COPY es-rollover-linux-$TARGETARCH /go/bin/es-rollover ENTRYPOINT ["/go/bin/es-rollover"] USER ${USER_UID} ================================================ FILE: cmd/es-rollover/app/actions.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package app import ( "context" "crypto/tls" "fmt" "net/http" "time" "github.com/spf13/viper" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/client" ) func newESClient(endpoint string, cfg *Config, tlsCfg *tls.Config) client.Client { httpClient := &http.Client{ Timeout: time.Duration(cfg.Timeout) * time.Second, Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, TLSClientConfig: tlsCfg, }, } return client.Client{ Endpoint: endpoint, Client: httpClient, BasicAuth: client.BasicAuth(cfg.Username, cfg.Password), } } // Action is an interface that each action (init, rollover and lookback) of the es-rollover should implement type Action interface { Do() error } // ActionExecuteOptions are the options passed to the execute action function type ActionExecuteOptions struct { Args []string Viper *viper.Viper Logger *zap.Logger } // ActionCreatorFunction type is the function type in charge of create the action to be executed type ActionCreatorFunction func(client.Client, Config) Action // ExecuteAction execute the action returned by the createAction function func ExecuteAction(opts ActionExecuteOptions, createAction ActionCreatorFunction) error { cfg := Config{} if err := cfg.InitFromViper(opts.Viper); err != nil { return fmt.Errorf("failed to initialize config: %w", err) } ctx := context.Background() tlsCfg, err := cfg.TLSConfig.LoadTLSConfig(ctx) if err != nil { return fmt.Errorf("TLS configuration failed: %w", err) } esClient := newESClient(opts.Args[0], &cfg, tlsCfg) action := createAction(esClient, cfg) return action.Do() } ================================================ FILE: cmd/es-rollover/app/actions_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package app import ( "errors" "net/http" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/config" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/client" ) var errActionTest = errors.New("action error") type dummyAction struct { TestFn func() error } func (a *dummyAction) Do() error { return a.TestFn() } func TestExecuteAction(t *testing.T) { tests := []struct { name string flags []string expectedExecuteAction bool expectedSkip bool expectedError error actionFunction func() error configError bool }{ { name: "execute errored action", flags: []string{ "--es.tls.skip-host-verify=true", }, expectedExecuteAction: true, expectedSkip: true, expectedError: errActionTest, }, { name: "execute success action", flags: []string{ "--es.tls.skip-host-verify=true", }, expectedExecuteAction: true, expectedSkip: true, expectedError: nil, }, { name: "don't action because error in tls options", flags: []string{ "--es.tls.cert=/invalid/path/for/cert", }, expectedExecuteAction: false, configError: true, }, } logger := zap.NewNop() args := []string{ "https://localhost:9300", } for _, test := range tests { t.Run(test.name, func(t *testing.T) { v, command := config.Viperize(AddFlags) cmdLine := append([]string{"--es.tls.enabled=true"}, test.flags...) require.NoError(t, command.ParseFlags(cmdLine)) executedAction := false err := ExecuteAction(ActionExecuteOptions{ Args: args, Viper: v, Logger: logger, }, func(c client.Client, _ Config) Action { assert.Equal(t, "https://localhost:9300", c.Endpoint) transport, ok := c.Client.Transport.(*http.Transport) require.True(t, ok) assert.True(t, transport.TLSClientConfig.InsecureSkipVerify) return &dummyAction{ TestFn: func() error { executedAction = true return test.expectedError }, } }) assert.Equal(t, test.expectedExecuteAction, executedAction) if test.configError { require.Error(t, err) } else { assert.Equal(t, test.expectedError, err) } }) } } func TestExecuteAction_ConfigError(t *testing.T) { v, command := config.Viperize(AddFlags) cmdLine := []string{ "--es.tls.ca=/nonexistent/ca.crt", } require.NoError(t, command.ParseFlags(cmdLine)) logger := zap.NewNop() args := []string{ "https://localhost:9300", } err := ExecuteAction(ActionExecuteOptions{ Args: args, Viper: v, Logger: logger, }, func(_ client.Client, _ Config) Action { return &dummyAction{ TestFn: func() error { return nil }, } }) require.Error(t, err) assert.ErrorContains(t, err, "failed to initialize config") } ================================================ FILE: cmd/es-rollover/app/flags.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package app import ( "flag" "github.com/spf13/viper" "go.opentelemetry.io/collector/config/configtls" "github.com/jaegertracing/jaeger/internal/config/tlscfg" ) var tlsFlagsCfg = tlscfg.ClientFlagsConfig{Prefix: "es"} const ( indexPrefix = "index-prefix" archive = "archive" username = "es.username" password = "es.password" useILM = "es.use-ilm" ilmPolicyName = "es.ilm-policy-name" timeout = "timeout" skipDependencies = "skip-dependencies" adaptiveSampling = "adaptive-sampling" ) // Config holds the global configurations for the es rollover, common to all actions type Config struct { IndexPrefix string Archive bool Username string Password string //nolint:gosec // G117 TLSEnabled bool ILMPolicyName string UseILM bool Timeout int SkipDependencies bool AdaptiveSampling bool TLSConfig configtls.ClientConfig } // AddFlags adds flags func AddFlags(flags *flag.FlagSet) { flags.String(indexPrefix, "", "Index prefix") flags.Bool(archive, false, "Handle archive indices") flags.String(username, "", "The username required by storage") flags.String(password, "", "The password required by storage") flags.Bool(useILM, false, "Use ILM to manage jaeger indices") flags.String(ilmPolicyName, "jaeger-ilm-policy", "The name of the ILM policy to use if ILM is active") flags.Int(timeout, 120, "Number of seconds to wait for master node response") flags.Bool(skipDependencies, false, "Disable rollover for dependencies index") flags.Bool(adaptiveSampling, false, "Enable rollover for adaptive sampling index") tlsFlagsCfg.AddFlags(flags) } // InitFromViper initializes config from viper.Viper. func (c *Config) InitFromViper(v *viper.Viper) error { c.IndexPrefix = v.GetString(indexPrefix) if c.IndexPrefix != "" { c.IndexPrefix += "-" } c.Archive = v.GetBool(archive) c.Username = v.GetString(username) c.Password = v.GetString(password) c.ILMPolicyName = v.GetString(ilmPolicyName) c.UseILM = v.GetBool(useILM) c.Timeout = v.GetInt(timeout) c.SkipDependencies = v.GetBool(skipDependencies) c.AdaptiveSampling = v.GetBool(adaptiveSampling) tlsCfg, err := tlsFlagsCfg.InitFromViper(v) if err != nil { return err } c.TLSConfig = tlsCfg return nil } ================================================ FILE: cmd/es-rollover/app/flags_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package app import ( "flag" "testing" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestBindFlags(t *testing.T) { v := viper.New() c := &Config{} command := cobra.Command{} flags := &flag.FlagSet{} AddFlags(flags) command.PersistentFlags().AddGoFlagSet(flags) v.BindPFlags(command.PersistentFlags()) err := command.ParseFlags([]string{ "--index-prefix=tenant1", "--archive=true", "--timeout=150", "--es.username=admin", "--es.password=qwerty123", "--es.use-ilm=true", "--es.ilm-policy-name=jaeger-ilm", "--skip-dependencies=true", "--adaptive-sampling=true", }) require.NoError(t, err) require.NoError(t, c.InitFromViper(v)) assert.Equal(t, "tenant1-", c.IndexPrefix) assert.True(t, c.Archive) assert.Equal(t, 150, c.Timeout) assert.Equal(t, "admin", c.Username) assert.Equal(t, "qwerty123", c.Password) assert.Equal(t, "jaeger-ilm", c.ILMPolicyName) assert.True(t, c.SkipDependencies) assert.True(t, c.AdaptiveSampling) } func TestInitFromViper_TLSError(t *testing.T) { v := viper.New() c := &Config{} command := cobra.Command{} flags := &flag.FlagSet{} AddFlags(flags) command.PersistentFlags().AddGoFlagSet(flags) v.BindPFlags(command.PersistentFlags()) err := command.ParseFlags([]string{ "--es.tls.ca=/nonexistent/ca.crt", }) require.NoError(t, err) err = c.InitFromViper(v) require.Error(t, err) } ================================================ FILE: cmd/es-rollover/app/index_options.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package app import ( "fmt" "strings" ) const ( writeAliasFormat = "%s-write" readAliasFormat = "%s-read" rolloverIndexFormat = "%s-000001" ) // IndexOption holds the information for the indices to rollover type IndexOption struct { prefix string indexType string Mapping string } // RolloverIndices return an array of indices to rollover func RolloverIndices(archive bool, skipDependencies bool, adaptiveSampling bool, prefix string) []IndexOption { if archive { return []IndexOption{ { prefix: prefix, indexType: "jaeger-span-archive", Mapping: "jaeger-span", }, } } indexOptions := []IndexOption{ { prefix: prefix, Mapping: "jaeger-span", indexType: "jaeger-span", }, { prefix: prefix, Mapping: "jaeger-service", indexType: "jaeger-service", }, } if !skipDependencies { indexOptions = append(indexOptions, IndexOption{ prefix: prefix, Mapping: "jaeger-dependencies", indexType: "jaeger-dependencies", }) } if adaptiveSampling { indexOptions = append(indexOptions, IndexOption{ prefix: prefix, Mapping: "jaeger-sampling", indexType: "jaeger-sampling", }) } return indexOptions } func (i *IndexOption) IndexName() string { return strings.TrimLeft(fmt.Sprintf("%s%s", i.prefix, i.indexType), "-") } // ReadAliasName returns read alias name of the index func (i *IndexOption) ReadAliasName() string { return fmt.Sprintf(readAliasFormat, i.IndexName()) } // WriteAliasName returns write alias name of the index func (i *IndexOption) WriteAliasName() string { return fmt.Sprintf(writeAliasFormat, i.IndexName()) } // InitialRolloverIndex returns the initial index rollover name func (i *IndexOption) InitialRolloverIndex() string { return fmt.Sprintf(rolloverIndexFormat, i.IndexName()) } // TemplateName returns the prefixed template name func (i *IndexOption) TemplateName() string { return strings.TrimLeft(fmt.Sprintf("%s%s", i.prefix, i.Mapping), "-") } ================================================ FILE: cmd/es-rollover/app/index_options_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package app import ( "testing" "github.com/stretchr/testify/assert" ) func TestRolloverIndices(t *testing.T) { type expectedValues struct { mapping string templateName string readAliasName string writeAliasName string initialRolloverIndex string } tests := []struct { name string archive bool prefix string skipDependencies bool adaptiveSampling bool expected []expectedValues }{ { name: "Empty prefix", expected: []expectedValues{ { templateName: "jaeger-span", mapping: "jaeger-span", readAliasName: "jaeger-span-read", writeAliasName: "jaeger-span-write", initialRolloverIndex: "jaeger-span-000001", }, { templateName: "jaeger-service", mapping: "jaeger-service", readAliasName: "jaeger-service-read", writeAliasName: "jaeger-service-write", initialRolloverIndex: "jaeger-service-000001", }, { templateName: "jaeger-dependencies", mapping: "jaeger-dependencies", readAliasName: "jaeger-dependencies-read", writeAliasName: "jaeger-dependencies-write", initialRolloverIndex: "jaeger-dependencies-000001", }, }, }, { name: "archive with prefix", archive: true, prefix: "mytenant", expected: []expectedValues{ { templateName: "mytenant-jaeger-span", mapping: "jaeger-span", readAliasName: "mytenant-jaeger-span-archive-read", writeAliasName: "mytenant-jaeger-span-archive-write", initialRolloverIndex: "mytenant-jaeger-span-archive-000001", }, }, }, { name: "archive empty prefix", archive: true, expected: []expectedValues{ { mapping: "jaeger-span", templateName: "jaeger-span", readAliasName: "jaeger-span-archive-read", writeAliasName: "jaeger-span-archive-write", initialRolloverIndex: "jaeger-span-archive-000001", }, }, }, { name: "with prefix", prefix: "mytenant", adaptiveSampling: true, expected: []expectedValues{ { mapping: "jaeger-span", templateName: "mytenant-jaeger-span", readAliasName: "mytenant-jaeger-span-read", writeAliasName: "mytenant-jaeger-span-write", initialRolloverIndex: "mytenant-jaeger-span-000001", }, { mapping: "jaeger-service", templateName: "mytenant-jaeger-service", readAliasName: "mytenant-jaeger-service-read", writeAliasName: "mytenant-jaeger-service-write", initialRolloverIndex: "mytenant-jaeger-service-000001", }, { mapping: "jaeger-dependencies", templateName: "mytenant-jaeger-dependencies", readAliasName: "mytenant-jaeger-dependencies-read", writeAliasName: "mytenant-jaeger-dependencies-write", initialRolloverIndex: "mytenant-jaeger-dependencies-000001", }, { mapping: "jaeger-sampling", templateName: "mytenant-jaeger-sampling", readAliasName: "mytenant-jaeger-sampling-read", writeAliasName: "mytenant-jaeger-sampling-write", initialRolloverIndex: "mytenant-jaeger-sampling-000001", }, }, }, { name: "skip-dependency enable", prefix: "mytenant", skipDependencies: true, expected: []expectedValues{ { mapping: "jaeger-span", templateName: "mytenant-jaeger-span", readAliasName: "mytenant-jaeger-span-read", writeAliasName: "mytenant-jaeger-span-write", initialRolloverIndex: "mytenant-jaeger-span-000001", }, { mapping: "jaeger-service", templateName: "mytenant-jaeger-service", readAliasName: "mytenant-jaeger-service-read", writeAliasName: "mytenant-jaeger-service-write", initialRolloverIndex: "mytenant-jaeger-service-000001", }, }, }, { name: "adaptive sampling enable", prefix: "mytenant", skipDependencies: true, adaptiveSampling: true, expected: []expectedValues{ { mapping: "jaeger-span", templateName: "mytenant-jaeger-span", readAliasName: "mytenant-jaeger-span-read", writeAliasName: "mytenant-jaeger-span-write", initialRolloverIndex: "mytenant-jaeger-span-000001", }, { mapping: "jaeger-service", templateName: "mytenant-jaeger-service", readAliasName: "mytenant-jaeger-service-read", writeAliasName: "mytenant-jaeger-service-write", initialRolloverIndex: "mytenant-jaeger-service-000001", }, { mapping: "jaeger-sampling", templateName: "mytenant-jaeger-sampling", readAliasName: "mytenant-jaeger-sampling-read", writeAliasName: "mytenant-jaeger-sampling-write", initialRolloverIndex: "mytenant-jaeger-sampling-000001", }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if test.prefix != "" { test.prefix += "-" } result := RolloverIndices(test.archive, test.skipDependencies, test.adaptiveSampling, test.prefix) assert.Len(t, result, len(test.expected)) for i, r := range result { assert.Equal(t, test.expected[i].templateName, r.TemplateName()) assert.Equal(t, test.expected[i].mapping, r.Mapping) assert.Equal(t, test.expected[i].readAliasName, r.ReadAliasName()) assert.Equal(t, test.expected[i].writeAliasName, r.WriteAliasName()) assert.Equal(t, test.expected[i].initialRolloverIndex, r.InitialRolloverIndex()) } }) } } ================================================ FILE: cmd/es-rollover/app/init/action.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package init import ( "errors" "fmt" "github.com/jaegertracing/jaeger/cmd/es-rollover/app" es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/client" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/filter" "github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch/mappings" ) const ilmVersionSupport = 7 // Action holds the configuration and clients for init action type Action struct { Config Config ClusterClient client.ClusterAPI IndicesClient client.IndexAPI ILMClient client.IndexManagementLifecycleAPI } func (c Action) getMapping(version uint, mappingType mappings.MappingType) (string, error) { c.Config.Indices.IndexPrefix = config.IndexPrefix(c.Config.Config.IndexPrefix) mappingBuilder := mappings.MappingBuilder{ TemplateBuilder: es.TextTemplateBuilder{}, Indices: c.Config.Indices, UseILM: c.Config.UseILM, ILMPolicyName: c.Config.ILMPolicyName, EsVersion: version, } return mappingBuilder.GetMapping(mappingType) } // Do the init action func (c Action) Do() error { version, err := c.ClusterClient.Version() if err != nil { return err } if c.Config.UseILM { if version < ilmVersionSupport { return errors.New("ILM is supported only for ES version 7+") } policyExist, err := c.ILMClient.Exists(c.Config.ILMPolicyName) if err != nil { return err } if !policyExist { return fmt.Errorf("ILM policy %s doesn't exist in Elasticsearch. Please create it and re-run init", c.Config.ILMPolicyName) } } rolloverIndices := app.RolloverIndices(c.Config.Archive, c.Config.SkipDependencies, c.Config.AdaptiveSampling, c.Config.Config.IndexPrefix) for _, indexName := range rolloverIndices { if err := c.init(version, indexName); err != nil { return err } } return nil } func createIndexIfNotExist(c client.IndexAPI, index string) error { exists, err := c.IndexExists(index) if err != nil { return err } if exists { return nil } aliasExists, err := c.AliasExists(index) if err != nil { return err } if aliasExists { return nil } return c.CreateIndex(index) } func (c Action) init(version uint, indexopt app.IndexOption) error { mappingType, err := mappings.MappingTypeFromString(indexopt.Mapping) if err != nil { return err } mapping, err := c.getMapping(version, mappingType) if err != nil { return err } err = c.IndicesClient.CreateTemplate(mapping, indexopt.TemplateName()) if err != nil { return err } index := indexopt.InitialRolloverIndex() err = createIndexIfNotExist(c.IndicesClient, index) if err != nil { return err } jaegerIndices, err := c.IndicesClient.GetJaegerIndices(c.Config.Config.IndexPrefix) if err != nil { return err } readAlias := indexopt.ReadAliasName() writeAlias := indexopt.WriteAliasName() aliases := []client.Alias{} if !filter.AliasExists(jaegerIndices, readAlias) { aliases = append(aliases, client.Alias{ Index: index, Name: readAlias, IsWriteIndex: false, }) } if !filter.AliasExists(jaegerIndices, writeAlias) { aliases = append(aliases, client.Alias{ Index: index, Name: writeAlias, IsWriteIndex: c.Config.UseILM, }) } if len(aliases) > 0 { err = c.IndicesClient.CreateAlias(aliases) if err != nil { return err } } return nil } ================================================ FILE: cmd/es-rollover/app/init/action_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package init import ( "errors" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/cmd/es-rollover/app" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/client" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/client/mocks" ) func applyTestDefaults(cfg *Config) { // Set defaults only if missing if cfg.Indices.Spans.Shards == 0 { cfg.Indices.Spans.Shards = 3 } if cfg.Indices.Spans.Replicas == nil { cfg.Indices.Spans.Replicas = new(int64(1)) } if cfg.Indices.Spans.Priority == 0 { cfg.Indices.Spans.Priority = 10 } } func TestIndexCreateIfNotExist(t *testing.T) { tests := []struct { name string indexExists bool indexExistsErr error aliasExists bool aliasExistsErr error createIndexErr error expectedError string }{ { name: "success when index exists", indexExists: true, }, { name: "generic error from IndexExists", indexExistsErr: errors.New("may be an http error from index exists"), expectedError: "may be an http error from index exists", }, { name: "success when alias exists", aliasExists: true, }, { name: "generic error from AliasExists", aliasExistsErr: errors.New("may be an http error from alias exists"), expectedError: "may be an http error from alias exists", }, { name: "generic error from create index", createIndexErr: errors.New("may be an http error from create index"), expectedError: "may be an http error from create index", }, { name: "success when index and alias does not exist", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { indexClient := &mocks.IndexAPI{} indexClient.On("IndexExists", "jaeger-span").Return(test.indexExists, test.indexExistsErr) indexClient.On("AliasExists", "jaeger-span").Return(test.aliasExists, test.aliasExistsErr) indexClient.On("CreateIndex", "jaeger-span").Return(test.createIndexErr) err := createIndexIfNotExist(indexClient, "jaeger-span") if test.expectedError != "" { assert.EqualError(t, err, test.expectedError) } }) } } func TestRolloverAction(t *testing.T) { tests := []struct { name string setupCallExpectations func(indexClient *mocks.IndexAPI, clusterClient *mocks.ClusterAPI, ilmClient *mocks.IndexManagementLifecycleAPI) config Config expectedErr error }{ { name: "Unsupported version", setupCallExpectations: func(_ *mocks.IndexAPI, clusterClient *mocks.ClusterAPI, _ *mocks.IndexManagementLifecycleAPI) { clusterClient.On("Version").Return(uint(5), nil) }, config: Config{ Config: app.Config{ Archive: true, UseILM: true, }, }, expectedErr: errors.New("ILM is supported only for ES version 7+"), }, { name: "error getting version", setupCallExpectations: func(_ *mocks.IndexAPI, clusterClient *mocks.ClusterAPI, _ *mocks.IndexManagementLifecycleAPI) { clusterClient.On("Version").Return(uint(0), errors.New("version error")) }, expectedErr: errors.New("version error"), config: Config{ Config: app.Config{ Archive: true, UseILM: true, }, }, }, { name: "ilm doesnt exist", setupCallExpectations: func(_ *mocks.IndexAPI, clusterClient *mocks.ClusterAPI, ilmClient *mocks.IndexManagementLifecycleAPI) { clusterClient.On("Version").Return(uint(7), nil) ilmClient.On("Exists", "myilmpolicy").Return(false, nil) }, expectedErr: errors.New("ILM policy myilmpolicy doesn't exist in Elasticsearch. Please create it and re-run init"), config: Config{ Config: app.Config{ Archive: true, UseILM: true, ILMPolicyName: "myilmpolicy", }, }, }, { name: "fail get ilm policy", setupCallExpectations: func(_ *mocks.IndexAPI, clusterClient *mocks.ClusterAPI, ilmClient *mocks.IndexManagementLifecycleAPI) { clusterClient.On("Version").Return(uint(7), nil) ilmClient.On("Exists", "myilmpolicy").Return(false, errors.New("error getting ilm policy")) }, expectedErr: errors.New("error getting ilm policy"), config: Config{ Config: app.Config{ Archive: true, UseILM: true, ILMPolicyName: "myilmpolicy", }, }, }, { name: "fail to create template", setupCallExpectations: func(indexClient *mocks.IndexAPI, clusterClient *mocks.ClusterAPI, _ *mocks.IndexManagementLifecycleAPI) { clusterClient.On("Version").Return(uint(7), nil) indexClient.On("CreateTemplate", mock.Anything, "jaeger-span").Return(errors.New("error creating template")) }, expectedErr: errors.New("error creating template"), config: Config{ Config: app.Config{ Archive: true, UseILM: false, }, }, }, { name: "fail to get jaeger indices", setupCallExpectations: func(indexClient *mocks.IndexAPI, clusterClient *mocks.ClusterAPI, _ *mocks.IndexManagementLifecycleAPI) { clusterClient.On("Version").Return(uint(7), nil) indexClient.On("IndexExists", "jaeger-span-archive-000001").Return(false, nil) indexClient.On("AliasExists", "jaeger-span-archive-000001").Return(false, nil) indexClient.On("CreateTemplate", mock.Anything, "jaeger-span").Return(nil) indexClient.On("CreateIndex", "jaeger-span-archive-000001").Return(nil) indexClient.On("GetJaegerIndices", "").Return([]client.Index{}, errors.New("error getting jaeger indices")) }, expectedErr: errors.New("error getting jaeger indices"), config: Config{ Config: app.Config{ Archive: true, UseILM: false, }, }, }, { name: "fail to create alias", setupCallExpectations: func(indexClient *mocks.IndexAPI, clusterClient *mocks.ClusterAPI, _ *mocks.IndexManagementLifecycleAPI) { clusterClient.On("Version").Return(uint(7), nil) indexClient.On("IndexExists", "jaeger-span-archive-000001").Return(false, nil) indexClient.On("AliasExists", "jaeger-span-archive-000001").Return(false, nil) indexClient.On("CreateTemplate", mock.Anything, "jaeger-span").Return(nil) indexClient.On("CreateIndex", "jaeger-span-archive-000001").Return(nil) indexClient.On("GetJaegerIndices", "").Return([]client.Index{}, nil) indexClient.On("CreateAlias", []client.Alias{ {Index: "jaeger-span-archive-000001", Name: "jaeger-span-archive-read", IsWriteIndex: false}, {Index: "jaeger-span-archive-000001", Name: "jaeger-span-archive-write", IsWriteIndex: false}, }).Return(errors.New("error creating aliases")) }, expectedErr: errors.New("error creating aliases"), config: Config{ Config: app.Config{ Archive: true, UseILM: false, }, }, }, { name: "create rollover index", setupCallExpectations: func(indexClient *mocks.IndexAPI, clusterClient *mocks.ClusterAPI, _ *mocks.IndexManagementLifecycleAPI) { clusterClient.On("Version").Return(uint(7), nil) indexClient.On("IndexExists", "jaeger-span-archive-000001").Return(false, nil) indexClient.On("AliasExists", "jaeger-span-archive-000001").Return(false, nil) indexClient.On("CreateTemplate", mock.Anything, "jaeger-span").Return(nil) indexClient.On("CreateIndex", "jaeger-span-archive-000001").Return(nil) indexClient.On("GetJaegerIndices", "").Return([]client.Index{}, nil) indexClient.On("CreateAlias", []client.Alias{ {Index: "jaeger-span-archive-000001", Name: "jaeger-span-archive-read", IsWriteIndex: false}, {Index: "jaeger-span-archive-000001", Name: "jaeger-span-archive-write", IsWriteIndex: false}, }).Return(nil) }, expectedErr: nil, config: Config{ Config: app.Config{ Archive: true, UseILM: false, }, }, }, { name: "create rollover index with ilm", setupCallExpectations: func(indexClient *mocks.IndexAPI, clusterClient *mocks.ClusterAPI, ilmClient *mocks.IndexManagementLifecycleAPI) { clusterClient.On("Version").Return(uint(7), nil) indexClient.On("IndexExists", "jaeger-span-archive-000001").Return(false, nil) indexClient.On("AliasExists", "jaeger-span-archive-000001").Return(false, nil) indexClient.On("CreateTemplate", mock.Anything, "jaeger-span").Return(nil) indexClient.On("CreateIndex", "jaeger-span-archive-000001").Return(nil) indexClient.On("GetJaegerIndices", "").Return([]client.Index{}, nil) ilmClient.On("Exists", "jaeger-ilm").Return(true, nil) indexClient.On("CreateAlias", []client.Alias{ {Index: "jaeger-span-archive-000001", Name: "jaeger-span-archive-read", IsWriteIndex: false}, {Index: "jaeger-span-archive-000001", Name: "jaeger-span-archive-write", IsWriteIndex: true}, }).Return(nil) }, expectedErr: nil, config: Config{ Config: app.Config{ Archive: true, UseILM: true, ILMPolicyName: "jaeger-ilm", }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { // Apply local test defaults applyTestDefaults(&test.config) indexClient := &mocks.IndexAPI{} clusterClient := &mocks.ClusterAPI{} ilmClient := &mocks.IndexManagementLifecycleAPI{} initAction := Action{ Config: test.config, IndicesClient: indexClient, ClusterClient: clusterClient, ILMClient: ilmClient, } test.setupCallExpectations(indexClient, clusterClient, ilmClient) err := initAction.Do() if test.expectedErr != nil { require.Error(t, err) assert.Equal(t, test.expectedErr, err) } indexClient.AssertExpectations(t) clusterClient.AssertExpectations(t) ilmClient.AssertExpectations(t) }) } } ================================================ FILE: cmd/es-rollover/app/init/flags.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package init import ( "flag" "github.com/spf13/viper" "github.com/jaegertracing/jaeger/cmd/es-rollover/app" cfg "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" ) const ( shards = "shards" replicas = "replicas" prioritySpanTemplate = "priority-span-template" priorityServiceTemplate = "priority-service-template" priorityDependenciesTemplate = "priority-dependencies-template" prioritySamplingTemplate = "priority-sampling-template" ) // Config holds configuration for index cleaner binary. // Config.IndexPrefix supersedes Indices.IndexPrefix type Config struct { app.Config cfg.Indices } // AddFlags adds flags for TLS to the FlagSet. func (*Config) AddFlags(flags *flag.FlagSet) { flags.Int(shards, 5, "Number of shards") flags.Int(replicas, 1, "Number of replicas") flags.Int(prioritySpanTemplate, 0, "Priority of jaeger-span index template (ESv8 only)") flags.Int(priorityServiceTemplate, 0, "Priority of jaeger-service index template (ESv8 only)") flags.Int(priorityDependenciesTemplate, 0, "Priority of jaeger-dependencies index template (ESv8 only)") flags.Int(prioritySamplingTemplate, 0, "Priority of jaeger-sampling index template (ESv8 only)") } // InitFromViper initializes config from viper.Viper. func (c *Config) InitFromViper(v *viper.Viper) { c.Indices.Spans.Shards = v.GetInt64(shards) c.Indices.Services.Shards = v.GetInt64(shards) c.Indices.Dependencies.Shards = v.GetInt64(shards) c.Indices.Sampling.Shards = v.GetInt64(shards) repsPtr := new(v.GetInt64(replicas)) c.Indices.Spans.Replicas = repsPtr c.Indices.Services.Replicas = repsPtr c.Indices.Dependencies.Replicas = repsPtr c.Indices.Sampling.Replicas = repsPtr c.Indices.Spans.Priority = v.GetInt64(prioritySpanTemplate) c.Indices.Services.Priority = v.GetInt64(priorityServiceTemplate) c.Indices.Dependencies.Priority = v.GetInt64(priorityDependenciesTemplate) c.Indices.Sampling.Priority = v.GetInt64(prioritySamplingTemplate) } ================================================ FILE: cmd/es-rollover/app/init/flags_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package init import ( "flag" "testing" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestBindFlags(t *testing.T) { v := viper.New() c := &Config{} command := cobra.Command{} flags := &flag.FlagSet{} c.AddFlags(flags) command.PersistentFlags().AddGoFlagSet(flags) v.BindPFlags(command.PersistentFlags()) err := command.ParseFlags([]string{ "--shards=8", "--replicas=16", "--priority-span-template=300", "--priority-service-template=301", "--priority-dependencies-template=302", "--priority-sampling-template=303", }) require.NoError(t, err) c.InitFromViper(v) assert.EqualValues(t, 8, c.Indices.Spans.Shards) require.NotNil(t, c.Indices.Spans.Replicas) assert.EqualValues(t, 16, *c.Indices.Spans.Replicas) assert.EqualValues(t, 300, c.Indices.Spans.Priority) assert.EqualValues(t, 301, c.Indices.Services.Priority) assert.EqualValues(t, 302, c.Indices.Dependencies.Priority) assert.EqualValues(t, 303, c.Indices.Sampling.Priority) } ================================================ FILE: cmd/es-rollover/app/init/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package init import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: cmd/es-rollover/app/lookback/action.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package lookback import ( "time" "go.uber.org/zap" "github.com/jaegertracing/jaeger/cmd/es-rollover/app" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/client" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/filter" ) var timeNow func() time.Time = time.Now // Action holds the configuration and clients for lookback action type Action struct { Config IndicesClient client.IndexAPI Logger *zap.Logger } // Do the lookback action func (a *Action) Do() error { rolloverIndices := app.RolloverIndices(a.Config.Archive, a.Config.SkipDependencies, a.Config.AdaptiveSampling, a.Config.IndexPrefix) for _, indexName := range rolloverIndices { if err := a.lookback(indexName); err != nil { return err } } return nil } func (a *Action) lookback(indexSet app.IndexOption) error { jaegerIndex, err := a.IndicesClient.GetJaegerIndices(a.Config.IndexPrefix) if err != nil { return err } readAliasName := indexSet.ReadAliasName() readAliasIndices := filter.ByAlias(jaegerIndex, []string{readAliasName}) excludedWriteIndex := filter.ByAliasExclude(readAliasIndices, []string{indexSet.WriteAliasName()}) finalIndices := filter.ByDate(excludedWriteIndex, getTimeReference(timeNow(), a.Unit, a.UnitCount)) if len(finalIndices) == 0 { a.Logger.Info("No indices to remove from alias", zap.String("readAliasName", readAliasName)) return nil } aliases := make([]client.Alias, 0, len(finalIndices)) a.Logger.Info("About to remove indices", zap.String("readAliasName", readAliasName), zap.Int("indicesCount", len(finalIndices))) for _, index := range finalIndices { aliases = append(aliases, client.Alias{ Index: index.Index, Name: readAliasName, }) a.Logger.Info("To be removed", zap.String("index", index.Index), zap.String("creationTime", index.CreationTime.String())) } return a.IndicesClient.DeleteAlias(aliases) } ================================================ FILE: cmd/es-rollover/app/lookback/action_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package lookback import ( "errors" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/jaegertracing/jaeger/cmd/es-rollover/app" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/client" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/client/mocks" ) func TestLookBackAction(t *testing.T) { nowTime := time.Date(2021, 10, 12, 10, 10, 10, 10, time.Local) indices := []client.Index{ { Index: "jaeger-span-archive-0000", Aliases: map[string]bool{ "jaeger-span-archive-other-alias": true, }, CreationTime: time.Date(2021, 10, 10, 10, 10, 10, 10, time.Local), }, { Index: "jaeger-span-archive-0001", Aliases: map[string]bool{ "jaeger-span-archive-read": true, }, CreationTime: time.Date(2021, 10, 10, 10, 10, 10, 10, time.Local), }, { Index: "jaeger-span-archive-0002", Aliases: map[string]bool{ "jaeger-span-archive-read": true, "jaeger-span-archive-write": true, }, CreationTime: time.Date(2021, 10, 11, 10, 10, 10, 10, time.Local), }, { Index: "jaeger-span-archive-0002", Aliases: map[string]bool{ "jaeger-span-archive-read": true, }, CreationTime: nowTime, }, { Index: "jaeger-span-archive-0004", Aliases: map[string]bool{ "jaeger-span-archive-read": true, "jaeger-span-archive-write": true, }, CreationTime: nowTime, }, } timeNow = func() time.Time { return nowTime } tests := []struct { name string setupCallExpectations func(indexClient *mocks.IndexAPI) config Config expectedErr error }{ { name: "success", setupCallExpectations: func(indexClient *mocks.IndexAPI) { indexClient.On("GetJaegerIndices", "").Return(indices, nil) indexClient.On("DeleteAlias", []client.Alias{ { Index: "jaeger-span-archive-0001", Name: "jaeger-span-archive-read", }, }).Return(nil) }, config: Config{ Unit: "days", UnitCount: 1, Config: app.Config{ Archive: true, UseILM: true, }, }, expectedErr: nil, }, { name: "get indices error", setupCallExpectations: func(indexClient *mocks.IndexAPI) { indexClient.On("GetJaegerIndices", "").Return(indices, errors.New("get indices error")) }, config: Config{ Unit: "days", UnitCount: 1, Config: app.Config{ Archive: true, UseILM: true, }, }, expectedErr: errors.New("get indices error"), }, { name: "empty indices", setupCallExpectations: func(indexClient *mocks.IndexAPI) { indexClient.On("GetJaegerIndices", "").Return([]client.Index{}, nil) }, config: Config{ Unit: "days", UnitCount: 1, Config: app.Config{ Archive: true, UseILM: true, }, }, expectedErr: nil, }, } logger, _ := zap.NewProduction() for _, test := range tests { t.Run(test.name, func(t *testing.T) { indexClient := &mocks.IndexAPI{} lookbackAction := Action{ Config: test.config, IndicesClient: indexClient, Logger: logger, } test.setupCallExpectations(indexClient) err := lookbackAction.Do() if test.expectedErr != nil { require.Error(t, err) assert.Equal(t, test.expectedErr, err) } }) } } ================================================ FILE: cmd/es-rollover/app/lookback/flags.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package lookback import ( "flag" "github.com/spf13/viper" "github.com/jaegertracing/jaeger/cmd/es-rollover/app" ) const ( unit = "unit" unitCount = "unit-count" defaultUnit = "days" defaultUnitCount = 1 ) // Config holds configuration for index cleaner binary. type Config struct { app.Config Unit string UnitCount int } // AddFlags adds flags for TLS to the FlagSet. func (*Config) AddFlags(flags *flag.FlagSet) { flags.String(unit, defaultUnit, "used with lookback to remove indices from read alias e.g, days, weeks, months, years") flags.Int(unitCount, defaultUnitCount, "count of UNITs") } // InitFromViper initializes config from viper.Viper. func (c *Config) InitFromViper(v *viper.Viper) { c.Unit = v.GetString(unit) c.UnitCount = v.GetInt(unitCount) } ================================================ FILE: cmd/es-rollover/app/lookback/flags_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package lookback import ( "flag" "testing" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestBindFlags(t *testing.T) { v := viper.New() c := &Config{} command := cobra.Command{} flags := &flag.FlagSet{} c.AddFlags(flags) command.PersistentFlags().AddGoFlagSet(flags) v.BindPFlags(command.PersistentFlags()) err := command.ParseFlags([]string{ "--unit=days", "--unit-count=16", }) require.NoError(t, err) c.InitFromViper(v) assert.Equal(t, "days", c.Unit) assert.Equal(t, 16, c.UnitCount) } ================================================ FILE: cmd/es-rollover/app/lookback/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package lookback import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: cmd/es-rollover/app/lookback/time_reference.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package lookback import "time" func getTimeReference(currentTime time.Time, units string, unitCount int) time.Time { switch units { case "minutes": return currentTime.Truncate(time.Minute).Add(-time.Duration(unitCount) * time.Minute) case "hours": return currentTime.Truncate(time.Hour).Add(-time.Duration(unitCount) * time.Hour) case "days": year, month, day := currentTime.Date() tomorrowMidnight := time.Date(year, month, day, 0, 0, 0, 0, currentTime.Location()).AddDate(0, 0, 1) return tomorrowMidnight.Add(-time.Hour * 24 * time.Duration(unitCount)) case "weeks": year, month, day := currentTime.Date() tomorrowMidnight := time.Date(year, month, day, 0, 0, 0, 0, currentTime.Location()).AddDate(0, 0, 1) return tomorrowMidnight.Add(-time.Hour * 24 * time.Duration(7*unitCount)) case "months": year, month, day := currentTime.Date() return time.Date(year, month, day, 0, 0, 0, 0, currentTime.Location()).AddDate(0, -1*unitCount, 0) case "years": year, month, day := currentTime.Date() return time.Date(year, month, day, 0, 0, 0, 0, currentTime.Location()).AddDate(-1*unitCount, 0, 0) default: return currentTime.Truncate(time.Second).Add(-time.Duration(unitCount) * time.Second) } } ================================================ FILE: cmd/es-rollover/app/lookback/time_reference_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package lookback import ( "testing" "time" "github.com/stretchr/testify/assert" ) func TestGetTimeReference(t *testing.T) { now := time.Date(2021, time.October, 10, 10, 10, 10, 10, time.UTC) tests := []struct { name string unit string unitCount int expectedTime time.Time }{ { name: "seconds unit", unit: "seconds", unitCount: 30, expectedTime: time.Date(2021, time.October, 10, 10, 9, 40, 0, time.UTC), }, { name: "minutes unit", unit: "minutes", unitCount: 30, expectedTime: time.Date(2021, time.October, 10, 9, 40, 0, 0, time.UTC), }, { name: "hours unit", unit: "hours", unitCount: 2, expectedTime: time.Date(2021, time.October, 10, 8, 0, 0, 0, time.UTC), }, { name: "days unit", unit: "days", unitCount: 2, expectedTime: time.Date(2021, 10, 9, 0, 0, 0, 0, time.UTC), }, { name: "weeks unit", unit: "weeks", unitCount: 2, expectedTime: time.Date(2021, time.September, 27, 0, 0, 0, 0, time.UTC), }, { name: "months unit", unit: "months", unitCount: 2, expectedTime: time.Date(2021, time.August, 10, 0, 0, 0, 0, time.UTC), }, { name: "years unit", unit: "years", unitCount: 2, expectedTime: time.Date(2019, time.October, 10, 0, 0, 0, 0, time.UTC), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { ref := getTimeReference(now, test.unit, test.unitCount) assert.Equal(t, test.expectedTime, ref) }) } } func TestGetTimeReference_DefaultCase(t *testing.T) { now := time.Date(2021, time.October, 10, 10, 10, 10, 10, time.UTC) unknownUnit := "unknown-unit" unitCount := 30 ref := getTimeReference(now, unknownUnit, unitCount) expectedTime := time.Date(2021, time.October, 10, 10, 9, 40, 0, time.UTC) assert.Equal(t, expectedTime, ref) anotherUnknownUnit := "milliseconds" ref2 := getTimeReference(now, anotherUnknownUnit, unitCount) assert.Equal(t, expectedTime, ref2) } ================================================ FILE: cmd/es-rollover/app/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package app import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: cmd/es-rollover/app/rollover/action.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package rollover import ( "encoding/json" "github.com/jaegertracing/jaeger/cmd/es-rollover/app" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/client" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/filter" ) // Action holds the configuration and clients for rollover action type Action struct { Config IndicesClient client.IndexAPI } // Do the rollover action func (a *Action) Do() error { rolloverIndices := app.RolloverIndices(a.Config.Archive, a.Config.SkipDependencies, a.Config.AdaptiveSampling, a.Config.IndexPrefix) for _, indexName := range rolloverIndices { if err := a.rollover(indexName); err != nil { return err } } return nil } func (a *Action) rollover(indexSet app.IndexOption) error { conditionsMap := map[string]any{} if a.Conditions != "" { err := json.Unmarshal([]byte(a.Config.Conditions), &conditionsMap) if err != nil { return err } } writeAlias := indexSet.WriteAliasName() readAlias := indexSet.ReadAliasName() err := a.IndicesClient.Rollover(writeAlias, conditionsMap) if err != nil { return err } jaegerIndex, err := a.IndicesClient.GetJaegerIndices(a.Config.IndexPrefix) if err != nil { return err } indicesWithWriteAlias := filter.ByAlias(jaegerIndex, []string{writeAlias}) aliases := make([]client.Alias, 0, len(indicesWithWriteAlias)) for _, index := range indicesWithWriteAlias { aliases = append(aliases, client.Alias{ Index: index.Index, Name: readAlias, }) } if len(aliases) == 0 { return nil } return a.IndicesClient.CreateAlias(aliases) } ================================================ FILE: cmd/es-rollover/app/rollover/action_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package rollover import ( "errors" "testing" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/cmd/es-rollover/app" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/client" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/client/mocks" ) func TestRolloverAction(t *testing.T) { readIndices := []client.Index{ { Index: "jaeger-read-span", Aliases: map[string]bool{ "jaeger-span-archive-write": true, }, }, } aliasToCreate := []client.Alias{{Index: "jaeger-read-span", Name: "jaeger-span-archive-read", IsWriteIndex: false}} type testCase struct { name string conditions string unmarshalErrExpected bool getJaegerIndicesErr error rolloverErr error createAliasErr error expectedError bool indices []client.Index setupCallExpectations func(indexClient *mocks.IndexAPI, t *testCase) } tests := []testCase{ { name: "success", conditions: "{\"max_age\": \"2d\"}", expectedError: false, indices: readIndices, setupCallExpectations: func(indexClient *mocks.IndexAPI, test *testCase) { indexClient.On("GetJaegerIndices", "").Return(test.indices, test.getJaegerIndicesErr) indexClient.On("CreateAlias", aliasToCreate).Return(test.createAliasErr) indexClient.On("Rollover", "jaeger-span-archive-write", map[string]any{"max_age": "2d"}).Return(test.rolloverErr) }, }, { name: "no alias write alias", conditions: "{\"max_age\": \"2d\"}", expectedError: false, indices: []client.Index{ { Index: "jaeger-read-span", Aliases: map[string]bool{ "jaeger-span-archive-read": true, }, }, }, setupCallExpectations: func(indexClient *mocks.IndexAPI, test *testCase) { indexClient.On("GetJaegerIndices", "").Return(test.indices, test.getJaegerIndicesErr) indexClient.On("Rollover", "jaeger-span-archive-write", map[string]any{"max_age": "2d"}).Return(test.rolloverErr) }, }, { name: "get jaeger indices error", conditions: "{\"max_age\": \"2d\"}", expectedError: true, getJaegerIndicesErr: errors.New("unable to get indices"), indices: readIndices, setupCallExpectations: func(indexClient *mocks.IndexAPI, test *testCase) { indexClient.On("Rollover", "jaeger-span-archive-write", map[string]any{"max_age": "2d"}).Return(test.rolloverErr) indexClient.On("GetJaegerIndices", "").Return(test.indices, test.getJaegerIndicesErr) }, }, { name: "rollover error", conditions: "{\"max_age\": \"2d\"}", expectedError: true, rolloverErr: errors.New("unable to rollover"), indices: readIndices, setupCallExpectations: func(indexClient *mocks.IndexAPI, test *testCase) { indexClient.On("Rollover", "jaeger-span-archive-write", map[string]any{"max_age": "2d"}).Return(test.rolloverErr) }, }, { name: "create alias error", conditions: "{\"max_age\": \"2d\"}", expectedError: true, createAliasErr: errors.New("unable to create alias"), indices: readIndices, setupCallExpectations: func(indexClient *mocks.IndexAPI, test *testCase) { indexClient.On("GetJaegerIndices", "").Return(test.indices, test.getJaegerIndicesErr) indexClient.On("CreateAlias", aliasToCreate).Return(test.createAliasErr) indexClient.On("Rollover", "jaeger-span-archive-write", map[string]any{"max_age": "2d"}).Return(test.rolloverErr) }, }, { name: "unmarshal conditions error", conditions: "{\"max_age\" \"2d\"},", unmarshalErrExpected: true, createAliasErr: errors.New("unable to create alias"), indices: readIndices, setupCallExpectations: func(_ *mocks.IndexAPI, _ *testCase) {}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { indexClient := &mocks.IndexAPI{} rolloverAction := Action{ Config: Config{ Conditions: test.conditions, Config: app.Config{ Archive: true, }, }, IndicesClient: indexClient, } test.setupCallExpectations(indexClient, &test) err := rolloverAction.Do() if test.expectedError || test.unmarshalErrExpected { require.Error(t, err) } else { require.NoError(t, err) } indexClient.AssertExpectations(t) }) } } ================================================ FILE: cmd/es-rollover/app/rollover/flags.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package rollover import ( "flag" "github.com/spf13/viper" "github.com/jaegertracing/jaeger/cmd/es-rollover/app" ) const ( conditions = "conditions" defaultRollbackCondition = "{\"max_age\": \"2d\"}" ) // Config holds configuration for index cleaner binary. type Config struct { app.Config Conditions string } // AddFlags adds flags for TLS to the FlagSet. func (*Config) AddFlags(flags *flag.FlagSet) { flags.String(conditions, defaultRollbackCondition, "conditions used to rollover to a new write index") } // InitFromViper initializes config from viper.Viper. func (c *Config) InitFromViper(v *viper.Viper) { c.Conditions = v.GetString(conditions) } ================================================ FILE: cmd/es-rollover/app/rollover/flags_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package rollover import ( "flag" "testing" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestBindFlags(t *testing.T) { v := viper.New() c := &Config{} command := cobra.Command{} flags := &flag.FlagSet{} c.AddFlags(flags) command.PersistentFlags().AddGoFlagSet(flags) v.BindPFlags(command.PersistentFlags()) err := command.ParseFlags([]string{ "--conditions={\"max_age\": \"20000d\"}", }) require.NoError(t, err) c.InitFromViper(v) assert.JSONEq(t, `{"max_age": "20000d"}`, c.Conditions) } ================================================ FILE: cmd/es-rollover/app/rollover/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package rollover import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: cmd/es-rollover/main.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package main import ( "flag" "os" "github.com/spf13/cobra" "github.com/spf13/viper" "go.uber.org/zap" "github.com/jaegertracing/jaeger/cmd/es-rollover/app" initialize "github.com/jaegertracing/jaeger/cmd/es-rollover/app/init" "github.com/jaegertracing/jaeger/cmd/es-rollover/app/lookback" "github.com/jaegertracing/jaeger/cmd/es-rollover/app/rollover" "github.com/jaegertracing/jaeger/internal/config" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/client" ) func main() { v := viper.New() logger, _ := zap.NewProduction() rootCmd := &cobra.Command{ Use: "jaeger-es-rollover", Short: "Jaeger es-rollover manages Jaeger indices", Long: "Jaeger es-rollover manages Jaeger indices", } // Init command initCfg := &initialize.Config{} initCommand := &cobra.Command{ Use: "init http://HOSTNAME:PORT", Short: "creates indices and aliases", Long: "creates indices and aliases", Args: cobra.ExactArgs(1), SilenceUsage: true, RunE: func(_ *cobra.Command, args []string) error { return app.ExecuteAction(app.ActionExecuteOptions{ Args: args, Viper: v, Logger: logger, }, func(c client.Client, cfg app.Config) app.Action { initCfg.Config = cfg initCfg.InitFromViper(v) indicesClient := &client.IndicesClient{ Client: c, MasterTimeoutSeconds: initCfg.Timeout, } clusterClient := &client.ClusterClient{ Client: c, } ilmClient := &client.ILMClient{ Client: c, Logger: logger, } return &initialize.Action{ IndicesClient: indicesClient, ClusterClient: clusterClient, ILMClient: ilmClient, Config: *initCfg, } }) }, } // Rollover command rolloverCfg := &rollover.Config{} rolloverCommand := &cobra.Command{ Use: "rollover http://HOSTNAME:PORT", Short: "rollover to new write index", Long: "rollover to new write index", Args: cobra.ExactArgs(1), RunE: func(_ *cobra.Command, args []string) error { rolloverCfg.InitFromViper(v) return app.ExecuteAction(app.ActionExecuteOptions{ Args: args, Viper: v, Logger: logger, }, func(c client.Client, cfg app.Config) app.Action { rolloverCfg.Config = cfg rolloverCfg.InitFromViper(v) indicesClient := &client.IndicesClient{ Client: c, MasterTimeoutSeconds: rolloverCfg.Timeout, } return &rollover.Action{ IndicesClient: indicesClient, Config: *rolloverCfg, } }) }, } lookbackCfg := lookback.Config{} lookbackCommand := &cobra.Command{ Use: "lookback http://HOSTNAME:PORT", Short: "removes old indices from read alias", Long: "removes old indices from read alias", Args: cobra.ExactArgs(1), RunE: func(_ *cobra.Command, args []string) error { lookbackCfg.InitFromViper(v) return app.ExecuteAction(app.ActionExecuteOptions{ Args: args, Viper: v, Logger: logger, }, func(c client.Client, cfg app.Config) app.Action { lookbackCfg.Config = cfg lookbackCfg.InitFromViper(v) indicesClient := &client.IndicesClient{ Client: c, MasterTimeoutSeconds: lookbackCfg.Timeout, } return &lookback.Action{ IndicesClient: indicesClient, Config: lookbackCfg, Logger: logger, } }) }, } addPersistentFlags(v, rootCmd, app.AddFlags) addSubCommand(v, rootCmd, initCommand, initCfg.AddFlags) addSubCommand(v, rootCmd, rolloverCommand, rolloverCfg.AddFlags) addSubCommand(v, rootCmd, lookbackCommand, lookbackCfg.AddFlags) if err := rootCmd.Execute(); err != nil { os.Exit(1) } } func addSubCommand(v *viper.Viper, rootCmd, cmd *cobra.Command, addFlags func(*flag.FlagSet)) { rootCmd.AddCommand(cmd) config.AddFlags( v, cmd, addFlags, ) } func addPersistentFlags(v *viper.Viper, rootCmd *cobra.Command, inits ...func(*flag.FlagSet)) { flagSet := new(flag.FlagSet) for i := range inits { inits[i](flagSet) } rootCmd.PersistentFlags().AddGoFlagSet(flagSet) v.BindPFlags(rootCmd.PersistentFlags()) } ================================================ FILE: cmd/esmapping-generator/main.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package main import ( "fmt" "os" "github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch/mappings" "github.com/jaegertracing/jaeger/internal/version" ) func main() { esmappingsCmd := mappings.Command() esmappingsCmd.AddCommand(version.Command()) if err := esmappingsCmd.Execute(); err != nil { fmt.Println(err.Error()) os.Exit(1) } } ================================================ FILE: cmd/internal/docs/.gitignore ================================================ *.md *.rst *.1 *.yaml ================================================ FILE: cmd/internal/docs/command.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package docs import ( "flag" "fmt" "log" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" "github.com/spf13/viper" ) const ( formatFlag = "format" dirFlag = "dir" ) var formats = []string{"md", "man", "rst", "yaml"} // Command for generating flags/commands documentation. // It generates the documentation for all commands starting at parent. func Command(v *viper.Viper) *cobra.Command { c := &cobra.Command{ Use: "docs", Short: "Generates documentation", Long: `Generates command and flags documentation`, RunE: func(cmd *cobra.Command, _ /* args */ []string) error { for cmd.Parent() != nil { cmd = cmd.Parent() } dir := v.GetString(dirFlag) log.Printf("Generating documentation in %v", dir) switch v.GetString(formatFlag) { case "md": return doc.GenMarkdownTree(cmd, dir) case "man": return genMan(cmd, dir) case "rst": return doc.GenReSTTree(cmd, dir) case "yaml": return doc.GenYamlTree(cmd, dir) default: return fmt.Errorf("undefined value of %v, possible values are: %v", formatFlag, formats) } }, } c.Flags().AddGoFlagSet(flags(&flag.FlagSet{})) v.BindPFlags(c.Flags()) return c } func flags(flagSet *flag.FlagSet) *flag.FlagSet { flagSet.String( formatFlag, formats[0], fmt.Sprintf("Supported formats: %v.", formats)) flagSet.String( dirFlag, "./", "Directory where generate the documentation.") return flagSet } func genMan(cmd *cobra.Command, dir string) error { header := &doc.GenManHeader{ Title: cmd.Use, Section: "1", } return doc.GenManTree(cmd, header, dir) } ================================================ FILE: cmd/internal/docs/command_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package docs import ( "os" "testing" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestOutputFormats(t *testing.T) { tests := []struct { file string flag string err string }{ {file: "docs.md"}, {file: "docs.1", flag: "--format=man"}, {file: "docs.rst", flag: "--format=rst"}, {file: "docs.yaml", flag: "--format=yaml"}, {flag: "--format=foo", err: "undefined value of format, possible values are: [md man rst yaml]"}, } for _, test := range tests { v := viper.New() cmd := Command(v) cmd.ParseFlags([]string{test.flag}) err := cmd.Execute() if err == nil { f, err := os.ReadFile(test.file) require.NoError(t, err) assert.Contains(t, string(f), "documentation") } else { assert.Equal(t, test.err, err.Error()) } } } func TestDocsForParent(t *testing.T) { parent := &cobra.Command{ Use: "root_command", Short: "some description", } v := viper.New() docs := Command(v) parent.AddCommand(docs) err := docs.RunE(docs, []string{}) require.NoError(t, err) f, err := os.ReadFile("root_command.md") require.NoError(t, err) assert.Contains(t, string(f), "some description") } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: cmd/internal/featuregate/command.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package featuregate import ( "github.com/spf13/cobra" "go.opentelemetry.io/collector/otelcol" ) func Command() *cobra.Command { return newCommand(func() *cobra.Command { settings := otelcol.CollectorSettings{} return otelcol.NewCommand(settings) }) } func newCommand(otelCmdFn func() *cobra.Command) *cobra.Command { otelCmd := otelCmdFn() for _, cmd := range otelCmd.Commands() { if cmd.Name() == "featuregate" { otelCmd.RemoveCommand(cmd) return cmd } } panic("could not find 'featuregate' command") } ================================================ FILE: cmd/internal/featuregate/command_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package featuregate import ( "testing" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) func TestCommand(t *testing.T) { cmd := Command() assert.Equal(t, "featuregate [feature-id]", cmd.Use) } func TestCommand_Panic(t *testing.T) { assert.PanicsWithValue(t, "could not find 'featuregate' command", func() { newCommand(func() *cobra.Command { return &cobra.Command{} }) }) } ================================================ FILE: cmd/internal/featuregate/package_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package featuregate import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: cmd/internal/flags/admin.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package flags import ( "context" "errors" "flag" "fmt" "net" "net/http" "net/http/pprof" "sync" "github.com/spf13/viper" "go.opentelemetry.io/collector/config/confighttp" "go.opentelemetry.io/collector/config/confignet" "go.uber.org/zap" "go.uber.org/zap/zapcore" "github.com/jaegertracing/jaeger/internal/config/tlscfg" "github.com/jaegertracing/jaeger/internal/recoveryhandler" "github.com/jaegertracing/jaeger/internal/telemetry" "github.com/jaegertracing/jaeger/internal/version" ) const ( adminHTTPHostPort = "admin.http.host-port" ) var tlsAdminHTTPFlagsConfig = tlscfg.ServerFlagsConfig{ Prefix: "admin.http", } // AdminServer runs an HTTP server with admin endpoints, such as /metrics, /debug/pprof, health check, etc. type AdminServer struct { logger *zap.Logger mux *http.ServeMux server *http.Server serverCfg confighttp.ServerConfig stopped sync.WaitGroup hc *HealthHost } // NewAdminServer creates a new admin server. func NewAdminServer(hostPort string) *AdminServer { return &AdminServer{ logger: zap.NewNop(), mux: http.NewServeMux(), serverCfg: confighttp.ServerConfig{ NetAddr: confignet.AddrConfig{ Endpoint: hostPort, Transport: confignet.TransportTypeTCP, }, }, hc: NewHealthHost(), } } // Host returns the health host for this admin server. // It implements component.Host and componentstatus.Reporter, // allowing it to be used with telemetry.Settings and componentstatus.ReportStatus. func (s *AdminServer) Host() *HealthHost { return s.hc } // setLogger initializes logger. func (s *AdminServer) setLogger(logger *zap.Logger) { s.logger = logger } // AddFlags registers CLI flags. func (s *AdminServer) AddFlags(flagSet *flag.FlagSet) { flagSet.String(adminHTTPHostPort, s.serverCfg.NetAddr.Endpoint, fmt.Sprintf("The host:port (e.g. 127.0.0.1%s or %s) for the admin server, including health check, /metrics, etc.", s.serverCfg.NetAddr.Endpoint, s.serverCfg.NetAddr.Endpoint)) tlsAdminHTTPFlagsConfig.AddFlags(flagSet) } // InitFromViper initializes the server with properties retrieved from Viper. func (s *AdminServer) initFromViper(v *viper.Viper, logger *zap.Logger) error { s.setLogger(logger) tlsAdminHTTP, err := tlsAdminHTTPFlagsConfig.InitFromViper(v) if err != nil { return fmt.Errorf("failed to parse admin server TLS options: %w", err) } s.serverCfg.NetAddr.Endpoint = v.GetString(adminHTTPHostPort) s.serverCfg.TLS = tlsAdminHTTP return nil } // Handle adds a new handler to the admin server. func (s *AdminServer) Handle(path string, handler http.Handler) { s.mux.Handle(path, handler) } // Serve starts HTTP server. func (s *AdminServer) Serve() error { l, err := s.serverCfg.ToListener(context.Background()) if err != nil { s.logger.Error("Admin server failed to listen", zap.Error(err)) return err } return s.serveWithListener(l) } func (s *AdminServer) serveWithListener(l net.Listener) (err error) { s.logger.Info("Mounting health check on admin server", zap.String("route", "/")) s.mux.Handle("/", s.hc.Handler()) version.RegisterHandler(s.mux, s.logger) s.registerPprofHandlers() recoveryHandler := recoveryhandler.NewRecoveryHandler(s.logger, true) s.server, err = s.serverCfg.ToServer( context.Background(), nil, // host telemetry.NoopSettings().ToOtelComponent(), recoveryHandler(s.mux), ) if err != nil { return fmt.Errorf("failed to create admin server: %w", err) } errorLog, _ := zap.NewStdLogAt(s.logger, zapcore.ErrorLevel) s.server.ErrorLog = errorLog s.logger.Info("Starting admin HTTP server") var wg sync.WaitGroup //nolint:revive // not the same as wg.Go() which would call Done() on exit, not on start wg.Add(1) s.stopped.Add(1) go func() { wg.Done() defer s.stopped.Done() err := s.server.Serve(l) if err != nil && !errors.Is(err, http.ErrServerClosed) { s.logger.Error("failed to serve", zap.Error(err)) s.hc.SetUnavailable() } }() wg.Wait() // wait for the server to start listening s.logger.Info("Admin server started", zap.String("http.host-port", l.Addr().String())) return nil } func (s *AdminServer) registerPprofHandlers() { s.mux.HandleFunc("/debug/pprof/", pprof.Index) s.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) s.mux.HandleFunc("/debug/pprof/profile", pprof.Profile) s.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) s.mux.HandleFunc("/debug/pprof/trace", pprof.Trace) s.mux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine")) s.mux.Handle("/debug/pprof/heap", pprof.Handler("heap")) s.mux.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate")) s.mux.Handle("/debug/pprof/block", pprof.Handler("block")) } // Close stops the HTTP server func (s *AdminServer) Close() error { err := s.server.Shutdown(context.Background()) s.stopped.Wait() return err } ================================================ FILE: cmd/internal/flags/admin_test.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package flags import ( "context" "crypto/tls" "fmt" "net" "net/http" "strconv" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/config/configtls" "go.uber.org/zap" "go.uber.org/zap/zaptest" "go.uber.org/zap/zaptest/observer" "github.com/jaegertracing/jaeger/internal/config" "github.com/jaegertracing/jaeger/ports" ) var testCertKeyLocation = "../../../internal/config/tlscfg/testdata" func TestAdminServerHealthCheck(t *testing.T) { adminServer := NewAdminServer(":0") v, _ := config.Viperize(adminServer.AddFlags) zapCore, logs := observer.New(zap.InfoLevel) logger := zap.New(zapCore) require.NoError(t, adminServer.initFromViper(v, logger)) require.NoError(t, adminServer.Serve()) defer adminServer.Close() // Get the actual address from the log message := logs.FilterMessage("Admin server started") require.Equal(t, 1, message.Len()) hostPort := message.All()[0].ContextMap()["http.host-port"].(string) // Health check should initially be unavailable (503) resp, err := http.Get(fmt.Sprintf("http://%s/", hostPort)) require.NoError(t, err) resp.Body.Close() assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode) // Set to ready - should return 204 adminServer.Host().Ready() resp, err = http.Get(fmt.Sprintf("http://%s/", hostPort)) require.NoError(t, err) resp.Body.Close() assert.Equal(t, http.StatusNoContent, resp.StatusCode) // Set to unavailable - should return 503 adminServer.Host().SetUnavailable() resp, err = http.Get(fmt.Sprintf("http://%s/", hostPort)) require.NoError(t, err) resp.Body.Close() assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode) } func TestAdminServerHandlesPortZero(t *testing.T) { adminServer := NewAdminServer(":0") v, _ := config.Viperize(adminServer.AddFlags) zapCore, logs := observer.New(zap.InfoLevel) logger := zap.New(zapCore) adminServer.initFromViper(v, logger) require.NoError(t, adminServer.Serve()) defer adminServer.Close() message := logs.FilterMessage("Admin server started") assert.Equal(t, 1, message.Len(), "Expected Admin server started log message.") onlyEntry := message.All()[0] hostPort := onlyEntry.ContextMap()["http.host-port"].(string) port, _ := strconv.Atoi(strings.Split(hostPort, ":")[3]) assert.Positive(t, port) } func TestAdminWithFailedFlags(t *testing.T) { adminServer := NewAdminServer(fmt.Sprintf(":%d", ports.RemoteStorageAdminHTTP)) zapCore, _ := observer.New(zap.InfoLevel) logger := zap.New(zapCore) v, command := config.Viperize(adminServer.AddFlags) err := command.ParseFlags([]string{ "--admin.http.tls.enabled=false", "--admin.http.tls.cert=blah", // invalid unless tls.enabled }) require.NoError(t, err) err = adminServer.initFromViper(v, logger) assert.ErrorContains(t, err, "failed to parse admin server TLS options") } func TestAdminServerTLS(t *testing.T) { testCases := []struct { name string serverTLSFlags []string clientTLS configtls.ClientConfig }{ { name: "should pass with TLS client to trusted TLS server with correct hostname", serverTLSFlags: []string{ "--admin.http.tls.enabled=true", "--admin.http.tls.cert=" + testCertKeyLocation + "/example-server-cert.pem", "--admin.http.tls.key=" + testCertKeyLocation + "/example-server-key.pem", }, clientTLS: configtls.ClientConfig{ Insecure: false, Config: configtls.Config{ CAFile: testCertKeyLocation + "/example-CA-cert.pem", }, ServerName: "example.com", }, }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { adminServer := NewAdminServer(fmt.Sprintf(":%d", ports.RemoteStorageAdminHTTP)) v, command := config.Viperize(adminServer.AddFlags) err := command.ParseFlags(test.serverTLSFlags) require.NoError(t, err) err = adminServer.initFromViper(v, zaptest.NewLogger(t)) require.NoError(t, err) adminServer.Serve() defer adminServer.Close() clientTLSCfg, err0 := test.clientTLS.LoadTLSConfig(context.Background()) require.NoError(t, err0) dialer := &net.Dialer{Timeout: 2 * time.Second} conn, clientError := tls.DialWithDialer(dialer, "tcp", fmt.Sprintf("localhost:%d", ports.RemoteStorageAdminHTTP), clientTLSCfg) require.NoError(t, clientError) require.NoError(t, conn.Close()) client := &http.Client{ Transport: &http.Transport{ TLSClientConfig: clientTLSCfg, }, } url := fmt.Sprintf("https://localhost:%d", ports.RemoteStorageAdminHTTP) req, err := http.NewRequest(http.MethodGet, url, http.NoBody) require.NoError(t, err) req.Close = true // avoid persistent connections which leak goroutines response, requestError := client.Do(req) require.NoError(t, requestError) defer response.Body.Close() require.NotNil(t, response) }) } } ================================================ FILE: cmd/internal/flags/doc.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 // Package flags defines command line flags that are shared by several jaeger components. // They are defined in this shared location so that if several components are wired into // a single binary (e.g. a local container of complete Jaeger backend) they can all share // the flags. package flags ================================================ FILE: cmd/internal/flags/flags.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package flags import ( "flag" "fmt" "os" "strings" "github.com/spf13/viper" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) const ( logLevel = "log-level" logEncoding = "log-encoding" // json or console configFile = "config-file" ) // AddConfigFileFlag adds flags for ExternalConfFlags func AddConfigFileFlag(flagSet *flag.FlagSet) { flagSet.String(configFile, "", "Configuration file in JSON, TOML, YAML, HCL, or Java properties formats (default none). See spf13/viper for precedence.") } // TryLoadConfigFile initializes viper with config file specified as flag func TryLoadConfigFile(v *viper.Viper) error { if file := v.GetString(configFile); file != "" { v.SetConfigFile(file) err := v.ReadInConfig() if err != nil { return fmt.Errorf("cannot load config file %s: %w", file, err) } } return nil } // ParseJaegerTags parses the Jaeger tags string into a map. func ParseJaegerTags(jaegerTags string) (map[string]string, error) { if jaegerTags == "" { return nil, nil } tagPairs := strings.Split(string(jaegerTags), ",") tags := make(map[string]string) for _, p := range tagPairs { kv := strings.SplitN(p, "=", 2) if len(kv) != 2 { return nil, fmt.Errorf("invalid Jaeger tag pair %q, expected key=value", p) } k, v := strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1]) if strings.HasPrefix(v, "${") && strings.HasSuffix(v, "}") { skipWhenEmpty := false ed := strings.SplitN(string(v[2:len(v)-1]), ":", 2) if len(ed) == 1 { // no default value specified, set to empty skipWhenEmpty = true ed = append(ed, "") } e, d := ed[0], ed[1] v = os.Getenv(e) if v == "" && d != "" { v = d } // no value is set, skip this entry if v == "" && skipWhenEmpty { continue } } tags[k] = v } return tags, nil } // SharedFlags holds flags configuration type SharedFlags struct { // Logging holds logging configuration Logging logging } type logging struct { Level string Encoding string } // AddLoggingFlag adds logging flag for SharedFlags func AddLoggingFlags(flagSet *flag.FlagSet) { flagSet.String(logLevel, "info", "Minimal allowed log Level. For more levels see https://github.com/uber-go/zap") flagSet.String(logEncoding, "json", "Log encoding. Supported values are 'json' and 'console'.") } // InitFromViper initializes SharedFlags with properties from viper func (flags *SharedFlags) InitFromViper(v *viper.Viper) *SharedFlags { flags.Logging.Level = v.GetString(logLevel) flags.Logging.Encoding = v.GetString(logEncoding) return flags } // NewLogger returns logger based on configuration in SharedFlags func (flags *SharedFlags) NewLogger(conf zap.Config, options ...zap.Option) (*zap.Logger, error) { var level zapcore.Level err := (&level).UnmarshalText([]byte(flags.Logging.Level)) if err != nil { return nil, err } conf.Level = zap.NewAtomicLevelAt(level) conf.Encoding = flags.Logging.Encoding if flags.Logging.Encoding == "console" { conf.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder } return conf.Build(options...) } ================================================ FILE: cmd/internal/flags/flags_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package flags import ( "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestParseJaegerTags(t *testing.T) { tags, err := ParseJaegerTags("") require.NoError(t, err) assert.Nil(t, tags) jaegerTags := fmt.Sprintf("%s,%s,%s,%s,%s,%s", "key=value", "envVar1=${envKey1:defaultVal1}", "envVar2=${envKey2:defaultVal2}", "envVar3=${envKey3}", "envVar4=${envKey4}", "envVar5=${envVar5:}", ) t.Setenv("envKey1", "envVal1") t.Setenv("envKey4", "envVal4") expectedTags := map[string]string{ "key": "value", "envVar1": "envVal1", "envVar2": "defaultVal2", "envVar4": "envVal4", "envVar5": "", } tags, err = ParseJaegerTags(jaegerTags) require.NoError(t, err) assert.Equal(t, expectedTags, tags) } func TestParseJaegerTagsError(t *testing.T) { _, err := ParseJaegerTags("no-equals-sign") require.Error(t, err) assert.ErrorContains(t, err, "invalid Jaeger tag pair") } ================================================ FILE: cmd/internal/flags/healthhost.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package flags import ( "net/http" "sync/atomic" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/component/componentstatus" ) var ( _ component.Host = (*HealthHost)(nil) _ componentstatus.Reporter = (*HealthHost)(nil) ) // HealthHost implements component.Host and componentstatus.Reporter // and provides an HTTP handler for health checks. type HealthHost struct { ready atomic.Bool } // NewHealthHost creates a new HealthHost in not-ready state. func NewHealthHost() *HealthHost { return &HealthHost{} } // GetExtensions implements component.Host. func (*HealthHost) GetExtensions() map[component.ID]component.Component { return nil } // Report implements componentstatus.Reporter. func (h *HealthHost) Report(event *componentstatus.Event) { switch event.Status() { case componentstatus.StatusOK, componentstatus.StatusRecoverableError: h.Ready() default: h.SetUnavailable() } } // Ready sets the health status to ready. func (h *HealthHost) Ready() { h.ready.Store(true) } // SetUnavailable sets the health status to unavailable. func (h *HealthHost) SetUnavailable() { h.ready.Store(false) } // Handler returns an HTTP handler for the health endpoint. // Returns 204 No Content when ready, 503 Service Unavailable otherwise. func (h *HealthHost) Handler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { if h.ready.Load() { w.WriteHeader(http.StatusNoContent) } else { w.WriteHeader(http.StatusServiceUnavailable) } }) } ================================================ FILE: cmd/internal/flags/healthhost_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package flags import ( "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "go.opentelemetry.io/collector/component/componentstatus" ) func TestHealthHost_Handler(t *testing.T) { hh := NewHealthHost() handler := hh.Handler() // Initially unavailable - should return 503 req := httptest.NewRequest(http.MethodGet, "/", http.NoBody) w := httptest.NewRecorder() handler.ServeHTTP(w, req) assert.Equal(t, http.StatusServiceUnavailable, w.Code) // Set to ready - should return 204 hh.Ready() w = httptest.NewRecorder() handler.ServeHTTP(w, req) assert.Equal(t, http.StatusNoContent, w.Code) // Set to unavailable - should return 503 hh.SetUnavailable() w = httptest.NewRecorder() handler.ServeHTTP(w, req) assert.Equal(t, http.StatusServiceUnavailable, w.Code) } func TestHealthHost_Report(t *testing.T) { hh := NewHealthHost() handler := hh.Handler() req := httptest.NewRequest(http.MethodGet, "/", http.NoBody) // StatusOK should set Ready hh.Report(componentstatus.NewEvent(componentstatus.StatusOK)) w := httptest.NewRecorder() handler.ServeHTTP(w, req) assert.Equal(t, http.StatusNoContent, w.Code) // StatusStopping should set Unavailable hh.Report(componentstatus.NewEvent(componentstatus.StatusStopping)) w = httptest.NewRecorder() handler.ServeHTTP(w, req) assert.Equal(t, http.StatusServiceUnavailable, w.Code) // StatusRecoverableError should set Ready hh.Report(componentstatus.NewRecoverableErrorEvent(nil)) w = httptest.NewRecorder() handler.ServeHTTP(w, req) assert.Equal(t, http.StatusNoContent, w.Code) // StatusFatalError should set Unavailable hh.Report(componentstatus.NewFatalErrorEvent(nil)) w = httptest.NewRecorder() handler.ServeHTTP(w, req) assert.Equal(t, http.StatusServiceUnavailable, w.Code) } func TestHealthHost_GetExtensions(t *testing.T) { hh := NewHealthHost() assert.Nil(t, hh.GetExtensions()) } ================================================ FILE: cmd/internal/flags/package_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package flags import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: cmd/internal/flags/service.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package flags import ( "expvar" "flag" "fmt" "os" "os/signal" "syscall" "github.com/spf13/viper" "go.opentelemetry.io/collector/featuregate" "go.uber.org/zap" "go.uber.org/zap/zapgrpc" "google.golang.org/grpc/grpclog" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/metrics/metricsbuilder" "github.com/jaegertracing/jaeger/ports" ) // Service represents an abstract Jaeger backend component with some basic shared functionality. type Service struct { // AdminPort is the HTTP port number for admin server. AdminPort int // Admin is the admin server that hosts the health check and metrics endpoints. Admin *AdminServer // Logger is initialized after parsing Viper flags like --log-level. Logger *zap.Logger // MetricsFactory is the root factory without a namespace. MetricsFactory metrics.Factory signalsChannel chan os.Signal } // NewService creates a new Service. func NewService(adminPort int) *Service { signalsChannel := make(chan os.Signal, 1) signal.Notify(signalsChannel, os.Interrupt, syscall.SIGTERM) return &Service{ Admin: NewAdminServer(ports.PortToHostPort(adminPort)), signalsChannel: signalsChannel, } } // AddFlags registers CLI flags. func (s *Service) AddFlags(flagSet *flag.FlagSet) { AddConfigFileFlag(flagSet) AddLoggingFlags(flagSet) metricsbuilder.AddFlags(flagSet) s.Admin.AddFlags(flagSet) featuregate.GlobalRegistry().RegisterFlags(flagSet) } // Start bootstraps the service and starts the admin server. func (s *Service) Start(v *viper.Viper) error { if err := TryLoadConfigFile(v); err != nil { return fmt.Errorf("cannot load config file: %w", err) } sFlags := new(SharedFlags).InitFromViper(v) newProdConfig := zap.NewProductionConfig() newProdConfig.Sampling = nil logger, err := sFlags.NewLogger(newProdConfig) if err != nil { return fmt.Errorf("cannot create logger: %w", err) } s.Logger = logger grpclog.SetLoggerV2(zapgrpc.NewLogger( logger.WithOptions( zap.AddCallerSkip(5), // ensure the actual caller:lineNo is shown ))) metricsBuilder := new(metricsbuilder.Builder).InitFromViper(v) metricsFactory, err := metricsBuilder.CreateMetricsFactory("") if err != nil { return fmt.Errorf("cannot create metrics factory: %w", err) } s.MetricsFactory = metricsFactory if err = s.Admin.initFromViper(v, s.Logger); err != nil { return fmt.Errorf("cannot initialize admin server: %w", err) } if h := metricsBuilder.Handler(); h != nil { route := metricsBuilder.HTTPRoute s.Logger.Info("Mounting metrics handler on admin server", zap.String("route", route)) s.Admin.Handle(route, h) } // Mount expvar routes on different backends if metricsBuilder.Backend != "expvar" { s.Logger.Info("Mounting expvar handler on admin server", zap.String("route", "/debug/vars")) s.Admin.Handle("/debug/vars", expvar.Handler()) } if err := s.Admin.Serve(); err != nil { return fmt.Errorf("cannot start the admin server: %w", err) } return nil } // RunAndThen sets the health check to Ready and blocks until SIGTERM is received. // It then runs the shutdown function and exits. func (s *Service) RunAndThen(shutdown func()) error { s.Admin.Host().Ready() <-s.signalsChannel s.Logger.Info("Shutting down") s.Admin.Host().SetUnavailable() if shutdown != nil { shutdown() } err := s.Admin.Close() if err == nil { s.Logger.Info("Shutdown complete") } return err } ================================================ FILE: cmd/internal/flags/service_test.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package flags import ( "flag" "os" "reflect" "sync/atomic" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/config" ) func TestAddFlags(*testing.T) { s := NewService(0) s.AddFlags(new(flag.FlagSet)) } func TestStartErrors(t *testing.T) { scenarios := []struct { name string flags []string expErr string }{ { name: "bad config", flags: []string{"--config-file=invalid-file-name"}, expErr: "cannot load config file", }, { name: "bad log level", flags: []string{"--log-level=invalid-log-level"}, expErr: "cannot create logger", }, { name: "bad metrics backend", flags: []string{"--metrics-backend=invalid-metrics-backend"}, expErr: "cannot create metrics factory", }, { name: "bad admin TLS", flags: []string{"--admin.http.tls.enabled=true", "--admin.http.tls.cert=invalid-cert"}, expErr: "cannot start the admin server: failed to load TLS config", }, { name: "bad host:port", flags: []string{"--admin.http.host-port=invalid"}, expErr: "cannot start the admin server", }, { name: "clean start", flags: []string{}, }, } for _, test := range scenarios { t.Run(test.name, func(t *testing.T) { s := NewService( /* default port= */ 0) v, cmd := config.Viperize(s.AddFlags) err := cmd.ParseFlags(test.flags) require.NoError(t, err) err = s.Start(v) if test.expErr != "" { require.ErrorContains(t, err, test.expErr) return } require.NoError(t, err) var stopped atomic.Bool shutdown := func() { stopped.Store(true) } go s.RunAndThen(shutdown) // Give time for RunAndThen to start time.Sleep(100 * time.Millisecond) s.signalsChannel <- os.Interrupt waitForEqual(t, true, func() any { return stopped.Load() }) }) } } func waitForEqual(t *testing.T, expected any, getter func() any) { for range 1000 { value := getter() if reflect.DeepEqual(value, expected) { return } time.Sleep(10 * time.Millisecond) } assert.Equal(t, expected, getter()) } ================================================ FILE: cmd/internal/printconfig/command.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package printconfig import ( "fmt" "sort" "strings" "github.com/spf13/cobra" "github.com/spf13/viper" ) func printDivider(cmd *cobra.Command, n int) { fmt.Fprint(cmd.OutOrStdout(), strings.Repeat("-", n), "\n") } func printConfigurations(cmd *cobra.Command, v *viper.Viper, includeEmpty bool) { keys := v.AllKeys() sort.Strings(keys) maxKeyLength, maxValueLength := len("Configuration Option Name"), len("Value") maxSourceLength := len("user-assigned") for _, key := range keys { value := v.GetString(key) if len(key) > maxKeyLength { maxKeyLength = len(key) } if len(value) > maxValueLength { maxValueLength = len(value) } } maxRowLength := maxKeyLength + maxValueLength + maxSourceLength + 6 printDivider(cmd, maxRowLength) fmt.Fprintf(cmd.OutOrStdout(), "| %-*s %-*s %-*s |\n", maxKeyLength, "Configuration Option Name", maxValueLength, "Value", maxSourceLength, "Source") printDivider(cmd, maxRowLength) for _, key := range keys { value := v.GetString(key) source := "default" if v.IsSet(key) { source = "user-assigned" } if includeEmpty || value != "" { fmt.Fprintf(cmd.OutOrStdout(), "| %-*s %-*s %-*s |\n", maxKeyLength, key, maxValueLength, value, maxSourceLength, source) } } printDivider(cmd, maxRowLength) } func Command(v *viper.Viper) *cobra.Command { allFlag := true cmd := &cobra.Command{ Use: "print-config", Short: "Print names and values of configuration options", Long: "Print names and values of configuration options, distinguishing between default and user-assigned values", RunE: func(cmd *cobra.Command, _ /* args */ []string) error { printConfigurations(cmd, v, allFlag) return nil }, } cmd.Flags().BoolVarP(&allFlag, "all", "a", false, "Print all configuration options including those with empty values") return cmd } ================================================ FILE: cmd/internal/printconfig/command_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // 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. package printconfig import ( "bytes" "flag" "testing" "time" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/config" "github.com/jaegertracing/jaeger/internal/config/tlscfg" "github.com/jaegertracing/jaeger/internal/tenancy" "github.com/jaegertracing/jaeger/internal/testutils" ) const ( testPluginBinary = "test-plugin.binary" testPluginConfigurationFile = "test-plugin.configuration-file" testPluginLogLevel = "test-plugin.log-level" testRemotePrefix = "test-remote" testRemoteServer = testRemotePrefix + ".server" testRemoteConnectionTimeout = testRemotePrefix + ".connection-timeout" defaultTestPluginLogLevel = "warn" defaultTestConnectionTimeout = time.Duration(5 * time.Second) ) func addFlags(flagSet *flag.FlagSet) { tlscfg.ClientFlagsConfig{ Prefix: "test", }.AddFlags(flagSet) flagSet.String(testPluginBinary, "", "") flagSet.String(testPluginConfigurationFile, "", "") flagSet.String(testPluginLogLevel, defaultTestPluginLogLevel, "") flagSet.String(testRemoteServer, "", "") flagSet.Duration(testRemoteConnectionTimeout, defaultTestConnectionTimeout, "") } func setConfig(t *testing.T) *viper.Viper { v, command := config.Viperize(addFlags, tenancy.AddFlags) err := command.ParseFlags([]string{ "--test-plugin.binary=noop-test-plugin", "--test-plugin.configuration-file=config.json", "--test-plugin.log-level=debug", "--multi-tenancy.header=x-scope-orgid", }) require.NoError(t, err) return v } func runPrintConfigCommand(v *viper.Viper, t *testing.T, allFlag bool) string { buf := new(bytes.Buffer) printCmd := Command(v) printCmd.SetOut(buf) if allFlag { err := printCmd.Flags().Set("all", "true") require.NoError(t, err, "printCmd.Flags() returned the error %v", err) } _, err := printCmd.ExecuteC() require.NoError(t, err, "printCmd.ExecuteC() returned the error %v", err) return buf.String() } func TestAllFlag(t *testing.T) { expected := `----------------------------------------------------------------- | Configuration Option Name Value Source | ----------------------------------------------------------------- | multi-tenancy.enabled false default | | multi-tenancy.header x-scope-orgid user-assigned | | multi-tenancy.tenants default | | test-plugin.binary noop-test-plugin user-assigned | | test-plugin.configuration-file config.json user-assigned | | test-plugin.log-level debug user-assigned | | test-remote.connection-timeout 5s default | | test-remote.server default | | test.tls.ca default | | test.tls.cert default | | test.tls.enabled false default | | test.tls.key default | | test.tls.server-name default | | test.tls.skip-host-verify false default | ----------------------------------------------------------------- ` v := setConfig(t) actual := runPrintConfigCommand(v, t, true) assert.Equal(t, expected, actual) } func TestPrintConfigCommand(t *testing.T) { expected := `----------------------------------------------------------------- | Configuration Option Name Value Source | ----------------------------------------------------------------- | multi-tenancy.enabled false default | | multi-tenancy.header x-scope-orgid user-assigned | | test-plugin.binary noop-test-plugin user-assigned | | test-plugin.configuration-file config.json user-assigned | | test-plugin.log-level debug user-assigned | | test-remote.connection-timeout 5s default | | test.tls.enabled false default | | test.tls.skip-host-verify false default | ----------------------------------------------------------------- ` v := setConfig(t) actual := runPrintConfigCommand(v, t, false) assert.Equal(t, expected, actual) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: cmd/internal/status/command.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package status import ( "context" "flag" "fmt" "io" "net/http" "strings" "time" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/jaegertracing/jaeger/ports" ) const statusHTTPHostPort = "status.http.host-port" // Command for check component status. func Command(v *viper.Viper, adminPort int) *cobra.Command { c := &cobra.Command{ Use: "status", Short: "Print the status.", Long: `Print Jaeger component status information, exit non-zero on any error.`, RunE: func(_ *cobra.Command, _ /* args */ []string) error { url := convert(v.GetString(statusHTTPHostPort)) ctx, cx := context.WithTimeout(context.Background(), time.Second) defer cx() req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) resp, err := http.DefaultClient.Do(req) //nolint:gosec // G704 - URL from internal config if err != nil { return err } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) fmt.Println(string(body)) if resp.StatusCode != http.StatusOK { return fmt.Errorf("abnormal value of http status code: %v", resp.StatusCode) } return nil }, } c.Flags().AddGoFlagSet(flags(&flag.FlagSet{}, adminPort)) v.BindPFlags(c.Flags()) return c } func flags(flagSet *flag.FlagSet, adminPort int) *flag.FlagSet { adminPortStr := ports.PortToHostPort(adminPort) flagSet.String(statusHTTPHostPort, adminPortStr, fmt.Sprintf( "The host:port (e.g. 127.0.0.1%s or %s) for the health check", adminPortStr, adminPortStr)) return flagSet } func convert(httpHostPort string) string { if strings.HasPrefix(httpHostPort, ":") { return "http://127.0.0.1" + httpHostPort } return "http://" + httpHostPort } ================================================ FILE: cmd/internal/status/command_test.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package status import ( "net/http" "net/http/httptest" "strings" "testing" "github.com/spf13/viper" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/testutils" ) func readyHandler(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("{\"status\":\"Server available\"}")) } func unavailableHandler(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusServiceUnavailable) w.Write([]byte("{\"status\":\"Server not available\"}")) } func TestReady(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(readyHandler)) defer ts.Close() v := viper.New() cmd := Command(v, 80) cmd.ParseFlags([]string{"--status.http.host-port=" + strings.TrimPrefix(ts.URL, "http://")}) err := cmd.Execute() require.NoError(t, err) } func TestOnlyPortConfig(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(readyHandler)) defer ts.Close() v := viper.New() cmd := Command(v, 80) cmd.ParseFlags([]string{"--status.http.host-port=:" + strings.Split(ts.URL, ":")[len(strings.Split(ts.URL, ":"))-1]}) err := cmd.Execute() require.NoError(t, err) } func TestUnready(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(unavailableHandler)) defer ts.Close() v := viper.New() cmd := Command(v, 80) cmd.ParseFlags([]string{"--status.http.host-port=" + strings.TrimPrefix(ts.URL, "http://")}) err := cmd.Execute() require.Error(t, err) } func TestNoService(t *testing.T) { v := viper.New() cmd := Command(v, 12345) err := cmd.Execute() require.Error(t, err) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: cmd/internal/storageconfig/config.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package storageconfig import ( "errors" "fmt" "time" "go.opentelemetry.io/collector/confmap" "github.com/jaegertracing/jaeger/internal/config/promcfg" cascfg "github.com/jaegertracing/jaeger/internal/storage/cassandra/config" escfg "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" "github.com/jaegertracing/jaeger/internal/storage/metricstore/prometheus" "github.com/jaegertracing/jaeger/internal/storage/v1/badger" "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra" es "github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse" "github.com/jaegertracing/jaeger/internal/storage/v2/grpc" "github.com/jaegertracing/jaeger/internal/storage/v2/memory" ) var ( _ confmap.Unmarshaler = (*TraceBackend)(nil) _ confmap.Unmarshaler = (*MetricBackend)(nil) ) // Config contains configuration(s) for Jaeger trace storage. type Config struct { TraceBackends map[string]TraceBackend `mapstructure:"backends"` MetricBackends map[string]MetricBackend `mapstructure:"metric_backends"` } // TraceBackend contains configuration for a single trace storage backend. type TraceBackend struct { Memory *memory.Configuration `mapstructure:"memory"` Badger *badger.Config `mapstructure:"badger"` GRPC *grpc.Config `mapstructure:"grpc"` Cassandra *cassandra.Options `mapstructure:"cassandra"` Elasticsearch *escfg.Configuration `mapstructure:"elasticsearch"` Opensearch *escfg.Configuration `mapstructure:"opensearch"` ClickHouse *clickhouse.Configuration `mapstructure:"clickhouse"` } // MetricBackend contains configuration for a single metric storage backend. type MetricBackend struct { Prometheus *PrometheusConfiguration `mapstructure:"prometheus"` Elasticsearch *escfg.Configuration `mapstructure:"elasticsearch"` Opensearch *escfg.Configuration `mapstructure:"opensearch"` } type PrometheusConfiguration struct { Configuration promcfg.Configuration `mapstructure:",squash"` Authentication escfg.Authentication `mapstructure:"auth"` } // Unmarshal implements confmap.Unmarshaler. This allows us to provide // defaults for different configs. func (cfg *TraceBackend) Unmarshal(conf *confmap.Conf) error { // apply defaults if conf.IsSet("memory") { cfg.Memory = &memory.Configuration{ MaxTraces: 1_000_000, } } if conf.IsSet("badger") { v := badger.DefaultConfig() cfg.Badger = v } if conf.IsSet("grpc") { v := grpc.DefaultConfig() cfg.GRPC = &v } if conf.IsSet("cassandra") { cfg.Cassandra = &cassandra.Options{ Configuration: cascfg.DefaultConfiguration(), SpanStoreWriteCacheTTL: 12 * time.Hour, Index: cassandra.IndexConfig{ Tags: true, ProcessTags: true, Logs: true, }, ArchiveEnabled: false, } } if conf.IsSet("elasticsearch") { v := es.DefaultConfig() cfg.Elasticsearch = &v } if conf.IsSet("opensearch") { v := es.DefaultConfig() cfg.Opensearch = &v } if conf.IsSet("clickhouse") { cfg.ClickHouse = &clickhouse.Configuration{} } return conf.Unmarshal(cfg) } func (cfg *TraceBackend) Validate() error { var backends []string if cfg.Memory != nil { backends = append(backends, "memory") } if cfg.Badger != nil { backends = append(backends, "badger") } if cfg.GRPC != nil { backends = append(backends, "grpc") } if cfg.Cassandra != nil { backends = append(backends, "cassandra") } if cfg.Elasticsearch != nil { backends = append(backends, "elasticsearch") } if cfg.Opensearch != nil { backends = append(backends, "opensearch") } if cfg.ClickHouse != nil { backends = append(backends, "clickhouse") } if len(backends) == 0 { return errors.New("empty configuration") } if len(backends) > 1 { return fmt.Errorf("multiple backend types found for trace storage: %v", backends) } return nil } // Unmarshal implements confmap.Unmarshaler for MetricBackend. func (cfg *MetricBackend) Unmarshal(conf *confmap.Conf) error { // apply defaults if conf.IsSet("prometheus") { v := prometheus.DefaultConfig() cfg.Prometheus = &PrometheusConfiguration{ Configuration: v, } } if conf.IsSet("elasticsearch") { v := es.DefaultConfig() cfg.Elasticsearch = &v } if conf.IsSet("opensearch") { v := es.DefaultConfig() cfg.Opensearch = &v } return conf.Unmarshal(cfg) } func (cfg *MetricBackend) Validate() error { var backends []string if cfg.Prometheus != nil { backends = append(backends, "prometheus") } if cfg.Elasticsearch != nil { backends = append(backends, "elasticsearch") } if cfg.Opensearch != nil { backends = append(backends, "opensearch") } if len(backends) == 0 { return errors.New("empty configuration") } if len(backends) > 1 { return fmt.Errorf("multiple backend types found for metric storage: %v", backends) } return nil } // Validate validates the storage configuration. func (c *Config) Validate() error { if len(c.TraceBackends) == 0 { return errors.New("at least one storage backend is required") } for name, b := range c.TraceBackends { if err := b.Validate(); err != nil { return fmt.Errorf("trace storage '%s': %w", name, err) } } for name, b := range c.MetricBackends { if err := b.Validate(); err != nil { return fmt.Errorf("metric storage '%s': %w", name, err) } } return nil } ================================================ FILE: cmd/internal/storageconfig/config_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package storageconfig import ( "fmt" "reflect" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/confmap" escfg "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" "github.com/jaegertracing/jaeger/internal/storage/v1/badger" "github.com/jaegertracing/jaeger/internal/storage/v2/memory" ) func TestConfigValidate(t *testing.T) { tests := []struct { name string config Config expectError bool errorMsg string }{ { name: "valid config with one backend", config: Config{ TraceBackends: map[string]TraceBackend{ "memory": { Memory: &memory.Configuration{MaxTraces: 10000}, }, }, }, expectError: false, }, { name: "valid config with multiple backends", config: Config{ TraceBackends: map[string]TraceBackend{ "memory1": { Memory: &memory.Configuration{MaxTraces: 10000}, }, "memory2": { Memory: &memory.Configuration{MaxTraces: 20000}, }, }, }, expectError: false, }, { name: "no backends", config: Config{ TraceBackends: map[string]TraceBackend{}, }, expectError: true, errorMsg: "at least one storage backend is required", }, { name: "empty backend configuration", config: Config{ TraceBackends: map[string]TraceBackend{ "empty": {}, }, }, expectError: true, errorMsg: "trace storage 'empty': empty configuration", }, { name: "valid metric backend", config: Config{ TraceBackends: map[string]TraceBackend{ "memory": {Memory: &memory.Configuration{}}, }, MetricBackends: map[string]MetricBackend{ "prometheus": {Prometheus: &PrometheusConfiguration{}}, }, }, expectError: false, }, { name: "invalid trace backend", config: Config{ TraceBackends: map[string]TraceBackend{ "invalid": { Memory: &memory.Configuration{}, Badger: &badger.Config{}, }, }, }, expectError: true, errorMsg: "trace storage 'invalid': multiple backend types found", }, { name: "invalid metric backend", config: Config{ TraceBackends: map[string]TraceBackend{ "memory": {Memory: &memory.Configuration{}}, }, MetricBackends: map[string]MetricBackend{ "invalid": { Prometheus: &PrometheusConfiguration{}, Elasticsearch: &escfg.Configuration{}, }, }, }, expectError: true, errorMsg: "metric storage 'invalid': multiple backend types found", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.config.Validate() if tt.expectError { require.Error(t, err) assert.Contains(t, err.Error(), tt.errorMsg) } else { require.NoError(t, err) } }) } } func TestTraceBackendUnmarshal(t *testing.T) { tests := []struct { name string configMap map[string]any expectError bool validateFunc func(*testing.T, *TraceBackend) }{ { name: "memory backend with defaults", configMap: map[string]any{ "memory": map[string]any{}, }, expectError: false, validateFunc: func(t *testing.T, tb *TraceBackend) { require.NotNil(t, tb.Memory) assert.Equal(t, 1_000_000, tb.Memory.MaxTraces) }, }, { name: "memory backend with custom value", configMap: map[string]any{ "memory": map[string]any{ "max_traces": 50000, }, }, expectError: false, validateFunc: func(t *testing.T, tb *TraceBackend) { require.NotNil(t, tb.Memory) assert.Equal(t, 50000, tb.Memory.MaxTraces) }, }, { name: "badger backend with defaults", configMap: map[string]any{ "badger": map[string]any{ "ephemeral": true, }, }, expectError: false, validateFunc: func(t *testing.T, tb *TraceBackend) { require.NotNil(t, tb.Badger) assert.True(t, tb.Badger.Ephemeral) }, }, { name: "grpc backend with defaults", configMap: map[string]any{ "grpc": map[string]any{ "endpoint": "localhost:17271", }, }, expectError: false, validateFunc: func(t *testing.T, tb *TraceBackend) { require.NotNil(t, tb.GRPC) assert.Equal(t, "localhost:17271", tb.GRPC.ClientConfig.Endpoint) }, }, { name: "cassandra backend with defaults", configMap: map[string]any{ "cassandra": map[string]any{}, }, expectError: false, validateFunc: func(t *testing.T, tb *TraceBackend) { require.NotNil(t, tb.Cassandra) assert.True(t, tb.Cassandra.Index.Tags) assert.True(t, tb.Cassandra.Index.ProcessTags) assert.True(t, tb.Cassandra.Index.Logs) }, }, { name: "elasticsearch backend with defaults", configMap: map[string]any{ "elasticsearch": map[string]any{}, }, expectError: false, validateFunc: func(t *testing.T, tb *TraceBackend) { require.NotNil(t, tb.Elasticsearch) }, }, { name: "opensearch backend with defaults", configMap: map[string]any{ "opensearch": map[string]any{}, }, expectError: false, validateFunc: func(t *testing.T, tb *TraceBackend) { require.NotNil(t, tb.Opensearch) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { conf := confmap.NewFromStringMap(tt.configMap) var tb TraceBackend err := tb.Unmarshal(conf) if tt.expectError { require.Error(t, err) } else { require.NoError(t, err) if tt.validateFunc != nil { tt.validateFunc(t, &tb) } } }) } } func TestMetricBackendUnmarshal(t *testing.T) { tests := []struct { name string configMap map[string]any expectError bool validateFunc func(*testing.T, *MetricBackend) }{ { name: "prometheus backend with defaults", configMap: map[string]any{ "prometheus": map[string]any{}, }, expectError: false, validateFunc: func(t *testing.T, mb *MetricBackend) { require.NotNil(t, mb.Prometheus) }, }, { name: "elasticsearch backend", configMap: map[string]any{ "elasticsearch": map[string]any{}, }, expectError: false, validateFunc: func(t *testing.T, mb *MetricBackend) { require.NotNil(t, mb.Elasticsearch) }, }, { name: "opensearch backend", configMap: map[string]any{ "opensearch": map[string]any{}, }, expectError: false, validateFunc: func(t *testing.T, mb *MetricBackend) { require.NotNil(t, mb.Opensearch) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { conf := confmap.NewFromStringMap(tt.configMap) var mb MetricBackend err := mb.Unmarshal(conf) if tt.expectError { require.Error(t, err) } else { require.NoError(t, err) if tt.validateFunc != nil { tt.validateFunc(t, &mb) } } }) } } func getStorageKeys(t reflect.Type) []string { var keys []string for field := range t.Fields() { tag := field.Tag.Get("mapstructure") if tag != "" && tag != ",squash" { keys = append(keys, tag) } } return keys } func TestTraceBackendExclusive(t *testing.T) { keys := getStorageKeys(reflect.TypeFor[TraceBackend]()) for i := range keys { for j := i + 1; j < len(keys); j++ { key1 := keys[i] key2 := keys[j] t.Run(fmt.Sprintf("%s+%s", key1, key2), func(t *testing.T) { conf := confmap.NewFromStringMap(map[string]any{ key1: map[string]any{}, key2: map[string]any{}, }) var tb TraceBackend err := tb.Unmarshal(conf) require.NoError(t, err) err = tb.Validate() require.Error(t, err) assert.Contains(t, err.Error(), "multiple backend types found") }) } } } func TestMetricBackendExclusive(t *testing.T) { keys := getStorageKeys(reflect.TypeFor[MetricBackend]()) for i := range keys { for j := i + 1; j < len(keys); j++ { key1 := keys[i] key2 := keys[j] t.Run(fmt.Sprintf("%s+%s", key1, key2), func(t *testing.T) { conf := confmap.NewFromStringMap(map[string]any{ key1: map[string]any{}, key2: map[string]any{}, }) var mb MetricBackend err := mb.Unmarshal(conf) require.NoError(t, err) err = mb.Validate() require.Error(t, err) assert.Contains(t, err.Error(), "multiple backend types found") }) } } } ================================================ FILE: cmd/internal/storageconfig/factory.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package storageconfig import ( "context" "errors" "fmt" "go.opentelemetry.io/collector/extension/extensionauth" "github.com/jaegertracing/jaeger/internal/metrics" escfg "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" "github.com/jaegertracing/jaeger/internal/storage/v2/badger" "github.com/jaegertracing/jaeger/internal/storage/v2/cassandra" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse" es "github.com/jaegertracing/jaeger/internal/storage/v2/elasticsearch" "github.com/jaegertracing/jaeger/internal/storage/v2/grpc" "github.com/jaegertracing/jaeger/internal/storage/v2/memory" "github.com/jaegertracing/jaeger/internal/telemetry" ) // AuthResolver is a function type that resolves an authenticator by name. // This allows the jaegerstorage extension to provide its authenticator resolution logic. type AuthResolver func(authCfg escfg.Authentication, backendType, backendName string) (extensionauth.HTTPClient, error) // CreateTraceStorageFactory creates a trace storage factory from the backend configuration. // This is extracted from jaegerstorage extension to be shared between jaeger and remote-storage. // authResolver is optional; if nil, no authentication will be configured for ES/OS backends. func CreateTraceStorageFactory( ctx context.Context, name string, backend TraceBackend, telset telemetry.Settings, authResolver AuthResolver, ) (tracestore.Factory, error) { telset.Logger.Sugar().Infof("Initializing storage '%s'", name) // Create scoped metrics factory telset.Metrics = telset.Metrics.Namespace(metrics.NSOptions{ Name: "storage", Tags: map[string]string{ "name": name, "role": "tracestore", }, }) var factory tracestore.Factory var err error switch { case backend.Memory != nil: factory, err = memory.NewFactory(*backend.Memory, telset) case backend.Badger != nil: factory, err = badger.NewFactory(*backend.Badger, telset) case backend.GRPC != nil: factory, err = grpc.NewFactory(ctx, *backend.GRPC, telset) case backend.Cassandra != nil: factory, err = cassandra.NewFactory(*backend.Cassandra, telset) case backend.Elasticsearch != nil: var httpAuth extensionauth.HTTPClient if authResolver != nil { httpAuth, err = authResolver(backend.Elasticsearch.Authentication, "elasticsearch", name) if err != nil { return nil, err } } factory, err = es.NewFactory(ctx, *backend.Elasticsearch, telset, httpAuth) case backend.Opensearch != nil: var httpAuth extensionauth.HTTPClient if authResolver != nil { httpAuth, err = authResolver(backend.Opensearch.Authentication, "opensearch", name) if err != nil { return nil, err } } factory, err = es.NewFactory(ctx, *backend.Opensearch, telset, httpAuth) case backend.ClickHouse != nil: factory, err = clickhouse.NewFactory(ctx, *backend.ClickHouse, telset) default: err = errors.New("empty configuration") } if err != nil { return nil, fmt.Errorf("failed to initialize storage '%s': %w", name, err) } return factory, nil } ================================================ FILE: cmd/internal/storageconfig/factory_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package storageconfig import ( "context" "encoding/json" "errors" "io" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/config/configgrpc" "go.opentelemetry.io/collector/extension/extensionauth" escfg "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" "github.com/jaegertracing/jaeger/internal/storage/v1/badger" "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/clickhousetest" "github.com/jaegertracing/jaeger/internal/storage/v2/grpc" "github.com/jaegertracing/jaeger/internal/storage/v2/memory" "github.com/jaegertracing/jaeger/internal/telemetry" ) func getTelemetrySettings() telemetry.Settings { return telemetry.NoopSettings() } func setupMockServer(t *testing.T, response []byte, statusCode int) *httptest.Server { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) w.Write(response) })) require.NotNil(t, mockServer) t.Cleanup(mockServer.Close) return mockServer } func getVersionResponse(t *testing.T) []byte { versionResponse, e := json.Marshal(map[string]any{ "Version": map[string]any{ "Number": "7", }, }) require.NoError(t, e) return versionResponse } func TestCreateTraceStorageFactory_Memory(t *testing.T) { backend := TraceBackend{ Memory: &memory.Configuration{ MaxTraces: 10000, }, } factory, err := CreateTraceStorageFactory( context.Background(), "memory-test", backend, getTelemetrySettings(), nil, ) require.NoError(t, err) require.NotNil(t, factory) t.Cleanup(func() { if closer, ok := factory.(io.Closer); ok { require.NoError(t, closer.Close()) } }) } func TestCreateTraceStorageFactory_Badger(t *testing.T) { backend := TraceBackend{ Badger: &badger.Config{ Ephemeral: true, MaintenanceInterval: 5, MetricsUpdateInterval: 10, }, } factory, err := CreateTraceStorageFactory( context.Background(), "badger-test", backend, getTelemetrySettings(), nil, ) require.NoError(t, err) require.NotNil(t, factory) t.Cleanup(func() { if closer, ok := factory.(io.Closer); ok { require.NoError(t, closer.Close()) } }) } func TestCreateTraceStorageFactory_GRPC(t *testing.T) { backend := TraceBackend{ GRPC: &grpc.Config{ ClientConfig: configgrpc.ClientConfig{ Endpoint: "localhost:12345", }, }, } factory, err := CreateTraceStorageFactory( context.Background(), "grpc-test", backend, getTelemetrySettings(), nil, ) require.NoError(t, err) require.NotNil(t, factory) t.Cleanup(func() { if closer, ok := factory.(io.Closer); ok { require.NoError(t, closer.Close()) } }) } func TestCreateTraceStorageFactory_Cassandra(t *testing.T) { backend := TraceBackend{ Cassandra: &cassandra.Options{}, } _, err := CreateTraceStorageFactory( context.Background(), "cassandra-test", backend, getTelemetrySettings(), nil, ) // Cassandra will fail without proper servers config, but we're testing the factory creation path require.Error(t, err) require.Contains(t, err.Error(), "failed to initialize storage 'cassandra-test'") } func TestCreateTraceStorageFactory_Elasticsearch(t *testing.T) { server := setupMockServer(t, getVersionResponse(t), http.StatusOK) backend := TraceBackend{ Elasticsearch: &escfg.Configuration{ Servers: []string{server.URL}, LogLevel: "error", }, } factory, err := CreateTraceStorageFactory( context.Background(), "es-test", backend, getTelemetrySettings(), nil, ) require.NoError(t, err) require.NotNil(t, factory) t.Cleanup(func() { if closer, ok := factory.(io.Closer); ok { require.NoError(t, closer.Close()) } }) } func TestCreateTraceStorageFactory_ElasticsearchWithAuthResolver(t *testing.T) { server := setupMockServer(t, getVersionResponse(t), http.StatusOK) backend := TraceBackend{ Elasticsearch: &escfg.Configuration{ Servers: []string{server.URL}, LogLevel: "error", }, } authResolver := func(_ escfg.Authentication, _, _ string) (extensionauth.HTTPClient, error) { return nil, nil // No auth needed for this test } factory, err := CreateTraceStorageFactory( context.Background(), "es-test", backend, getTelemetrySettings(), authResolver, ) require.NoError(t, err) require.NotNil(t, factory) t.Cleanup(func() { if closer, ok := factory.(io.Closer); ok { require.NoError(t, closer.Close()) } }) } func TestCreateTraceStorageFactory_ElasticsearchAuthResolverError(t *testing.T) { server := setupMockServer(t, getVersionResponse(t), http.StatusOK) backend := TraceBackend{ Elasticsearch: &escfg.Configuration{ Servers: []string{server.URL}, LogLevel: "error", }, } authResolver := func(_ escfg.Authentication, _, _ string) (extensionauth.HTTPClient, error) { return nil, errors.New("auth error") } _, err := CreateTraceStorageFactory( context.Background(), "es-test", backend, getTelemetrySettings(), authResolver, ) require.Error(t, err) require.Contains(t, err.Error(), "auth error") } func TestCreateTraceStorageFactory_Opensearch(t *testing.T) { server := setupMockServer(t, getVersionResponse(t), http.StatusOK) backend := TraceBackend{ Opensearch: &escfg.Configuration{ Servers: []string{server.URL}, LogLevel: "error", }, } factory, err := CreateTraceStorageFactory( context.Background(), "os-test", backend, getTelemetrySettings(), nil, ) require.NoError(t, err) require.NotNil(t, factory) t.Cleanup(func() { if closer, ok := factory.(io.Closer); ok { require.NoError(t, closer.Close()) } }) } func TestCreateTraceStorageFactory_OpensearchWithAuthResolver(t *testing.T) { server := setupMockServer(t, getVersionResponse(t), http.StatusOK) backend := TraceBackend{ Opensearch: &escfg.Configuration{ Servers: []string{server.URL}, LogLevel: "error", }, } authResolver := func(_ escfg.Authentication, _, _ string) (extensionauth.HTTPClient, error) { return nil, nil // No auth needed for this test } factory, err := CreateTraceStorageFactory( context.Background(), "os-test", backend, getTelemetrySettings(), authResolver, ) require.NoError(t, err) require.NotNil(t, factory) t.Cleanup(func() { if closer, ok := factory.(io.Closer); ok { require.NoError(t, closer.Close()) } }) } func TestCreateTraceStorageFactory_OpensearchAuthResolverError(t *testing.T) { server := setupMockServer(t, getVersionResponse(t), http.StatusOK) backend := TraceBackend{ Opensearch: &escfg.Configuration{ Servers: []string{server.URL}, LogLevel: "error", }, } authResolver := func(_ escfg.Authentication, _, _ string) (extensionauth.HTTPClient, error) { return nil, errors.New("auth error for opensearch") } _, err := CreateTraceStorageFactory( context.Background(), "os-test", backend, getTelemetrySettings(), authResolver, ) require.Error(t, err) require.Contains(t, err.Error(), "auth error for opensearch") } func TestCreateTraceStorageFactory_ClickHouse(t *testing.T) { testServer := clickhousetest.NewServer(clickhousetest.FailureConfig{}) t.Cleanup(testServer.Close) backend := TraceBackend{ ClickHouse: &clickhouse.Configuration{ Protocol: "http", Addresses: []string{ testServer.Listener.Addr().String(), }, }, } factory, err := CreateTraceStorageFactory( context.Background(), "clickhouse-test", backend, getTelemetrySettings(), nil, ) require.NoError(t, err) require.NotNil(t, factory) t.Cleanup(func() { if closer, ok := factory.(io.Closer); ok { require.NoError(t, closer.Close()) } }) } func TestCreateTraceStorageFactory_ClickHouseError(t *testing.T) { backend := TraceBackend{ ClickHouse: &clickhouse.Configuration{}, } _, err := CreateTraceStorageFactory( context.Background(), "clickhouse-test", backend, getTelemetrySettings(), nil, ) // ClickHouse will fail without proper config, but we're testing the factory creation path require.Error(t, err) require.Contains(t, err.Error(), "failed to initialize storage 'clickhouse-test'") } func TestCreateTraceStorageFactory_EmptyBackend(t *testing.T) { backend := TraceBackend{} _, err := CreateTraceStorageFactory( context.Background(), "empty-test", backend, getTelemetrySettings(), nil, ) require.Error(t, err) require.Contains(t, err.Error(), "failed to initialize storage 'empty-test'") require.Contains(t, err.Error(), "empty configuration") } ================================================ FILE: cmd/internal/storageconfig/package_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package storageconfig import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: cmd/remote-storage/Dockerfile ================================================ # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 ARG base_image ARG debug_image ARG SVC=remote-storage FROM $base_image AS release ARG TARGETARCH ARG USER_UID=10001 COPY remote-storage-linux-$TARGETARCH /go/bin/remote-storage-linux EXPOSE 16686/tcp ENTRYPOINT ["/go/bin/remote-storage-linux"] USER ${USER_UID} FROM $debug_image AS debug ARG TARGETARCH=amd64 ARG USER_UID=10001 COPY remote-storage-debug-linux-$TARGETARCH /go/bin/remote-storage-linux EXPOSE 12345/tcp 16686/tcp ENTRYPOINT ["/go/bin/dlv", "exec", "/go/bin/remote-storage-linux", "--headless", "--listen=:12345", "--api-version=2", "--accept-multiclient", "--log", "--"] USER ${USER_UID} ================================================ FILE: cmd/remote-storage/README.md ================================================ # Jaeger Remote Storage The `jaeger-remote-storage` binary allows sharing single-node storage implementations like memory or Badger over gRPC. It implements the Jaeger Remote Storage gRPC API, enabling Jaeger components to use these storage backends remotely. ## Configuration ### YAML Configuration Configure remote-storage using a YAML configuration file with the `--config-file` flag: ```bash ./jaeger-remote-storage --config-file config.yaml ``` #### Configuration File Structure ```yaml # Server configuration grpc: endpoint: :17271 # gRPC endpoint for remote storage API # Storage configuration storage: backends: default-storage: memory: max_traces: 100000 # Multi-tenancy configuration (optional) multi_tenancy: enabled: false ``` #### Storage Backends The storage configuration follows the same format as the `jaeger_storage` extension in Jaeger v2. All official backends are supported. ##### Memory Storage ```yaml storage: backends: memory-storage: memory: max_traces: 100000 ``` ##### Badger Storage ```yaml storage: backends: badger-storage: badger: directories: keys: /tmp/jaeger/badger/keys values: /tmp/jaeger/badger/values ephemeral: false ttl: spans: 168h # 7 days ``` ##### gRPC Storage ```yaml storage: backends: grpc-storage: grpc: endpoint: remote-server:17271 tls: insecure: true ``` See example configuration files: - `config.yaml` - Memory storage example - `config-badger.yaml` - Badger storage example ## Usage ### Start with Memory Backend ```bash ./jaeger-remote-storage --config-file config.yaml ``` ### Start with Badger Backend ```bash ./jaeger-remote-storage --config-file config-badger.yaml ``` ### Multi-tenancy To enable multi-tenancy: ```yaml grpc: host-port: :17271 multi_tenancy: enabled: true header: x-tenant tenants: - tenant1 - tenant2 storage: backends: default-storage: memory: max_traces: 100000 ``` ## Integration with Jaeger To use remote-storage with Jaeger components, configure them to use the gRPC storage backend: ```yaml extensions: jaeger_storage: backends: some-storage: grpc: endpoint: localhost:17271 tls: insecure: true ``` For more details, see the [gRPC storage documentation](../../internal/storage/v2/grpc/README.md). ================================================ FILE: cmd/remote-storage/app/config.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package app import ( "fmt" "github.com/spf13/viper" "go.opentelemetry.io/collector/config/configgrpc" "go.opentelemetry.io/collector/config/confignet" "github.com/jaegertracing/jaeger/cmd/internal/storageconfig" "github.com/jaegertracing/jaeger/internal/storage/v2/memory" "github.com/jaegertracing/jaeger/internal/tenancy" ) // Config represents the configuration for remote-storage service. type Config struct { GRPC configgrpc.ServerConfig `mapstructure:"grpc"` Tenancy tenancy.Options `mapstructure:"multi_tenancy"` // This configuration is the same as of the main `jaeger` binary, // but only one backend should be defined. Storage storageconfig.Config `mapstructure:"storage"` } // LoadConfigFromViper loads the configuration from Viper. func LoadConfigFromViper(v *viper.Viper) (*Config, error) { cfg := &Config{} // Unmarshal the entire configuration if err := v.Unmarshal(cfg); err != nil { return nil, fmt.Errorf("failed to unmarshal configuration: %w", err) } // Validate storage configuration if err := cfg.Validate(); err != nil { return nil, fmt.Errorf("invalid configuration: %w", err) } return cfg, nil } // Validate validates the configuration. func (c *Config) Validate() error { // Validate storage configuration if err := c.Storage.Validate(); err != nil { return err } // Ensure only one backend is defined for remote-storage if len(c.Storage.TraceBackends) > 1 { return fmt.Errorf("remote-storage only supports a single storage backend, but %d were configured", len(c.Storage.TraceBackends)) } return nil } // GetStorageName returns the name of the first configured storage backend. // This is used as the default storage when not otherwise specified. func (c *Config) GetStorageName() string { for name := range c.Storage.TraceBackends { return name } return "" } // DefaultConfig returns a default configuration with memory storage. // This is used when no configuration file is provided. func DefaultConfig() *Config { return &Config{ GRPC: configgrpc.ServerConfig{ NetAddr: confignet.AddrConfig{ Endpoint: ":17271", Transport: confignet.TransportTypeTCP, }, }, Storage: storageconfig.Config{ TraceBackends: map[string]storageconfig.TraceBackend{ "memory": { Memory: &memory.Configuration{ MaxTraces: 1_000_000, }, }, }, }, } } ================================================ FILE: cmd/remote-storage/app/config_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package app import ( "os" "path/filepath" "testing" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestLoadConfigFromViper(t *testing.T) { tests := []struct { name string yamlConfig string expectError string validate func(*testing.T, *Config) }{ { name: "valid memory backend", yamlConfig: ` grpc: endpoint: :17271 storage: backends: default-storage: memory: max_traces: 50000 `, validate: func(t *testing.T, cfg *Config) { assert.Equal(t, ":17271", cfg.GRPC.NetAddr.Endpoint) assert.Len(t, cfg.Storage.TraceBackends, 1) assert.NotNil(t, cfg.Storage.TraceBackends["default-storage"].Memory) assert.Equal(t, 50000, cfg.Storage.TraceBackends["default-storage"].Memory.MaxTraces) assert.Equal(t, "default-storage", cfg.GetStorageName()) }, }, { name: "valid memory backend", yamlConfig: ` grpc: endpoint: :17271 storage: backends: default-storage: memory: max_traces: NOT-A-NUMBER `, expectError: "memory.max_traces", }, { name: "valid badger backend", yamlConfig: ` grpc: endpoint: :17272 storage: backends: badger-storage: badger: directories: keys: /tmp/test-keys values: /tmp/test-values ephemeral: true `, validate: func(t *testing.T, cfg *Config) { assert.Equal(t, ":17272", cfg.GRPC.NetAddr.Endpoint) assert.Len(t, cfg.Storage.TraceBackends, 1) assert.NotNil(t, cfg.Storage.TraceBackends["badger-storage"].Badger) assert.Equal(t, "badger-storage", cfg.GetStorageName()) }, }, { name: "missing storage backend", yamlConfig: ` grpc: endpoint: :17271 storage: backends: {} `, expectError: "at least one storage backend is required", }, { name: "empty backend configuration", yamlConfig: ` grpc: endpoint: :17271 storage: backends: empty-storage: {} `, expectError: "at least one storage backend is required", }, { name: "multiple backends should fail", yamlConfig: ` grpc: endpoint: :17271 storage: backends: memory-storage: memory: max_traces: 10000 another-storage: memory: max_traces: 20000 `, expectError: "remote-storage only supports a single storage backend", }, { name: "with multi-tenancy enabled", yamlConfig: ` grpc: endpoint: :17271 multi_tenancy: enabled: true header: x-tenant tenants: - tenant1 - tenant2 storage: backends: default-storage: memory: max_traces: 10000 `, validate: func(t *testing.T, cfg *Config) { assert.True(t, cfg.Tenancy.Enabled) assert.Equal(t, "x-tenant", cfg.Tenancy.Header) assert.Equal(t, []string{"tenant1", "tenant2"}, cfg.Tenancy.Tenants) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create temporary config file tmpDir := t.TempDir() configFile := filepath.Join(tmpDir, "config.yaml") err := os.WriteFile(configFile, []byte(tt.yamlConfig), 0o600) require.NoError(t, err) // Load config with Viper v := viper.New() v.SetConfigFile(configFile) err = v.ReadInConfig() require.NoError(t, err) // Load config from Viper cfg, err := LoadConfigFromViper(v) if tt.expectError != "" { require.Contains(t, err.Error(), tt.expectError) } else { require.NoError(t, err) require.NotNil(t, cfg) } if tt.validate != nil { tt.validate(t, cfg) } }) } } func TestDefaultConfig(t *testing.T) { cfg := DefaultConfig() require.NotNil(t, cfg) require.Equal(t, ":17271", cfg.GRPC.NetAddr.Endpoint) require.Len(t, cfg.Storage.TraceBackends, 1) require.NotNil(t, cfg.Storage.TraceBackends["memory"].Memory) require.Equal(t, "memory", cfg.GetStorageName()) } ================================================ FILE: cmd/remote-storage/app/package_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package app import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: cmd/remote-storage/app/server.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package app import ( "context" "fmt" "net" "sync" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/component/componentstatus" "go.opentelemetry.io/collector/config/configgrpc" "go.opentelemetry.io/collector/config/confignet" "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/health" "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/reflection" "github.com/jaegertracing/jaeger/internal/auth/bearertoken" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" grpcstorage "github.com/jaegertracing/jaeger/internal/storage/v2/grpc" "github.com/jaegertracing/jaeger/internal/telemetry" "github.com/jaegertracing/jaeger/internal/tenancy" ) // Server runs a gRPC server type Server struct { grpcCfg configgrpc.ServerConfig grpcConn net.Listener grpcServer *grpc.Server stopped sync.WaitGroup telset telemetry.Settings } // NewServer creates and initializes Server. func NewServer( ctx context.Context, grpcCfg configgrpc.ServerConfig, ts tracestore.Factory, ds depstore.Factory, tm *tenancy.Manager, telset telemetry.Settings, ) (*Server, error) { reader, err := ts.CreateTraceReader() if err != nil { return nil, err } writer, err := ts.CreateTraceWriter() if err != nil { return nil, err } depReader, err := ds.CreateDependencyReader() if err != nil { return nil, err } // This is required because we are using the config to start the server. // If the config is created manually (e.g. in tests), the transport might not be set. grpcCfg.NetAddr.Transport = confignet.TransportTypeTCP v2Handler := grpcstorage.NewHandler(reader, writer, depReader) grpcServer, err := createGRPCServer(ctx, grpcCfg, tm, v2Handler, telset) if err != nil { return nil, err } return &Server{ grpcCfg: grpcCfg, grpcServer: grpcServer, telset: telset, }, nil } func createGRPCServer( ctx context.Context, cfg configgrpc.ServerConfig, tm *tenancy.Manager, v2Handler *grpcstorage.Handler, telset telemetry.Settings, ) (*grpc.Server, error) { unaryInterceptors := []grpc.UnaryServerInterceptor{ bearertoken.NewUnaryServerInterceptor(), } streamInterceptors := []grpc.StreamServerInterceptor{ bearertoken.NewStreamServerInterceptor(), } //nolint:contextcheck // The context is handled by the interceptors if tm.Enabled { unaryInterceptors = append(unaryInterceptors, tenancy.NewGuardingUnaryInterceptor(tm)) streamInterceptors = append(streamInterceptors, tenancy.NewGuardingStreamInterceptor(tm)) } cfg.NetAddr.Transport = confignet.TransportTypeTCP var extensions map[component.ID]component.Component if telset.Host != nil { extensions = telset.Host.GetExtensions() } server, err := cfg.ToServer(ctx, extensions, telset.ToOtelComponent(), configgrpc.WithGrpcServerOption(grpc.ChainUnaryInterceptor(unaryInterceptors...)), configgrpc.WithGrpcServerOption(grpc.ChainStreamInterceptor(streamInterceptors...)), ) if err != nil { return nil, fmt.Errorf("failed to create gRPC server: %w", err) } healthServer := health.NewServer() reflection.Register(server) v2Handler.Register(server, healthServer) grpc_health_v1.RegisterHealthServer(server, healthServer) return server, nil } // Start gRPC server concurrently func (s *Server) Start(ctx context.Context) error { var err error s.grpcConn, err = s.grpcCfg.NetAddr.Listen(ctx) if err != nil { return fmt.Errorf("failed to listen on gRPC port: %w", err) } s.telset.Logger.Info("Starting GRPC server", zap.Stringer("addr", s.grpcConn.Addr())) s.stopped.Go(func() { if err := s.grpcServer.Serve(s.grpcConn); err != nil { s.telset.Logger.Error("GRPC server exited", zap.Error(err)) s.telset.ReportStatus(componentstatus.NewFatalErrorEvent(err)) } }) return nil } // Close stops http, GRPC servers and closes the port listener. func (s *Server) Close() error { s.grpcServer.Stop() s.stopped.Wait() s.telset.ReportStatus(componentstatus.NewEvent(componentstatus.StatusStopped)) return nil } func (s *Server) GRPCAddr() string { return s.grpcConn.Addr().String() } ================================================ FILE: cmd/remote-storage/app/server_test.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package app import ( "context" "errors" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/config/configgrpc" "go.opentelemetry.io/collector/config/confignet" "go.opentelemetry.io/collector/config/configoptional" "go.opentelemetry.io/collector/config/configtls" "go.uber.org/zap" "go.uber.org/zap/zaptest/observer" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" "github.com/jaegertracing/jaeger/internal/grpctest" "github.com/jaegertracing/jaeger/internal/proto-gen/storage/v2" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" tracestoremocks "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore/mocks" "github.com/jaegertracing/jaeger/internal/telemetry" "github.com/jaegertracing/jaeger/internal/tenancy" ) var testCertKeyLocation = "../../../internal/config/tlscfg/testdata" func TestNewServer_CreateStorageErrors(t *testing.T) { createServer := func(factory *fakeFactory) (*Server, error) { return NewServer( context.Background(), configgrpc.ServerConfig{ NetAddr: confignet.AddrConfig{ Endpoint: ":0", }, }, factory, factory, tenancy.NewManager(&tenancy.Options{}), telemetry.NoopSettings(), ) } factory := &fakeFactory{readerErr: errors.New("no reader")} _, err := createServer(factory) require.ErrorContains(t, err, "no reader") factory = &fakeFactory{writerErr: errors.New("no writer")} _, err = createServer(factory) require.ErrorContains(t, err, "no writer") factory = &fakeFactory{depReaderErr: errors.New("no deps")} _, err = createServer(factory) require.ErrorContains(t, err, "no deps") factory = &fakeFactory{} s, err := createServer(factory) require.NoError(t, err) require.NoError(t, s.Start(context.Background())) validateGRPCServer(t, s.GRPCAddr()) require.NoError(t, s.grpcConn.Close()) } func TestServerStart_BadPortErrors(t *testing.T) { srv := &Server{ grpcCfg: configgrpc.ServerConfig{ NetAddr: confignet.AddrConfig{ Endpoint: ":-1", }, }, } require.Error(t, srv.Start(context.Background())) } type fakeFactory struct { reader tracestore.Reader writer tracestore.Writer depReader depstore.Reader readerErr error writerErr error depReaderErr error } func (f *fakeFactory) CreateTraceReader() (tracestore.Reader, error) { if f.readerErr != nil { return nil, f.readerErr } return f.reader, nil } func (f *fakeFactory) CreateTraceWriter() (tracestore.Writer, error) { if f.writerErr != nil { return nil, f.writerErr } return f.writer, nil } func (f *fakeFactory) CreateDependencyReader() (depstore.Reader, error) { if f.depReaderErr != nil { return nil, f.depReaderErr } return f.depReader, nil } func (*fakeFactory) InitArchiveStorage(*zap.Logger) (spanstore.Reader, spanstore.Writer) { return nil, nil } func TestNewServer_TLSConfigError(t *testing.T) { tlsCfg := configtls.ServerConfig{ ClientCAFile: "invalid/path", Config: configtls.Config{ CertFile: "invalid/path", KeyFile: "invalid/path", }, } telset := telemetry.NoopSettings() telset.Logger = zap.NewNop() _, err := NewServer( context.Background(), configgrpc.ServerConfig{ NetAddr: confignet.AddrConfig{ Endpoint: ":8081", }, TLS: configoptional.Some(tlsCfg), }, &fakeFactory{}, &fakeFactory{}, tenancy.NewManager(&tenancy.Options{}), telset, ) assert.ErrorContains(t, err, "failed to load TLS config") } var testCases = []struct { name string TLS *configtls.ServerConfig clientTLS *configtls.ClientConfig expectError bool expectClientError bool expectServerFail bool }{ { name: "should pass with insecure connection", TLS: nil, clientTLS: &configtls.ClientConfig{ Insecure: true, }, expectError: false, expectClientError: false, expectServerFail: false, }, { name: "should fail with TLS client to untrusted TLS server", TLS: &configtls.ServerConfig{ Config: configtls.Config{ CertFile: testCertKeyLocation + "/example-server-cert.pem", KeyFile: testCertKeyLocation + "/example-server-key.pem", }, }, clientTLS: &configtls.ClientConfig{ ServerName: "example.com", }, expectError: true, expectClientError: true, expectServerFail: false, }, { name: "should fail with TLS client to trusted TLS server with incorrect hostname", TLS: &configtls.ServerConfig{ Config: configtls.Config{ CertFile: testCertKeyLocation + "/example-server-cert.pem", KeyFile: testCertKeyLocation + "/example-server-key.pem", }, }, clientTLS: &configtls.ClientConfig{ Config: configtls.Config{ CAFile: testCertKeyLocation + "/example-CA-cert.pem", }, ServerName: "nonEmpty", }, expectError: true, expectClientError: true, expectServerFail: false, }, { name: "should pass with TLS client to trusted TLS server with correct hostname", TLS: &configtls.ServerConfig{ Config: configtls.Config{ CertFile: testCertKeyLocation + "/example-server-cert.pem", KeyFile: testCertKeyLocation + "/example-server-key.pem", }, }, clientTLS: &configtls.ClientConfig{ Config: configtls.Config{ CAFile: testCertKeyLocation + "/example-CA-cert.pem", }, ServerName: "example.com", }, expectError: false, expectClientError: false, expectServerFail: false, }, { name: "should fail with TLS client without cert to trusted TLS server requiring cert", TLS: &configtls.ServerConfig{ Config: configtls.Config{ CertFile: testCertKeyLocation + "/example-server-cert.pem", KeyFile: testCertKeyLocation + "/example-server-key.pem", }, ClientCAFile: testCertKeyLocation + "/example-CA-cert.pem", }, clientTLS: &configtls.ClientConfig{ Config: configtls.Config{ CAFile: testCertKeyLocation + "/example-CA-cert.pem", }, ServerName: "example.com", }, expectError: false, expectServerFail: false, expectClientError: true, }, { name: "should pass with TLS client with cert to trusted TLS server requiring cert", TLS: &configtls.ServerConfig{ Config: configtls.Config{ CertFile: testCertKeyLocation + "/example-server-cert.pem", KeyFile: testCertKeyLocation + "/example-server-key.pem", }, ClientCAFile: testCertKeyLocation + "/example-CA-cert.pem", }, clientTLS: &configtls.ClientConfig{ Config: configtls.Config{ CAFile: testCertKeyLocation + "/example-CA-cert.pem", CertFile: testCertKeyLocation + "/example-client-cert.pem", KeyFile: testCertKeyLocation + "/example-client-key.pem", }, ServerName: "example.com", }, expectError: false, expectServerFail: false, expectClientError: false, }, { name: "should fail with TLS client without cert to trusted TLS server requiring cert from a different CA", TLS: &configtls.ServerConfig{ Config: configtls.Config{ CertFile: testCertKeyLocation + "/example-server-cert.pem", KeyFile: testCertKeyLocation + "/example-server-key.pem", }, ClientCAFile: testCertKeyLocation + "/wrong-CA-cert.pem", }, clientTLS: &configtls.ClientConfig{ Config: configtls.Config{ CAFile: testCertKeyLocation + "/example-CA-cert.pem", CertFile: testCertKeyLocation + "/example-client-cert.pem", KeyFile: testCertKeyLocation + "/example-client-key.pem", }, ServerName: "example.com", }, expectError: false, expectServerFail: false, expectClientError: true, }, } type grpcClient struct { storage.TraceReaderClient conn *grpc.ClientConn } func newGRPCClient(t *testing.T, addr string, creds credentials.TransportCredentials, tm *tenancy.Manager) *grpcClient { dialOpts := []grpc.DialOption{ grpc.WithUnaryInterceptor(tenancy.NewClientUnaryInterceptor(tm)), } if creds != nil { dialOpts = append(dialOpts, grpc.WithTransportCredentials(creds)) } else { dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) } conn, err := grpc.NewClient(addr, dialOpts...) require.NoError(t, err) return &grpcClient{ TraceReaderClient: storage.NewTraceReaderClient(conn), conn: conn, } } func TestServerGRPCTLS(t *testing.T) { for _, test := range testCases { t.Run(test.name, func(t *testing.T) { tls := configoptional.None[configtls.ServerConfig]() if test.TLS != nil { tls = configoptional.Some(*test.TLS) } serverOptions := configgrpc.ServerConfig{ NetAddr: confignet.AddrConfig{ Endpoint: ":0", }, TLS: tls, } reader := new(tracestoremocks.Reader) f := &fakeFactory{ reader: reader, } expectedServices := []string{"test"} reader.On("GetServices", mock.AnythingOfType("*context.valueCtx")).Return(expectedServices, nil) tm := tenancy.NewManager(&tenancy.Options{Enabled: true}) telset := telemetry.NoopSettings() telset.Logger = zap.NewNop() server, err := NewServer( context.Background(), serverOptions, f, f, tm, telset, ) require.NoError(t, err) require.NoError(t, server.Start(context.Background())) var clientError error var client *grpcClient if serverOptions.TLS.HasValue() { clientTLSCfg, err0 := test.clientTLS.LoadTLSConfig(context.Background()) require.NoError(t, err0) creds := credentials.NewTLS(clientTLSCfg) client = newGRPCClient(t, server.GRPCAddr(), creds, tm) } else { client = newGRPCClient(t, server.GRPCAddr(), nil, tm) } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() ctx = tenancy.WithTenant(ctx, "foo") res, clientError := client.GetServices(ctx, &storage.GetServicesRequest{}) if test.expectClientError { require.Error(t, clientError) } else { require.NoError(t, clientError) assert.Equal(t, expectedServices, res.Services) } require.NoError(t, client.conn.Close()) server.Close() }) } } func TestServerHandlesPortZero(t *testing.T) { zapCore, logs := observer.New(zap.InfoLevel) logger := zap.New(zapCore) telset := telemetry.NoopSettings() telset.Logger = logger server, err := NewServer( context.Background(), configgrpc.ServerConfig{ NetAddr: confignet.AddrConfig{Endpoint: ":0"}, }, &fakeFactory{}, &fakeFactory{}, tenancy.NewManager(&tenancy.Options{}), telset, ) require.NoError(t, err) require.NoError(t, server.Start(context.Background())) const line = "Starting GRPC server" message := logs.FilterMessage(line) require.Equal(t, 1, message.Len(), "Expected '%s' log message, actual logs: %+v", line, logs) onlyEntry := message.All()[0] hostPort := onlyEntry.ContextMap()["addr"].(string) validateGRPCServer(t, hostPort) server.Close() } func validateGRPCServer(t *testing.T, hostPort string) { grpctest.ReflectionServiceValidator{ HostPort: hostPort, ExpectedServices: []string{ // writer "opentelemetry.proto.collector.trace.v1.TraceService", // reader "jaeger.storage.v2.TraceReader", "jaeger.storage.v2.DependencyReader", // health "grpc.health.v1.Health", }, }.Execute(t) } ================================================ FILE: cmd/remote-storage/config-badger.yaml ================================================ # Example configuration for remote-storage service with Badger backend # Server configuration grpc: # Host:Port to listen on endpoint: :17271 # Storage configuration - Badger backend storage: backends: badger-storage: badger: directories: keys: /tmp/jaeger/badger/keys values: /tmp/jaeger/badger/values ephemeral: false maintenance_interval: 5m metrics_update_interval: 10s ttl: spans: 168h # 7 days # Multi-tenancy configuration (optional) multi_tenancy: enabled: false ================================================ FILE: cmd/remote-storage/config.yaml ================================================ # Example configuration for remote-storage service # This service exposes a gRPC API for remote storage backends # and allows sharing single-node storage implementations like memory or Badger. # Server configuration grpc: # Host:Port to listen on endpoint: :17271 # Storage configuration using the same format as jaeger v2 storage: backends: default-storage: memory: max_traces: 100000 # Multi-tenancy configuration (optional) multi_tenancy: enabled: false # header: x-tenant # tenants: # - tenant1 # - tenant2 ================================================ FILE: cmd/remote-storage/main.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "fmt" "io" "os" "github.com/spf13/cobra" "github.com/spf13/viper" "go.opentelemetry.io/otel/metric/noop" _ "go.uber.org/automaxprocs" "go.uber.org/zap" "github.com/jaegertracing/jaeger/cmd/internal/docs" "github.com/jaegertracing/jaeger/cmd/internal/featuregate" "github.com/jaegertracing/jaeger/cmd/internal/flags" "github.com/jaegertracing/jaeger/cmd/internal/printconfig" "github.com/jaegertracing/jaeger/cmd/internal/status" "github.com/jaegertracing/jaeger/cmd/internal/storageconfig" "github.com/jaegertracing/jaeger/cmd/remote-storage/app" "github.com/jaegertracing/jaeger/internal/config" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" "github.com/jaegertracing/jaeger/internal/telemetry" "github.com/jaegertracing/jaeger/internal/tenancy" "github.com/jaegertracing/jaeger/internal/version" "github.com/jaegertracing/jaeger/ports" ) const serviceName = "jaeger-remote-storage" // loadConfig loads configuration from viper, or returns default configuration if no config file is provided. func loadConfig(v *viper.Viper, logger *zap.Logger) (*app.Config, error) { // If viper config is not provided, use defaults if v.ConfigFileUsed() == "" { logger.Info("No configuration file provided, using default configuration (memory storage on :17271)") return app.DefaultConfig(), nil } return app.LoadConfigFromViper(v) } func main() { svc := flags.NewService(ports.RemoteStorageAdminHTTP) v := viper.New() command := &cobra.Command{ Use: serviceName, Short: serviceName + " allows sharing single-node storage implementations like memstore or Badger.", Long: serviceName + ` allows sharing single-node storage implementations like memstore or Badger. It implements Jaeger Remote Storage gRPC API.`, RunE: func(_ *cobra.Command, _ /* args */ []string) error { if err := svc.Start(v); err != nil { return err } logger := svc.Logger baseFactory := svc.MetricsFactory.Namespace(metrics.NSOptions{Name: "jaeger"}) metricsFactory := baseFactory.Namespace(metrics.NSOptions{Name: "remote-storage"}) version.NewInfoMetrics(metricsFactory) // Load configuration from YAML file, or use defaults if not provided cfg, err := loadConfig(v, logger) if err != nil { logger.Fatal("Failed to load configuration", zap.Error(err)) } baseTelset := telemetry.Settings{ Logger: svc.Logger, Metrics: baseFactory, Host: svc.Admin.Host(), MeterProvider: noop.NewMeterProvider(), } tm := tenancy.NewManager(&cfg.Tenancy) telset := baseTelset telset.Metrics = metricsFactory // Get the storage name (first backend configured) storageName := cfg.GetStorageName() if storageName == "" { logger.Fatal("No storage backend configured") } // Get the backend configuration backend, ok := cfg.Storage.TraceBackends[storageName] if !ok { logger.Fatal("Storage backend not found", zap.String("name", storageName)) } // Create storage factory from configuration (no auth resolver for remote-storage) traceFactory, err := storageconfig.CreateTraceStorageFactory( context.Background(), storageName, backend, telset, nil, // no auth resolver for remote-storage ) if err != nil { logger.Fatal("Failed to create storage factory", zap.Error(err)) } depFactory, ok := traceFactory.(depstore.Factory) if !ok { logger.Fatal("Storage does not implement dependency store", zap.String("name", storageName)) } // Create and start server server, err := app.NewServer( context.Background(), cfg.GRPC, traceFactory, depFactory, tm, telset, ) if err != nil { logger.Fatal("Failed to create server", zap.Error(err)) } if err := server.Start(context.Background()); err != nil { logger.Fatal("Could not start servers", zap.Error(err)) } return svc.RunAndThen(func() { server.Close() if closer, ok := traceFactory.(io.Closer); ok { if err := closer.Close(); err != nil { logger.Error("Failed to close storage factory", zap.Error(err)) } } }) }, } command.AddCommand(version.Command()) command.AddCommand(docs.Command(v)) command.AddCommand(status.Command(v, ports.RemoteStorageAdminHTTP)) command.AddCommand(printconfig.Command(v)) command.AddCommand(featuregate.Command()) // Add only basic flags (not storage flags) config.AddFlags( v, command, svc.AddFlags, ) if err := command.Execute(); err != nil { fmt.Println(err.Error()) os.Exit(1) } } ================================================ FILE: cmd/tracegen/Dockerfile ================================================ # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 FROM scratch ARG TARGETARCH ARG USER_UID=10001 COPY tracegen-linux-$TARGETARCH /go/bin/tracegen-linux ENTRYPOINT ["/go/bin/tracegen-linux"] USER ${USER_UID} ================================================ FILE: cmd/tracegen/README.md ================================================ # tracegen `tracegen` is a utility that can generate a steady flow of simple traces useful for performance tuning. Traces are produced concurrently from one or more worker goroutines. Run with `-h` to see all cli flags. The binary is available from the Releases page, as well as a Docker image: ```sh $ docker run jaegertracing/jaeger-tracegen -service abcd -traces 10 ``` The generator can be configured to export traces in different formats, via `-exporter` flag. By default, the exporters send data to `localhost`. If running in a container, this refers to the networking namespace of the container itself, so to export to another container, the exporters need to be provided with appropriate location. OTLP exporter accepts configuration via environment variables. For more information about configuring OTLP exporter, see [OpenTelemetry Protocol Exporter](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md). See example in the included [docker-compose](./docker-compose.yml) file. ================================================ FILE: cmd/tracegen/docker-compose.yml ================================================ services: jaeger: image: cr.jaegertracing.io/jaegertracing/jaeger:2.15.1@sha256:a7dd965687d45507072676db81e6903706ba334dfc92f0c248125cfd9a70c483 ports: - '16686:16686' - '4318:4318' tracegen: image: cr.jaegertracing.io/jaegertracing/jaeger-tracegen:2.15.1@sha256:8149733c9c54c2b272d2141388aa4f0ae95704c814df976b434fba162752a235 environment: - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318 command: ["-duration", "10s", "-workers", "3", "-pause", "250ms"] depends_on: - jaeger ================================================ FILE: cmd/tracegen/main.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "errors" "flag" "fmt" "time" "github.com/go-logr/zapr" "go.opentelemetry.io/contrib/samplers/jaegerremote" "go.opentelemetry.io/otel" "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/exporters/stdout/stdouttrace" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "go.uber.org/zap/zapcore" "github.com/jaegertracing/jaeger/internal/jaegerclientenv2otel" "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" "github.com/jaegertracing/jaeger/internal/tracegen" "github.com/jaegertracing/jaeger/internal/version" ) var flagAdaptiveSamplingEndpoint string func main() { zc := zap.NewDevelopmentConfig() zc.Level = zap.NewAtomicLevelAt(zapcore.Level(-8)) // level used by OTEL's Debug() logger, err := zc.Build() if err != nil { panic(err) } otel.SetLogger(zapr.NewLogger(logger)) fs := flag.CommandLine cfg := new(tracegen.Config) cfg.Flags(fs) fs.StringVar( &flagAdaptiveSamplingEndpoint, "adaptive-sampling", "", "HTTP endpoint to use to retrieve sampling strategies, "+ "e.g. http://localhost:14268/api/sampling. "+ "When not specified a standard SDK sampler will be used "+ "(see OTEL_TRACES_SAMPLER env var in OTEL docs)") flag.Parse() logger.Info(version.Get().String()) otel.SetTextMapPropagator(propagation.TraceContext{}) jaegerclientenv2otel.MapJaegerToOtelEnvVars(logger) tracers, shutdown := createTracers(cfg, logger) defer shutdown(context.Background()) tracegen.Run(cfg, tracers, logger) } func createTracers(cfg *tracegen.Config, logger *zap.Logger) ([]trace.Tracer, func(context.Context) error) { if cfg.Services < 1 { cfg.Services = 1 } var shutdown []func(context.Context) error var tracers []trace.Tracer for s := 0; s < cfg.Services; s++ { svc := cfg.Service if cfg.Services > 1 { svc = fmt.Sprintf("%s-%02d", svc, s) } exp, err := createOtelExporter(cfg.TraceExporter) if err != nil { logger.Sugar().Fatalf("cannot create trace exporter %s: %s", cfg.TraceExporter, err) } logger.Sugar().Infof("using %s trace exporter for service %s", cfg.TraceExporter, svc) res, err := resource.New( context.Background(), resource.WithSchemaURL(otelsemconv.SchemaURL), resource.WithAttributes(otelsemconv.ServiceNameAttribute(svc)), resource.WithTelemetrySDK(), resource.WithHost(), resource.WithOSType(), ) if err != nil { logger.Sugar().Fatalf("resource creation failed: %s", err) } opts := []sdktrace.TracerProviderOption{ sdktrace.WithBatcher(exp, sdktrace.WithBlocking()), sdktrace.WithResource(res), } if flagAdaptiveSamplingEndpoint != "" { jaegerRemoteSampler := jaegerremote.New( svc, jaegerremote.WithSamplingServerURL(flagAdaptiveSamplingEndpoint), jaegerremote.WithSamplingRefreshInterval(5*time.Second), jaegerremote.WithInitialSampler(sdktrace.TraceIDRatioBased(0.5)), ) opts = append(opts, sdktrace.WithSampler(jaegerRemoteSampler)) logger.Sugar().Infof("using adaptive sampling URL: %s", flagAdaptiveSamplingEndpoint) } tp := sdktrace.NewTracerProvider(opts...) tracers = append(tracers, tp.Tracer(cfg.Service)) shutdown = append(shutdown, tp.Shutdown) } return tracers, func(ctx context.Context) error { var errs []error for _, f := range shutdown { errs = append(errs, f(ctx)) } return errors.Join(errs...) } } func createOtelExporter(exporterType string) (sdktrace.SpanExporter, error) { var exporter sdktrace.SpanExporter var err error switch exporterType { case "jaeger": return nil, errors.New("jaeger exporter is no longer supported, please use otlp") case "otlp", "otlp-http": client := otlptracehttp.NewClient( otlptracehttp.WithInsecure(), ) exporter, err = otlptrace.New(context.Background(), client) case "otlp-grpc": client := otlptracegrpc.NewClient( otlptracegrpc.WithInsecure(), ) exporter, err = otlptrace.New(context.Background(), client) case "stdout": exporter, err = stdouttrace.New() default: return nil, fmt.Errorf("unrecognized exporter type %s", exporterType) } return exporter, err } ================================================ FILE: doc.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 // Do not delete this file, it's needed for `go get` to work. // Package jaeger contains the code for Jaeger backend. package jaeger ================================================ FILE: docker-compose/cassandra/v4/docker-compose.yaml ================================================ services: cassandra: image: cassandra:4.1@sha256:b9451ebdfa53f9e22b470e1420f2a94a3433738b7f25350472d3443f0b203b75 container_name: "cassandra-4" ports: - "9042:9042" - "9160:9160" # We enable password authentication that defaults to cassandra/cassandra superuser / pwd. # https://cassandra.apache.org/doc/stable/cassandra/operating/security.html#authentication command: > /bin/sh -c "echo 'authenticator: PasswordAuthenticator' >> /etc/cassandra/cassandra.yaml && docker-entrypoint.sh cassandra -f" environment: - CASSANDRA_DC=dc1 - CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch networks: - cassandra-net healthcheck: test: ["CMD", "cqlsh", "-u", "cassandra", "-p", "cassandra", "-e", "describe keyspaces"] interval: 30s timeout: 10s retries: 5 networks: cassandra-net: driver: bridge ================================================ FILE: docker-compose/cassandra/v5/docker-compose.yaml ================================================ services: cassandra: image: cassandra:5.0@sha256:70b40a2025d450f7865c5ec6f1ebea13108166f81fe41462069690cb4d9690f2 container_name: "cassandra-5" ports: - "9042:9042" - "9160:9160" # We enable password authentication that defaults to cassandra/cassandra superuser / pwd. # https://cassandra.apache.org/doc/stable/cassandra/operating/security.html#authentication command: > /bin/sh -c "echo 'authenticator: PasswordAuthenticator' >> /etc/cassandra/cassandra.yaml && docker-entrypoint.sh cassandra -f" environment: - CASSANDRA_DC=dc1 - CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch networks: - cassandra-net healthcheck: test: ["CMD", "cqlsh", "-u", "cassandra", "-p", "cassandra", "-e", "describe keyspaces"] interval: 30s timeout: 10s retries: 5 networks: cassandra-net: driver: bridge ================================================ FILE: docker-compose/clickhouse/docker-compose.yml ================================================ services: clickhouse: image: clickhouse/clickhouse-server:25.12.1@sha256:7234d193fbeb3a375f21f0bb99040396f68bb2e82cada4f4947d7fcf9638bbf3 container_name: clickhouse environment: - CLICKHOUSE_USER=default - CLICKHOUSE_PASSWORD=password - CLICKHOUSE_DB=jaeger ports: - "8123:8123" - "9000:9000" healthcheck: test: ["CMD", "clickhouse-client", "--query=SELECT 1"] interval: 10s timeout: 5s retries: 5 ================================================ FILE: docker-compose/elasticsearch/v6/docker-compose.yml ================================================ services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.8.23@sha256:740c3614289539e9782b8a3c4de5100599bbec669d6c56bc21a76eea661e80a5 environment: - discovery.type=single-node - http.host=0.0.0.0 - transport.host=127.0.0.1 ports: - "9200:9200" ================================================ FILE: docker-compose/elasticsearch/v7/docker-compose.yml ================================================ services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.17.22@sha256:a0e01a200a316bac8d661e1acae368fd40c9c618847205a12f443c09c36e6eb3 environment: - discovery.type=single-node - http.host=0.0.0.0 - transport.host=127.0.0.1 - xpack.security.enabled=false # Disable security features - xpack.security.http.ssl.enabled=false # Disable HTTPS - xpack.monitoring.enabled=false # Disable monitoring features ports: - "9200:9200" ================================================ FILE: docker-compose/elasticsearch/v8/docker-compose.yml ================================================ services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:8.19.0@sha256:e1e66bfabae0fd03a0a36651a9bb198e7f061e0c99f457a6203b116e053e9cdb environment: - discovery.type=single-node - http.host=0.0.0.0 - transport.host=127.0.0.1 - xpack.security.enabled=false # Disable security features - xpack.security.http.ssl.enabled=false # Disable HTTPS - action.destructive_requires_name=false - xpack.monitoring.collection.enabled=false # Disable monitoring features ports: - "9200:9200" ================================================ FILE: docker-compose/elasticsearch/v9/docker-compose.yml ================================================ services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:9.3.0@sha256:4f6bdcb742e892539c6ac49b0dd3e4e182e90218546e8c6a22db378c344acb60 environment: - discovery.type=single-node - http.host=0.0.0.0 - transport.host=127.0.0.1 - xpack.security.enabled=false # Disable security features - xpack.security.http.ssl.enabled=false # Disable HTTPS - action.destructive_requires_name=false - xpack.monitoring.collection.enabled=false # Disable monitoring features ports: - "9200:9200" ================================================ FILE: docker-compose/kafka/README.md ================================================ # Sample configuration with Kafka This `docker compose` environment provides a sample configuration of Jaeger deployment utilizing collector-Kafka-ingester pipeline with jaeger-v2 unified binary. Storage is provided by the `jaeger-remote-storage` service running memstore. The setup uses **Apache Kafka 3.9.0** running in KRaft mode. Jaeger UI can be accessed at http://localhost:16686/, as usual, and refreshing the screen should produce internal traces. ```mermaid graph LR C[jaeger v2
collector mode] --> KafkaBroker KafkaBroker --> I[jaeger v2
ingester mode] I --> S[jaeger-remote-storage] UI[jaeger v2
query mode
Jaeger UI] --> S S --> MemStore subgraph Kafka KRaft KafkaBroker end subgraph Shared Storage S MemStore end ``` ================================================ FILE: docker-compose/kafka/docker-compose.yml ================================================ include: - path: v3/docker-compose.yml services: jaeger-remote-storage: image: cr.jaegertracing.io/jaegertracing/jaeger-remote-storage@sha256:e78c6093ac38f7cdccf0877750bb21f2cbcc08c2fd3e578966b5796a31f26643 ports: - 17271:17271 environment: - SPAN_STORAGE_TYPE=memory healthcheck: test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:17270/ || exit 1"] interval: 5s timeout: 5s retries: 3 jaeger-collector: image: cr.jaegertracing.io/jaegertracing/jaeger:2.15.1@sha256:a7dd965687d45507072676db81e6903706ba334dfc92f0c248125cfd9a70c483 volumes: - ../../cmd/jaeger/config-kafka-collector.yaml:/etc/jaeger/config.yaml command: - "--config=/etc/jaeger/config.yaml" environment: - KAFKA_BROKER=kafka:9092 - KAFKA_TOPIC=jaeger-spans - KAFKA_ENCODING=otlp_proto ports: - 4318:4318 - 14250:14250 healthcheck: test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:13133/status || exit 1"] interval: 5s timeout: 5s retries: 3 depends_on: kafka: condition: service_healthy links: - kafka jaeger-ingester: image: cr.jaegertracing.io/jaegertracing/jaeger:2.15.1@sha256:a7dd965687d45507072676db81e6903706ba334dfc92f0c248125cfd9a70c483 volumes: - ./jaeger-ingester-remote-storage.yaml:/etc/jaeger/config.yaml command: - "--config=/etc/jaeger/config.yaml" environment: - KAFKA_BROKER=kafka:9092 - KAFKA_TOPIC=jaeger-spans - KAFKA_ENCODING=otlp_proto healthcheck: test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:14133/status || exit 1"] interval: 5s timeout: 5s retries: 3 depends_on: kafka: condition: service_healthy jaeger-remote-storage: condition: service_healthy jaeger-collector: condition: service_healthy links: - kafka - jaeger-remote-storage jaeger-query: image: cr.jaegertracing.io/jaegertracing/jaeger:2.15.1@sha256:a7dd965687d45507072676db81e6903706ba334dfc92f0c248125cfd9a70c483 volumes: - ../../cmd/jaeger/config-query.yaml:/etc/jaeger/config.yaml - ../../cmd/jaeger/config-ui.json:/cmd/jaeger/config-ui.json:ro command: - "--config=/etc/jaeger/config.yaml" environment: - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger-collector:4318 ports: - "16686:16686" - "16687" restart: on-failure healthcheck: test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:13133/status || exit 1"] interval: 5s timeout: 5s retries: 3 depends_on: jaeger-remote-storage: condition: service_healthy links: - jaeger-remote-storage ================================================ FILE: docker-compose/kafka/jaeger-ingester-remote-storage.yaml ================================================ # This config is needed because config-kafka-ingester.yaml uses memory storage, # but this docker-compose setup uses jaeger-remote-storage (grpc). # Based on config-kafka-ingester.yaml with grpc storage backend. service: extensions: [jaeger_storage, healthcheckv2] pipelines: traces: receivers: [kafka] processors: [batch] exporters: [jaeger_storage_exporter] telemetry: resource: service.name: jaeger_ingester logs: level: info extensions: healthcheckv2: use_v2: true http: endpoint: 0.0.0.0:14133 jaeger_storage: backends: some_storage: grpc: endpoint: jaeger-remote-storage:17271 tls: insecure: true receivers: kafka: brokers: - ${env:KAFKA_BROKER:-kafka:9092} traces: topics: - ${env:KAFKA_TOPIC:-jaeger-spans} encoding: ${env:KAFKA_ENCODING:-otlp_proto} initial_offset: earliest processors: batch: exporters: jaeger_storage_exporter: trace_storage: some_storage ================================================ FILE: docker-compose/kafka/v3/docker-compose.yml ================================================ services: kafka: image: apache/kafka:3.9.0@sha256:fbc7d7c428e3755cf36518d4976596002477e4c052d1f80b5b9eafd06d0fff2f hostname: kafka ports: - "9092:9092" volumes: - kafka-data:/var/lib/kafka/data environment: # KRaft settings (no ZooKeeper needed) KAFKA_NODE_ID: 1 KAFKA_PROCESS_ROLES: broker,controller KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9094 KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9094 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT # Additional settings KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 # CLUSTER_ID for KRaft CLUSTER_ID: test_kafka_cluster_id healthcheck: test: [ "CMD-SHELL", "/opt/kafka/bin/kafka-topics.sh --list --bootstrap-server localhost:9092", ] interval: 30s timeout: 10s retries: 5 start_period: 30s volumes: kafka-data: ================================================ FILE: docker-compose/monitor/.gitignore ================================================ .venv ================================================ FILE: docker-compose/monitor/Makefile ================================================ # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 BINARY ?= jaeger # Default value uses v2 binary .PHONY: build build: clean-jaeger cd ../../ && make build-$(BINARY) GOOS=linux cd ../../ && make create-baseimg PLATFORMS=linux/$(shell go env GOARCH) cd ../../ && docker buildx build --target release \ --tag jaegertracing/$(BINARY):dev \ --build-arg base_image=localhost:5000/baseimg_alpine:latest \ --build-arg debug_image=not-used \ --build-arg TARGETARCH=$(shell go env GOARCH) \ --load \ cmd/$(BINARY) # starts up the system required for SPM using the latest otel image and a development jaeger image. # Note: the jaeger "dev" image can be built with "make build". .PHONY: dev dev: export JAEGER_VERSION = dev dev: docker compose up $(DOCKER_COMPOSE_ARGS) .PHONY: elasticsearch elasticsearch: export JAEGER_VERSION = dev elasticsearch: docker compose -f docker-compose-elasticsearch.yml up $(DOCKER_COMPOSE_ARGS) .PHONY: opensearch opensearch: export JAEGER_VERSION = dev opensearch: docker compose -f docker-compose-opensearch.yml up $(DOCKER_COMPOSE_ARGS) .PHONY: clean-jaeger clean-jaeger: # Also cleans up intermediate cached containers. docker system prune -f .PHONY: clean-all clean-all: clean-jaeger docker rmi -f jaegertracing/jaeger:dev ; \ docker rmi -f jaegertracing/jaeger:latest ; \ docker rmi -f otel/opentelemetry-collector-contrib:latest ; \ docker rmi -f prom/prometheus:latest ; \ ================================================ FILE: docker-compose/monitor/README.md ================================================ # Service Performance Monitoring (SPM) Development/Demo Environment Service Performance Monitoring (SPM) is an opt-in feature introduced to Jaeger that provides Request, Error and Duration (RED) metrics grouped by service name and operation that are derived from span data. These metrics are programmatically available through an API exposed by jaeger-query along with a "Monitor" UI tab that visualizes these metrics as graphs. 1. **Prometheus as metrics backend**: Metrics are computed by the OpenTelemetry Collector and aggregated in Prometheus (see [tracking issue](https://github.com/jaegertracing/jaeger/issues/2954)). 2. **Directly querying from trace storage (Elasticsearch/OpenSearch)**: Metrics are computed directly from trace data stored in Elasticsearch or OpenSearch, eliminating the need for a separate metrics storage backend like Prometheus (see [issue \#6641](https://github.com/jaegertracing/jaeger/issues/6641) for details). The motivation for providing this environment is to allow developers to either test Jaeger UI or their own applications against jaeger-query's metrics query API, as well as a quick and simple way for users to bring up the entire stack required to visualize RED metrics from simulated traces or from their own application. This environment supports the following backend components, depending on the chosen metrics storage: - **Common Components**: - [MicroSim](https://github.com/yurishkuro/microsim): A program to simulate traces. - [Jaeger](https://www.jaegertracing.io/docs/latest/getting-started/): The full Jaeger stack in a single container image. - [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/): A vendor-agnostic integration layer for traces and metrics. For the Prometheus option, it receives Jaeger spans, forwards them to Jaeger, and aggregates metrics from span data (see [spanmetrics connector documentation][spanmetricsconnectorreadme]). - **For Prometheus as metrics backend**: - [Prometheus](https://prometheus.io/): A metrics collection and query engine that scrapes metrics computed by the OpenTelemetry Collector and provides an API for Jaeger All-in-one to query these metrics. - **For directly querying from trace storage (ES/OS)**: - [Elasticsearch/OpenSearch](https://www.elastic.co/elasticsearch/): The trace storage backend for Jaeger, used for both trace storage and direct metrics querying in this configuration. The following diagram illustrates the relationship between these components when using Prometheus as the metrics backend: ```mermaid flowchart LR SDK -->|traces| Receiver Receiver --> MG Receiver --> Batch MG --> ExpMetrics Batch --> ExpTraces ExpMetrics -->|metrics| Prometheus[(Prometheus)] ExpTraces -->|traces| Jaeger[Jaeger] Prometheus -.-> JaegerUI Jaeger --> Storage[(Storage)] Storage -.-> JaegerUI[Jaeger UI] style Prometheus fill:red,color:white style Jaeger fill:blue,color:white style JaegerUI fill:blue,color:white style Storage fill:gray,color:white subgraph Application SDK[OTel SDK] end subgraph OTEL[OTel Collector] Receiver Batch MG[Span Metrics Connector] ExpTraces[Traces Exporter] ExpMetrics[Metrics Exporter] end ``` # Getting Started ## Quickstart This brings up the system necessary to use the SPM feature locally. It uses the latest image tags from both Jaeger and OpenTelemetry. ### Option 1: Prometheus as metrics backend ```shell docker compose up ``` ### Option 2: Directly querying from trace storage (ES/OS) ```shell docker compose -f docker-compose-elasticsearch.yml up ``` ```shell docker compose -f docker-compose-opensearch.yml up ``` **Tips:** - Let the application run for a couple of minutes to ensure there is enough time series data to plot in the dashboard. - Navigate to Jaeger UI at http://localhost:16686/ and inspect the Monitor tab. Select `redis` service from the dropdown to see more than one endpoint. - For the Prometheus option, visualize raw metrics in the Prometheus UI at http://localhost:9090/query (e.g., [example query for trace spans](http://localhost:9090/query?g0.expr=traces_span_metrics_calls_total&g0.tab=0&g0.range_input=5m)). **Warning:** The included ` docker compose` files use the `latest` version of Jaeger and other components. If your local Docker registry already contains older versions, which may still be tagged as `latest`, you may want to delete those images before running the full set, to ensure consistent behavior: ```bash make clean-all ``` To use an official published image of Jaeger, specify the version via environment variable: ```shell JAEGER_VERSION=2.0.0 docker compose -f docker-compose.yml up ``` ## Development These steps allow for running the system necessary for SPM, built from Jaeger's source. The primary use case is for testing source code changes to the SPM feature locally. ### Build jaeger-v2 docker image ```shell make build ``` ## Bring up the dev environment ```bash make dev ``` ## Sending traces We will use [tracegen](https://github.com/jaegertracing/jaeger/tree/main/cmd/tracegen) to emit traces to the OpenTelemetry Collector which, in turn, will aggregate the trace data into metrics. Start the local stack needed for SPM, if not already done: ```shell docker compose up ``` Generate a specific number of traces with: ```shell docker run --env OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="http://jaeger:4318/v1/traces" \ --network monitor_backend \ --rm \ jaegertracing/jaeger-tracegen:latest \ -trace-exporter otlp-http \ -traces 1 ``` Or, emit traces over a period of time with: ```shell docker run --env OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="http://jaeger:4318/v1/traces" \ --network monitor_backend \ --rm \ jaegertracing/jaeger-tracegen:latest \ -trace-exporter otlp-http \ -duration 5s ``` Navigate to Jaeger UI at http://localhost:16686/ and you should be able to see traces from this demo application under the `tracegen` service: ![TraceGen Traces](images/tracegen_traces.png) Then navigate to the Monitor tab at http://localhost:16686/monitor to view the RED metrics: ![TraceGen RED Metrics](images/tracegen_metrics.png) ## Querying the HTTP API ### Example 1 Fetch call rates for both the driver and frontend services, grouped by operation, from now, looking back 1 second with a sliding rate-calculation window of 1m and step size of 1 millisecond ```bash curl "http://localhost:16686/api/metrics/calls?service=driver&service=frontend&groupByOperation=true&endTs=$(date +%s)000&lookback=1000&step=100&ratePer=60000" | jq . ``` ### Example 2 Fetch P95 latencies for both the driver and frontend services from now, looking back 1 second with a sliding rate-calculation window of 1m and step size of 1 millisecond, where the span kind is either "server" or "client". ```bash curl "http://localhost:16686/api/metrics/latencies?service=driver&service=frontend&quantile=0.95&endTs=$(date +%s)000&lookback=1000&step=100&ratePer=60000&spanKind=server&spanKind=client" | jq . ``` ### Example 3 Fetch error rates for both driver and frontend services using default parameters. ```bash curl "http://localhost:16686/api/metrics/errors?service=driver&service=frontend" | jq . ``` ### Example 4 Fetch the minimum step size supported by the underlying metrics store. ```bash curl "http://localhost:16686/api/metrics/minstep" | jq . ``` # HTTP API Specification ## Query Metrics `/api/metrics/{metric_type}?{query}` Where (Backus-Naur form): ``` metric_type = 'latencies' | 'calls' | 'errors' query = services , [ '&' optionalParams ] optionalParams = param | param '&' optionalParams param = groupByOperation | quantile | endTs | lookback | step | ratePer | spanKinds services = service | service '&' services service = 'service=' strValue - The list of services to include in the metrics selection filter, which are logically 'OR'ed. - Mandatory. quantile = 'quantile=' floatValue - The quantile to compute the latency 'P' value. Valid range (0,1]. - Mandatory for 'latencies' type. groupByOperation = 'groupByOperation=' boolValue boolValue = '1' | 't' | 'T' | 'true' | 'TRUE' | 'True' | 0 | 'f' | 'F' | 'false' | 'FALSE' | 'False' - A boolean value which will determine if the metrics query will also group by operation. - Optional with default: false endTs = 'endTs=' intValue - The posix milliseconds timestamp of the end time range of the metrics query. - Optional with default: now lookback = 'lookback=' intValue - The duration, in milliseconds, from endTs to look back on for metrics data points. - For example, if set to `3600000` (1 hour), the query would span from `endTs - 1 hour` to `endTs`. - Optional with default: 3600000 (1 hour). step = 'step=' intValue - The duration, in milliseconds, between data points of the query results. - For example, if set to 5s, the results would produce a data point every 5 seconds from the `endTs - lookback` to `endTs`. - Optional with default: 5000 (5 seconds). ratePer = 'ratePer=' intValue - The duration, in milliseconds, in which the per-second rate of change is calculated for a cumulative counter metric. - Optional with default: 600000 (10 minutes). spanKinds = spanKind | spanKind '&' spanKinds spanKind = 'spanKind=' spanKindType spanKindType = 'unspecified' | 'internal' | 'server' | 'client' | 'producer' | 'consumer' - The list of spanKinds to include in the metrics selection filter, which are logically 'OR'ed. - Optional with default: 'server' ``` ## Min Step `/api/metrics/minstep` Gets the min time resolution supported by the backing metrics store, in milliseconds, that can be used in the `step` parameter. e.g. a min step of 1 means the backend can only return data points that are at least 1ms apart, not closer. ## Responses The response data model is based on [`MetricsFamily`](https://github.com/jaegertracing/jaeger/blob/main/internal/proto/metrics/openmetrics.proto#L53). For example: ``` { "name": "service_call_rate", "type": "GAUGE", "help": "calls/sec, grouped by service", "metrics": [ { "labels": [ { "name": "service_name", "value": "driver" } ], "metricPoints": [ { "gaugeValue": { "doubleValue": 0.005846808321083344 }, "timestamp": "2021-06-03T09:12:06Z" }, { "gaugeValue": { "doubleValue": 0.006960443672323934 }, "timestamp": "2021-06-03T09:12:11Z" }, ] ... } ... ] ... } ``` If the `groupByOperation=true` parameter is set, the response will include the operation name in the labels like so: ``` "labels": [ { "name": "operation", "value": "/FindNearest" }, { "name": "service_name", "value": "driver" } ], ``` # Disabling Metrics Querying As this is feature is opt-in only, disabling metrics querying simply involves omitting the `METRICS_STORAGE_TYPE` environment variable when starting-up jaeger-query or jaeger all-in-one. For example, try removing the `METRICS_STORAGE_TYPE=prometheus` environment variable from the [docker-compose.yml](./docker-compose.yml) file. Then querying any metrics endpoints results in an error message: ``` $ curl http://localhost:16686/api/metrics/minstep | jq . { "data": null, "total": 0, "limit": 0, "offset": 0, "errors": [ { "code": 405, "msg": "metrics querying is currently disabled" } ] } ``` [spanmetricsconnector]: https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/connector/spanmetricsconnector [spanmetricsconnectorreadme]: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/connector/spanmetricsconnector/README.md ================================================ FILE: docker-compose/monitor/datasource.yml ================================================ apiVersion: 1 datasources: - name: Prometheus type: prometheus url: http://prometheus:9090 isDefault: true access: proxy editable: true ================================================ FILE: docker-compose/monitor/docker-compose-elasticsearch.yml ================================================ services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:9.3.0@sha256:4f6bdcb742e892539c6ac49b0dd3e4e182e90218546e8c6a22db378c344acb60 networks: - backend environment: - discovery.type=single-node - http.host=0.0.0.0 - transport.host=127.0.0.1 - xpack.security.enabled=false # Disable security features - xpack.security.http.ssl.enabled=false # Disable HTTPS - action.destructive_requires_name=false - xpack.monitoring.collection.enabled=false # Disable monitoring features - ES_LOG_LEVEL=trace # Set log level to capture all queries - logger.org.elasticsearch.index.search.slowlog=TRACE # Log all slow queries ports: - "9200:9200" healthcheck: test: [ "CMD-SHELL", "curl -f http://localhost:9200 || exit 1" ] interval: 10s timeout: 10s retries: 30 jaeger: networks: backend: # This is the host name used in Prometheus scrape configuration. aliases: [ spm_metrics_source ] image: jaegertracing/jaeger:${JAEGER_VERSION:-latest} volumes: - "./jaeger-ui.json:/etc/jaeger/jaeger-ui.json" # Do we need this for v2 ? Seems to be running without this. - "../../cmd/jaeger/config-spm-elasticsearch.yaml:/etc/jaeger/config.yml" command: ["--config", "/etc/jaeger/config.yml"] environment: - SPANMETRICS_FLUSH_INTERVAL=${SPANMETRICS_FLUSH_INTERVAL:-60s} ports: - "16686:16686" # Jaeger UI http://localhost:16686 - "8888:8888" - "8889:8889" - "4317:4317" - "4318:4318" depends_on: elasticsearch: condition: service_healthy microsim: networks: - backend image: yurishkuro/microsim:v0.6.0@sha256:fd75a9b3dd1bb4d7d305a562edeac60051a7fec784b898ff7ab834eacc73f41e command: "-d 24h -s 500ms" environment: - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://jaeger:4318/v1/traces depends_on: - jaeger networks: backend: volumes: esdata: driver: local ================================================ FILE: docker-compose/monitor/docker-compose-opensearch.yml ================================================ services: opensearch: image: opensearchproject/opensearch:3.5.0@sha256:919ff4e7d0d57dbc4bd0999ddf0e43e961bba844ec2a5b6734fc979eb4e32399 networks: - backend environment: - discovery.type=single-node - plugins.security.disabled=true - http.host=0.0.0.0 - transport.host=127.0.0.1 - OPENSEARCH_INITIAL_ADMIN_PASSWORD=passRT%^#234 ports: - "9200:9200" healthcheck: test: [ "CMD-SHELL", "curl -f http://localhost:9200 || exit 1" ] interval: 10s timeout: 10s retries: 30 jaeger: networks: backend: # This is the host name used in Prometheus scrape configuration. aliases: [ spm_metrics_source ] image: jaegertracing/jaeger:${JAEGER_VERSION:-latest} volumes: - "./jaeger-ui.json:/etc/jaeger/jaeger-ui.json" - "../../cmd/jaeger/config-spm-opensearch.yaml:/etc/jaeger/config.yml" command: ["--config", "/etc/jaeger/config.yml"] environment: - SPANMETRICS_FLUSH_INTERVAL=${SPANMETRICS_FLUSH_INTERVAL:-60s} ports: - "16686:16686" # Jaeger UI http://localhost:16686 - "8888:8888" - "8889:8889" - "4317:4317" - "4318:4318" depends_on: opensearch: condition: service_healthy microsim: networks: - backend image: yurishkuro/microsim:v0.6.0@sha256:fd75a9b3dd1bb4d7d305a562edeac60051a7fec784b898ff7ab834eacc73f41e command: "-d 24h -s 500ms" environment: - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://jaeger:4318/v1/traces depends_on: - jaeger networks: backend: volumes: esdata: driver: local ================================================ FILE: docker-compose/monitor/docker-compose.yml ================================================ services: jaeger: networks: backend: # This is the host name used in Prometheus scrape configuration. aliases: [ spm_metrics_source ] image: jaegertracing/jaeger:${JAEGER_VERSION:-latest} volumes: - "./jaeger-ui.json:/etc/jaeger/jaeger-ui.json" # Do we need this for v2 ? Seems to be running without this. - "../../cmd/jaeger/config-spm.yaml:/etc/jaeger/config.yml" command: ["--config", "/etc/jaeger/config.yml"] environment: - SPANMETRICS_FLUSH_INTERVAL=${SPANMETRICS_FLUSH_INTERVAL:-60s} ports: - "16686:16686" - "16687:16687" - "8888:8888" - "8889:8889" - "4317:4317" - "4318:4318" microsim: networks: - backend image: yurishkuro/microsim:v0.6.0@sha256:fd75a9b3dd1bb4d7d305a562edeac60051a7fec784b898ff7ab834eacc73f41e command: "-d 24h -s 500ms" environment: - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://jaeger:4318/v1/traces depends_on: - jaeger prometheus: networks: - backend image: prom/prometheus:v3.10.0@sha256:4a61322ac1103a0e3aea2a61ef1718422a48fa046441f299d71e660a3bc71ae9 volumes: - "./prometheus.yml:/etc/prometheus/prometheus.yml" ports: - "9090:9090" networks: backend: ================================================ FILE: docker-compose/monitor/jaeger-ui.json ================================================ { "monitor": { "menuEnabled": true }, "dependencies": { "menuEnabled": true } } ================================================ FILE: docker-compose/monitor/otel-collector-config-connector.yml ================================================ receivers: otlp: protocols: grpc: http: endpoint: "0.0.0.0:4318" exporters: prometheus: endpoint: "0.0.0.0:8889" otlp: endpoint: jaeger:4317 tls: insecure: true connectors: spanmetrics: processors: batch: service: pipelines: traces: receivers: [otlp] processors: [batch] exporters: [spanmetrics, otlp] # The metrics pipeline receives generated span metrics from 'spanmetrics' connector # and pushes to Prometheus exporter, which makes them available for scraping on :8889. metrics/spanmetrics: receivers: [spanmetrics] exporters: [prometheus] ================================================ FILE: docker-compose/monitor/prometheus.yml ================================================ global: # Set low scrape and evaluation intervals to speed up e2e test scrape_interval: 5s # Default is every 15sec evaluation_interval: 5s # Default is every 15sec scrape_configs: - job_name: aggregated-trace-metrics static_configs: - targets: ['spm_metrics_source:8889'] - job_name: jaeger-collector-metrics static_configs: - targets: [ 'spm_metrics_source:8888' ] ================================================ FILE: docker-compose/opensearch/v1/docker-compose.yml ================================================ services: opensearch: image: opensearchproject/opensearch:1.3.17@sha256:749c568af3106a12b1600673ab8dd2d1980d1c699a09f10cbcf6a003d8e38aed environment: - discovery.type=single-node - plugins.security.disabled=true - http.host=0.0.0.0 - transport.host=127.0.0.1 ports: - "9200:9200" ================================================ FILE: docker-compose/opensearch/v2/docker-compose.yml ================================================ services: opensearch: image: opensearchproject/opensearch:2.19.0@sha256:1f8b88245a6af61e7aa500afe0e87d43401e4b33140bb47230a919428ce3f7cb environment: - discovery.type=single-node - plugins.security.disabled=true - http.host=0.0.0.0 - transport.host=127.0.0.1 - OPENSEARCH_INITIAL_ADMIN_PASSWORD=passRT%^#234 ports: - "9200:9200" ================================================ FILE: docker-compose/opensearch/v3/docker-compose.yml ================================================ services: opensearch: image: opensearchproject/opensearch:3.5.0@sha256:2f2244c7c0ad3a4a0d09977c2b519977b77bacc92e1eaf995c080c1d22f517b6 environment: - discovery.type=single-node - plugins.security.disabled=true - http.host=0.0.0.0 - transport.host=127.0.0.1 - OPENSEARCH_INITIAL_ADMIN_PASSWORD=passRT%^#234 ports: - "9200:9200" ================================================ FILE: docker-compose/scylladb/README.md ================================================ ## ScyllaDB as a storage backend Jaeger could be configured to use ScyllaDB as a storage backend. This is an experimental feature and this is not an officially supported backend, meaning that Jaeger team will not proactively address any issues that may arise from incompatibilities between the ScyllaDB and Cassandra databases (the team may still accept PRs). ### Configuration Setup Jaeger server to use Cassandra database and just replace conn string to ScyllaDB cluster. No additional configuration is required. ### Known issues #### Protocol version Jaeger server detects Cassandra protocol version automatically. At the date of the demo with specified versions server detects that it connected via protocol version 3 while it is actually 4. This leads to warn log in cassandra-schema container: ``` WARN: DESCRIBE|DESC was moved to server side in Cassandra 4.0. As a consequence DESRIBE|DESC will not work in cqlsh '6.0.0' connected to Cassandra '3.0.8', the version that you are connected to. DESCRIBE does not exist server side prior Cassandra 4.0. Cassandra version detected: 3 ``` Otherwise, it should be fully compatible. ### Demo Docker compose file consists of Jaeger server, Jaeger Cassandra schema writer, Jaeger UI, Jaeger Demo App `HotRod` and a ScyllaDB cluster. There is a known issue with docker compose network configuration and containers connectivity on Apple silicone. Sometimes it's helpful to manually create the docker network before running `docker compose up`: ```shell docker network create --driver bridge jaeger-scylladb ``` #### Spin up all infrastructure: ```shell docker compose up -d ``` Will: 1. Create ScyllaDB cluster with 3 nodes(about 1 min to initialize) 2. Generate the schema for jaeger key space 3. Start Jaeger server, Jaeger UI and Jaeger Demo App `HotRod` #### Run demo app 1. Wait till all containers are up and running 2. Open Demo app in your browser: http://localhost:8080 and click some buttons. 3. Open Jaeger UI in your browser: http://localhost:16686 and check traces ================================================ FILE: docker-compose/scylladb/docker-compose.yml ================================================ # docker compose file to test Scylla with Jaeger. # Disclaimer: This defaults to using 'latest' image tag for Jaeger images, # which can be stale in your local repository. In case of issues try running # against the actual Jaeger version like JAEGER_VERSION=1.59.0. networks: jaeger-scylladb: services: jaeger: restart: unless-stopped image: cr.jaegertracing.io/jaegertracing/jaeger:${JAEGER_VERSION:-latest} volumes: - ../../cmd/jaeger/config-cassandra.yaml:/etc/jaeger/config.yaml command: - "--config=/etc/jaeger/config.yaml" environment: - CASSANDRA_CONTACT_POINTS=scylladb:9042 ports: - 16686:16686 - 16687:16687 - 4317:4317 - 4318:4318 networks: - jaeger-scylladb depends_on: - cassandra-schema cassandra-schema: image: cr.jaegertracing.io/jaegertracing/jaeger-cassandra-schema:${JAEGER_VERSION:-latest} environment: CASSANDRA_PROTOCOL_VERSION: 4 CASSANDRA_VERSION: 4 CQLSH_HOST: scylladb DATACENTER: test MODE: test networks: - jaeger-scylladb depends_on: scylladb: condition: service_healthy scylladb: restart: always image: scylladb/scylla:5.4.7@sha256:d73f652cbce3622827eeff35a650936d70b2bf2939ea5dd6b7e6c3e8944537fe ports: - 9042:9042 volumes: - .docker/scylladb/1:/var/lib/scylla networks: - jaeger-scylladb healthcheck: test: ["CMD", "cqlsh", "-e", "describe keyspaces"] interval: 1s retries: 120 timeout: 1s scylladb2: restart: always image: scylladb/scylla:5.4.7@sha256:d73f652cbce3622827eeff35a650936d70b2bf2939ea5dd6b7e6c3e8944537fe command: --seeds=scylladb volumes: - .docker/scylladb/2:/var/lib/scylla networks: - jaeger-scylladb scylladb3: restart: always image: scylladb/scylla:5.4.7@sha256:d73f652cbce3622827eeff35a650936d70b2bf2939ea5dd6b7e6c3e8944537fe command: --seeds=scylladb volumes: - .docker/scylladb/3:/var/lib/scylla networks: - jaeger-scylladb hotrod: image: cr.jaegertracing.io/jaegertracing/example-hotrod:${JAEGER_VERSION:-latest} container_name: hotrod ports: - 8080:8080 command: [ "all" ] environment: - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318 networks: - jaeger-scylladb depends_on: - jaeger ================================================ FILE: docker-compose/tail-sampling/Makefile ================================================ # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 BINARY ?= jaeger .PHONY: build build: clean-jaeger cd ../../ && make build-$(BINARY) GOOS=linux cd ../../ && make create-baseimg PLATFORMS=linux/$(shell go env GOARCH) cd ../../ && docker buildx build --target release \ --tag jaegertracing/$(BINARY):dev \ --build-arg base_image=localhost:5000/baseimg_alpine:latest \ --build-arg debug_image=not-used \ --build-arg TARGETARCH=$(shell go env GOARCH) \ --load \ cmd/$(BINARY) .PHONY: dev dev: export JAEGER_VERSION = dev dev: build docker compose -f docker-compose.yml up $(DOCKER_COMPOSE_ARGS) .PHONY: clean-jaeger clean-jaeger: # Also cleans up intermediate cached containers. docker system prune -f .PHONY: clean-all clean-all: clean-jaeger docker rmi -f otel/opentelemetry-collector-contrib:latest ================================================ FILE: docker-compose/tail-sampling/README.md ================================================ # Tail-Based Sampling Processor This `docker compose` environment provides a sample configuration of a Jaeger collector utilizing the [Tail-Based Sampling Processor in OpenTelemtry](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/tailsamplingprocessor/README.md). ## Description of Setup The `docker-compose.yml` contains three services and their functions are outlined as follows: 1. `jaeger` - This is the Jaeger V2 collector that samples traces using the `tail_sampling` processor. The configuration for this service is in [jaeger-v2-config.yml](./jaeger-v2-config.yml). The `tail_sampling` processor has one policy that only captures traces from the services `tracegen-02` and `tracegen-04`. For a full list of policies that can be added to the `tail_sampling` processor, check out [this README](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/tailsamplingprocessor/README.md). 2. `otel_collector` - This is an OpenTelemtry collector with a `loadbalancing` exporter that routes requests to `jaeger`. The configuration for this service is in [otel-collector-config-connector.yml](./otel-collector-config-connector.yml). The purpose of this collector is to collect spans from different services and forward all spans with the same `traceID` to the same downstream collector instance (`jaeger` in this case), so that sampling decisions for a given trace can be made in the same collector instance. 3. `tracegen` - This is a service that generates traces for 5 different services and sends them to `otel_collector` (which will in turn send them to `jaeger`). Note that in this minimal setup, a `loadbalancer` collector is not necessary since we are only running a single instance of the `jaeger` collector. In a real-world distributed system running multiple instances of the `jaeger` collector, a load balancer is necessary to avoid spans from the same trace being routed to different collector instances. ## Running the Example The example can be run using the following command: ```bash make dev ``` To see the tail-based sampling processor in action, go to the Jaeger UI at . You will see that only traces for the services outlined in the policy in [jaeger-v2-config.yml](./jaeger-v2-config.yml) are sampled. ================================================ FILE: docker-compose/tail-sampling/docker-compose.yml ================================================ services: jaeger: networks: backend: image: cr.jaegertracing.io/jaegertracing/jaeger:${JAEGER_VERSION:-latest} volumes: - "./jaeger-v2-config.yml:/etc/jaeger/config.yml" command: ["--config", "/etc/jaeger/config.yml"] ports: - "16686:16686" - "4317" otel_collector: networks: backend: image: otel/opentelemetry-collector-contrib:${OTEL_IMAGE_TAG:-0.108.0} volumes: - ${OTEL_CONFIG_SRC:-./otel-collector-config-connector.yml}:/etc/otelcol/otel-collector-config.yml command: --config /etc/otelcol/otel-collector-config.yml depends_on: - jaeger ports: - "4318" tracegen: networks: - backend image: cr.jaegertracing.io/jaegertracing/jaeger-tracegen:2.15.1@sha256:8149733c9c54c2b272d2141388aa4f0ae95704c814df976b434fba162752a235 environment: - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://otel_collector:4318/v1/traces command: ["-workers", "3", "-pause", "250ms", "-services", "5", "-duration", "10s"] depends_on: - jaeger networks: backend: ================================================ FILE: docker-compose/tail-sampling/jaeger-v2-config.yml ================================================ service: extensions: [jaeger_storage, jaeger_query, healthcheckv2] pipelines: traces: receivers: [otlp] processors: [tail_sampling] exporters: [jaeger_storage_exporter] telemetry: logs: level: DEBUG extensions: healthcheckv2: use_v2: true http: jaeger_query: storage: traces: some_storage jaeger_storage: backends: some_storage: memory: max_traces: 100000 receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 processors: tail_sampling: decision_wait: 5s policies: [ { name: filter-by-attribute, type: string_attribute, string_attribute: { key: service.name, values: [tracegen-02, tracegen-04] }, }, ] exporters: jaeger_storage_exporter: trace_storage: some_storage ================================================ FILE: docker-compose/tail-sampling/otel-collector-config-connector.yml ================================================ receivers: otlp: protocols: http: endpoint: 0.0.0.0:4318 exporters: loadbalancing: routing_key: "traceID" protocol: otlp: timeout: 1s tls: insecure: true resolver: static: hostnames: - jaeger:4317 service: pipelines: traces: receivers: - otlp processors: [] exporters: - loadbalancing ================================================ FILE: docs/SCARF.md ================================================ # Setup for Scarf This document outlines our implementation details for [Scarf](https://scarf.sh) which provides usage and download analytics for the Jaeger project. ## DNS Configuration The following CNAMES were setup in Netlify for us to utilize the services: * `scarf.jaegertracing.io` -> `static.scarf.sh` (used for tracking pixel on webpages) * `cr.jaegertracing.io` -> `gateway.scarf.sh` (used for container registries) * `download.jaegertracing.io` -> `gateway.scarf.sh` (used for file downloads of Jaeger artifacts) We also had to add the following TXT verification records: 1. _scarf-sh-challenge-jaeger.cr.jaegertracing.io - ZN4RLVE3CENVUIXDBNYCa 2. _scarf-sh-challenge-jaeger.download.jaegertracing.io - U2GBROI64YGH2JLRTXPI 3. _scarf-sh-challenge-jaeger.scarf.jaegertracing.io - AKB26262A53WP55R4EXR ## Download and Docker Configuration The following setup has been done on Scarf. Previously what was the download link for example https://github.com/jaegertracing/jaeger/releases/download/v1.69.0/jaeger-2.6.0-darwin-amd64.tar.gz should now be https://download.jaegertracing.io/v1.69.0/jaeger-2.6.0-darwin-amd64.tar.gz for us to get analytics. For Docker containers the previous command : docker pull jaegertracing/all-in-one should now be docker pull cr.jaegertracing.io/jaegertracing/all-in-one # Integrating Scarf Analytics with `www.jaegertracing.io` on Netlify Injecting the Scarf analytics tracking pixel to the `www.jaegertracing.io` website hosted on Netlify is done using Netlify's **Snippet Injection** feature. ## Steps to Add the Scarf Pixel 1. **Log in to Netlify:** * Head over to [app.netlify.com](https://app.netlify.com/) and sign in to your account which has access to the `www.jaegertracing.io` site configuration. 2. **Select Your Site:** * From the list of sites, click on `www.jaegertracing.io`. 3. **Go to Project Configuration:** * On the `www.jaegertracing.io` site dashboard, click on **"Site settings"** (you'll usually find this at the top right). 4. **Access Build & Deploy Settings:** * In the left-hand sidebar, under "Site settings," click on **"Build & deploy."** 5. **Find Snippet Injection:** * Scroll down to the "Post processing" section and find the **"Snippet Injection"** section. * Click the **"Add Snippet"** button. 6. **Configure the Scarf Snippet:** * A form will pop up for your new snippet: * **Snippet Name:** Give it a clear name like `Scarf Tracking Pixel`. * **Position:** Choose **"Before ``"**. This is generally a good spot for image-based pixels as it doesn't block the initial page render. * **Snippet Body:** Paste the following HTML code block into this text area. **The pixel ID `cf7517a5-bfa0-4796-b760-1bb4e302e541` is already included.** ```html ``` 7. **Save the Snippet:** * Click the **"Save"** button to apply the changes. ----- ## Verification Once you save, Netlify will automatically inject this image tag into the HTML pages of `www.jaegertracing.io`. You won't need to trigger a new deployment. Scarf.sh should start receiving analytics data from the website shortly after. To verify the pixel is loading correctly: * Visit `www.jaegertracing.io` in a browser. * Open your browser's **developer tools** (usually by pressing F12 or right-clicking and selecting "Inspect"). * Go to the **"Network"** tab and filter by "a.png". You should see requests being made to `https://scarf.jaegertracing.io/a.png`. ================================================ FILE: docs/adr/001-cassandra-find-traces-duration.md ================================================ # Cassandra FindTraceIDs Duration Query Behavior * **Status**: Documented existing implementation * **Date**: 2026-01-03 ## Context The Cassandra spanstore implementation in Jaeger handles trace queries with duration filters (DurationMin/DurationMax) through a separate code path that cannot efficiently intersect with other query parameters like tags or general operation name filters. This behavior differs from other storage backends like Badger and may seem counterintuitive to users. ### Data Model and Cassandra Constraints Cassandra's data model imposes specific constraints on query patterns. The `duration_index` table is defined with the following schema structure (as referenced in the CQL insertion query in [`internal/storage/v1/cassandra/spanstore/writer.go`](../../internal/storage/v1/cassandra/spanstore/writer.go)): ```cql INSERT INTO duration_index(service_name, operation_name, bucket, duration, start_time, trace_id) VALUES (?, ?, ?, ?, ?, ?) ``` This schema uses a composite partition key consisting of `service_name`, `operation_name`, and `bucket` (an hourly time bucket), with `duration` as a clustering column. In Cassandra, **partition keys require equality constraints** in WHERE clauses - you cannot perform range queries or arbitrary intersections across different partition keys efficiently. ### Duration Index Structure The duration index is bucketed by hour to limit partition size and improve query performance. From [`internal/storage/v1/cassandra/spanstore/writer.go`](../../internal/storage/v1/cassandra/spanstore/writer.go) (line 57): ```go durationBucketSize = time.Hour ``` When a span is indexed, its start time is rounded to the nearest hour bucket (line 231 in writer.go): ```go timeBucket := startTime.Round(durationBucketSize) ``` The indexing function in `indexByDuration` (lines 229-243) creates two index entries per span: 1. One indexed by service name alone (with empty operation name) 2. One indexed by both service name and operation name ```go indexByOperationName("") // index by service name alone indexByOperationName(span.OperationName) // index by service name and operation name ``` ### Query Path Implementation In [`internal/storage/v1/cassandra/spanstore/reader.go`](../../internal/storage/v1/cassandra/spanstore/reader.go), the `findTraceIDs` method (lines 275-301) performs an early return when duration parameters are present: ```go func (s *SpanReader) findTraceIDs(ctx context.Context, traceQuery *spanstore.TraceQueryParameters) (dbmodel.UniqueTraceIDs, error) { if traceQuery.DurationMin != 0 || traceQuery.DurationMax != 0 { return s.queryByDuration(ctx, traceQuery) } // ... other query paths } ``` This early return means that when a duration query is detected, **all other query parameters except ServiceName and OperationName are effectively ignored** (tags, for instance, are not processed). The `queryByDuration` method (lines 333-375) iterates over hourly buckets within the query time range and issues a Cassandra query for each bucket: ```go startTimeByHour := traceQuery.StartTimeMin.Round(durationBucketSize) endTimeByHour := traceQuery.StartTimeMax.Round(durationBucketSize) for timeBucket := endTimeByHour; timeBucket.After(startTimeByHour) || timeBucket.Equal(startTimeByHour); timeBucket = timeBucket.Add(-1 * durationBucketSize) { query := s.session.Query( queryByDuration, timeBucket, traceQuery.ServiceName, traceQuery.OperationName, minDurationMicros, maxDurationMicros, traceQuery.NumTraces*limitMultiple) // execute query... } ``` Each query specifies exact values for `bucket`, `service_name`, and `operation_name` (the partition key components), along with a range filter on `duration` (the clustering column). The query definition (lines 51-55) is: ```cql SELECT trace_id FROM duration_index WHERE bucket = ? AND service_name = ? AND operation_name = ? AND duration > ? AND duration < ? LIMIT ? ``` ### Why Not Intersect with Other Indices? Unlike storage backends such as Badger (which can perform hash-joins and arbitrary index intersections), Cassandra's partition-based architecture makes cross-index intersections expensive and impractical: 1. **Partition key constraints**: The duration index requires equality on `(service_name, operation_name, bucket)`. You cannot efficiently query across multiple operations or join with the tag index without scanning many partitions. 2. **No server-side joins**: Cassandra does not support server-side joins. To intersect duration results with tag results, the client would need to: - Query the duration index for all matching trace IDs - Query the tag index for all matching trace IDs - Perform a client-side intersection This would be inefficient for large result sets and would require fetching potentially many trace IDs over the network. 3. **Hourly bucket iteration**: The duration query already iterates over hourly buckets. Adding tag intersections would multiply the number of queries and result sets to merge. ### Comparison with Badger The Badger storage backend handles duration queries differently. In [`internal/storage/v1/badger/spanstore/reader.go`](../../internal/storage/v1/badger/spanstore/reader.go) (around line 486), the `FindTraceIDs` method performs duration queries and then uses the results as a filter (`hashOuter`) that can be intersected with other index results: ```go if query.DurationMax != 0 || query.DurationMin != 0 { plan.hashOuter = r.durationQueries(plan, query) } ``` Badger uses an embedded key-value store where range scans and in-memory filtering are efficient, allowing it to merge results from multiple indices. This is a fundamental difference from Cassandra's distributed, partition-oriented design. ## Decision **The Cassandra spanstore will continue to treat duration queries as a separate query path that does not intersect with tag indices or other non-service/operation filters.** When a `TraceQueryParameters` contains `DurationMin` or `DurationMax`: - The query will use the `duration_index` table exclusively - Only `ServiceName` and `OperationName` parameters will be respected (used as partition key components) - Tag filters and other parameters will be ignored - The code will iterate over hourly time buckets within the query time range This approach is documented in code comments and in this ADR to set proper expectations. ## Consequences ### Positive 1. **Performance**: Duration queries execute efficiently by scanning only relevant Cassandra partitions (scoped to service, operation, and hourly bucket). 2. **Scalability**: The bucketed partition strategy prevents hot partitions and distributes load across the cluster. 3. **Simplicity**: The implementation is straightforward and leverages Cassandra's strengths (partition-scoped queries with range filtering on clustering columns). ### Negative 1. **Limited query expressiveness**: Users cannot combine duration filters with tag filters in a single query. They must choose one or the other. 2. **Expectation mismatch**: Users familiar with other backends (like Badger) may expect duration and tags to be combinable. 3. **Workarounds required**: Applications that need both duration and tag filtering must: - Issue separate queries (one with duration, one with tags) - Perform client-side intersection of results - Or use a different storage backend that supports combined queries ### Guidance for Users - **When using Cassandra spanstore**: Be aware that specifying `DurationMin` or `DurationMax` will cause tag filters to be ignored. Validate that `ErrDurationAndTagQueryNotSupported` is returned if both are specified (enforced in `validateQuery` at line 227-229 in reader.go). - **For combined filtering needs**: Consider using the Badger backend, or implement client-side filtering by: 1. Querying with duration filters to get a candidate set of trace IDs 2. Fetching those traces 3. Filtering the results by tag values in your application code - **Query design**: Structure queries to leverage the indices available. Use `ServiceName` and `OperationName` in conjunction with duration queries for best results. ## References - Implementation files: - [`internal/storage/v1/cassandra/spanstore/reader.go`](../../internal/storage/v1/cassandra/spanstore/reader.go) - Query logic and duration query path - [`internal/storage/v1/cassandra/spanstore/writer.go`](../../internal/storage/v1/cassandra/spanstore/writer.go) - Duration index schema and insertion logic - [`internal/storage/v1/badger/spanstore/reader.go`](../../internal/storage/v1/badger/spanstore/reader.go) - Badger implementation for comparison - Cassandra documentation: - [Cassandra Data Modeling](https://cassandra.apache.org/doc/latest/data_modeling/index.html) - [CQL Partition Keys and Clustering Columns](https://cassandra.apache.org/doc/latest/cql/ddl.html#partition-key) - Related code: - `durationIndex` constant (writer.go line 47-50): CQL insert statement - `queryByDuration` constant (reader.go line 51-55): CQL select statement - `durationBucketSize` constant (writer.go line 57): Hourly bucketing - Error `ErrDurationAndTagQueryNotSupported` (reader.go line 77): Validation that prevents combining duration and tag queries ================================================ FILE: docs/adr/002-mcp-server.md ================================================ # MCP Server Extension for Jaeger * **Status**: Proposed * **Date**: 2026-01-23 ## Context Large Language Models (LLMs) are increasingly being used as assistants for debugging and analyzing distributed systems. Jaeger, as a distributed tracing platform, contains rich observability data that could help LLMs diagnose issues in microservice architectures. However, distributed traces can be massive—a single trace might contain hundreds or thousands of spans—and loading full trace data directly into an LLM's context window is impractical and often counterproductive. The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open standard that facilitates integration between LLM applications and external data sources. MCP defines a structured way for AI agents to discover and invoke tools, access resources, and receive responses in a format optimized for LLM consumption. ### Progressive Disclosure Architecture The key insight driving this design is **progressive disclosure**: rather than dumping entire traces into an LLM context, we provide tools that allow the LLM to follow a guided "drill-down" workflow: 1. **Search** → Find candidate traces matching specific criteria (service, time range, attributes, duration) 2. **Map** → Visualize trace structure (topology) without loading attribute data 3. **Diagnose** → Identify the critical execution path that contributed to latency 4. **Inspect** → Load full details only for specific, suspicious spans This approach prevents context-window exhaustion and forces structured reasoning. ### Dependencies The official MCP Go SDK is available at [`github.com/modelcontextprotocol/go-sdk`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp), maintained in collaboration with Google. This SDK supports: - Tool registration with JSON schema validation - StdIO and HTTP transports (including Streamable HTTP) - Server-Sent Events (SSE) for real-time updates - Concurrent session management ### Existing Infrastructure Jaeger already provides most of the backend functionality needed: | MCP Tool | Existing Jaeger Component | Notes | |----------|---------------------------|-------| | `get_services` | `QueryService.GetServices()` | Direct mapping | | `search_traces` | `QueryService.FindTraces()` | Returns metadata; needs filtering | | `get_trace_topology` | `QueryService.GetTrace()` | Needs post-processing to strip attributes | | `get_span_details` | `QueryService.GetTrace()` | Needs span-level filtering | | `get_trace_errors` | `QueryService.GetTrace()` | Needs error status filtering | | `get_critical_path` | **Not available in backend** | Only exists in UI (TypeScript) | > [!IMPORTANT] > The critical path algorithm currently exists only in the Jaeger UI codebase ([`jaeger-ui/packages/jaeger-ui/src/components/TracePage/CriticalPath/index.tsx`](../../jaeger-ui/packages/jaeger-ui/src/components/TracePage/CriticalPath/index.tsx)). This algorithm must be re-implemented in Go for the MCP server. ### Extension Architecture Following the pattern established by `jaegerquery`, the MCP server will be implemented as an OpenTelemetry Collector extension. This provides: - Lifecycle management (Start/Shutdown) - Configuration validation - Dependency injection via `jaegerquery` extension - Separate HTTP/SSE endpoint for MCP protocol > [!NOTE] > **Phase 2 Requirement**: The MCP extension will need to retrieve the `QueryService` instance from the `jaegerquery` extension. This will require `jaegerquery` to expose `QueryService` through an Extension interface, similar to how `jaegerstorage` exposes storage factories via the `jaegerstorage.Extension` interface and `GetTraceStoreFactory()` helper function. See `cmd/jaeger/internal/exporters/storageexporter/exporter.go:35` for reference implementation pattern. ## Decision Implement an MCP server as a new extension under `cmd/jaeger/internal/extension/mcpserver/` that: 1. **Exposes MCP tools** for trace search, topology viewing, critical path analysis, and span inspection 2. **Runs on a separate HTTP port** (default: 4320) with Streamable HTTP transport 3. **Depends on `jaegerstorage`** for trace data access, similar to `jaegerquery` 4. **Implements critical path algorithm** in Go, ported from the UI's TypeScript implementation 5. **Uses progressive disclosure** to minimize token consumption in LLM contexts ### MCP Tools Specification ```yaml tools: - name: get_services description: List available service names. Use this first to discover valid service names for search_traces. input_schema: pattern: string (optional) - Filter services by pattern (substring match). Future: may support regex or semantic search. limit: integer (optional, default: 100) - Maximum number of services to return output: List of service names (strings) - name: get_span_names description: List available span names for a service. Useful for discovering valid span names before using search_traces. input_schema: service_name: string (required) - Filter by service name. Use get_services to discover valid names. pattern: string (optional) - Optional regex pattern to filter span names span_kind: string (optional) - Optional span kind filter (e.g., SERVER, CLIENT, PRODUCER, CONSUMER, INTERNAL) limit: integer (optional, default: 100) - Maximum number of span names to return output: List of span names with span kind information - name: search_traces description: Find traces matching service, time, attributes, and duration criteria. Returns metadata only. input_schema: start_time_min: string (optional, default: "-1h") - Start of time interval. Supports RFC3339 or relative (e.g., "-1h", "-30m") start_time_max: string (optional) - End of time interval. Supports RFC3339 or relative (e.g., "now", "-1m"). Default: now service_name: string (required) - Filter by service name. Use get_services to discover valid names. span_name: string (optional) - Filter by span name. Use get_span_names to discover valid names. attributes: object (optional) - Key-value pairs to match against span/resource attributes (e.g., {"http.status_code": "500"}) with_errors: boolean (optional) - If true, only return traces containing error spans duration_min: duration string (optional, e.g., "2s", "100ms") duration_max: duration string (optional) limit: integer (default: 10, max: 100) output: List of trace summaries (trace_id, service_count, span_count, duration, has_errors) - name: get_trace_topology description: Get the structural tree of a trace showing parent-child relationships, timing, and error locations. Does NOT return attributes or logs. input_schema: trace_id: string (required) depth: integer (optional, default: 3) - Maximum depth of the tree. 0 for full tree. output: Tree structure with span metadata (id, service, span_name, duration, error flag, children[]) - name: get_critical_path description: Identify the sequence of spans forming the critical latency path (the blocking execution path). input_schema: trace_id: string (required) output: Ordered list of spans on the critical path with timing information - name: get_span_details description: Fetch full details (attributes, events, links, status) for specific spans. input_schema: trace_id: string (required) span_ids: string[] (required, max 20) output: Full OTLP span data for requested spans - name: get_trace_errors description: Get full details for all spans with error status. input_schema: trace_id: string (required) output: Full OTLP span data for error spans only ``` ### Sample Tool Outputs #### get_services Returns available service names for use in `search_traces`. **Input:** ```json { "pattern": "payment", // optional: substring filter "limit": 100 // optional: max results (default: 100) } ``` **Output:** ```json { "services": ["payment-service", "payment-gateway", "payment-processor"] } ``` --- #### search_traces Find traces matching criteria. Returns lightweight metadata only (no attributes/events). **Input:** ```json { "start_time_min": "-1h", // required: RFC3339 or relative "start_time_max": "now", // optional: default "now" "service_name": "frontend", // required "span_name": "/api/checkout", // optional "attributes": { // optional: match span/resource attributes "http.status_code": "500", "user.id": "12345" }, "with_errors": true, // optional: filter to error traces "duration_min": "2s", // optional "duration_max": "10s", // optional "limit": 10 // optional: default 10, max 100 } ``` **Output:** ```json { "traces": [ { "trace_id": "1a2b3c4d5e6f7890", "root_service": "frontend", "root_span_name": "/api/checkout", "start_time": "2024-01-15T10:30:00Z", "duration_ms": 2450, "span_count": 47, "service_count": 8, "has_errors": true } ] } ``` --- #### get_trace_topology Returns the structural skeleton of a trace—parent-child relationships, timing, and error locations—**without** loading attributes or events. This keeps the response small for LLM context. **Input:** ```json { "trace_id": "1a2b3c4d5e6f7890" } ``` **Output:** ```json { "trace_id": "1a2b3c4d5e6f7890", "root": { "span_id": "span_A", "service": "frontend", "span_name": "/api/checkout", "start_time": "2024-01-15T10:30:00Z", "duration_ms": 2450, "status": "OK", "children": [ { "span_id": "span_B", "service": "cart-service", "span_name": "getCart", "start_time": "2024-01-15T10:30:00.050Z", "duration_ms": 120, "status": "OK", "children": [] }, { "span_id": "span_C", "service": "payment-service", "span_name": "processPayment", "start_time": "2024-01-15T10:30:00.200Z", "duration_ms": 2200, "status": "ERROR", "children": [ { "span_id": "span_D", "service": "payment-gateway", "span_name": "chargeCard", "start_time": "2024-01-15T10:30:00.250Z", "duration_ms": 2100, "status": "ERROR", "children": [] } ] } ] } } ``` --- #### get_critical_path Returns the sequence of spans that form the critical latency path—the "blocking" execution path that directly contributed to total trace duration. **Input:** ```json { "trace_id": "1a2b3c4d5e6f7890" } ``` **Output:** ```json { "trace_id": "1a2b3c4d5e6f7890", "total_duration_ms": 2450, "critical_path_duration_ms": 2400, "path": [ { "span_id": "span_A", "service": "frontend", "span_name": "/api/checkout", "self_time_ms": 50, "section_start_ms": 0, "section_end_ms": 50 }, { "span_id": "span_C", "service": "payment-service", "span_name": "processPayment", "self_time_ms": 100, "section_start_ms": 50, "section_end_ms": 150 }, { "span_id": "span_D", "service": "payment-gateway", "span_name": "chargeCard", "self_time_ms": 2100, "section_start_ms": 150, "section_end_ms": 2250 }, { "span_id": "span_A", "service": "frontend", "span_name": "/api/checkout", "self_time_ms": 200, "section_start_ms": 2250, "section_end_ms": 2450 } ] } ``` > [!NOTE] > A span may appear multiple times on the critical path (e.g., `span_A` above) if it has work both before and after its children execute. --- #### get_span_details Fetch full OTLP span data for specific spans. Use this only after identifying suspicious spans via topology or critical path. **Input:** ```json { "trace_id": "1a2b3c4d5e6f7890", "span_ids": ["span_C", "span_D"] // max 20 spans } ``` **Output:** ```json { "trace_id": "1a2b3c4d5e6f7890", "spans": [ { "span_id": "span_C", "trace_id": "1a2b3c4d5e6f7890", "parent_span_id": "span_A", "service": "payment-service", "span_name": "processPayment", "start_time": "2024-01-15T10:30:00.200Z", "duration_ms": 2200, "status": { "code": "ERROR", "message": "Upstream service timeout" }, "attributes": { "http.method": "POST", "http.url": "http://payment-gateway/charge", "http.status_code": "504", "retry.count": "3" }, "events": [ { "name": "retry_attempt", "timestamp": "2024-01-15T10:30:00.700Z", "attributes": {"attempt": "1"} }, { "name": "retry_attempt", "timestamp": "2024-01-15T10:30:01.200Z", "attributes": {"attempt": "2"} } ], "links": [] }, { "span_id": "span_D", "trace_id": "1a2b3c4d5e6f7890", "parent_span_id": "span_C", "service": "payment-gateway", "span_name": "chargeCard", "start_time": "2024-01-15T10:30:00.250Z", "duration_ms": 2100, "status": { "code": "ERROR", "message": "Connection timeout to payment processor" }, "attributes": { "db.system": "postgresql", "db.statement": "SELECT * FROM transactions WHERE...", "net.peer.name": "payment-db.internal", "net.peer.port": "5432" }, "events": [], "links": [] } ] } ``` --- #### get_trace_errors Shortcut to get full details for all error spans in a trace. **Input:** ```json { "trace_id": "1a2b3c4d5e6f7890" } ``` **Output:** ```json { "trace_id": "1a2b3c4d5e6f7890", "error_count": 2, "spans": [ // Same format as get_span_details output // Contains only spans where status.code == "ERROR" ] } ``` ### Configuration ```yaml extensions: jaeger_mcp: # HTTP endpoint for MCP protocol (Streamable HTTP transport) http: endpoint: "0.0.0.0:4320" # Storage configuration (references jaegerstorage extension) storage: traces: "some_storage" # Server identification for MCP protocol server_name: "jaeger" server_version: "${version}" # Limits max_span_details_per_request: 20 max_search_results: 100 ``` ### Extension Directory Structure ``` cmd/jaeger/internal/extension/jaegermcp/ ├── README.md ├── config.go # Configuration struct and validation ├── config_test.go ├── factory.go # Extension factory (NewFactory, createDefaultConfig) ├── factory_test.go ├── server.go # Extension lifecycle (Start, Shutdown, Dependencies) ├── server_test.go └── internal/ ├── criticalpath/ # Critical path algorithm (ported from UI) │ ├── criticalpath.go │ └── criticalpath_test.go ├── handlers/ # MCP tool handlers │ ├── search_traces.go │ ├── search_traces_test.go │ ├── get_trace_topology.go │ ├── get_critical_path.go │ ├── get_span_details.go │ ├── get_span_details_test.go │ ├── get_trace_errors.go │ └── get_trace_errors_test.go └── types/ # Response types for MCP tools (one file per handler) ├── search_traces.go ├── get_span_details.go └── get_trace_errors.go ``` ## Consequences ### Positive 1. **AI Integration**: Enables LLM-based assistants to query and analyze Jaeger traces efficiently 2. **Token Optimization**: Progressive disclosure architecture prevents context exhaustion 3. **Standards Compliance**: Uses official MCP protocol, compatible with Claude, GPT, and other MCP-enabled agents 4. **Reusable Algorithm**: Go implementation of critical path can be used for API responses, not just MCP 5. **Clean Separation**: Runs on separate port, doesn't affect existing query service ### Negative 1. **Algorithm Duplication**: Critical path algorithm exists in both TypeScript (UI) and Go (MCP server) 2. **New Dependency**: Adds `github.com/modelcontextprotocol/go-sdk` to dependencies 3. **Maintenance Overhead**: Additional extension to maintain and test ### Mitigation - Consider eventually exposing critical path via the gRPC query API for UI consumption, eliminating the TypeScript implementation - The MCP SDK is official and well-maintained; dependency risk is low - Extension follows established patterns, reducing maintenance burden --- ## Implementation Roadmap ### Phase 1: Foundation 1. **Extension Scaffold** ✅ - Create `jaegermcp` extension directory structure - Implement `config.go` with configuration validation - Implement `factory.go` following `jaegerquery` pattern - Implement `server.go` with lifecycle management - Wire extension into component registration 2. **MCP Server Setup** ✅ - Add `github.com/modelcontextprotocol/go-sdk` dependency - Initialize MCP server with Streamable HTTP transport - Implement server start/shutdown with graceful cleanup --- ### Phase 2: Basic Tools 3. **Storage Integration** ✅ - Connect to `jaegerstorage` extension for trace reader access - Create internal service layer for trace operations 4. **Implement `get_services` Tool** ✅ - Wrap `QueryService.GetServices()` - Support optional regex pattern filtering - Apply configurable limit (default: 100) - Return list of service names 4b. **Implement `get_span_names` Tool** ✅ - Wrap `QueryService.GetOperations()` - Support optional regex pattern filtering - Support optional span kind filtering - Apply configurable limit (default: 100) - Return list of span names with span kind information 5. **Implement `search_traces` Tool** ✅ - Wrap `QueryService.FindTraces()` - Transform response to MCP-optimized format (metadata only) - Add input validation and error handling 6. **Implement `get_span_details` Tool** ✅ - Wrap `QueryService.GetTrace()` - Filter to requested span IDs only - Return full OTLP attribute data 7. **Implement `get_trace_errors` Tool** ✅ - Wrap `QueryService.GetTrace()` - Filter to spans with error status - Return full OTLP attribute data --- ### Phase 3: Advanced Tools 7. **Implement `get_trace_topology` Tool** ✅ - Fetch trace via `QueryService.GetTrace()` - Build tree structure from flat span list - **Strip attributes and events** before response - Include timing and error flags 8. **Port Critical Path Algorithm** ✅ - Study TypeScript implementation in `jaeger-ui/packages/jaeger-ui/src/components/TracePage/CriticalPath/` - Implement equivalent Go algorithm in `internal/criticalpath/` - Key components: - `findLastFinishingChildSpan()` - Find LFC for a span - `sanitizeOverFlowingChildren()` - Handle child spans that exceed parent duration - `computeCriticalPath()` - Main recursive algorithm - Add comprehensive unit tests with same test cases as UI 9. **Implement `get_critical_path` Tool** ✅ - Use critical path algorithm from step 8 - Return ordered list of spans on critical path - Include timing breakdown --- ### Phase 4: Polish and Extend 10. **Configuration and Observability** - Add OpenTelemetry metrics for MCP tool invocations - Add structured logging for debugging - Implement rate limiting if needed 11. **Documentation** - Write `README.md` for the extension - Document MCP server instructions (system prompt) for LLM configuration - Add example configurations 12. **Integration Testing** - End-to-end tests with mock storage - Test MCP protocol compliance - Performance testing with large traces --- ## Testing Strategy ### Unit Tests | Component | Testing Approach | |-----------|------------------| | `config.go` | Test validation with valid/invalid configs | | `factory.go` | Test factory creation and default config | | `server.go` | Test lifecycle with mock storage extension | | Critical path algorithm | Port test cases from TypeScript tests; use same expected results | | Tool handlers | Mock `QueryService`; test input validation, response format, error handling | ### Integration Tests 1. **Extension Lifecycle** - Test extension starts with valid configuration - Test graceful shutdown - Test dependency resolution with `jaegerstorage` 2. **MCP Protocol Compliance** - Use MCP SDK client to connect to server - Verify tool discovery (`tools/list`) - Verify tool invocation and response format - Test error handling (invalid inputs, missing traces) 3. **End-to-End Scenarios** - Use memory storage with sample traces - Execute progressive disclosure workflow (search → topology → critical path → details) - Verify token efficiency (topology response is smaller than full trace) ### Test Fixtures Reuse existing test fixtures from: - `cmd/jaeger/internal/extension/jaegerquery/internal/fixture/` - Sample traces - `jaeger-ui/packages/jaeger-ui/src/components/TracePage/CriticalPath/testCases/` - Critical path test cases ### CI Integration - Add to existing CI workflow - Include in `make test` target - Add to code coverage requirements --- ## References - [Model Context Protocol Specification](https://modelcontextprotocol.io/) - [MCP Go SDK Documentation](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp) - Jaeger Extension Pattern: [`cmd/jaeger/internal/extension/jaegerquery/`](../../cmd/jaeger/internal/extension/jaegerquery/) - Critical Path UI Implementation: [`jaeger-ui/packages/jaeger-ui/src/components/TracePage/CriticalPath/index.tsx`](../../jaeger-ui/packages/jaeger-ui/src/components/TracePage/CriticalPath/index.tsx) - Original Design Document: [`design.md`](../../design.md) ================================================ FILE: docs/adr/003-lazy-storage-factory-initialization.md ================================================ # Lazy Storage Factory Initialization * **Status**: Implemented -- https://github.com/jaegertracing/jaeger/pull/7887 * **Date**: 2026-01-20 ## Context The `jaegerstorage` extension (`cmd/jaeger/internal/extension/jaegerstorage/extension.go`) is responsible for managing storage backends in Jaeger. Its configuration allows declaring arbitrary numbers of storage backends under `trace_backends` and `metric_backends`. However, not all configured storages are necessarily used: consumers request specific storages by name via `TraceStorageFactory(name)`. ### Current Behavior Currently, the extension initializes **all** configured storage factories during `Start()`: ```go func (s *storageExt) Start(ctx context.Context, host component.Host) error { // ... for storageName, cfg := range s.config.TraceBackends { factory, err := storageconfig.CreateTraceStorageFactory(...) // Connects immediately s.factories[storageName] = factory } // ... } ``` Each factory's `NewFactory()` function performs actual initialization: | Backend | Initialization Actions | |---------------|------------------------------------------------------------------| | Cassandra | Creates session, connects to cluster, optionally creates schema | | Elasticsearch | Creates HTTP client, establishes connection pool | | ClickHouse | Opens connection, pings server, optionally creates schema | | gRPC | Establishes gRPC connections (reader and writer) | | Badger | Opens database files, starts background maintenance goroutines | | Memory | Allocates in-memory store | ### Problems 1. **Wasted Resources**: Storage backends that are configured but never used still consume connections, memory, and background goroutines. 2. **Startup Failures for Unused Backends**: If a configured backend is unavailable (e.g., Cassandra cluster down), the entire extension fails to start, even if that storage isn't actually needed by any pipeline component. 3. **Configuration Use Cases**: Users may want to define multiple storage backends in a shared configuration file and selectively enable them in different deployment scenarios without modifying the configuration. ### Real-World Scenario Consider a configuration with: ```yaml extensions: jaeger_storage: trace_backends: primary_es: elasticsearch: { ... } archive_cassandra: cassandra: { ... } debug_memory: memory: { max_traces: 10000 } jaeger_query: storage: traces: primary_es traces_archive: debug_memory ``` With current behavior, Jaeger attempts to connect to Cassandra at startup, even though `archive_cassandra` isn't used. If Cassandra is unavailable, Jaeger fails to start despite the primary storage (Elasticsearch) being fully operational. ## Decision This ADR evaluates two approaches to implement lazy initialization. --- ## Option 1: Two-Phase Factory Framework (Configure + Initialize) ### Design Refactor the factory framework to separate configuration validation from backend initialization: ```go // New interface additions to tracestore.Factory type ConfigurableFactory interface { Factory // Configure validates configuration without establishing connections. // Called during extension Start() for all configured backends. Configure(ctx context.Context) error } type InitializableFactory interface { Factory // Initialize establishes connections and allocates resources. // Called lazily when TraceStorageFactory() is invoked. Initialize(ctx context.Context) error // IsInitialized returns true if Initialize() has been called. IsInitialized() bool } ``` ### Extension Changes ```go type storageExt struct { config *Config telset component.TelemetrySettings factories map[string]tracestore.Factory initialized map[string]bool initMu sync.Mutex // Serializes initialization // ... } func (s *storageExt) Start(ctx context.Context, host component.Host) error { for storageName, cfg := range s.config.TraceBackends { // Phase 1: Configuration only - validate without connecting factory, err := storageconfig.CreateUninitializedFactory(ctx, storageName, cfg, telset) if err != nil { return fmt.Errorf("invalid configuration for storage '%s': %w", storageName, err) } if configurable, ok := factory.(ConfigurableFactory); ok { if err := configurable.Configure(ctx); err != nil { return fmt.Errorf("configuration validation failed for storage '%s': %w", storageName, err) } } s.factories[storageName] = factory } return nil } func (s *storageExt) TraceStorageFactory(name string) (tracestore.Factory, bool) { s.initMu.Lock() defer s.initMu.Unlock() f, ok := s.factories[name] if !ok { return nil, false } // Phase 2: Lazy initialization on first access if !s.initialized[name] { if initializable, ok := f.(InitializableFactory); ok { if err := initializable.Initialize(context.Background()); err != nil { s.telset.Logger.Error("Failed to initialize storage", zap.String("name", name), zap.Error(err)) return nil, false } } s.initialized[name] = true } return f, true } ``` ### Factory Implementation Changes Each factory needs refactoring. Example for Cassandra: ```go type Factory struct { config cassandra.Options telset telemetry.Settings session cassandra.Session // nil until initialized configured bool initialized bool } func NewFactory(opts cassandra.Options, telset telemetry.Settings) (*Factory, error) { return &Factory{ config: opts, telset: telset, }, nil } func (f *Factory) Configure(ctx context.Context) error { // Validate configuration without connecting if err := f.config.Configuration.Validate(); err != nil { return err } f.configured = true return nil } func (f *Factory) Initialize(ctx context.Context) error { if f.initialized { return nil } // Establish actual connection session, err := cassandra.NewSession(&f.config.Configuration) if err != nil { return err } f.session = session f.initialized = true return nil } func (f *Factory) IsInitialized() bool { return f.initialized } ``` ### Pros 1. **Early Configuration Validation**: Invalid configurations are caught at startup, even for unused storages. This prevents runtime surprises. 2. **Clear Separation of Concerns**: Configuration validation and resource initialization are distinct phases with clear semantics. 3. **Predictable Startup Behavior**: All configuration errors surface during `Start()`, making debugging easier. 4. **Consistent Interface**: All factories follow the same lifecycle pattern. ### Cons 1. **Significant Refactoring**: All 6+ factory implementations require changes: - `internal/storage/v2/cassandra/factory.go` - `internal/storage/v2/elasticsearch/factory.go` - `internal/storage/v2/clickhouse/factory.go` - `internal/storage/v2/grpc/factory.go` - `internal/storage/v2/badger/factory.go` - `internal/storage/v2/memory/factory.go` - `cmd/internal/storageconfig/factory.go` 2. **API Breaking Change**: The `tracestore.Factory` interface changes, potentially affecting external consumers. 3. **Complex State Management**: Factories must track configuration vs. initialization state. 4. **Testing Complexity**: Tests need to account for the two-phase lifecycle. ### Implementation Effort | Component | Effort | |-----------|--------| | Interface definitions | Low | | Extension refactoring | Medium | | Cassandra factory | Medium | | Elasticsearch factory | Medium | | ClickHouse factory | Medium | | gRPC factory | Medium | | Badger factory | Medium | | Memory factory | Low | | storageconfig helper | Medium | | Test updates | High | | **Total** | **High** | --- ## Option 2: Simple Lazy Initialization (Defer Everything) ### Design Move all factory creation to `TraceStorageFactory()` without modifying the factory interfaces: ```go type storageExt struct { config *Config telset component.TelemetrySettings host component.Host factories map[string]tracestore.Factory factoryMu sync.Mutex // ... } func (s *storageExt) Start(ctx context.Context, host component.Host) error { s.host = host // Store for later use s.factories = make(map[string]tracestore.Factory) // No factory initialization - just validation that config keys exist return nil } // Changed signature: (Factory, bool) -> (Factory, error) // This allows callers to distinguish "not configured" from "initialization failed" func (s *storageExt) TraceStorageFactory(name string) (tracestore.Factory, error) { s.factoryMu.Lock() defer s.factoryMu.Unlock() // Return cached factory if already created if f, ok := s.factories[name]; ok { return f, nil } // Check if configuration exists cfg, ok := s.config.TraceBackends[name] if !ok { return nil, fmt.Errorf( "storage '%s' not declared in '%s' extension configuration", name, componentType, ) } // Create factory on demand telset := telemetry.FromOtelComponent(s.telset, s.host) factory, err := storageconfig.CreateTraceStorageFactory( context.Background(), name, cfg, telset, func(authCfg config.Authentication, backendType, backendName string) (extensionauth.HTTPClient, error) { return s.resolveAuthenticator(s.host, authCfg, backendType, backendName) }, ) if err != nil { return nil, fmt.Errorf("failed to initialize storage '%s': %w", name, err) } s.factories[name] = factory return factory, nil } ``` ### Pros 1. **Minimal Code Changes**: Only `extension.go` and its callers need modification. 2. **No Factory Interface Changes**: Existing factory implementations (`tracestore.Factory`) remain unchanged. 3. **No External API Breaking Changes**: The extension is internal; external consumers are unaffected. 4. **Simple Mental Model**: Factories are created when needed, cached for reuse. 5. **Quick Implementation**: Can be completed in a single PR. 6. **Clear Error Messages**: Changing signature from `(Factory, bool)` to `(Factory, error)` allows callers to distinguish "storage not configured" from "initialization failed" and provides actionable error messages. ### Cons 1. **Deferred Configuration Errors**: Invalid configurations for unused storages are never detected. A typo in an unused backend's config silently passes. 2. **Runtime Initialization Failures**: Connection failures happen when a pipeline component first requests the storage, not at startup. This could cause unexpected failures during operation. 3. **Less Predictable Startup**: Startup succeeds even with broken configurations, potentially masking issues. 4. **Interface Signature Change**: The `Extension` interface methods change from `(Factory, bool)` to `(Factory, error)`. While not a breaking change for external consumers (the extension is internal), it requires updating all callers within the codebase. ### Potential Mitigation Add optional configuration validation at startup: ```go func (s *storageExt) Start(ctx context.Context, host component.Host) error { s.host = host // Optional: validate configurations without initializing for name, cfg := range s.config.TraceBackends { if err := cfg.Validate(); err != nil { return fmt.Errorf("invalid configuration for storage '%s': %w", name, err) } } return nil } ``` This requires adding `Validate()` methods to backend configs, which is simpler than full two-phase factories. ### Implementation Effort | Component | Effort | |-----------|--------| | Extension interface change | Low | | Extension lazy init logic | Low | | Update callers (GetTraceStoreFactory, etc.) | Low | | Config validation (optional) | Low-Medium | | Test updates | Low | | **Total** | **Low** | --- ## Comparison Summary | Criterion | Option 1: Two-Phase | Option 2: Simple Lazy | |-----------|--------------------|-----------------------| | Implementation effort | High | Low | | Factory interface changes | Yes (breaking) | No | | Extension interface changes | No | Yes (minor) | | Early config validation | Yes | Partial (with mitigation) | | Runtime failure risk | Low | Medium | | Code complexity | Higher | Lower | | Factory changes required | All backends | None | | Error message clarity | Good | Good (with error return) | | Time to implement | Weeks | Days | ## Recommendation **Option 2 (Simple Lazy Initialization) with Config Validation Mitigation** is recommended as the initial implementation because: 1. It solves the primary problem (wasted resources, startup failures for unused backends) with minimal risk. 2. It can be implemented quickly without breaking changes. 3. Adding `Validate()` methods to configs provides early error detection without the complexity of two-phase factories. 4. Option 1 can be pursued later if stronger guarantees are needed. ### Suggested Implementation Steps 1. Change `Extension` interface signatures from `(Factory, bool)` to `(Factory, error)` for `TraceStorageFactory()` and `MetricStorageFactory()`. 2. Refactor `extension.go` to defer factory creation to `TraceStorageFactory()`/`MetricStorageFactory()`. 3. Update all callers (`GetTraceStoreFactory`, `GetMetricStorageFactory`, `GetSamplingStoreFactory`, `GetPurger`) to handle the new error return. 4. Add `Validate()` methods to `TraceBackend` and `MetricBackend` config structs. 5. Call validation in `Start()` to catch configuration errors early. 6. Update tests to verify lazy initialization behavior. 7. Document the behavior change in release notes. ## Consequences ### Positive - Unused storage backends no longer consume resources. - Startup succeeds even when unused backends are unavailable. - Minimal code changes reduce risk of regressions. ### Negative - Configuration errors for unused storages may go unnoticed (mitigated by validation). - First access to a storage may fail if the backend becomes unavailable after startup. - Requires updating all callers to handle the new `error` return type. ### Neutral - Logging will shift from startup to first-access for storage initialization messages. - Shutdown logic must handle partially-initialized factory maps. --- ## References - Extension implementation: `cmd/jaeger/internal/extension/jaegerstorage/extension.go` - Factory creation: `cmd/internal/storageconfig/factory.go` - Factory interface: `internal/storage/v2/api/tracestore/factory.go` - Backend implementations: - `internal/storage/v2/cassandra/factory.go` - `internal/storage/v2/elasticsearch/factory.go` - `internal/storage/v2/clickhouse/factory.go` - `internal/storage/v2/grpc/factory.go` - `internal/storage/v2/badger/factory.go` - `internal/storage/v2/memory/factory.go` ================================================ FILE: docs/adr/004-migrating-coverage-gating-to-github-actions.md ================================================ # Migrate Coverage Gating from Codecov to GitHub Actions * **Status**: Accepted (implemented) * **Date**: 2026-03-01 ## Context Jaeger uses [Codecov](https://codecov.io) for two functions: 1. **Long-term trend tracking**: Coverage is uploaded after each CI run via the Codecov Action. 2. **PR gating**: Codecov's GitHub status check blocks merges when coverage drops below a threshold. Coverage is collected across 11 CI jobs (unit tests + E2E), uploaded through `.github/actions/upload-codecov/action.yml`. ### Problem Codecov's PR status checks suffer from latency (results lag behind CI completion) and intermittent rate-limit failures that block PRs even when coverage is healthy. The gating logic should run entirely within GitHub Actions for faster, more reliable feedback. ## Decision Extend the existing `CI Summary Report` fan-in workflow to add coverage aggregation and gating alongside the existing metrics comparison. Codecov uploads are retained for long-term historical trending and per-flag breakdown views. ### Requirements 1. Coverage must be merged from all CI jobs (unit tests and E2E) into a single profile. 2. Two independent gates must be applied: - **Absolute floor**: total coverage ≥ 95%, matching the Codecov project target. - **No regression**: total coverage must not drop compared to the `main` baseline. 3. The merged profile must be filtered using the same exclusions as `.codecov.yml` (generated files, mocks, integration test infrastructure) so both tools report from a single source of truth. 4. A `Coverage Gate` check-run must always be posted to the PR — even when no coverage data is available — so it can be used as a required status check in branch protection. 5. The workflow must run for `pull_request`, `merge_group`, and `push` (to `main`) events triggered through the CI Orchestrator, as well as via manual `workflow_dispatch`. 6. On `main`-branch runs, the coverage baseline must be cached for future PR comparisons. ### Success Criteria - `Coverage Gate` and `Metrics Comparison` check-runs appear on every PR and merge-queue run. - Coverage regressions block PRs when `Coverage Gate` is added to required status checks. - Manual re-runs via `workflow_dispatch` allow re-posting checks from any branch. ## Implementation Overview ### Coverage Artifact Pipeline Each CI job uploads its coverage profile as a `coverage-` artifact (7-day retention) via `.github/actions/upload-codecov/action.yml`, alongside the existing Codecov upload. ### Fan-in Workflow (`ci-summary-report.yml`) The single `summary-report` job: 1. **Resolves the source run** — determines the CI Orchestrator run ID (from `workflow_run` event or `workflow_dispatch` input), validates it succeeded, and extracts PR metadata (number + head SHA) via the GitHub API. 2. **Downloads all artifacts** — uses `gh run download` to fetch all artifacts from the source run. 3. **Merges and gates coverage** — merges all `coverage-*/*.out` profiles with `gocovmerge`, filters excluded paths, and applies the two coverage gates. 4. **Posts results** — creates `Metrics Comparison` and `Coverage Gate` check-runs on the PR. When no coverage data exists, `Coverage Gate` reports success with a "skipped" note to satisfy branch protection. 5. **Saves baseline on `main`** — caches the coverage percentage for future PR comparisons. ### Key Files | File | Role | |------|------| | `.github/workflows/ci-summary-report.yml` | Fan-in workflow | | `.github/actions/upload-codecov/action.yml` | Coverage artifact upload | | `.github/workflows/ci-orchestrator.yml` | Triggers the fan-in | | `scripts/e2e/filter_coverage.py` | Applies `.codecov.yml` exclusions | | `internal/tools/tools.go` | `gocovmerge` tool dependency | | `.codecov.yml` | Single source of truth for ignore patterns | ## Consequences ### Positive - **Faster feedback**: coverage gate result appears as soon as the CI Orchestrator completes. - **Reliability**: eliminates Codecov rate-limit failures blocking PRs. - **Consolidated reporting**: performance metrics and coverage appear in a single sticky PR comment. - **Required status check safe**: `Coverage Gate` is always created, even when coverage is skipped. ### Negative - **Artifact storage cost**: `coverage-*` artifacts add ~50–100 MB per CI run (7-day retention). - **One tool dependency**: `github.com/wadey/gocovmerge` in `internal/tools/go.mod`. ### Neutral - Codecov remains active for long-term trending; removing it can be a follow-up decision. ## References - [CI Summary Report workflow](/.github/workflows/ci-summary-report.yml) - [Coverage upload action](/.github/actions/upload-codecov/action.yml) - [CI Orchestrator](/.github/workflows/ci-orchestrator.yml) - [Coverage filter script](/scripts/e2e/filter_coverage.py) - [Tool registry](/internal/tools/tools.go) - [Coverage policy](/.codecov.yml) ================================================ FILE: docs/adr/005-badger-storage-record-layouts.md ================================================ # Badger Storage Record Layouts * **Status**: Documented existing implementation * **Date**: 2026-03-12 ## Context Jaeger supports [Badger](https://github.com/dgraph-io/badger) as an embedded, local key-value store backend. Badger is primarily intended for all-in-one deployments where a lightweight storage solution is desirable without an external database dependency. Because Badger is a generic sorted key-value store, Jaeger must impose its own logical record structure on top of it. This ADR documents the record layouts used in the Badger storage implementation as they exist today, covering key formats, value formats, and the overall design rationale. The intent is to make the storage design visible to contributors and to serve as a reference when reasoning about query behavior or considering future changes. The implementation lives in: - [`internal/storage/v1/badger/spanstore/writer.go`](../../internal/storage/v1/badger/spanstore/writer.go) — key generation and span writes - [`internal/storage/v1/badger/spanstore/reader.go`](../../internal/storage/v1/badger/spanstore/reader.go) — query execution and span reads - [`internal/storage/v1/badger/spanstore/cache.go`](../../internal/storage/v1/badger/spanstore/cache.go) — in-memory service/operation cache - [`internal/storage/v1/badger/samplingstore/storage.go`](../../internal/storage/v1/badger/samplingstore/storage.go) — sampling data storage - [`internal/storage/v1/badger/config.go`](../../internal/storage/v1/badger/config.go) — configuration and defaults ### Design Principles All keys are encoded in **big-endian** byte order. This ensures that integer values sort lexicographically in the same order as their numeric values, which is a prerequisite for the range-scan and reverse-iteration queries used throughout the implementation. All span-related records (primary span records and all secondary indexes) share a common property: **the most significant bit of the first byte is always set** (`0x80` or higher). This cleanly separates span data from sampling data (prefixes `0x08` and `0x09`) in the keyspace. A single Badger database instance holds all record types. There is no separate "index store" — different logical tables are distinguished solely by the prefix byte of their keys. --- ## Record Layouts ### Key Prefix Summary | Record type | Prefix byte | Used by | |------------------------|-------------|------------------| | Span (primary record) | `0x80` | Span store | | Service name index | `0x81` | Span store | | Operation name index | `0x82` | Span store | | Tag index | `0x83` | Span store | | Duration index | `0x84` | Span store | | Throughput | `0x08` | Sampling store | | Probabilities/QPS | `0x09` | Sampling store | --- ### 1. Primary Span Record (`0x80`) Each span is stored as a single key-value entry. **Key** (33 bytes, fixed size): ``` [0x80][traceID.High: 8B][traceID.Low: 8B][startTime: 8B][spanID: 8B] ``` - `traceID.High` and `traceID.Low` — the 128-bit trace ID split into two `uint64` values - `startTime` — `uint64`, microseconds since Unix epoch (via `model.TimeAsEpochMicroseconds`) - `spanID` — `uint64` **Value**: the serialized span, encoded as either: - Protobuf (`proto.Marshal`, encoding type `0x02`) — the default - JSON (`json.Marshal`, encoding type `0x01`) — available as an alternative **`UserMeta` byte**: stores the encoding type in the lower 4 bits. Reads use `item.UserMeta() & 0x0F` to determine how to deserialize the value. **TTL**: all entries expire after the configured span TTL (default 72 hours). The expiry is set as `uint64(time.Now().Add(ttl).Unix())` (seconds since Unix epoch) in the Badger entry's `ExpiresAt` field. **Sorting behavior**: because all three components after the prefix byte are encoded in big-endian, all spans belonging to the same trace cluster together, and within a trace they are sorted by start time and then span ID. This allows `GetTrace` to retrieve all spans of a trace via a single prefix scan without any additional filtering. --- ### 2. Service Name Index (`0x81`) An index entry is written for each span, keyed by the service name of the span's process. **Key** (variable size): ``` [0x81][serviceName: variable][startTime: 8B][traceID.High: 8B][traceID.Low: 8B] ``` - `serviceName` — UTF-8 bytes of the service name, no length prefix or separator - `startTime` — `uint64`, microseconds since Unix epoch - `traceID` — the 16-byte trace ID (High then Low, big-endian) **Value**: empty (`nil`) **Purpose**: enables scanning all trace IDs associated with a given service within a time range. The reader seeks to `[0x81][serviceName]` and iterates in reverse (latest first), extracting the trailing 16 bytes of each key as the trace ID. **TTL**: same as the corresponding primary span record. --- ### 3. Operation Name Index (`0x82`) An index entry is written for each span, keyed by the concatenation of service name and operation name. **Key** (variable size): ``` [0x82][serviceName + operationName: variable][startTime: 8B][traceID.High: 8B][traceID.Low: 8B] ``` - `serviceName + operationName` — the two strings concatenated directly, no separator **Value**: empty (`nil`) **Purpose**: enables finding trace IDs for a specific service + operation pair within a time range. **Note**: because the service name and operation name are concatenated without a separator, a service named `"foo"` with operation `"bar"` produces the same prefix as a service named `"foobar"` with operation `""`. The reader guards against this ambiguity by checking that the full key prefix (up to the timestamp) matches exactly. **TTL**: same as the corresponding primary span record. --- ### 4. Tag Index (`0x83`) For each searchable tag key-value pair associated with a span, a separate index entry is written. Tags are indexed from three sources: `span.Tags`, `span.Process.Tags`, and `log.Fields` for each log entry. **Key** (variable size): ``` [0x83][serviceName + tagKey + tagValue: variable][startTime: 8B][traceID.High: 8B][traceID.Low: 8B] ``` - `serviceName + tagKey + tagValue` — all three strings concatenated directly, no separators - Tag values are converted to their string representation via `kv.AsString()` before being embedded in the key **Value**: empty (`nil`) **Purpose**: enables finding trace IDs for spans that carry a specific tag key-value pair within a given service. **TTL**: same as the corresponding primary span record. --- ### 5. Duration Index (`0x84`) One index entry is written per span, encoding the span's duration. **Key** (variable size, fixed numeric portion): ``` [0x84][duration: 8B][startTime: 8B][traceID.High: 8B][traceID.Low: 8B] ``` - `duration` — `uint64`, span duration in microseconds (via `model.DurationAsMicroseconds`) - `startTime` — `uint64`, microseconds since Unix epoch - `traceID` — 16-byte trace ID **Value**: empty (`nil`) **Purpose**: enables range scans over span duration. A duration query scans forward from `[0x84][minDuration]` to `[0x84][maxDuration]`, collecting trace IDs. The result is used as a hash-set filter (`hashOuter`) that is intersected with results from other indexes before final trace retrieval. **Key design rationale**: by placing `duration` before `startTime`, all keys for a given duration value are contiguous in the sorted keyspace, making range scans efficient. The time range filter is applied as a secondary check during the scan. **TTL**: same as the corresponding primary span record. --- ### 6. Sampling Throughput Record (`0x08`) Written by the adaptive sampling component to record observed request throughput. **Key** (16 bytes allocated, 9 bytes used): ``` [0x08][startTime: 8B][0x00 × 7] ``` - `startTime` — `uint64`, microseconds since Unix epoch - The remaining 7 bytes of the 16-byte allocation are implicitly zero **Value**: JSON-encoded `[]*model.Throughput` **No TTL**: sampling entries do not have an explicit expiry set via Badger's `ExpiresAt`. Cleanup relies on explicit deletion or Badger's value-log GC. --- ### 7. Sampling Probabilities/QPS Record (`0x09`) Written by the adaptive sampling component to record computed sampling probabilities and QPS estimates. **Key** (16 bytes allocated, 9 bytes used): ``` [0x09][startTime: 8B][0x00 × 7] ``` **Value**: JSON-encoded `ProbabilitiesAndQPS` struct: ```go type ProbabilitiesAndQPS struct { Hostname string Probabilities model.ServiceOperationProbabilities // map[service]map[operation]float64 QPS model.ServiceOperationQPS // map[service]map[operation]float64 } ``` **No TTL**: same as throughput records. --- ## Query Execution The reader builds an *execution plan* that describes how to combine index results: 1. **Duration filter** (if present): scanned via `scanRangeIndex` using the duration index. Results are stored in `plan.hashOuter` (a set of trace IDs) for subsequent intersection. 2. **Index seeks** (service, operation, tags): for each index key prefix derived from the query parameters, `scanIndexKeys` iterates in reverse order (latest first) within the time range, extracting the trailing 16 bytes of each matching key as a trace ID. - When multiple index seeks are needed (e.g., tag + operation), all but the last are scanned first and their results are combined via `mergeJoinIds` (a sorted merge intersection). The final seek then filters against this merged set. 3. **Full table scan** (fallback, when no service name is specified): `scanTimeRange` iterates all primary span keys (`0x80` prefix) within the time range, sorted descending by start time. 4. **Trace hydration**: the resulting trace ID list is resolved to full `*model.Trace` objects by prefix-scanning primary span records for each trace ID. --- ## Service and Operation Discovery Service names and operation names are not queried directly from Badger on every request. Instead, they are maintained in an **in-memory cache** (`CacheStore`) that mirrors the TTL semantics of the underlying index entries: - `services: map[string]uint64` — maps service name to its expiry time (Unix seconds) - `operations: map[string]map[string]uint64` — maps service → operation → expiry time The cache is populated in two ways: 1. **On write**: `cache.Update(serviceName, operationName, expireTime)` is called after every `WriteSpan`, keeping the cache current without re-scanning Badger. 2. **On startup** (if `prefillCache=true`): the reader scans all service name index keys (`0x81`) and operation name index keys (`0x82`) to preload any entries persisted from a previous run. Expired entries are lazily removed from the cache when `GetServices` or `GetOperations` is called. --- ## Storage Configuration Key operational parameters (from [`config.go`](../../internal/storage/v1/badger/config.go)): | Parameter | Default | Notes | |------------------------|------------------|---------------------------------------------| | `TTL.Spans` | 72 hours | Expiry for all span-related Badger entries | | `Ephemeral` | `true` | Uses a temp directory; data lost on restart | | `SyncWrites` | `false` | Async writes for performance | | `MaintenanceInterval` | 5 minutes | Frequency of value-log GC runs | | `MetricsUpdateInterval`| 10 seconds | Frequency of metric collection | | `Directories.Keys` | `/data/keys`| Directory for LSM key index (SSD preferred) | | `Directories.Values` | `/data/values` | Directory for value log (HDD acceptable) | The separation of key and value directories allows placing the key index on faster SSD storage while the value log (which is written sequentially) can reside on slower spinning disk. --- ## Decision The record layouts described above were chosen to satisfy the following requirements: 1. **Lexicographic range scans**: all time-based queries rely on big-endian encoding of timestamps so that key iteration maps directly to time-range iteration. 2. **Single-instance simplicity**: all record types share one Badger database, distinguished by prefix byte. This avoids the complexity of managing multiple database handles. 3. **Index-only secondary records**: secondary index keys carry no values (nil), keeping the index footprint minimal. The full span data is always fetched from the primary record after the index identifies the relevant trace IDs. 4. **TTL-driven expiry**: Badger's native per-entry TTL mechanism is used for automatic data expiration, eliminating the need for a background deletion job. 5. **Embedded operation**: Badger requires no external process, making the Badger backend suitable for single-binary, all-in-one deployments. ## Consequences ### Positive - Simple deployment: no external storage infrastructure required. - Automatic expiry via native Badger TTL. - Efficient prefix and range scans over sorted keys. - Duration queries can be intersected with other query criteria (contrast with Cassandra; see [ADR-001](001-cassandra-find-traces-duration.md)). ### Negative / Limitations - **Not distributed**: Badger is a single-node store. It is not suitable for high-throughput or multi-instance deployments. - **No spanKind in operations**: the operation name index does not encode span kind, so `GetOperations` returns operations without span kind information (tracked in [issue #1922](https://github.com/jaegertracing/jaeger/issues/1922)). - **String concatenation without separators**: the absence of separators between service name, tag key, and tag value in composite index keys means that a suffix of one component can collide with a prefix of the next. The implementation handles this with exact-prefix length checks but it is a latent source of subtle bugs if the key format is extended. - **No dependency index**: the dependency store computes dependency links via a full trace scan on every request rather than maintaining a dedicated index, which may be slow for large datasets. - **Sampling entries have no TTL**: throughput and probabilities records are not automatically expired and accumulate indefinitely unless explicitly pruned. - **Ephemeral by default**: the default configuration (`Ephemeral: true`) stores data in a temporary directory that is deleted on process exit, which may surprise users who expect data to persist across restarts. ## References - [`internal/storage/v1/badger/spanstore/writer.go`](../../internal/storage/v1/badger/spanstore/writer.go) — `createTraceKV`, `createIndexKey`, `WriteSpan` - [`internal/storage/v1/badger/spanstore/reader.go`](../../internal/storage/v1/badger/spanstore/reader.go) — `FindTraceIDs`, `scanIndexKeys`, `scanRangeIndex`, `scanTimeRange`, `getTraces` - [`internal/storage/v1/badger/spanstore/cache.go`](../../internal/storage/v1/badger/spanstore/cache.go) — `CacheStore` - [`internal/storage/v1/badger/samplingstore/storage.go`](../../internal/storage/v1/badger/samplingstore/storage.go) — `createThroughputKV`, `createProbabilitiesKV` - [`internal/storage/v1/badger/config.go`](../../internal/storage/v1/badger/config.go) — `Config`, `DefaultConfig` - [Badger documentation](https://dgraph.io/docs/badger/) - [ADR-001: Cassandra FindTraceIDs Duration Query Behavior](001-cassandra-find-traces-duration.md) ================================================ FILE: docs/adr/006-internal-tracing-via-otelcol-telemetry-factory.md ================================================ # Internal Tracing via OTel Collector TelemetryFactory * **Status**: Implemented * **Date**: 2026-03-19 ## Context Jaeger v2 is built as an OpenTelemetry Collector distribution. Like any well-instrumented service, it benefits from internal self-tracing: recording spans for query requests, MCP tool calls, and other extension-level operations. At the same time, self-tracing must not create recursive trace loops when Jaeger's own OTLP receiver is the export destination for internal telemetry. Previously, two extensions (`jaegerquery` and `remotestorage`) each called `jtracer.NewProvider` manually at startup — a workaround for upstream Collector issue [#7532](https://github.com/open-telemetry/opentelemetry-collector/issues/7532), which is now closed. With the issue resolved, the Collector properly populates `component.TelemetrySettings.TracerProvider` for every component via `otelcol.Factories.Telemetry`. ### Problem Three interlocking issues motivated this change: 1. **Recursive self-tracing loop.** When Jaeger's OTLP receiver is the export destination for internal telemetry (the common deployment), each trace batch processed by the receiver generates an internal span that is exported as a new batch — ad infinitum. 2. **Per-extension manual initialization.** Each extension that wanted self-tracing had to independently call `jtracer.NewProvider`, manage provider lifecycle, and override `telset.TracerProvider`. This is error-prone and bypasses the Collector framework's lifecycle management. 3. **No per-component provider differentiation.** The standard `otelconftelemetry` factory creates one `TracerProvider` shared by all components. Upstream issue [#10663](https://github.com/open-telemetry/opentelemetry-collector/issues/10663), which would allow per-component customization, has had no progress. ## Decision Replace the `otelconftelemetry.NewFactory()` call in `components.go` with a custom `WrapFactory` that delegates everything to `otelconftelemetry` except `CreateTracerProvider`. The custom factory creates one real `TracerProvider` (via the existing `jtracer` initialization) wrapped in a `FilteringTracerProvider`. This wrapper inspects the `otelcol.component.id` instrumentation attribute that the Collector framework injects into every `Tracer()` call, and routes to the real provider only for an explicit allowlist of extensions known to produce meaningful internal spans: - `jaeger_query` - `jaeger_mcp` All other components — receivers, processors, exporters, connectors, and unlisted extensions — receive a noop tracer. This default-off / explicit-allowlist policy closes the recursive loop by design and prevents uninstrumented components from accidentally emitting spans when they add internal instrumentation in the future. Manual `jtracer.NewProvider` calls are removed from `jaegerquery/server.go` and `remotestorage/server.go`. Both extensions now use `telset.TracerProvider` directly, populated by the framework. The `enable_tracing: false` config field in `jaeger_query` is preserved as a per-extension opt-out applied after the framework provides the provider. ## Consequences - Recursive self-tracing loop is closed by design, not by documentation. - Extensions no longer manage tracer lifecycle; the Collector framework owns it. - New extensions get internal tracing by being added to the allowlist — no per-extension boilerplate. - The `otelcol.component.id` attribute injection is observed behavior from an internal Collector package, not a contractual API. An in-process test catches any upstream breakage at the next dependency bump. ## Alternatives Considered **Use `otelconftelemetry` YAML tracing config as-is.** Requires users to add a `service.telemetry.traces` YAML block with an OTLP exporter config — a different and less familiar configuration paradigm compared to `OTEL_*` env vars. Does not solve the recursive loop by default (loop still occurs if the OTLP endpoint in the YAML points to Jaeger itself). Rejected. **Keep per-extension `jtracer.NewProvider`.** Does not solve the loop for receivers (which never called `jtracer.NewProvider`, so they always got the framework's noop). Does not benefit new extensions automatically. Rejected as a dead end. **Filter by instrumentation scope name prefix `go.opentelemetry.io/collector/receiver/`.** Fragile: depends on receiver authors following the naming convention, and would need updating as new receivers are added. Superseded by the component attribute approach. **Allowlist by `otelcol.component.kind = "extension"`, denylist receivers.** Allows all extensions through rather than only the two that have meaningful internal tracing today. Any future extension that adds instrumentation would emit spans without an explicit decision. Rejected in favour of the more conservative component-id allowlist. **Wait for upstream issue #10663.** No progress; no roadmap. Low confidence. ================================================ FILE: docs/adr/README.md ================================================ # Architecture Decision Records (ADRs) This directory contains Architecture Decision Records (ADRs) for the Jaeger project. ADRs document important architectural decisions made during the development of Jaeger, including the context, decision, and consequences of each choice. ## What is an ADR? An Architecture Decision Record (ADR) is a document that captures an important architectural decision made along with its context and consequences. ADRs help teams understand why certain decisions were made and provide historical context for future contributors. ## ADRs in This Repository - [ADR-001: Cassandra FindTraceIDs Duration Query Behavior](001-cassandra-find-traces-duration.md) - Explains why duration queries in the Cassandra spanstore use a separate code path and cannot be efficiently combined with other query parameters. - [ADR-002: MCP Server Extension](002-mcp-server.md) - Design for implementing Model Context Protocol server as a Jaeger extension for LLM integration. - [ADR-003: Lazy Storage Factory Initialization](003-lazy-storage-factory-initialization.md) - Comparative analysis of approaches to defer storage backend initialization until actually needed. - [ADR-004: Migrate Coverage Gating from Codecov to GitHub Actions](004-migrating-coverage-gating-to-github-actions.md) - Design for replacing Codecov PR gating with a local fan-in workflow that merges coverage profiles, gates on regression, and consolidates reporting with the existing metrics summary. - [ADR-005: Badger Storage Record Layouts](005-badger-storage-record-layouts.md) - Documents the key and value formats used to store spans, secondary indexes, and sampling data in the Badger embedded key-value store backend. - [ADR-006: Internal Tracing via OTel Collector TelemetryFactory](006-internal-tracing-via-otelcol-telemetry-factory.md) - Design for centralizing Jaeger's internal self-tracing through the Collector's TelemetryFactory hook, replacing per-extension manual tracer initialization and preventing recursive self-tracing loops in receivers. ================================================ FILE: docs/release/remove-v1-checklist.md ================================================ # Remove v1 release logic — incremental milestone checklist (updated) Owner: @yurishkuro Related: https://github.com/jaegertracing/jaeger/issues/7497 Prepared: 2025-11-12 ## Summary We will perform a clean, audited migration from dual v1/v2 releases to v2-only releases. The migration is split into small, testable milestones so we do not break the ability to produce v1 artifacts until we intentionally stop publishing them. This document is an update to the previously merged checklist and reflects the agreed milestone ordering and file allocations: - Milestone 0 — Coordination / snapshot (already done) - Milestone 1 — RE-NUMBER BUILD TARGETS TO USE v2 BY DEFAULT (build and image targets) - Milestone 2 — REMOVE ALL USAGE of v1 artifacts everywhere that could be invoked by maintainers or CI (non-breaking to release/publish) - Milestone 3 — STOP PUBLISHING v1 artifacts (release/publish changes) - Milestone 4 — Release notes & user-facing scripts (docs and helper finalization) - Milestone 5 — Cleanup remaining references (examples, tests, docs) - Milestone 6 — Final removal and prune (policy-based post-sunset) Notes: - "Re-number build targets" (Milestone 1) means change the defaults in build scripts and Makefiles so that most targets produce v2 artifacts by default, with explicit exceptions for selected v1 targets and the ability to override to v1 when needed. - "Remove usage" (Milestone 2) means update any convenience targets, examples, dev Makefiles, CI test helper scripts and READMEs that would cause contributors or CI to pick or run v1 artifacts by default. Do not change the core release/publish automation that we still need to be able to produce v1 artifacts until Milestone 3 (except where those core pieces are strictly only dev convenience and not needed for releases). - "Stop publishing" (Milestone 3) is the step where we change release automation so v1 artifacts are no longer produced/uploaded. --- ## Milestone 0 — Coordination (done) - Create a rollback snapshot branch/tag: `pre-remove-v1-YYYY-MM-DD`. - Baseline checklist merged: `docs/release/remove-v1-checklist.md`. --- ## Milestone 1 — RE-NUMBER BUILD TARGETS TO USE v2 BY DEFAULT Owner: @yurishkuro Goal - Ensure most build and image targets default to producing v2 artifacts. v1 should only be produced for the following targets (they remain v1): - build-all-in-one - build-query - build-collector - build-ingester - All other build targets (binaries and docker images) should default to v2. Maintain the ability to override to v1 via an explicit env var/Makefile variable (e.g., JAEGER_VERSION=1 or similar) but make v2 the default. Acceptance criteria - `scripts/makefiles/BuildBinaries.mk` and other Makefiles/targets produce v2 artifacts by default except for the explicit exceptions listed above. - Docker build scripts and helpers (examples: `scripts/build/build-upload-a-docker-image.sh`, docker-related Makefiles) default to v2 tags; v1 tag generation is only produced when explicitly requested. - CI or documented developer convenience targets no longer pull/build v1 artifacts by default. Files / targets assigned to this milestone (non-exhaustive — guidance to scan repo) - [ ] `scripts/makefiles/BuildBinaries.mk` - Change defaults for targets other than the four exceptions listed above. - [ ] `scripts/build/build-upload-a-docker-image.sh` - Default to v2 push/tags. - [ ] `scripts/utils/compute-tags.sh` - Ensure default computed tags are v2-first. Implementation guidance - Make minimal edits: flip default variables so v2 is implied, leave an explicit override to v1. - Avoid changing core release/publishing automation that must still be able to publish v1 until the later milestone (this is M1 and non-publishing). - Apply same principle to Docker image builders and helpers. Milestone 1 testing - Run CI test jobs in staging and confirm builds do not produce or pull v1 artifacts by default. - Run Makefile targets and build scripts locally to validate v2 defaults and v1 override behaviour. --- ## Milestone 2 — REMOVE ALL USAGE of v1 artifacts (non-breaking to release/publish) Goal - Ensure no scripts, automated tests, documentation examples, or convenience targets that maintainers or CI use will pull, build, or reference v1 artifacts by default. - Do NOT change core release/publishing workflows that are required to produce v1 artifacts (those belong to Milestone 3). Acceptance criteria - CI test jobs & documented maintainer commands do not reference v1 by default. - Developer convenience targets and READMEs used in release/test flows are updated to v2 or removed. - Release/publish scripts remain able to produce v1 artifacts (unchanged in this milestone). Files assigned to Milestone 2 (update usage only) - [ ] `docker-compose/tail-sampling/Makefile` - Replace `JAEGER_VERSION=1...` convenience defaults with v2 or remove v1 convenience targets. - [ ] `docker-compose/monitor/Makefile` - Update dev convenience targets and README examples to use v2 by default. - [ ] `examples/otel-demo/deploy-all.sh` - If the script is referenced by CI/docs, default to v2 (or make v1 explicit/legacy). - [ ] `examples/*` and README example lines that are invoked by CI or referenced in release docs - Update documented example commands to v2. - [ ] small convenience Makefile targets / scripts referenced in documentation or used by CI tests (identify by scan) - Replace v1 defaults with v2; remove legacy v1 targets where appropriate. - [ ] `scripts/e2e/*` (only test helpers invoked by CI, if they default to v1) - Update defaults used by CI test jobs to v2 (but do not modify release/publish scripts). - [ ] `scripts/utils/compare_metrics.py` (if used in tests or example automation) - Make v2 metrics the default for compare helpers invoked by CI. - [ ] Any other example/demo helpers that are used by CI or are part of the documented maintainer workflow (identify & update). Implementation guidance - Make minimal edits: change default literals, remove v1 convenience targets, update README example lines. - Avoid touching core release code paths (packaging, workflows that create upload actions, top-level make targets used by release automation). Milestone 2 testing - Run CI test jobs (staging) and ensure they don't pull v1 images by default. - Run example/demo commands from docs and confirm they use v2. - Sanity-check that release automation still can build v1 artifacts (no changes to release publish workflows in this milestone). --- ## Milestone 3 — STOP PUBLISHING v1 artifacts (release/publish changes) Goal - Change packaging and CI release automation so v1 artifacts are not built/pushed/uploaded for official releases. Acceptance criteria - Performing a release with a v2 tag (dry-run in a fork or staging) results in only v2 artifacts being published. - No v1 images/binaries are uploaded to registries or GitHub Releases. Files assigned to Milestone 3 (publish removal) - [ ] `.github/workflows/ci-release.yml` - Remove steps that create/upload v1 release artifacts; ensure upload steps use v2 artifact names only. - [ ] `.github/workflows/ci-docker-build.yml` (publish-related steps) - Do not push v1 tags for official releases; push only v2. - [ ] `.github/workflows/ci-docker-hotrod.yml` (if it participates in release publish) - Ensure demo/image publishing uses v2 tags only. - [ ] `scripts/build/build-upload-a-docker-image.sh` - Remove v1 push logic and ensure push paths (for releases) only push v2 tags. - [ ] `scripts/build/package-deploy.sh` - Stop packaging/uploading `VERSION_V1` artifacts; upload only v2 artifacts. Remove checks that required both versions. - [ ] `scripts/utils/compute-tags.sh` - Ensure the computed publish tags for release flows are v2-only; remove v1 tag generation on release branch. - [ ] any other upload/publish helper invoked by the release workflow - Remove v1 publish behavior. Implementation guidance - These changes can safely alter the ability to publish v1 artifacts because we will have validated Milestone 1 and 2 first. - Keep changes explicit and reversible. Test on a fork/staging release. Milestone 3 testing - Run the CI release workflow on a fork with a v2 tag (dry-run) and verify only v2 artifacts are uploaded. - Verify Docker registry and GitHub Release contents. --- ## Milestone 4 — Release notes & user-facing scripts Goal - Update user-facing release docs and helper scripts so maintainers have a clean v2-only flow and instructions. Files assigned to Milestone 4 - [ ] `RELEASE.md` - Update instructions to be v2-only (replace "tag v1 & v2" with v2-only). - [ ] `CHANGELOG.md` (and any tools that parse its headers) - Ensure automated changelog tooling extracts v2 headers correctly; be tolerant of legacy format for a short transition time. - [ ] `scripts/release/start.sh` - Finalize prompts to v2-only (after Milestone 3). - [ ] `scripts/release/draft.py` - Draft v2-only GitHub releases; update headers and `gh release` invocations to use v2 tag. Testing - Run `start.sh -d` and `draft.py` in dry-run to validate v2-first outputs. - Validate maintainers can follow `RELEASE.md` to produce a v2 release. --- ## Milestone 5 — Cleanup remaining references (many small PRs) Goal - Sweep the repo and clean remaining `v1` references in examples, tests, CONTRIBUTING.md, and other non-critical areas. Split into small PRs. Files / areas - [ ] `scripts/e2e/elasticsearch.sh` (finalize v2 default) - [ ] `scripts/utils/compare_metrics.py` (final cleanup) - [ ] `CONTRIBUTING.md` (document v2 as primary; note v1 status) - [ ] any remaining docker-compose examples, READMEs and sample scripts - [ ] any other files found by repo-wide `v1` sweep Testing - Run examples, e2e, and developer quickstarts; verify expected behavior. --- ## Milestone 6 — Final removal and prune (policy-based) Goal - After the sunset/support window ends, remove v1-only code, CI shards, docs and directories. Action - Delete v1-only directories and targets; remove legacy CI workflows and scripts. - Announce removal and update docs/website. --- ## PR strategy (recommended) - Keep PRs small and focused. - PR A — Milestone 1: `chore/reassign-to-v2-defaults` — re-number build targets to use v2 by default (change Makefiles and build scripts). Include test plan: run CI builds, verify v2 artifacts are produced by default and v1 override works. - PR B — Milestone 2: `chore/remove-v1-usage` — change convenience targets and examples (non-breaking). Include test plan: run CI tests, local smoke tests for example flows. - PR C — Milestone 3: `chore/remove-v1-publish` — change release/publish workflows and packaging scripts. Test on fork with dry-run release. - PR D — Milestone 4: docs & helper finalization. - PR E+ — Milestone 5: many small PRs for examples/tests cleanup. - Each PR must include: - short description of changes, - explicit test plan (how to dry-run/validate), - reviewer list (CI/release owners & @yurishkuro). --- ## QA & rollback - Always create a rollback snapshot branch before changing publishing logic: `pre-remove-v1-YYYY-MM-DD`. - For each PR: - run CI tests in a fork/staging, - run the release dry-run (for Milestone 3 PR), - perform a quick sanity check of docs and examples. - If an urgent re-publish of v1 is required after removal, revert the Milestone 3 PR(s) and re-run the legacy snapshot branch to produce missing artifacts. --- ## Next actions Pick one: - A) I will prepare a draft PR for **Milestone 1** (`chore/reassign-to-v2-defaults`) that re-numbers build targets to use v2 by default (change Makefiles and build scripts). (Recommended first step.) - B) I will prepare a draft PR for **Milestone 2** (`chore/remove-v1-usage`) that implements the minimal, safe changes to convenience Makefiles and example scripts and add a testing plan. - C) I will prepare patch diffs for review (no PRs). - D) You assign tasks to your team and I provide review guidance and diffs on demand. Please confirm which path you prefer. ================================================ FILE: docs/security/architecture.md ================================================ # Jaeger Security Architecture This document outlines the security architecture of Jaeger, focusing on cryptographic practices, input validation, and system hardening. ## TLS and Cryptographic Practices Jaeger supports TLS for all its network communications, including span ingestion, internal component communication, and access to the Query API and UI. ### TLS Configuration TLS can be configured for both clients and servers across all Jaeger components (Collector, Query, Ingester, Agent). - **Supported TLS Versions**: Jaeger can be configured to use TLS 1.2 and 1.3, and these are the only versions that should be used in production. Users should configure the minimum supported version as TLS 1.2 or higher using the `--tls.min-version` flag (or corresponding YAML configuration), with TLS 1.3 recommended where available. While TLS 1.0 and 1.1 may still be technically supported for legacy interoperability, they are deprecated, have known security weaknesses, and **must not be enabled in production environments**. - **Cipher Suites**: A custom list of allowed cipher suites can be configured to ensure only strong cryptographic algorithms are used. - **Certificate Management**: - **CA Certificate**: Can be provided to verify the server's or client's certificate. - **Server Certificate and Key**: Required for enabling TLS on servers. - **Client Authentication (mTLS)**: Jaeger supports mutual TLS, requiring clients to provide a valid certificate for authentication. - **Reloading Certificates**: Jaeger supports hot-reloading of TLS certificates and keys from the filesystem without restarting the service, controlled by a configurable reload interval. ### Secure Defaults - **Certificate Verification**: When TLS is enabled, certificate verification is performed by default. - **Insecure Communication**: Users must explicitly set `insecure: true` or `insecure_skip_verify: true` to bypass security controls, which is strongly discouraged for production environments. ## Input Validation Jaeger performs strict input validation to prevent injection attacks and ensure system stability. - **OTLP and Protobuf**: Jaeger primarily uses structured data formats like OTLP (via gRPC and HTTP) and Protobuf for internal communication. These formats provide inherent protection against many common injection vulnerabilities. - **Schema Validation**: Inbound spans are validated against the defined schemas. - **Size Limits**: - **gRPC Message Size**: Limits are enforced on the maximum size of incoming gRPC messages. - **HTTP Request Size**: Limits are enforced for HTTP-based ingestion. - **Storage Queries**: Queries to storage backends (Elasticsearch, Cassandra, etc.) are constructed using parameterized queries or dedicated client libraries that prevent injection. ## System Hardening Jaeger is designed to be deployed in a hardened manner. ### Container Security - **Minimal Base Images**: Jaeger's official Docker images are built using minimal base images like `alpine` or `scratch` to reduce the attack surface. - **Non-Root User**: Containers are designed to run as a non-privileged user where possible. ### Dependency Management - **Vulnerability Scanning**: The project uses Dependabot for automated dependency monitoring and daily vulnerability scans. - **Software Bill of Materials (SBOM)**: An SBOM is generated for each release to provide transparency into the included components. ### Secure Build Pipeline - **Signed Commits**: All contributions are required to follow the Developer Certificate of Origin (DCO) and should be signed. - **GitHub Actions Security**: The build pipeline uses security features like `harden-runner` to monitor and restrict network access during the build process. - **Release Signing**: All release artifacts and Git tags are GPG-signed by the maintainers. ## Credential Management - **No Hardcoded Credentials**: Jaeger does not contain any hardcoded credentials. All secrets (passwords, tokens, etc.) must be provided via environment variables, command-line flags, or configuration files. - **Environment Variables**: Recommended for providing sensitive information in containerized environments. - **Secure Storage**: Users are encouraged to use secure secret management systems (like Kubernetes Secrets or HashiCorp Vault) to manage Jaeger's credentials. ## References - [Securing Jaeger Installation](https://www.jaegertracing.io/docs/latest/security/) - [OpenTelemetry TLS Configuration](https://github.com/open-telemetry/opentelemetry-collector/blob/main/config/configtls/README.md) ================================================ FILE: docs/security/assurance-case.md ================================================ # Jaeger Security Assurance Case This document provides a security assurance case for the Jaeger project, demonstrating how security requirements are met through the application of secure design principles and mitigation of common implementation weaknesses. ## Table of Contents - [Threat Model Summary](#threat-model-summary) - [Trust Boundaries](#trust-boundaries) - [Secure Design Principles](#secure-design-principles) - [Common Weakness Mitigations](#common-weakness-mitigations) - [Security Controls](#security-controls) ## Threat Model Summary Jaeger is a distributed tracing system that collects, stores, and visualizes trace data from instrumented applications. The primary security concerns are: 1. **Data Confidentiality**: Trace data may contain sensitive information (service names, endpoints, timing data) 2. **Data Integrity**: Trace data should not be tampered with 3. **Availability**: The tracing infrastructure should not become a DoS vector 4. **Access Control**: Only authorized users should access trace data ### Threat Actors | Actor | Motivation | Capability | | -- | -- | -- | | Malicious Internal Service | DoS, data injection | Network access to collector | | External Attacker | Data exfiltration, reconnaissance | Varies based on deployment | | Unauthorized User | Access to sensitive traces | UI/API access | For detailed threat analysis, see [threat-model.md](threat-model.md). ## Trust Boundaries ``` ┌─────────────────────────────────────────────────────────────────┐ │ External Network │ │ ┌──────────────┐ │ │ │ Instrumented │ │ │ │ Applications │ ─────────── BOUNDARY 1 ───────────────────┐ │ │ │ (OTel SDK) │ │ │ │ └──────────────┘ ▼ │ │ ┌────────────┤ │ │ Jaeger │ │ │ Collector │ │ └─────┬──────┤ │ │ │ │ ─────── BOUNDARY 2 ─────────┤ │ │ ▼ │ │ ┌────────────┤ │ │ Storage │ │ │ Backend │ │ └─────┬──────┤ │ │ │ │ ─────── BOUNDARY 3 ─────────┤ │ │ ▼ │ │ ┌──────────────┐ ┌────────────┤ │ │ Users │ ─────────── BOUNDARY 4 ────────▶│ Jaeger │ │ │ (Browser) │ │ Query/UI │ │ └──────────────┘ └────────────┤ └─────────────────────────────────────────────────────────────────┘ ``` | Boundary | From | To | Security Controls | | -- | -- | -- | -- | | 1 | OTel SDK | Collector | TLS/mTLS, rate limiting | | 2 | Collector | Storage | TLS, authentication | | 3 | Storage | Query | TLS, authentication | | 4 | Users | Query/UI | TLS, bearer tokens, RBAC | ## Secure Design Principles ### Economy of Mechanism - **Implementation**: Jaeger leverages established protocols (OTLP, gRPC) rather than custom implementations - **Evidence**: Uses OpenTelemetry Collector framework for core functionality ### Fail-Safe Defaults - **Implementation**: TLS certificate verification is enabled by default when TLS is configured - **Evidence**: `insecure_skip_verify` must be explicitly set to disable verification - **Note**: TLS itself is opt-in to simplify initial testing and non-production deployments; for all production deployments, TLS (preferably mTLS where supported) MUST be enabled on all external and inter-service connections. ### Complete Mediation - **Implementation**: All API endpoints require passing through authentication when configured - **Evidence**: Bearer token and RBAC support at Query service level ### Open Design - **Implementation**: All source code is publicly available on GitHub - **Evidence**: Apache 2.0 license, public security documentation ### Separation of Privilege - **Implementation**: Different components (Collector, Query) can be deployed with different access levels - **Evidence**: Collector only writes, Query only reads from storage ### Least Privilege - **Implementation**: Storage credentials can be scoped to minimum required permissions - **Evidence**: Separate read/write keyspaces supported for Cassandra ### Least Common Mechanism - **Implementation**: Admin endpoints separated from data endpoints - **Evidence**: Separate ports for admin, metrics, and data APIs ### Psychological Acceptability - **Implementation**: Security is configurable via standard YAML configuration - **Evidence**: Consistent TLS configuration across all components ## Common Weakness Mitigations ### OWASP Top 10 / CWE Top 25 Coverage | Weakness | Mitigation | | -- | -- | | **Injection (CWE-89, CWE-79)** | Structured data formats (protobuf/OTLP), parameterized storage queries | | **Broken Authentication (CWE-287)** | Bearer tokens, OAuth2, mTLS support | | **Sensitive Data Exposure (CWE-200)** | TLS for all communications, no credentials in traces | | **XML External Entities** | Not applicable - uses protobuf/JSON | | **Broken Access Control (CWE-284)** | RBAC support in Query service | | **Security Misconfiguration** | Secure defaults where possible, configuration validation | | **Cross-Site Scripting (CWE-79)** | UI built with React (auto-escaping), CSP headers | | **Insecure Deserialization (CWE-502)** | Uses protobuf with schema validation | | **Insufficient Logging** | Comprehensive logging in all components | | **SSRF (CWE-918)** | No user-controlled URLs in backend requests | ### Go-Specific Security | Practice | Implementation | | -- | -- | | Memory Safety | Go's inherent memory safety | | Integer Overflow | Go's bounds checking | | Race Conditions | Go's race detector in CI | | Dependency Security | Dependabot, daily vulnerability scans | ## Security Controls ### Build and Release | Control | Implementation | | -- | -- | | Signed Commits | DCO required for all contributions | | Signed Releases | GPG-signed tags and artifacts | | SBOM | Generated for each release | | Container Security | Minimal base images (alpine/scratch) | | Supply Chain | Harden-Runner, pinned dependencies | ### Runtime | Control | Implementation | | -- | -- | | TLS/mTLS | Configurable for all connections | | Authentication | Bearer tokens, OAuth2, Kerberos | | Rate Limiting | Configurable at collector | | Input Validation | OTLP schema validation, size limits | ## References - [SECURITY.md](../../SECURITY.md) - Vulnerability reporting - [Threat Model](threat-model.md) - Detailed threat analysis - [Security Architecture](architecture.md) - Cryptographic practices - [Securing Jaeger Installation](https://www.jaegertracing.io/docs/latest/security/) ================================================ FILE: docs/security/self-assessment.md ================================================ # Jaeger Self-Assessment This document is a local copy of the Jaeger project's security self-assessment, originally conducted following the CNCF TAG Security assessment process. ## Project Overview Jaeger is a distributed tracing system originally developed at Uber Technologies and now a graduated project within the Cloud Native Computing Foundation (CNCF). ## Security Profile | Attribute | Value | | -- | -- | | Security Policy | [SECURITY.md](https://github.com/jaegertracing/jaeger/blob/main/SECURITY.md) | | Threat Model | [threat-model.md](threat-model.md) | | Assurance Case | [assurance-case.md](assurance-case.md) | | Security file | [SECURITY.md](https://github.com/jaegertracing/jaeger/blob/main/SECURITY.md) | ## Self-Assessment Summary ### Secure Design Principles Jaeger adheres to established secure design principles: - **Economy of Mechanism**: Uses standard protocols (OTLP, gRPC). - **Fail-Safe Defaults**: TLS verification enabled by default. - **Open Design**: Fully open-source and publicly documented. ### Trust Boundaries Trust boundaries exist between instrumented applications and the collector, between the collector and storage, and between the query service and users. Each boundary is protected by TLS and authentication controls. ### Security Testing - **Unit/Integration Tests**: Comprehensive test suite with high coverage requirements. - **Static Analysis**: Uses `golangci-lint` and `gosec`. - **Dependency Scanning**: Daily scans via Dependabot. - **Vulnerability Reporting**: Formal process documented in `SECURITY.md`. ## Metadata | Attribute | Details | | -- | -- | | Last Updated | 2026-01-16 | | Status | Completed | | Assessment Process | CNCF TAG Security Self-Assessment | ## Vulnerability Handling Refer to [SECURITY.md](https://github.com/jaegertracing/jaeger/blob/main/SECURITY.md) and [Report Security Issue](https://www.jaegertracing.io/report-security-issue/). ================================================ FILE: docs/security/threat-model.md ================================================ # Jaeger Threat Model This document describes the threat model for the Jaeger distributed tracing system. ## Overview Jaeger is a distributed tracing platform that collects, processes, and visualizes trace data from instrumented applications. This threat model identifies potential threats and the controls implemented to mitigate them. ## System Architecture ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Instrumented │ │ Jaeger │ │ Storage │ │ Applications │─────▶│ Collector │─────▶│ Backend │ │ (OTel SDK) │ │ │ │ (ES/Cassandra) │ └─────────────────┘ └─────────────────┘ └────────┬────────┘ │ ┌─────────────────┐ │ │ Jaeger │◀──────────────┘ │ Query + UI │ └────────┬────────┘ │ ┌────────▼────────┐ │ Users │ │ (Browser) │ └─────────────────┘ ``` ## Trust Boundaries | Boundary | Description | Security Controls | | -- | -- | -- | | **B1: SDK → Collector** | External applications sending spans | TLS/mTLS, rate limiting, schema validation | | **B2: Collector → Storage** | Internal service to database | TLS, authentication, authorized credentials | | **B3: Storage → Query** | Database to internal service | TLS, authentication, read-only access | | **B4: Query → Users** | Internal service to end users | TLS, bearer tokens, RBAC | ## Threat Actors | Actor | Description | Motivation | | -- | -- | -- | | **Malicious Application** | Compromised or rogue service sending traces | Data poisoning, DoS, information injection | | **External Attacker** | Attacker with network access | Data exfiltration, reconnaissance, DoS | | **Malicious Insider** | User with legitimate access | Unauthorized data access, privilege escalation | | **Man-in-the-Middle** | Attacker on network path | Data interception, tampering | ## Threats and Mitigations ### T1: Denial of Service via Span Flooding **Description**: Malicious or misconfigured application sends excessive spans. | Attribute | Value | | -- | -- | | Threat Actor | Malicious Application | | Impact | High - Can overwhelm collector and storage | | Likelihood | Medium | **Mitigations**: - Rate limiting at collector - Adaptive sampling to reduce volume - Resource quotas per service - Kafka buffering for burst handling ### T2: Sensitive Data Exposure in Traces **Description**: Traces may inadvertently contain sensitive data (PII, credentials). | Attribute | Value | | -- | -- | | Threat Actor | External Attacker, Malicious Insider | | Impact | High - Data breach | | Likelihood | Medium | **Mitigations**: - TLS encryption for all connections - Access control (RBAC) on Query service - Data retention policies - Guidance for users on what not to trace ### T3: Man-in-the-Middle Attack **Description**: Attacker intercepts unencrypted trace traffic. | Attribute | Value | | -- | -- | | Threat Actor | Man-in-the-Middle | | Impact | High - Data interception and tampering | | Likelihood | Low (with TLS) | **Mitigations**: - TLS/mTLS for all communications - Certificate verification enabled by default - Certificate pinning optional ### T4: Unauthorized Access to Trace Data **Description**: Unauthorized user accesses the Query UI/API. | Attribute | Value | | -- | -- | | Threat Actor | External Attacker, Malicious Insider | | Impact | Medium - Information disclosure | | Likelihood | Medium | **Mitigations**: - Bearer token authentication - OAuth2 integration - RBAC for access control - Audit logging ### T5: Storage Backend Compromise **Description**: Attacker gains access to the storage backend directly. | Attribute | Value | | -- | -- | | Threat Actor | External Attacker | | Impact | High - Full data access | | Likelihood | Low | **Mitigations**: - Storage-level authentication - Network isolation - Encrypted connections to storage - Storage-level access controls ### T6: Supply Chain Attack **Description**: Compromised dependency introduced into build. | Attribute | Value | | -- | -- | | Threat Actor | External Attacker | | Impact | Critical - Code execution | | Likelihood | Low | **Mitigations**: - Dependabot vulnerability scanning - Signed commits (DCO) - GPG-signed releases - SBOM generation - Pinned dependencies with checksums ## Security Recommendations ### For Operators 1. **Enable TLS everywhere** - Use `tls.insecure: false` for all connections 2. **Use mTLS** - Especially for collector ingestion 3. **Configure authentication** - Enable bearer tokens or OAuth2 4. **Set up RBAC** - Limit who can access trace data 5. **Enable audit logging** - Track access to sensitive traces 6. **Use network segmentation** - Isolate Jaeger components ### For Developers Instrumenting Applications 1. **Never trace credentials** - Avoid logging passwords, tokens, API keys 2. **Sanitize PII** - Don't include personal information in spans 3. **Use sampling** - Reduce volume and exposure 4. **Review span content** - Audit what data is being traced ## References - [SECURITY.md](../../SECURITY.md) - Security policy and vulnerability reporting - [Security Architecture](architecture.md) - Cryptographic practices - [Assurance Case](assurance-case.md) - Security assurance case - [Securing Jaeger Installation](https://www.jaegertracing.io/docs/latest/security/) - [OpenSSF Threat Modeling Standards](https://github.com/ossf/security-insights-spec/tree/main/docs/threat-model) ================================================ FILE: docs/security/verifying-releases.md ================================================ # Verifying Jaeger Releases All Jaeger releases are cryptographically signed. Users should verify signatures before using release artifacts to ensure they have not been tampered with. ## Signed Artifacts | Artifact Type | Signing Method | |---------------|----------------| | Git tags | GPG signed (`git tag -s`) | | Binary archives | GPG detached signatures (`.asc` files) | | Container images | Verify image digest from official Docker Hub and Quay.io repositories | | SBOM | Included with each release | ## Verifying Container Image Authenticity Jaeger container images are published to official repositories on Docker Hub and Quay.io. To verify that you are using the intended image: 1. Pull images from the official Jaeger organization repositories on Docker Hub or Quay.io. 2. Use image digests (for example, `jaegertracing/all-in-one@sha256:`) rather than mutable tags where possible. 3. Compare the digest you deploy with the expected digest published in your deployment configuration, automation, or release notes. ## Verifying Binary Signatures 1. **Import the Jaeger GPG public key**: The Jaeger public key (`C043A4D2B3F2AC31`) is available on all major key servers. See [SECURITY.md](../../SECURITY.md#our-public-key) for the full key block. ```bash gpg --keyserver keyserver.ubuntu.com --recv-keys C043A4D2B3F2AC31 ``` 2. **Download the release artifact and its signature**: ```bash # Example for version v1.55.0 wget https://github.com/jaegertracing/jaeger/releases/download/v1.55.0/jaeger-1.55.0-linux-amd64.tar.gz wget https://github.com/jaegertracing/jaeger/releases/download/v1.55.0/jaeger-1.55.0-linux-amd64.tar.gz.asc ``` 3. **Verify the signature**: ```bash gpg --verify jaeger-1.55.0-linux-amd64.tar.gz.asc jaeger-1.55.0-linux-amd64.tar.gz ``` ## Verifying Git Tag Signatures You can verify the signature of any Jaeger Git tag using the following commands: ```bash git fetch --tags git tag -v v1.55.0 ``` ================================================ FILE: empty_test.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jaeger import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestDummy(*testing.T) { // This is a dummy test in the root package. // Without it `go test -v .` prints "testing: warning: no tests to run". } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: examples/grafana-integration/README.md ================================================ # Hot R.O.D. - Rides on Demand - Grafana integration This example combines the Hot R.O.D. demo application ([examples/hotrod/](../hotrod/)) with Grafana, Loki and Prometheus integration, to demonstrate logs, metrics and traces correlation. ## Running via `docker compose` ### Prerequisites * Clone the Jaeger repository `git clone https://github.com/jaegertracing/jaeger.git`, then `cd examples/grafana-integration` * All services will log to Loki via the [official Docker driver plugin](https://grafana.com/docs/loki/latest/clients/docker-driver/). Install the Loki logging plugin for Docker: ```bash docker plugin install \ grafana/loki-docker-driver:latest \ --alias loki \ --grant-all-permissions ``` ### Run the services `docker compose up` ### Access the services * HotROD application at http://localhost:8080 * Grafana UI at http://localhost:3000 (default credentials: admin/admin) ### Explore with Loki It is possible to correlate application logs with traces via Grafana's Explore interface. After setting the datasource to Loki, all the log labels become available, and can be easily filtered using [Loki's LogQL query language](https://grafana.com/docs/loki/latest/logql/). For example, after selecting the compose project/service under Log labels , errors can be filtered with the following expression: ``` {compose_project="grafana-integration"} |= "error" ``` which will list the redis timeout events. ### HotROD - Metrics and logs overview dashboard Since the HotROD application can expose its metrics in Prometheus' format, these can be also used during investigation. This example includes a dashboard that contains a log panel for the selected services in real time. These can be also filtered by a search field, that provides `grep`-like features. There are also panels to display the ratio/percentage of errors in the current timeframe. Additionally, there are graphs for each service, visualizing the rate of the requests and showing latency percentiles. ### Clean up `docker compose down` ================================================ FILE: examples/grafana-integration/docker-compose.yaml ================================================ services: grafana: image: grafana/grafana:10.2.2 ports: - '3000:3000' volumes: - ./grafana/datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml - ./grafana/dashboard.yml:/etc/grafana/provisioning/dashboards/dashboard.yml - ./grafana/hotrod_metrics_logs.json:/etc/grafana/provisioning/dashboards/hotrod_metrics_logs.json logging: driver: loki options: loki-url: 'http://localhost:3100/api/prom/push' loki: image: grafana/loki:2.9.2 ports: - '3100:3100' command: -config.file=/etc/loki/local-config.yaml # send Loki traces to Jaeger environment: - JAEGER_AGENT_HOST=jaeger - JAEGER_AGENT_PORT=6831 - JAEGER_SAMPLER_TYPE=const - JAEGER_SAMPLER_PARAM=1 logging: driver: loki options: loki-url: 'http://localhost:3100/api/prom/push' # Prevent container from being stuck when shutting down # https://github.com/grafana/loki/issues/2361#issuecomment-718024318 loki-timeout: 1s loki-max-backoff: 1s loki-retries: 1 jaeger: image: cr.jaegertracing.io/jaegertracing/all-in-one:1.51 ports: - '6831:6831' - '16686:16686' - '4318:4318' logging: driver: loki options: loki-url: 'http://localhost:3100/api/prom/push' hotrod: image: cr.jaegertracing.io/jaegertracing/example-hotrod:1.51 depends_on: - jaeger ports: - '8080:8080' - '8083:8083' command: ["-m","prometheus","all"] environment: - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318 logging: driver: loki options: loki-url: 'http://localhost:3100/api/prom/push' prometheus: image: prom/prometheus:v2.48.0 volumes: - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro ports: - '9090:9090' command: - --config.file=/etc/prometheus/prometheus.yml logging: driver: loki options: loki-url: 'http://localhost:3100/api/prom/push' ================================================ FILE: examples/grafana-integration/grafana/dashboard.yml ================================================ apiVersion: 1 providers: - name: 'HotROD' orgId: 1 folder: '' type: file disableDeletion: false editable: true options: path: /etc/grafana/provisioning/dashboards ================================================ FILE: examples/grafana-integration/grafana/datasources.yaml ================================================ apiVersion: 1 datasources: - name: Loki type: loki access: proxy url: http://loki:3100 editable: true isDefault: true - name: Jaeger type: jaeger access: browser url: http://jaeger:16686 editable: true - name: Prometheus type: prometheus access: proxy url: http://prometheus:9090 ================================================ FILE: examples/grafana-integration/grafana/hotrod_metrics_logs.json ================================================ { "annotations": { "list": [ { "builtIn": 1, "datasource": { "type": "datasource", "uid": "grafana" }, "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" } ] }, "description": "", "editable": true, "fiscalYearStartMonth": 0, "gnetId": 12611, "graphTooltip": 0, "links": [], "liveNow": false, "panels": [ { "collapsed": false, "datasource": { "type": "loki", "uid": "P8E80F9AEF21F6940" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 43, "panels": [], "targets": [ { "datasource": { "type": "loki", "uid": "P8E80F9AEF21F6940" }, "refId": "A" } ], "title": "Loki Overview", "type": "row" }, { "datasource": { "type": "loki", "uid": "P8E80F9AEF21F6940" }, "description": "Total Count of log lines in the specified time range", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ { "options": { "match": "null", "result": { "color": "rgb(31, 255, 7)", "text": "0" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "rgb(31, 255, 7)", "value": null }, { "color": "rgb(31, 255, 7)", "value": 10 }, { "color": "rgb(31, 255, 7)", "value": 50 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 3, "w": 12, "x": 0, "y": 1 }, "id": 11, "links": [], "maxDataPoints": 100, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "sum" ], "fields": "/^{}$/", "values": false }, "textMode": "auto", "wideLayout": true }, "pluginVersion": "10.2.2", "targets": [ { "datasource": { "type": "loki", "uid": "P8E80F9AEF21F6940" }, "expr": "sum(count_over_time(({container_name=\"$container_name\"})[$__interval]))", "hide": false, "legendFormat": "", "refId": "A" } ], "title": "Total Count of logs", "type": "stat" }, { "datasource": { "type": "loki", "uid": "P8E80F9AEF21F6940" }, "description": "Total Count: of pattern: $searchable_pattern in the specified time range", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ { "options": { "match": "null", "result": { "color": "rgb(222, 15, 43)", "text": "0" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "rgb(222, 15, 43)", "value": null }, { "color": "rgb(222, 15, 43)", "value": 10 }, { "color": "rgb(222, 15, 43)", "value": 50 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 3, "w": 12, "x": 12, "y": 1 }, "id": 6, "links": [], "maxDataPoints": 100, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "sum" ], "fields": "/^{}$/", "values": false }, "textMode": "auto", "wideLayout": true }, "pluginVersion": "10.2.2", "targets": [ { "datasource": { "type": "loki", "uid": "P8E80F9AEF21F6940" }, "expr": "sum(count_over_time(({container_name=\"$container_name\"} |~ \"(?i)$searchable_pattern\")[$__interval]))", "hide": false, "legendFormat": "", "refId": "A" } ], "title": "Total Count: of pattern $searchable_pattern", "type": "stat" }, { "datasource": { "type": "loki", "uid": "P8E80F9AEF21F6940" }, "description": "Live logs is a like 'tail -f' in a real time", "gridPos": { "h": 8, "w": 18, "x": 0, "y": 4 }, "id": 2, "options": { "dedupStrategy": "none", "enableLogDetails": true, "prettifyLogMessage": false, "showCommonLabels": false, "showLabels": false, "showTime": false, "sortOrder": "Descending", "wrapLogMessage": false }, "targets": [ { "datasource": { "type": "loki", "uid": "P8E80F9AEF21F6940" }, "expr": "{container_name=\"$container_name\"} |~ \"(?i)$searchable_pattern\" ", "hide": false, "legendFormat": "", "refId": "A" } ], "title": "Live logs", "type": "logs" }, { "datasource": { "type": "loki", "uid": "P8E80F9AEF21F6940" }, "description": "", "fieldConfig": { "defaults": { "mappings": [ { "options": { "match": "null", "result": { "color": "#299c46", "text": "0" } }, "type": "special" } ], "max": 100, "min": 0, "noValue": "0", "thresholds": { "mode": "absolute", "steps": [ { "color": "#299c46", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 10 }, { "color": "#C4162A", "value": 50 } ] }, "unit": "percent" }, "overrides": [] }, "gridPos": { "h": 8, "w": 6, "x": 18, "y": 4 }, "id": 9, "links": [], "maxDataPoints": 100, "options": { "minVizHeight": 75, "minVizWidth": 75, "orientation": "auto", "reduceOptions": { "calcs": [ "mean" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": false }, "pluginVersion": "10.2.2", "targets": [ { "datasource": { "type": "loki", "uid": "P8E80F9AEF21F6940" }, "expr": "sum(count_over_time(({container_name=\"$container_name\"} |~ \"(?i)$searchable_pattern\")[$__interval])) * 100 / sum(count_over_time(({container_name=\"$container_name\"})[$__interval]))", "hide": false, "legendFormat": "%", "refId": "A" } ], "title": "Percentage of \"$searchable_pattern\" for specified time", "type": "gauge" }, { "collapsed": false, "datasource": { "type": "loki", "uid": "P8E80F9AEF21F6940" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 12 }, "id": 37, "panels": [], "repeat": "endpoint", "repeatDirection": "h", "targets": [ { "datasource": { "type": "loki", "uid": "P8E80F9AEF21F6940" }, "refId": "A" } ], "title": "Endpoint: $endpoint", "type": "row" }, { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": true, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 8, "w": 7, "x": 0, "y": 13 }, "id": 13, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "10.2.2", "targets": [ { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "editorMode": "code", "expr": "rate(hotrod_http_requests_total{endpoint=~\"$endpoint\"}[5m])", "interval": "", "legendFormat": "{{status_code}}", "range": true, "refId": "A" } ], "title": "Frontend HTTP response codes", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 1, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 8, "x": 7, "y": 13 }, "id": 25, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "10.2.2", "targets": [ { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "editorMode": "code", "expr": "sum(rate(hotrod_http_requests_total{endpoint=\"/\"}[5m])) by (error)", "interval": "", "legendFormat": "{{endpoint}} error: {{error}}", "range": true, "refId": "A" } ], "title": "Frontend requests total", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": true, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "s" }, "overrides": [] }, "gridPos": { "h": 8, "w": 9, "x": 15, "y": 13 }, "id": 15, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "10.2.2", "targets": [ { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "editorMode": "code", "expr": "histogram_quantile(0.99, sum(rate(hotrod_request_latency_bucket{endpoint=\"/\"}[5m])) by (le))", "interval": "", "legendFormat": "P99", "range": true, "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "editorMode": "code", "expr": "histogram_quantile(0.95, sum(rate(hotrod_request_latency_bucket{endpoint=\"/\"}[5m])) by (le))", "interval": "", "legendFormat": "P95", "range": true, "refId": "B" }, { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "editorMode": "code", "expr": "histogram_quantile(0.50, sum(rate(hotrod_request_latency_bucket{endpoint=\"/\"}[5m])) by (le))", "interval": "", "legendFormat": "P50", "range": true, "refId": "C" } ], "title": "Frontend latency percentiles", "type": "timeseries" } ], "refresh": "10s", "schemaVersion": 38, "tags": [ "Loki", "logging", "prometheus", "metrics", "jaeger", "hotrod" ], "templating": { "list": [ { "current": { "selected": false, "text": "grafana-integration-hotrod-1", "value": "grafana-integration-hotrod-1" }, "datasource": { "type": "loki", "uid": "P8E80F9AEF21F6940" }, "definition": "label_values({container_name=~\".+\"}, container_name)", "hide": 0, "includeAll": false, "label": "Service", "multi": false, "name": "container_name", "options": [], "query": "label_values({container_name=~\".+\"}, container_name)", "refresh": 2, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false }, { "current": { "selected": true, "text": "error", "value": "error" }, "hide": 0, "label": "Search (case insensitive)", "name": "searchable_pattern", "options": [ { "selected": true, "text": "error", "value": "error" } ], "query": "error", "skipUrlSync": false, "type": "textbox" }, { "current": { "selected": true, "text": [ "All" ], "value": [ "$__all" ] }, "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "definition": "label_values(hotrod_requests_total,endpoint)", "hide": 0, "includeAll": true, "label": "Service", "multi": true, "name": "endpoint", "options": [], "query": { "qryType": 1, "query": "label_values(hotrod_requests_total,endpoint)", "refId": "PrometheusVariableQueryEditor-VariableQuery" }, "refresh": 2, "regex": "", "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false } ] }, "time": { "from": "now-15m", "to": "now" }, "timepicker": { "refresh_intervals": [ "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d" ] }, "timezone": "", "title": "HotROD - Metrics and Logs Overview", "uid": "fRIvzUZMz", "version": 1, "weekStart": "" } ================================================ FILE: examples/grafana-integration/prometheus/prometheus.yml ================================================ global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: 'prometheus' scrape_interval: 5s static_configs: - targets: - localhost:9090 - job_name: 'hotrod-application' static_configs: - targets: - hotrod:8080 ================================================ FILE: examples/hotrod/.gitignore ================================================ hotrod-linux ================================================ FILE: examples/hotrod/Dockerfile ================================================ # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 FROM alpine:3.23.0@sha256:51183f2cfa6320055da30872f211093f9ff1d3cf06f39a0bdb212314c5dc7375 AS cert RUN apk add --update --no-cache ca-certificates FROM scratch ARG TARGETARCH EXPOSE 8080 8081 8082 8083 COPY --from=cert /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY hotrod-linux-$TARGETARCH /go/bin/hotrod-linux ENTRYPOINT ["/go/bin/hotrod-linux"] CMD ["all"] ================================================ FILE: examples/hotrod/README.md ================================================ # Hot R.O.D. - Rides on Demand This is a demo application that consists of several microservices and illustrates the use of the OpenTelemetry API & SDK. It can be run standalone, but requires Jaeger backend to view the traces. A tutorial / walkthrough is available: * as a blog post [Take Jaeger for a HotROD ride][hotrod-tutorial], * as a video [OpenShift Commons Briefing: Distributed Tracing with Jaeger & Prometheus on Kubernetes][hotrod-openshift]. As of Jaeger v1.42.0 this application was upgraded to use the OpenTelemetry SDK for traces. ## Features * Discover architecture of the whole system via data-driven dependency diagram * View request timeline & errors, understand how the app works * Find sources of latency, lack of concurrency * Highly contextualized logging * Use baggage propagation to * Diagnose inter-request contention (queueing) * Attribute time spent in a service * Use [opentelemetry-go-contrib](https://github.com/open-telemetry/opentelemetry-go-contrib) open source libraries to instrument HTTP and gRPC requests with minimal code changes ## Running 💥💥💥 Try it with Jaeger v2! See [cmd/jaeger](../../cmd/jaeger). ### Run everything via `docker compose` * Download `docker-compose.yml` from https://github.com/jaegertracing/jaeger/blob/main/examples/hotrod/docker-compose.yml * Optional: find the latest Jaeger version (see https://www.jaegertracing.io/download/) and pass it via environment variable `JAEGER_VERSION`. Otherwise `docker compose` will use the `latest` tag, which is fine for the first time you download the images, but once they are in your local registry the `latest` tag is never updated and you may be running stale (and possibly incompatible) verions of Jaeger and the HotROD app. * Run Jaeger backend and HotROD demo, e.g.: * `JAEGER_VERSION=2.14.0 docker compose -f path-to-yml-file up` * Access Jaeger UI at http://localhost:16686 and HotROD app at http://localhost:8080 * Shutdown / cleanup with `docker compose -f path-to-yml-file down` Alternatively, you can run each component separately as described below. ### Run everything in Kubernetes ```bash kustomize build ./kubernetes | kubectl apply -f - kubectl port-forward -n example-hotrod service/example-hotrod 8080:frontend # In another terminal kubectl port-forward -n example-hotrod service/jaeger 16686:frontend # To cleanup kustomize build ./kubernetes | kubectl delete -f - ``` Access Jaeger UI at http://localhost:16686 and HotROD app at http://localhost:8080 ### Run Jaeger backend Jaeger backend is packaged as a Docker container with in-memory storage. ```bash docker run \ --rm \ --name jaeger \ -p4318:4318 \ -p16686:16686 \ -p14268:14268 \ jaegertracing/jaeger:latest ``` Jaeger UI can be accessed at http://localhost:16686. ### Run HotROD from source ```bash git clone git@github.com:jaegertracing/jaeger.git jaeger cd jaeger go run ./examples/hotrod/main.go all ``` ### Run HotROD from docker ```bash docker run \ --rm \ --link jaeger \ --env OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318 \ -p8080-8083:8080-8083 \ jaegertracing/example-hotrod:latest \ all ``` Then open http://127.0.0.1:8080 ## Metrics The app exposes metrics in either Go's `expvar` format (by default) or in Prometheus format (enabled via `-m prometheus` flag). * `expvar`: `curl http://127.0.0.1:8080/debug/vars` * Prometheus: `curl http://127.0.0.1:8080/metrics` ## Linking to traces The HotROD UI can generate links to the Jaeger UI to find traces corresponding to each executed request. By default it uses the standard Jaeger UI address http://localhost:16686, but if your Jaeger UI is running at a different address, it can be customized via `-j
` flag passed to HotROD, e.g. ``` go run ./examples/hotrod/main.go all -j http://jaeger-ui:16686 ``` [hotrod-tutorial]: https://medium.com/jaegertracing/take-jaeger-for-a-hotrod-ride-233cf43e46c2 [hotrod-openshift]: https://blog.openshift.com/openshift-commons-briefing-82-distributed-tracing-with-jaeger-prometheus-on-kubernetes/ ================================================ FILE: examples/hotrod/cmd/all.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package cmd import ( "github.com/spf13/cobra" ) // allCmd represents the all command var allCmd = &cobra.Command{ Use: "all", Short: "Starts all services", Long: `Starts all services.`, RunE: func(_ *cobra.Command, args []string) error { logger.Info("Starting all services") go customerCmd.RunE(customerCmd, args) go driverCmd.RunE(driverCmd, args) go routeCmd.RunE(routeCmd, args) return frontendCmd.RunE(frontendCmd, args) }, } func init() { RootCmd.AddCommand(allCmd) } ================================================ FILE: examples/hotrod/cmd/customer.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package cmd import ( "net" "strconv" "github.com/spf13/cobra" "go.uber.org/zap" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" "github.com/jaegertracing/jaeger/examples/hotrod/services/customer" ) // customerCmd represents the customer command var customerCmd = &cobra.Command{ Use: "customer", Short: "Starts Customer service", Long: `Starts Customer service.`, RunE: func(_ *cobra.Command, _ /* args */ []string) error { zapLogger := logger.With(zap.String("service", "customer")) logger := log.NewFactory(zapLogger) server := customer.NewServer( net.JoinHostPort(customerHostname, strconv.Itoa(customerPort)), otelExporter, metricsFactory, logger, ) return logError(zapLogger, server.Run()) }, } func init() { RootCmd.AddCommand(customerCmd) } ================================================ FILE: examples/hotrod/cmd/driver.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package cmd import ( "net" "strconv" "github.com/spf13/cobra" "go.uber.org/zap" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" "github.com/jaegertracing/jaeger/examples/hotrod/services/driver" ) // driverCmd represents the driver command var driverCmd = &cobra.Command{ Use: "driver", Short: "Starts Driver service", Long: `Starts Driver service.`, RunE: func(_ *cobra.Command, _ /* args */ []string) error { zapLogger := logger.With(zap.String("service", "driver")) logger := log.NewFactory(zapLogger) server := driver.NewServer( net.JoinHostPort(driverHostname, strconv.Itoa(driverPort)), otelExporter, metricsFactory, logger, ) return logError(zapLogger, server.Run()) }, } func init() { RootCmd.AddCommand(driverCmd) } ================================================ FILE: examples/hotrod/cmd/empty_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package cmd import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: examples/hotrod/cmd/flags.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package cmd import ( "time" "github.com/spf13/cobra" ) var ( otelExporter string // otlp, stdout verbose bool fixDBConnDelay time.Duration fixDBConnDisableMutex bool fixRouteWorkerPoolSize int customerHostname string driverHostname string frontendHostname string routeHostname string customerPort int driverPort int frontendPort int routePort int basepath string jaegerUI string ) // used by root command func addFlags(cmd *cobra.Command) { cmd.PersistentFlags().StringVarP(&otelExporter, "otel-exporter", "x", "otlp", "OpenTelemetry exporter (otlp|stdout)") cmd.PersistentFlags().DurationVarP(&fixDBConnDelay, "fix-db-query-delay", "D", 300*time.Millisecond, "Average latency of MySQL DB query") cmd.PersistentFlags().BoolVarP(&fixDBConnDisableMutex, "fix-disable-db-conn-mutex", "M", false, "Disables the mutex guarding db connection") cmd.PersistentFlags().IntVarP(&fixRouteWorkerPoolSize, "fix-route-worker-pool-size", "W", 3, "Default worker pool size") // Add flags to choose hostnames for services cmd.PersistentFlags().StringVar(&customerHostname, "customer-service-hostname", "0.0.0.0", "Hostname for customer service") cmd.PersistentFlags().StringVar(&driverHostname, "driver-service-hostname", "0.0.0.0", "Hostname for driver service") cmd.PersistentFlags().StringVar(&frontendHostname, "frontend-service-hostname", "0.0.0.0", "Hostname for frontend service") cmd.PersistentFlags().StringVar(&routeHostname, "route-service-hostname", "0.0.0.0", "Hostname for routing service") // Add flags to choose ports for services cmd.PersistentFlags().IntVarP(&customerPort, "customer-service-port", "c", 8081, "Port for customer service") cmd.PersistentFlags().IntVarP(&driverPort, "driver-service-port", "d", 8082, "Port for driver service") cmd.PersistentFlags().IntVarP(&frontendPort, "frontend-service-port", "f", 8080, "Port for frontend service") cmd.PersistentFlags().IntVarP(&routePort, "route-service-port", "r", 8083, "Port for routing service") // Flag for serving frontend at custom basepath url cmd.PersistentFlags().StringVarP(&basepath, "basepath", "b", "", `Basepath for frontend service(default "/")`) cmd.PersistentFlags().StringVarP(&jaegerUI, "jaeger-ui", "j", "http://localhost:16686", "Address of Jaeger UI to create [find trace] links") cmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enables debug logging") } ================================================ FILE: examples/hotrod/cmd/frontend.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package cmd import ( "net" "strconv" "github.com/spf13/cobra" "go.uber.org/zap" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" "github.com/jaegertracing/jaeger/examples/hotrod/services/frontend" ) // frontendCmd represents the frontend command var frontendCmd = &cobra.Command{ Use: "frontend", Short: "Starts Frontend service", Long: `Starts Frontend service.`, RunE: func(_ *cobra.Command, _ /* args */ []string) error { options.FrontendHostPort = net.JoinHostPort(frontendHostname, strconv.Itoa(frontendPort)) options.DriverHostPort = net.JoinHostPort(driverHostname, strconv.Itoa(driverPort)) options.CustomerHostPort = net.JoinHostPort(customerHostname, strconv.Itoa(customerPort)) options.RouteHostPort = net.JoinHostPort(routeHostname, strconv.Itoa(routePort)) options.Basepath = basepath options.JaegerUI = jaegerUI zapLogger := logger.With(zap.String("service", "frontend")) logger := log.NewFactory(zapLogger) server := frontend.NewServer( options, tracing.InitOTEL("frontend", otelExporter, metricsFactory, logger), logger, ) return logError(zapLogger, server.Run()) }, } var options frontend.ConfigOptions func init() { RootCmd.AddCommand(frontendCmd) } ================================================ FILE: examples/hotrod/cmd/root.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package cmd import ( "os" "github.com/spf13/cobra" "go.uber.org/zap" "go.uber.org/zap/zapcore" "github.com/jaegertracing/jaeger/examples/hotrod/services/config" "github.com/jaegertracing/jaeger/internal/jaegerclientenv2otel" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/metrics/prometheus" ) var ( logger *zap.Logger metricsFactory metrics.Factory ) // RootCmd represents the base command when called without any subcommands var RootCmd = &cobra.Command{ Use: "examples-hotrod", Short: "HotR.O.D. - A tracing demo application", Long: `HotR.O.D. - A tracing demo application.`, } // Execute adds all child commands to the root command sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { if err := RootCmd.Execute(); err != nil { logger.Fatal("We bowled a googly", zap.Error(err)) os.Exit(-1) } } func init() { addFlags(RootCmd) cobra.OnInitialize(onInitialize) } // onInitialize is called before the command is executed. func onInitialize() { zapOptions := []zap.Option{ zap.AddStacktrace(zapcore.FatalLevel), zap.AddCallerSkip(1), } if !verbose { zapOptions = append(zapOptions, zap.IncreaseLevel(zap.LevelEnablerFunc(func(l zapcore.Level) bool { return l != zapcore.DebugLevel })), ) } logger, _ = zap.NewDevelopment(zapOptions...) jaegerclientenv2otel.MapJaegerToOtelEnvVars(logger) metricsFactory = prometheus.New().Namespace(metrics.NSOptions{Name: "hotrod", Tags: nil}) if config.MySQLGetDelay != fixDBConnDelay { logger.Info("fix: overriding MySQL query delay", zap.Duration("old", config.MySQLGetDelay), zap.Duration("new", fixDBConnDelay)) config.MySQLGetDelay = fixDBConnDelay } if fixDBConnDisableMutex { logger.Info("fix: disabling db connection mutex") config.MySQLMutexDisabled = true } if config.RouteWorkerPoolSize != fixRouteWorkerPoolSize { logger.Info("fix: overriding route worker pool size", zap.Int("old", config.RouteWorkerPoolSize), zap.Int("new", fixRouteWorkerPoolSize)) config.RouteWorkerPoolSize = fixRouteWorkerPoolSize } if customerHostname != "0.0.0.0" { logger.Info("changing customer service hostname", zap.String("old", "0.0.0.0"), zap.String("new", customerHostname)) } if driverHostname != "0.0.0.0" { logger.Info("changing driver service hostname", zap.String("old", "0.0.0.0"), zap.String("new", driverHostname)) } if frontendHostname != "0.0.0.0" { logger.Info("changing frontend service hostname", zap.String("old", "0.0.0.0"), zap.String("new", frontendHostname)) } if routeHostname != "0.0.0.0" { logger.Info("changing route service hostname", zap.String("old", "0.0.0.0"), zap.String("new", routeHostname)) } if customerPort != 8081 { logger.Info("changing customer service port", zap.Int("old", 8081), zap.Int("new", customerPort)) } if driverPort != 8082 { logger.Info("changing driver service port", zap.Int("old", 8082), zap.Int("new", driverPort)) } if frontendPort != 8080 { logger.Info("changing frontend service port", zap.Int("old", 8080), zap.Int("new", frontendPort)) } if routePort != 8083 { logger.Info("changing route service port", zap.Int("old", 8083), zap.Int("new", routePort)) } if basepath != "" { logger.Info("changing basepath for frontend", zap.String("old", "/"), zap.String("new", basepath)) } } func logError(logger *zap.Logger, err error) error { if err != nil { logger.Error("Error running command", zap.Error(err)) } return err } ================================================ FILE: examples/hotrod/cmd/route.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package cmd import ( "net" "strconv" "github.com/spf13/cobra" "go.uber.org/zap" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" "github.com/jaegertracing/jaeger/examples/hotrod/services/route" ) // routeCmd represents the route command var routeCmd = &cobra.Command{ Use: "route", Short: "Starts Route service", Long: `Starts Route service.`, RunE: func(_ *cobra.Command, _ /* args */ []string) error { zapLogger := logger.With(zap.String("service", "route")) logger := log.NewFactory(zapLogger) server := route.NewServer( net.JoinHostPort(routeHostname, strconv.Itoa(routePort)), tracing.InitOTEL("route", otelExporter, metricsFactory, logger), logger, ) return logError(zapLogger, server.Run()) }, } func init() { RootCmd.AddCommand(routeCmd) } ================================================ FILE: examples/hotrod/docker-compose.yml ================================================ # To run a specific version of Jaeger, use environment variable, e.g.: # JAEGER_VERSION=2.0.0 HOTROD_VERSION=1.63.0 docker compose up services: jaeger: image: ${REGISTRY:-}jaegertracing/jaeger:${JAEGER_VERSION:-latest} ports: - "16686:16686" - "16687:16687" - "4317:4317" - "4318:4318" environment: - LOG_LEVEL=debug networks: - jaeger-example hotrod: image: ${REGISTRY:-}jaegertracing/example-hotrod:${HOTROD_VERSION:-latest} # To run the latest trunk build, find the tag at Docker Hub and use the line below # https://hub.docker.com/r/jaegertracing/example-hotrod-snapshot/tags #image: jaegertracing/example-hotrod-snapshot:0ab8f2fcb12ff0d10830c1ee3bb52b745522db6c ports: - "8080:8080" - "8083:8083" command: ["all"] environment: - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318 networks: - jaeger-example depends_on: - jaeger networks: jaeger-example: ================================================ FILE: examples/hotrod/kubernetes/README.md ================================================ # Hot R.O.D. - Rides on Demand on Kubernetes Example deployment for the [hotrod app](..) using Helm charts in your Kubernetes environment (Kind, minikube, k3s, EKS, GKE). ## Prerequisites - Kubernetes cluster - Helm 3.x installed - kubectl configured ## Usage ### Deploy with Default Configuration ```bash cd examples/oci ./deploy-all.sh clean ``` ### Deploy with Local Images (for development) ```bash # For Jaeger v2 with local images cd examples/oci ./deploy-all.sh local ``` ### Deploy Modes - **`upgrade`** (default): Upgrade existing deployment or install if not present - **`local`**: Deploy using local registry images (localhost:5000) - **`clean`**: Clean install (removes existing deployment first) ### Access Applications After deployment completes, use port-forwarding: ```bash # Jaeger UI kubectl port-forward svc/jaeger-query 16686:16686 # HotROD application kubectl port-forward svc/jaeger-hotrod 8080:80 # Prometheus (optional) kubectl port-forward svc/prometheus 9090:9090 # Grafana (optional) kubectl port-forward svc/prometheus-grafana 9091:80 ``` Then access: - 🔍 **Jaeger UI**: http://localhost:16686/jaeger - 🚕 **HotROD App**: http://localhost:8080/hotrod - 📈 **Prometheus**: http://localhost:9090 - 📊 **Grafana**: http://localhost:9091 ## Configuration The deployment uses: - **Helm charts** from [jaeger-helm-charts](https://github.com/jaegertracing/helm-charts) - **Prometheus** for metrics collection - **Load generator** for creating sample traces Configuration files: - `jaeger-values.yaml` - Jaeger Helm chart values - `config.yaml` - Jaeger configuration - `ui-config.json` - Jaeger UI configuration - `monitoring-values.yaml` - Prometheus configuration ================================================ FILE: examples/hotrod/main.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package main import ( "github.com/jaegertracing/jaeger/examples/hotrod/cmd" ) func main() { cmd.Execute() } ================================================ FILE: examples/hotrod/pkg/delay/delay.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package delay import ( "math" "math/rand" "time" ) // Sleep generates a normally distributed random delay with given mean and stdDev // and blocks for that duration. func Sleep(mean time.Duration, stdDev time.Duration) { fMean := float64(mean) fStdDev := float64(stdDev) delay := time.Duration(math.Max(1, rand.NormFloat64()*fStdDev+fMean)) time.Sleep(delay) } ================================================ FILE: examples/hotrod/pkg/delay/empty_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package delay import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: examples/hotrod/pkg/httperr/empty_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package httperr import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: examples/hotrod/pkg/httperr/httperr.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package httperr import ( "net/http" ) // HandleError checks if the error is not nil, writes it to the output // with the specified status code, and returns true. If error is nil it returns false. func HandleError(w http.ResponseWriter, err error, statusCode int) bool { if err == nil { return false } http.Error(w, string(err.Error()), statusCode) return true } ================================================ FILE: examples/hotrod/pkg/log/empty_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package log import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: examples/hotrod/pkg/log/factory.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package log import ( "context" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // Factory is the default logging wrapper that can create // logger instances either for a given Context or context-less. type Factory struct { logger *zap.Logger } // NewFactory creates a new Factory. func NewFactory(logger *zap.Logger) Factory { return Factory{logger: logger} } // Bg creates a context-unaware logger. func (b Factory) Bg() Logger { return wrapper(b) } // For returns a context-aware Logger. If the context // contains a span, all logging calls are also // echo-ed into the span. func (b Factory) For(ctx context.Context) Logger { if span := trace.SpanFromContext(ctx); span != nil { logger := spanLogger{span: span, logger: b.logger} logger.spanFields = []zapcore.Field{ zap.String("trace_id", span.SpanContext().TraceID().String()), zap.String("span_id", span.SpanContext().SpanID().String()), } return logger } return b.Bg() } // With creates a child logger, and optionally adds some context fields to that logger. func (b Factory) With(fields ...zapcore.Field) Factory { return Factory{logger: b.logger.With(fields...)} } ================================================ FILE: examples/hotrod/pkg/log/logger.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package log import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // Logger is a simplified abstraction of the zap.Logger type Logger interface { Debug(msg string, fields ...zapcore.Field) Info(msg string, fields ...zapcore.Field) Error(msg string, fields ...zapcore.Field) Fatal(msg string, fields ...zapcore.Field) With(fields ...zapcore.Field) Logger } // wrapper delegates all calls to the underlying zap.Logger type wrapper struct { logger *zap.Logger } // Debug logs an debug msg with fields func (l wrapper) Debug(msg string, fields ...zapcore.Field) { l.logger.Debug(msg, fields...) } // Info logs an info msg with fields func (l wrapper) Info(msg string, fields ...zapcore.Field) { l.logger.Info(msg, fields...) } // Error logs an error msg with fields func (l wrapper) Error(msg string, fields ...zapcore.Field) { l.logger.Error(msg, fields...) } // Fatal logs a fatal error msg with fields func (l wrapper) Fatal(msg string, fields ...zapcore.Field) { l.logger.Fatal(msg, fields...) } // With creates a child logger, and optionally adds some context fields to that logger. func (l wrapper) With(fields ...zapcore.Field) Logger { return wrapper{logger: l.logger.With(fields...)} } ================================================ FILE: examples/hotrod/pkg/log/spanlogger.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package log import ( "fmt" "strconv" "time" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) type spanLogger struct { logger *zap.Logger span trace.Span spanFields []zapcore.Field } func (sl spanLogger) Debug(msg string, fields ...zapcore.Field) { sl.logToSpan("debug", msg, fields...) sl.logger.Debug(msg, append(sl.spanFields, fields...)...) } func (sl spanLogger) Info(msg string, fields ...zapcore.Field) { sl.logToSpan("info", msg, fields...) sl.logger.Info(msg, append(sl.spanFields, fields...)...) } func (sl spanLogger) Error(msg string, fields ...zapcore.Field) { sl.logToSpan("error", msg, fields...) sl.logger.Error(msg, append(sl.spanFields, fields...)...) } func (sl spanLogger) Fatal(msg string, fields ...zapcore.Field) { sl.logToSpan("fatal", msg, fields...) sl.span.SetStatus(codes.Error, msg) sl.logger.Fatal(msg, append(sl.spanFields, fields...)...) } // With creates a child logger, and optionally adds some context fields to that logger. func (sl spanLogger) With(fields ...zapcore.Field) Logger { return spanLogger{logger: sl.logger.With(fields...), span: sl.span, spanFields: sl.spanFields} } func (sl spanLogger) logToSpan(level, msg string, fields ...zapcore.Field) { fields = append(fields, zap.String("level", level)) sl.span.AddEvent( msg, trace.WithAttributes(logFieldsToOTelAttrs(fields)...), ) } func logFieldsToOTelAttrs(fields []zapcore.Field) []attribute.KeyValue { encoder := &bridgeFieldEncoder{} for _, field := range fields { field.AddTo(encoder) } return encoder.pairs } type bridgeFieldEncoder struct { pairs []attribute.KeyValue } func (e *bridgeFieldEncoder) AddArray(key string, marshaler zapcore.ArrayMarshaler) error { e.pairs = append(e.pairs, attribute.String(key, fmt.Sprint(marshaler))) return nil } func (e *bridgeFieldEncoder) AddObject(key string, marshaler zapcore.ObjectMarshaler) error { e.pairs = append(e.pairs, attribute.String(key, fmt.Sprint(marshaler))) return nil } func (e *bridgeFieldEncoder) AddBinary(key string, value []byte) { e.pairs = append(e.pairs, attribute.String(key, string(value))) } func (e *bridgeFieldEncoder) AddByteString(key string, value []byte) { e.pairs = append(e.pairs, attribute.String(key, string(value))) } func (e *bridgeFieldEncoder) AddBool(key string, value bool) { e.pairs = append(e.pairs, attribute.Bool(key, value)) } func (e *bridgeFieldEncoder) AddComplex128(key string, value complex128) { e.pairs = append(e.pairs, attribute.String(key, fmt.Sprint(value))) } func (e *bridgeFieldEncoder) AddComplex64(key string, value complex64) { e.pairs = append(e.pairs, attribute.String(key, fmt.Sprint(value))) } func (e *bridgeFieldEncoder) AddDuration(key string, value time.Duration) { e.pairs = append(e.pairs, attribute.String(key, value.String())) } func (e *bridgeFieldEncoder) AddFloat64(key string, value float64) { e.pairs = append(e.pairs, attribute.Float64(key, value)) } func (e *bridgeFieldEncoder) AddFloat32(key string, value float32) { e.pairs = append(e.pairs, attribute.Float64(key, float64(value))) } func (e *bridgeFieldEncoder) AddInt(key string, value int) { e.pairs = append(e.pairs, attribute.Int(key, value)) } func (e *bridgeFieldEncoder) AddInt64(key string, value int64) { e.pairs = append(e.pairs, attribute.Int64(key, value)) } func (e *bridgeFieldEncoder) AddInt32(key string, value int32) { e.pairs = append(e.pairs, attribute.Int64(key, int64(value))) } func (e *bridgeFieldEncoder) AddInt16(key string, value int16) { e.pairs = append(e.pairs, attribute.Int64(key, int64(value))) } func (e *bridgeFieldEncoder) AddInt8(key string, value int8) { e.pairs = append(e.pairs, attribute.Int64(key, int64(value))) } func (e *bridgeFieldEncoder) AddString(key, value string) { e.pairs = append(e.pairs, attribute.String(key, value)) } func (e *bridgeFieldEncoder) AddTime(key string, value time.Time) { e.pairs = append(e.pairs, attribute.String(key, value.String())) } func (e *bridgeFieldEncoder) AddUint(key string, value uint) { e.pairs = append(e.pairs, attribute.String(key, strconv.FormatUint(uint64(value), 10))) } func (e *bridgeFieldEncoder) AddUint64(key string, value uint64) { e.pairs = append(e.pairs, attribute.String(key, strconv.FormatUint(value, 10))) } func (e *bridgeFieldEncoder) AddUint32(key string, value uint32) { e.pairs = append(e.pairs, attribute.Int64(key, int64(value))) } func (e *bridgeFieldEncoder) AddUint16(key string, value uint16) { e.pairs = append(e.pairs, attribute.Int64(key, int64(value))) } func (e *bridgeFieldEncoder) AddUint8(key string, value uint8) { e.pairs = append(e.pairs, attribute.Int64(key, int64(value))) } func (e *bridgeFieldEncoder) AddUintptr(key string, value uintptr) { e.pairs = append(e.pairs, attribute.String(key, fmt.Sprint(value))) } func (*bridgeFieldEncoder) AddReflected(string /* key */, any /* value */) error { return nil } func (*bridgeFieldEncoder) OpenNamespace(string /* key */) {} ================================================ FILE: examples/hotrod/pkg/pool/empty_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package pool import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: examples/hotrod/pkg/pool/pool.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package pool // Pool is a simple worker pool type Pool struct { jobs chan func() stop chan struct{} } // New creates a new pool with the given number of workers func New(workers int) *Pool { jobs := make(chan func()) stop := make(chan struct{}) for range workers { go func() { for { select { case job := <-jobs: job() case <-stop: return } } }() } return &Pool{ jobs: jobs, stop: stop, } } // Execute enqueues the job to be executed by one of the workers in the pool func (p *Pool) Execute(job func()) { p.jobs <- job } ================================================ FILE: examples/hotrod/pkg/tracing/baggage.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracing import ( "context" "go.opentelemetry.io/otel/baggage" ) func BaggageItem(ctx context.Context, key string) string { b := baggage.FromContext(ctx) m := b.Member(key) return m.Value() } ================================================ FILE: examples/hotrod/pkg/tracing/empty_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracing import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: examples/hotrod/pkg/tracing/http.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package tracing import ( "context" "encoding/json" "errors" "io" "net/http" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel/trace" ) // HTTPClient wraps an http.Client with tracing instrumentation. type HTTPClient struct { TracerProvider trace.TracerProvider Client *http.Client } func NewHTTPClient(tp trace.TracerProvider) *HTTPClient { return &HTTPClient{ TracerProvider: tp, Client: &http.Client{ Transport: otelhttp.NewTransport( http.DefaultTransport, otelhttp.WithTracerProvider(tp), ), }, } } // GetJSON executes HTTP GET against specified url and tried to parse // the response into out object. func (c *HTTPClient) GetJSON(ctx context.Context, _ string /* endpoint */, url string, out any) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) if err != nil { return err } res, err := c.Client.Do(req) //nolint:gosec // G704 - URL from internal config if err != nil { return err } defer res.Body.Close() if res.StatusCode >= http.StatusBadRequest { body, err := io.ReadAll(res.Body) if err != nil { return err } return errors.New(string(body)) } decoder := json.NewDecoder(res.Body) return decoder.Decode(out) } ================================================ FILE: examples/hotrod/pkg/tracing/init.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package tracing import ( "context" "errors" "fmt" "os" "strings" "sync" "time" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing/rpcmetrics" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) var once sync.Once // InitOTEL initializes OpenTelemetry SDK. func InitOTEL(serviceName string, exporterType string, metricsFactory metrics.Factory, logger log.Factory) trace.TracerProvider { once.Do(func() { otel.SetTextMapPropagator( propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, propagation.Baggage{}, )) }) exp, err := createOtelExporter(exporterType) if err != nil { logger.Bg().Fatal("cannot create exporter", zap.String("exporterType", exporterType), zap.Error(err)) } logger.Bg().Debug("using " + exporterType + " trace exporter") rpcmetricsObserver := rpcmetrics.NewObserver(metricsFactory, rpcmetrics.DefaultNameNormalizer) res, err := resource.New( context.Background(), resource.WithSchemaURL(otelsemconv.SchemaURL), resource.WithAttributes(otelsemconv.ServiceNameAttribute(serviceName)), resource.WithTelemetrySDK(), resource.WithHost(), resource.WithOSType(), ) if err != nil { logger.Bg().Fatal("resource creation failed", zap.Error(err)) } tp := sdktrace.NewTracerProvider( sdktrace.WithBatcher(exp, sdktrace.WithBatchTimeout(1000*time.Millisecond)), sdktrace.WithSpanProcessor(rpcmetricsObserver), sdktrace.WithResource(res), ) logger.Bg().Debug("Created OTEL tracer", zap.String("service-name", serviceName)) return tp } // withSecure instructs the client to use HTTPS scheme, instead of hotrod's desired default HTTP func withSecure() bool { return strings.HasPrefix(os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT"), "https://") || strings.ToLower(os.Getenv("OTEL_EXPORTER_OTLP_INSECURE")) == "false" } func createOtelExporter(exporterType string) (sdktrace.SpanExporter, error) { var exporter sdktrace.SpanExporter var err error switch exporterType { case "jaeger": return nil, errors.New("jaeger exporter is no longer supported, please use otlp") case "otlp": var opts []otlptracehttp.Option if !withSecure() { opts = []otlptracehttp.Option{otlptracehttp.WithInsecure()} } exporter, err = otlptrace.New( context.Background(), otlptracehttp.NewClient(opts...), ) case "stdout": exporter, err = stdouttrace.New() default: return nil, fmt.Errorf("unrecognized exporter type %s", exporterType) } return exporter, err } ================================================ FILE: examples/hotrod/pkg/tracing/mutex.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package tracing import ( "context" "fmt" "sync" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" ) // Mutex is just like the standard sync.Mutex, except that it is aware of the Context // and logs some diagnostic information into the current span. type Mutex struct { SessionBaggageKey string LogFactory log.Factory realLock sync.Mutex holder string waiters []string waitersLock sync.Mutex } // Lock acquires an exclusive lock. func (sm *Mutex) Lock(ctx context.Context) { logger := sm.LogFactory.For(ctx) session := BaggageItem(ctx, sm.SessionBaggageKey) activeSpan := trace.SpanFromContext(ctx) activeSpan.SetAttributes(attribute.String(sm.SessionBaggageKey, session)) sm.waitersLock.Lock() if waiting := len(sm.waiters); waiting > 0 && activeSpan != nil { logger.Info( fmt.Sprintf("Waiting for lock behind %d transactions", waiting), zap.String("blockers", fmt.Sprintf("%v", sm.waiters)), ) } sm.waiters = append(sm.waiters, session) sm.waitersLock.Unlock() sm.realLock.Lock() sm.holder = session sm.waitersLock.Lock() behindLen := len(sm.waiters) - 1 behindIDs := fmt.Sprintf("%v", sm.waiters[1:]) // skip self sm.waitersLock.Unlock() logger.Info( fmt.Sprintf("Acquired lock; %d transactions waiting behind", behindLen), zap.String("waiters", behindIDs), ) } // Unlock releases the lock. func (sm *Mutex) Unlock() { sm.waitersLock.Lock() for i, v := range sm.waiters { if v == sm.holder { sm.waiters = append(sm.waiters[0:i], sm.waiters[i+1:]...) break } } sm.waitersLock.Unlock() sm.realLock.Unlock() } ================================================ FILE: examples/hotrod/pkg/tracing/mux.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package tracing import ( "net/http" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" ) // NewServeMux creates a new TracedServeMux. func NewServeMux(copyBaggage bool, tracer trace.TracerProvider, logger log.Factory) *TracedServeMux { return &TracedServeMux{ mux: http.NewServeMux(), copyBaggage: copyBaggage, tracer: tracer, logger: logger, } } // TracedServeMux is a wrapper around http.ServeMux that instruments handlers for tracing. type TracedServeMux struct { mux *http.ServeMux copyBaggage bool tracer trace.TracerProvider logger log.Factory } // Handle implements http.ServeMux#Handle, which is used to register new handler. func (tm *TracedServeMux) Handle(pattern string, handler http.Handler) { tm.logger.Bg().Debug("registering traced handler", zap.String("endpoint", pattern)) middleware := otelhttp.NewHandler( traceResponseHandler(handler), pattern, otelhttp.WithTracerProvider(tm.tracer)) tm.mux.Handle(pattern, middleware) } // ServeHTTP implements http.ServeMux#ServeHTTP. func (tm *TracedServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { tm.mux.ServeHTTP(w, r) } // Returns a handler that generates a traceresponse header. // https://github.com/w3c/trace-context/blob/main/spec/21-http_response_header_format.md func traceResponseHandler(handler http.Handler) http.Handler { // We use the standard TraceContext propagator, since the formats are identical. // But the propagator uses "traceparent" header name, so we inject it into a map // `carrier` and then use the result to set the "tracereponse" header. var prop propagation.TraceContext return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { carrier := make(map[string]string) prop.Inject(r.Context(), propagation.MapCarrier(carrier)) w.Header().Add("traceresponse", carrier["traceparent"]) handler.ServeHTTP(w, r) }) } ================================================ FILE: examples/hotrod/pkg/tracing/rpcmetrics/README.md ================================================ Package rpcmetrics implements an OpenTelemetry SpanProcessor that can be used to emit RPC metrics. This package is copied from jaeger-client-go and adapted to work with OpenTelemtery SDK. ================================================ FILE: examples/hotrod/pkg/tracing/rpcmetrics/endpoints.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package rpcmetrics import "sync" // normalizedEndpoints is a cache for endpointName -> safeName mappings. type normalizedEndpoints struct { names map[string]string maxSize int normalizer NameNormalizer mux sync.RWMutex } func newNormalizedEndpoints(maxSize int, normalizer NameNormalizer) *normalizedEndpoints { return &normalizedEndpoints{ maxSize: maxSize, normalizer: normalizer, names: make(map[string]string, maxSize), } } // normalize looks up the name in the cache, if not found it uses normalizer // to convert the name to a safe name. If called with more than maxSize unique // names it returns "" for all other names beyond those already cached. func (n *normalizedEndpoints) normalize(name string) string { n.mux.RLock() norm, ok := n.names[name] l := len(n.names) n.mux.RUnlock() if ok { return norm } if l >= n.maxSize { return "" } return n.normalizeWithLock(name) } func (n *normalizedEndpoints) normalizeWithLock(name string) string { norm := n.normalizer.Normalize(name) n.mux.Lock() defer n.mux.Unlock() // cache may have grown while we were not holding the lock if len(n.names) >= n.maxSize { return "" } n.names[name] = norm return norm } ================================================ FILE: examples/hotrod/pkg/tracing/rpcmetrics/endpoints_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package rpcmetrics import ( "testing" "github.com/stretchr/testify/assert" ) func TestNormalizedEndpoints(t *testing.T) { n := newNormalizedEndpoints(1, DefaultNameNormalizer) assertLen := func(l int) { n.mux.RLock() defer n.mux.RUnlock() assert.Len(t, n.names, l) } assert.Equal(t, "ab_cd", n.normalize("ab^cd"), "one translation") assert.Equal(t, "ab_cd", n.normalize("ab^cd"), "cache hit") assertLen(1) assert.Empty(t, n.normalize("xys"), "cache overflow") assertLen(1) } func TestNormalizedEndpointsDoubleLocking(t *testing.T) { n := newNormalizedEndpoints(1, DefaultNameNormalizer) assert.Equal(t, "ab_cd", n.normalize("ab^cd"), "fill out the cache") assert.Empty(t, n.normalizeWithLock("xys"), "cache overflow") } ================================================ FILE: examples/hotrod/pkg/tracing/rpcmetrics/metrics.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package rpcmetrics import ( "net/http" "sync" "github.com/jaegertracing/jaeger/internal/metrics" ) const ( otherEndpointsPlaceholder = "other" endpointNameMetricTag = "endpoint" ) // Metrics is a collection of metrics for an endpoint describing // throughput, success, errors, and performance. type Metrics struct { // RequestCountSuccess is a counter of the total number of successes. RequestCountSuccess metrics.Counter `metric:"requests" tags:"error=false"` // RequestCountFailures is a counter of the number of times any failure has been observed. RequestCountFailures metrics.Counter `metric:"requests" tags:"error=true"` // RequestLatencySuccess is a latency histogram of successful requests. RequestLatencySuccess metrics.Timer `metric:"request_latency" tags:"error=false"` // RequestLatencyFailures is a latency histogram of failed requests. RequestLatencyFailures metrics.Timer `metric:"request_latency" tags:"error=true"` // HTTPStatusCode2xx is a counter of the total number of requests with HTTP status code 200-299 HTTPStatusCode2xx metrics.Counter `metric:"http_requests" tags:"status_code=2xx"` // HTTPStatusCode3xx is a counter of the total number of requests with HTTP status code 300-399 HTTPStatusCode3xx metrics.Counter `metric:"http_requests" tags:"status_code=3xx"` // HTTPStatusCode4xx is a counter of the total number of requests with HTTP status code 400-499 HTTPStatusCode4xx metrics.Counter `metric:"http_requests" tags:"status_code=4xx"` // HTTPStatusCode5xx is a counter of the total number of requests with HTTP status code 500-599 HTTPStatusCode5xx metrics.Counter `metric:"http_requests" tags:"status_code=5xx"` } func (m *Metrics) recordHTTPStatusCode(statusCode int64) { switch { case statusCode >= http.StatusOK && statusCode < http.StatusMultipleChoices: m.HTTPStatusCode2xx.Inc(1) case statusCode >= http.StatusMultipleChoices && statusCode < http.StatusBadRequest: m.HTTPStatusCode3xx.Inc(1) case statusCode >= http.StatusBadRequest && statusCode < http.StatusInternalServerError: m.HTTPStatusCode4xx.Inc(1) case statusCode >= http.StatusInternalServerError && statusCode < 600: m.HTTPStatusCode5xx.Inc(1) default: // Status codes outside standard ranges (< 200 or >= 600) are ignored } } // MetricsByEndpoint is a registry/cache of metrics for each unique endpoint name. // Only maxNumberOfEndpoints Metrics are stored, all other endpoint names are mapped // to a generic endpoint name "other". type MetricsByEndpoint struct { metricsFactory metrics.Factory endpoints *normalizedEndpoints metricsByEndpoint map[string]*Metrics mux sync.RWMutex } func newMetricsByEndpoint( metricsFactory metrics.Factory, normalizer NameNormalizer, maxNumberOfEndpoints int, ) *MetricsByEndpoint { return &MetricsByEndpoint{ metricsFactory: metricsFactory, endpoints: newNormalizedEndpoints(maxNumberOfEndpoints, normalizer), metricsByEndpoint: make(map[string]*Metrics, maxNumberOfEndpoints+1), // +1 for "other" } } func (m *MetricsByEndpoint) get(endpoint string) *Metrics { safeName := m.endpoints.normalize(endpoint) if safeName == "" { safeName = otherEndpointsPlaceholder } m.mux.RLock() met := m.metricsByEndpoint[safeName] m.mux.RUnlock() if met != nil { return met } return m.getWithWriteLock(safeName) } // split to make easier to test func (m *MetricsByEndpoint) getWithWriteLock(safeName string) *Metrics { m.mux.Lock() defer m.mux.Unlock() // it is possible that the name has been already registered after we released // the read lock and before we grabbed the write lock, so check for that. if met, ok := m.metricsByEndpoint[safeName]; ok { return met } // it would be nice to create the struct before locking, since Init() is somewhat // expensive, however some metrics backends (e.g. expvar) may not like duplicate metrics. met := &Metrics{} tags := map[string]string{endpointNameMetricTag: safeName} metrics.Init(met, m.metricsFactory, tags) m.metricsByEndpoint[safeName] = met return met } ================================================ FILE: examples/hotrod/pkg/tracing/rpcmetrics/metrics_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package rpcmetrics import ( "testing" "github.com/stretchr/testify/assert" "github.com/jaegertracing/jaeger/internal/metricstest" ) // E.g. tags("key", "value", "key", "value") func tags(kv ...string) map[string]string { m := make(map[string]string) for i := 0; i < len(kv)-1; i += 2 { m[kv[i]] = kv[i+1] } return m } func endpointTags(endpoint string, kv ...string) map[string]string { return tags(append([]string{"endpoint", endpoint}, kv...)...) } func TestMetricsByEndpoint(t *testing.T) { met := metricstest.NewFactory(0) mbe := newMetricsByEndpoint(met, DefaultNameNormalizer, 2) m1 := mbe.get("abc1") m2 := mbe.get("abc1") // from cache m2a := mbe.getWithWriteLock("abc1") // from cache in double-checked lock assert.Equal(t, m1, m2) assert.Equal(t, m1, m2a) m3 := mbe.get("abc3") m4 := mbe.get("overflow") m5 := mbe.get("overflow2") for _, m := range []*Metrics{m1, m2, m2a, m3, m4, m5} { m.RequestCountSuccess.Inc(1) } met.AssertCounterMetrics(t, metricstest.ExpectedMetric{Name: "requests", Tags: endpointTags("abc1", "error", "false"), Value: 3}, metricstest.ExpectedMetric{Name: "requests", Tags: endpointTags("abc3", "error", "false"), Value: 1}, metricstest.ExpectedMetric{Name: "requests", Tags: endpointTags("other", "error", "false"), Value: 2}, ) } func TestRecordHTTPStatusCode_DefaultCase(t *testing.T) { met := metricstest.NewFactory(0) mbe := newMetricsByEndpoint(met, DefaultNameNormalizer, 2) metrics := mbe.get("test-endpoint") metrics.recordHTTPStatusCode(100) metrics.recordHTTPStatusCode(199) metrics.recordHTTPStatusCode(600) metrics.recordHTTPStatusCode(999) met.AssertCounterMetrics(t) metrics.recordHTTPStatusCode(200) metrics.recordHTTPStatusCode(404) metrics.recordHTTPStatusCode(500) met.AssertCounterMetrics(t, metricstest.ExpectedMetric{Name: "http_requests", Tags: endpointTags("test_endpoint", "status_code", "2xx"), Value: 1}, metricstest.ExpectedMetric{Name: "http_requests", Tags: endpointTags("test_endpoint", "status_code", "4xx"), Value: 1}, metricstest.ExpectedMetric{Name: "http_requests", Tags: endpointTags("test_endpoint", "status_code", "5xx"), Value: 1}, ) } ================================================ FILE: examples/hotrod/pkg/tracing/rpcmetrics/normalizer.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package rpcmetrics // NameNormalizer is used to convert the endpoint names to strings // that can be safely used as tags in the metrics. type NameNormalizer interface { Normalize(name string) string } // DefaultNameNormalizer converts endpoint names so that they contain only characters // from the safe charset [a-zA-Z0-9./_]. All other characters are replaced with '_'. var DefaultNameNormalizer = &SimpleNameNormalizer{ SafeSets: []SafeCharacterSet{ &Range{From: 'a', To: 'z'}, &Range{From: 'A', To: 'Z'}, &Range{From: '0', To: '9'}, &Char{'_'}, &Char{'/'}, &Char{'.'}, }, Replacement: '_', } // SimpleNameNormalizer uses a set of safe character sets. type SimpleNameNormalizer struct { SafeSets []SafeCharacterSet Replacement byte } // SafeCharacterSet determines if the given character is "safe" type SafeCharacterSet interface { IsSafe(c byte) bool } // Range implements SafeCharacterSet type Range struct { From, To byte } // IsSafe implements SafeCharacterSet func (r *Range) IsSafe(c byte) bool { return c >= r.From && c <= r.To } // Char implements SafeCharacterSet type Char struct { Val byte } // IsSafe implements SafeCharacterSet func (ch *Char) IsSafe(c byte) bool { return c == ch.Val } // Normalize checks each character in the string against SafeSets, // and if it's not safe substitutes it with Replacement. func (n *SimpleNameNormalizer) Normalize(name string) string { var retMe []byte nameBytes := []byte(name) for i, b := range nameBytes { if n.safeByte(b) { if retMe != nil { retMe[i] = b } } else { if retMe == nil { retMe = make([]byte, len(nameBytes)) copy(retMe[0:i], nameBytes[0:i]) } retMe[i] = n.Replacement } } if retMe == nil { return name } return string(retMe) } // safeByte checks if b against all safe charsets. func (n *SimpleNameNormalizer) safeByte(b byte) bool { for i := range n.SafeSets { if n.SafeSets[i].IsSafe(b) { return true } } return false } ================================================ FILE: examples/hotrod/pkg/tracing/rpcmetrics/normalizer_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package rpcmetrics import ( "testing" "github.com/stretchr/testify/assert" ) func TestSimpleNameNormalizer(t *testing.T) { n := &SimpleNameNormalizer{ SafeSets: []SafeCharacterSet{ &Range{From: 'a', To: 'z'}, &Char{'-'}, }, Replacement: '-', } assert.Equal(t, "ab-cd", n.Normalize("ab-cd"), "all valid") assert.Equal(t, "ab-cd", n.Normalize("ab.cd"), "single mismatch") assert.Equal(t, "a--cd", n.Normalize("aB-cd"), "range letter mismatch") } ================================================ FILE: examples/hotrod/pkg/tracing/rpcmetrics/observer.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package rpcmetrics import ( "context" "strconv" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) const defaultMaxNumberOfEndpoints = 200 var _ sdktrace.SpanProcessor = (*Observer)(nil) // Observer is an observer that can emit RPC metrics. type Observer struct { metricsByEndpoint *MetricsByEndpoint } // NewObserver creates a new observer that can emit RPC metrics. func NewObserver(metricsFactory metrics.Factory, normalizer NameNormalizer) *Observer { return &Observer{ metricsByEndpoint: newMetricsByEndpoint( metricsFactory, normalizer, defaultMaxNumberOfEndpoints, ), } } func (*Observer) OnStart(context.Context /* parent */, sdktrace.ReadWriteSpan) {} func (o *Observer) OnEnd(sp sdktrace.ReadOnlySpan) { operationName := sp.Name() if operationName == "" { return } if sp.SpanKind() != trace.SpanKindServer { return } mets := o.metricsByEndpoint.get(operationName) latency := sp.EndTime().Sub(sp.StartTime()) if status := sp.Status(); status.Code == codes.Error { mets.RequestCountFailures.Inc(1) mets.RequestLatencyFailures.Record(latency) } else { mets.RequestCountSuccess.Inc(1) mets.RequestLatencySuccess.Record(latency) } for _, attr := range sp.Attributes() { if string(attr.Key) == string(otelsemconv.HTTPResponseStatusCodeKey) { if attr.Value.Type() == attribute.INT64 { mets.recordHTTPStatusCode(attr.Value.AsInt64()) } else if attr.Value.Type() == attribute.STRING { s := attr.Value.AsString() if n, err := strconv.Atoi(s); err == nil { mets.recordHTTPStatusCode(int64(n)) } } } } } func (*Observer) Shutdown(context.Context) error { return nil } func (*Observer) ForceFlush(context.Context) error { return nil } ================================================ FILE: examples/hotrod/pkg/tracing/rpcmetrics/observer_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package rpcmetrics import ( "context" "fmt" "testing" "time" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace" u "github.com/jaegertracing/jaeger/internal/metricstest" "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) type testTracer struct { metrics *u.Factory tracer trace.Tracer } func withTestTracer(runTest func(tt *testTracer)) { metrics := u.NewFactory(time.Minute) defer metrics.Stop() observer := NewObserver(metrics, DefaultNameNormalizer) tp := sdktrace.NewTracerProvider( sdktrace.WithSpanProcessor(observer), sdktrace.WithResource(resource.NewWithAttributes( otelsemconv.SchemaURL, otelsemconv.ServiceNameAttribute("test"), )), ) runTest(&testTracer{ metrics: metrics, tracer: tp.Tracer("test"), }) } func TestObserver(t *testing.T) { withTestTracer(func(testTracer *testTracer) { ts := time.Now() finishOptions := trace.WithTimestamp(ts.Add((50 * time.Millisecond))) testCases := []struct { name string spanKind trace.SpanKind opNameOverride string err bool }{ {name: "local-span", spanKind: trace.SpanKindInternal}, {name: "get-user", spanKind: trace.SpanKindServer}, {name: "get-user", spanKind: trace.SpanKindServer, opNameOverride: "get-user-override"}, {name: "get-user", spanKind: trace.SpanKindServer, err: true}, {name: "get-user-client", spanKind: trace.SpanKindClient}, } for _, testCase := range testCases { _, span := testTracer.tracer.Start( context.Background(), testCase.name, trace.WithSpanKind(testCase.spanKind), trace.WithTimestamp(ts), ) if testCase.opNameOverride != "" { span.SetName(testCase.opNameOverride) } if testCase.err { span.SetStatus(codes.Error, "An error occurred") } span.End(finishOptions) } testTracer.metrics.AssertCounterMetrics(t, u.ExpectedMetric{Name: "requests", Tags: endpointTags("local_span", "error", "false"), Value: 0}, u.ExpectedMetric{Name: "requests", Tags: endpointTags("get_user", "error", "false"), Value: 1}, u.ExpectedMetric{Name: "requests", Tags: endpointTags("get_user", "error", "true"), Value: 1}, u.ExpectedMetric{Name: "requests", Tags: endpointTags("get_user_override", "error", "false"), Value: 1}, u.ExpectedMetric{Name: "requests", Tags: endpointTags("get_user_client", "error", "false"), Value: 0}, ) testTracer.metrics.AssertTimerMetrics(t, u.ExpectedTimerMetric{Name: "request_latency", Tags: endpointTags("get_user", "error", "false"), Percentile: "P99", Value: 50}, u.ExpectedTimerMetric{Name: "request_latency", Tags: endpointTags("get_user", "error", "true"), Percentile: "P99", Value: 50}, ) }) } func TestTags(t *testing.T) { type tagTestCase struct { attr attribute.KeyValue err bool metrics []u.ExpectedMetric } testCases := []tagTestCase{ {err: false, metrics: []u.ExpectedMetric{ {Name: "requests", Value: 1, Tags: tags("error", "false")}, }}, {err: true, metrics: []u.ExpectedMetric{ {Name: "requests", Value: 1, Tags: tags("error", "true")}, }}, } for i := 200; i <= 500; i += 100 { testCases = append(testCases, tagTestCase{ attr: otelsemconv.HTTPResponseStatusCode(i), metrics: []u.ExpectedMetric{ {Name: "http_requests", Value: 1, Tags: tags("status_code", fmt.Sprintf("%dxx", i/100))}, }, }) } for _, testCase := range testCases { for i := range testCase.metrics { testCase.metrics[i].Tags["endpoint"] = "span" } t.Run(fmt.Sprintf("%s-%v", testCase.attr.Key, testCase.attr.Value), func(t *testing.T) { withTestTracer(func(testTracer *testTracer) { _, span := testTracer.tracer.Start( context.Background(), "span", trace.WithSpanKind(trace.SpanKindServer), ) span.SetAttributes(testCase.attr) if testCase.err { span.SetStatus(codes.Error, "An error occurred") } span.End() testTracer.metrics.AssertCounterMetrics(t, testCase.metrics...) }) }) } } ================================================ FILE: examples/hotrod/pkg/tracing/rpcmetrics/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package rpcmetrics import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: examples/hotrod/services/config/config.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package config import ( "time" ) var ( // 'frontend' service // RouteWorkerPoolSize is the size of the worker pool used to query `route` service. // Can be overwritten from command line. RouteWorkerPoolSize = 3 // 'customer' service // MySQLGetDelay is how long retrieving a customer record takes. // Using large value mostly because I cannot click the button fast enough to cause a queue. MySQLGetDelay = 300 * time.Millisecond // MySQLGetDelayStdDev is standard deviation MySQLGetDelayStdDev = MySQLGetDelay / 10 // MySQLMutexDisabled controls whether there is a mutex guarding db query execution. // When not disabled it simulates a misconfigured connection pool of size 1. MySQLMutexDisabled = false // 'driver' service // RedisFindDelay is how long finding closest drivers takes. RedisFindDelay = 20 * time.Millisecond // RedisFindDelayStdDev is standard deviation. RedisFindDelayStdDev = RedisFindDelay / 4 // RedisGetDelay is how long retrieving a driver record takes. RedisGetDelay = 10 * time.Millisecond // RedisGetDelayStdDev is standard deviation RedisGetDelayStdDev = RedisGetDelay / 4 // 'route' service // RouteCalcDelay is how long a route calculation takes RouteCalcDelay = 50 * time.Millisecond // RouteCalcDelayStdDev is standard deviation RouteCalcDelayStdDev = RouteCalcDelay / 4 ) ================================================ FILE: examples/hotrod/services/config/empty_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package config import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: examples/hotrod/services/customer/client.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package customer import ( "context" "fmt" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" ) // Client is a remote client that implements customer.Interface type Client struct { logger log.Factory client *tracing.HTTPClient hostPort string } // NewClient creates a new customer.Client func NewClient(tracer trace.TracerProvider, logger log.Factory, hostPort string) *Client { return &Client{ logger: logger, client: tracing.NewHTTPClient(tracer), hostPort: hostPort, } } // Get implements customer.Interface#Get as an RPC func (c *Client) Get(ctx context.Context, customerID int) (*Customer, error) { c.logger.For(ctx).Info("Getting customer", zap.Int("customer_id", customerID)) url := fmt.Sprintf("http://"+c.hostPort+"/customer?customer=%d", customerID) var customer Customer if err := c.client.GetJSON(ctx, "/customer", url, &customer); err != nil { return nil, err } return &customer, nil } ================================================ FILE: examples/hotrod/services/customer/database.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package customer import ( "context" "errors" "fmt" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/delay" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" "github.com/jaegertracing/jaeger/examples/hotrod/services/config" "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) // database simulates Customer repository implemented on top of an SQL database type database struct { tracer trace.Tracer logger log.Factory customers map[int]*Customer lock *tracing.Mutex } func newDatabase(tracer trace.Tracer, logger log.Factory) *database { return &database{ tracer: tracer, logger: logger, lock: &tracing.Mutex{ SessionBaggageKey: "request", LogFactory: logger, }, customers: map[int]*Customer{ 123: { ID: "123", Name: "Rachel's_Floral_Designs", Location: "115,277", }, 567: { ID: "567", Name: "Amazing_Coffee_Roasters", Location: "211,653", }, 392: { ID: "392", Name: "Trom_Chocolatier", Location: "577,322", }, 731: { ID: "731", Name: "Japanese_Desserts", Location: "728,326", }, }, } } func (d *database) Get(ctx context.Context, customerID int) (*Customer, error) { d.logger.For(ctx).Info("Loading customer", zap.Int("customer_id", customerID)) ctx, span := d.tracer.Start(ctx, "SQL SELECT", trace.WithSpanKind(trace.SpanKindClient)) span.SetAttributes( otelsemconv.PeerServiceAttribute("mysql"), attribute. Key("sql.query"). String(fmt.Sprintf("SELECT * FROM customer WHERE customer_id=%d", customerID)), ) defer span.End() if !config.MySQLMutexDisabled { // simulate misconfigured connection pool that only gives one connection at a time d.lock.Lock(ctx) defer d.lock.Unlock() } // simulate RPC delay delay.Sleep(config.MySQLGetDelay, config.MySQLGetDelayStdDev) if customer, ok := d.customers[customerID]; ok { return customer, nil } return nil, errors.New("invalid customer ID") } ================================================ FILE: examples/hotrod/services/customer/empty_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package customer import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: examples/hotrod/services/customer/interface.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package customer import ( "context" ) // Customer contains data about a customer. type Customer struct { ID string Name string Location string } // Interface exposed by the Customer service. type Interface interface { Get(ctx context.Context, customerID int) (*Customer, error) } ================================================ FILE: examples/hotrod/services/customer/server.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package customer import ( "encoding/json" "net/http" "strconv" "time" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/httperr" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" "github.com/jaegertracing/jaeger/internal/metrics" ) // Server implements Customer service type Server struct { hostPort string tracer trace.TracerProvider logger log.Factory database *database } // NewServer creates a new customer.Server func NewServer(hostPort string, otelExporter string, metricsFactory metrics.Factory, logger log.Factory) *Server { return &Server{ hostPort: hostPort, tracer: tracing.InitOTEL("customer", otelExporter, metricsFactory, logger), logger: logger, database: newDatabase( tracing.InitOTEL("mysql", otelExporter, metricsFactory, logger).Tracer("mysql"), logger.With(zap.String("component", "mysql")), ), } } // Run starts the Customer server func (s *Server) Run() error { mux := s.createServeMux() s.logger.Bg().Info("Starting", zap.String("address", "http://"+s.hostPort)) server := &http.Server{ Addr: s.hostPort, Handler: mux, ReadHeaderTimeout: 3 * time.Second, } return server.ListenAndServe() } func (s *Server) createServeMux() http.Handler { mux := tracing.NewServeMux(false, s.tracer, s.logger) mux.Handle("/customer", http.HandlerFunc(s.customer)) return mux } func (s *Server) customer(w http.ResponseWriter, r *http.Request) { ctx := r.Context() s.logger.For(ctx).Info("HTTP request received", zap.String("method", r.Method), zap.Stringer("url", r.URL)) if err := r.ParseForm(); httperr.HandleError(w, err, http.StatusBadRequest) { s.logger.For(ctx).Error("bad request", zap.Error(err)) return } customer := r.Form.Get("customer") if customer == "" { http.Error(w, "Missing required 'customer' parameter", http.StatusBadRequest) return } customerID, err := strconv.Atoi(customer) if err != nil { http.Error(w, "Parameter 'customer' is not an integer", http.StatusBadRequest) return } response, err := s.database.Get(ctx, customerID) if httperr.HandleError(w, err, http.StatusInternalServerError) { s.logger.For(ctx).Error("request failed", zap.Error(err)) return } data, err := json.Marshal(response) if httperr.HandleError(w, err, http.StatusInternalServerError) { s.logger.For(ctx).Error("cannot marshal response", zap.Error(err)) return } w.Header().Set("Content-Type", "application/json") w.Write(data) //nolint:gosec // G705 - writing JSON response } ================================================ FILE: examples/hotrod/services/driver/client.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package driver import ( "context" "time" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "go.opentelemetry.io/otel/metric/noop" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" ) // Client is a remote client that implements driver.Interface type Client struct { logger log.Factory client DriverServiceClient } // NewClient creates a new driver.Client func NewClient(tracerProvider trace.TracerProvider, logger log.Factory, hostPort string) *Client { conn, err := grpc.NewClient(hostPort, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithStatsHandler(otelgrpc.NewClientHandler( otelgrpc.WithTracerProvider(tracerProvider), otelgrpc.WithMeterProvider(noop.NewMeterProvider()), )), ) if err != nil { logger.Bg().Fatal("Cannot create gRPC connection", zap.Error(err)) } client := NewDriverServiceClient(conn) return &Client{ logger: logger, client: client, } } // FindNearest implements driver.Interface#FindNearest as an RPC func (c *Client) FindNearest(ctx context.Context, location string) ([]Driver, error) { c.logger.For(ctx).Info("Finding nearest drivers", zap.String("location", location)) ctx, cancel := context.WithTimeout(ctx, 1*time.Second) defer cancel() response, err := c.client.FindNearest(ctx, &DriverLocationRequest{Location: location}) if err != nil { return nil, err } return fromProto(response), nil } func fromProto(response *DriverLocationResponse) []Driver { retMe := make([]Driver, len(response.Locations)) for i, result := range response.Locations { retMe[i] = Driver{ DriverID: result.DriverID, Location: result.Location, } } return retMe } ================================================ FILE: examples/hotrod/services/driver/driver.pb.go ================================================ // Code generated by protoc-gen-gogo. DO NOT EDIT. // source: examples/hotrod/services/driver/driver.proto package driver import ( context "context" fmt "fmt" proto "github.com/gogo/protobuf/proto" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" math "math" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package type DriverLocationRequest struct { Location string `protobuf:"bytes,1,opt,name=location,proto3" json:"location,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *DriverLocationRequest) Reset() { *m = DriverLocationRequest{} } func (m *DriverLocationRequest) String() string { return proto.CompactTextString(m) } func (*DriverLocationRequest) ProtoMessage() {} func (*DriverLocationRequest) Descriptor() ([]byte, []int) { return fileDescriptor_cdcd28b7ebdcd54f, []int{0} } func (m *DriverLocationRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_DriverLocationRequest.Unmarshal(m, b) } func (m *DriverLocationRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_DriverLocationRequest.Marshal(b, m, deterministic) } func (m *DriverLocationRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_DriverLocationRequest.Merge(m, src) } func (m *DriverLocationRequest) XXX_Size() int { return xxx_messageInfo_DriverLocationRequest.Size(m) } func (m *DriverLocationRequest) XXX_DiscardUnknown() { xxx_messageInfo_DriverLocationRequest.DiscardUnknown(m) } var xxx_messageInfo_DriverLocationRequest proto.InternalMessageInfo func (m *DriverLocationRequest) GetLocation() string { if m != nil { return m.Location } return "" } type DriverLocation struct { DriverID string `protobuf:"bytes,1,opt,name=driverID,proto3" json:"driverID,omitempty"` Location string `protobuf:"bytes,2,opt,name=location,proto3" json:"location,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *DriverLocation) Reset() { *m = DriverLocation{} } func (m *DriverLocation) String() string { return proto.CompactTextString(m) } func (*DriverLocation) ProtoMessage() {} func (*DriverLocation) Descriptor() ([]byte, []int) { return fileDescriptor_cdcd28b7ebdcd54f, []int{1} } func (m *DriverLocation) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_DriverLocation.Unmarshal(m, b) } func (m *DriverLocation) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_DriverLocation.Marshal(b, m, deterministic) } func (m *DriverLocation) XXX_Merge(src proto.Message) { xxx_messageInfo_DriverLocation.Merge(m, src) } func (m *DriverLocation) XXX_Size() int { return xxx_messageInfo_DriverLocation.Size(m) } func (m *DriverLocation) XXX_DiscardUnknown() { xxx_messageInfo_DriverLocation.DiscardUnknown(m) } var xxx_messageInfo_DriverLocation proto.InternalMessageInfo func (m *DriverLocation) GetDriverID() string { if m != nil { return m.DriverID } return "" } func (m *DriverLocation) GetLocation() string { if m != nil { return m.Location } return "" } type DriverLocationResponse struct { Locations []*DriverLocation `protobuf:"bytes,1,rep,name=locations,proto3" json:"locations,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *DriverLocationResponse) Reset() { *m = DriverLocationResponse{} } func (m *DriverLocationResponse) String() string { return proto.CompactTextString(m) } func (*DriverLocationResponse) ProtoMessage() {} func (*DriverLocationResponse) Descriptor() ([]byte, []int) { return fileDescriptor_cdcd28b7ebdcd54f, []int{2} } func (m *DriverLocationResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_DriverLocationResponse.Unmarshal(m, b) } func (m *DriverLocationResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_DriverLocationResponse.Marshal(b, m, deterministic) } func (m *DriverLocationResponse) XXX_Merge(src proto.Message) { xxx_messageInfo_DriverLocationResponse.Merge(m, src) } func (m *DriverLocationResponse) XXX_Size() int { return xxx_messageInfo_DriverLocationResponse.Size(m) } func (m *DriverLocationResponse) XXX_DiscardUnknown() { xxx_messageInfo_DriverLocationResponse.DiscardUnknown(m) } var xxx_messageInfo_DriverLocationResponse proto.InternalMessageInfo func (m *DriverLocationResponse) GetLocations() []*DriverLocation { if m != nil { return m.Locations } return nil } func init() { proto.RegisterType((*DriverLocationRequest)(nil), "driver.DriverLocationRequest") proto.RegisterType((*DriverLocation)(nil), "driver.DriverLocation") proto.RegisterType((*DriverLocationResponse)(nil), "driver.DriverLocationResponse") } func init() { proto.RegisterFile("examples/hotrod/services/driver/driver.proto", fileDescriptor_cdcd28b7ebdcd54f) } var fileDescriptor_cdcd28b7ebdcd54f = []byte{ // 207 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xd2, 0x49, 0xad, 0x48, 0xcc, 0x2d, 0xc8, 0x49, 0x2d, 0xd6, 0xcf, 0xc8, 0x2f, 0x29, 0xca, 0x4f, 0xd1, 0x2f, 0x4e, 0x2d, 0x2a, 0xcb, 0x4c, 0x4e, 0x2d, 0xd6, 0x4f, 0x29, 0xca, 0x2c, 0x4b, 0x2d, 0x82, 0x52, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42, 0x6c, 0x10, 0x9e, 0x92, 0x31, 0x97, 0xa8, 0x0b, 0x98, 0xe5, 0x93, 0x9f, 0x9c, 0x58, 0x92, 0x99, 0x9f, 0x17, 0x94, 0x5a, 0x58, 0x9a, 0x5a, 0x5c, 0x22, 0x24, 0xc5, 0xc5, 0x91, 0x03, 0x15, 0x92, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0c, 0x82, 0xf3, 0x95, 0x3c, 0xb8, 0xf8, 0x50, 0x35, 0x81, 0x54, 0x43, 0x0c, 0xf4, 0x74, 0x81, 0xa9, 0x86, 0xf1, 0x51, 0x4c, 0x62, 0x42, 0x33, 0xc9, 0x8f, 0x4b, 0x0c, 0xdd, 0xfa, 0xe2, 0x82, 0xfc, 0xbc, 0xe2, 0x54, 0x21, 0x13, 0x2e, 0x4e, 0x98, 0xaa, 0x62, 0x09, 0x46, 0x05, 0x66, 0x0d, 0x6e, 0x23, 0x31, 0x3d, 0xa8, 0x17, 0xd0, 0xb4, 0x20, 0x14, 0x1a, 0xc5, 0x72, 0xf1, 0x42, 0x24, 0x83, 0x21, 0x9e, 0x17, 0xf2, 0xe1, 0xe2, 0x76, 0xcb, 0xcc, 0x4b, 0xf1, 0x4b, 0x4d, 0x2c, 0x02, 0xf9, 0x4a, 0x16, 0x87, 0x11, 0x10, 0x4f, 0x4b, 0xc9, 0xe1, 0x92, 0x86, 0x38, 0xca, 0x89, 0x23, 0x0a, 0x1a, 0x6e, 0x49, 0x6c, 0xe0, 0x60, 0x34, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xc8, 0x90, 0x0b, 0x66, 0x76, 0x01, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. var _ context.Context var _ grpc.ClientConn // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. const _ = grpc.SupportPackageIsVersion4 // DriverServiceClient is the client API for DriverService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type DriverServiceClient interface { FindNearest(ctx context.Context, in *DriverLocationRequest, opts ...grpc.CallOption) (*DriverLocationResponse, error) } type driverServiceClient struct { cc *grpc.ClientConn } func NewDriverServiceClient(cc *grpc.ClientConn) DriverServiceClient { return &driverServiceClient{cc} } func (c *driverServiceClient) FindNearest(ctx context.Context, in *DriverLocationRequest, opts ...grpc.CallOption) (*DriverLocationResponse, error) { out := new(DriverLocationResponse) err := c.cc.Invoke(ctx, "/driver.DriverService/FindNearest", in, out, opts...) if err != nil { return nil, err } return out, nil } // DriverServiceServer is the server API for DriverService service. type DriverServiceServer interface { FindNearest(context.Context, *DriverLocationRequest) (*DriverLocationResponse, error) } // UnimplementedDriverServiceServer can be embedded to have forward compatible implementations. type UnimplementedDriverServiceServer struct { } func (*UnimplementedDriverServiceServer) FindNearest(ctx context.Context, req *DriverLocationRequest) (*DriverLocationResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method FindNearest not implemented") } func RegisterDriverServiceServer(s *grpc.Server, srv DriverServiceServer) { s.RegisterService(&_DriverService_serviceDesc, srv) } func _DriverService_FindNearest_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DriverLocationRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DriverServiceServer).FindNearest(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/driver.DriverService/FindNearest", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DriverServiceServer).FindNearest(ctx, req.(*DriverLocationRequest)) } return interceptor(ctx, in, info, handler) } var _DriverService_serviceDesc = grpc.ServiceDesc{ ServiceName: "driver.DriverService", HandlerType: (*DriverServiceServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "FindNearest", Handler: _DriverService_FindNearest_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "examples/hotrod/services/driver/driver.proto", } ================================================ FILE: examples/hotrod/services/driver/driver.proto ================================================ // Copyright (c) 2020 The Jaeger Authors. // // 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. syntax="proto3"; package driver; option go_package = "driver"; message DriverLocationRequest { string location = 1; } message DriverLocation { string driverID = 1; string location = 2; } message DriverLocationResponse { repeated DriverLocation locations = 1; } service DriverService { rpc FindNearest(DriverLocationRequest) returns (DriverLocationResponse); } ================================================ FILE: examples/hotrod/services/driver/empty_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package driver import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: examples/hotrod/services/driver/interface.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package driver import ( "context" ) // Driver describes a driver and the current car location. type Driver struct { DriverID string Location string } // Interface exposed by the Driver service. type Interface interface { FindNearest(ctx context.Context, location string) ([]Driver, error) } ================================================ FILE: examples/hotrod/services/driver/redis.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package driver import ( "context" "errors" "fmt" "math/rand" "sync" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/delay" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" "github.com/jaegertracing/jaeger/examples/hotrod/services/config" "github.com/jaegertracing/jaeger/internal/metrics" ) // Redis is a simulator of remote Redis cache type Redis struct { tracer trace.Tracer // simulate redis as a separate process logger log.Factory errorSimulator } func newRedis(otelExporter string, metricsFactory metrics.Factory, logger log.Factory) *Redis { tp := tracing.InitOTEL("redis-manual", otelExporter, metricsFactory, logger) return &Redis{ tracer: tp.Tracer("redis-manual"), logger: logger, } } // FindDriverIDs finds IDs of drivers who are near the location. func (r *Redis) FindDriverIDs(ctx context.Context, location string) []string { ctx, span := r.tracer.Start(ctx, "FindDriverIDs", trace.WithSpanKind(trace.SpanKindClient)) span.SetAttributes(attribute.Key("param.driver.location").String(location)) defer span.End() // simulate RPC delay delay.Sleep(config.RedisFindDelay, config.RedisFindDelayStdDev) drivers := make([]string, 10) for i := range drivers { // #nosec drivers[i] = fmt.Sprintf("T7%05dC", rand.Int()%100000) } r.logger.For(ctx).Info("Found drivers", zap.Strings("drivers", drivers)) return drivers } // GetDriver returns driver and the current car location func (r *Redis) GetDriver(ctx context.Context, driverID string) (Driver, error) { ctx, span := r.tracer.Start(ctx, "GetDriver", trace.WithSpanKind(trace.SpanKindClient)) span.SetAttributes(attribute.Key("param.driverID").String(driverID)) defer span.End() // simulate RPC delay delay.Sleep(config.RedisGetDelay, config.RedisGetDelayStdDev) if err := r.checkError(); err != nil { span.RecordError(err) span.SetStatus(codes.Error, "An error occurred") r.logger.For(ctx).Error("redis timeout", zap.String("driver_id", driverID), zap.Error(err)) return Driver{}, err } r.logger.For(ctx).Info("Got driver's ID", zap.String("driverID", driverID)) // #nosec return Driver{ DriverID: driverID, Location: fmt.Sprintf("%d,%d", rand.Int()%1000, rand.Int()%1000), }, nil } var errTimeout = errors.New("redis timeout") type errorSimulator struct { sync.Mutex countTillError int } func (es *errorSimulator) checkError() error { es.Lock() es.countTillError-- if es.countTillError > 0 { es.Unlock() return nil } es.countTillError = 5 es.Unlock() delay.Sleep(2*config.RedisGetDelay, 0) // add more delay for "timeout" return errTimeout } ================================================ FILE: examples/hotrod/services/driver/server.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package driver import ( "context" "encoding/json" "net" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "go.opentelemetry.io/otel/metric/noop" "go.uber.org/zap" "google.golang.org/grpc" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" "github.com/jaegertracing/jaeger/internal/metrics" ) // Server implements jaeger-demo-frontend service type Server struct { hostPort string logger log.Factory redis *Redis server *grpc.Server } var _ DriverServiceServer = (*Server)(nil) // NewServer creates a new driver.Server func NewServer(hostPort string, otelExporter string, metricsFactory metrics.Factory, logger log.Factory) *Server { tracerProvider := tracing.InitOTEL("driver", otelExporter, metricsFactory, logger) server := grpc.NewServer( grpc.StatsHandler(otelgrpc.NewServerHandler( otelgrpc.WithTracerProvider(tracerProvider), otelgrpc.WithMeterProvider(noop.NewMeterProvider()), )), ) return &Server{ hostPort: hostPort, logger: logger, server: server, redis: newRedis(otelExporter, metricsFactory, logger), } } // Run starts the Driver server func (s *Server) Run() error { lis, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", s.hostPort) if err != nil { s.logger.Bg().Fatal("Unable to create http listener", zap.Error(err)) } RegisterDriverServiceServer(s.server, s) s.logger.Bg().Info("Starting", zap.String("address", s.hostPort), zap.String("type", "gRPC")) err = s.server.Serve(lis) if err != nil { s.logger.Bg().Fatal("Unable to start gRPC server", zap.Error(err)) } return err } // FindNearest implements gRPC driver interface func (s *Server) FindNearest(ctx context.Context, location *DriverLocationRequest) (*DriverLocationResponse, error) { s.logger.For(ctx).Info("Searching for nearby drivers", zap.String("location", location.Location)) driverIDs := s.redis.FindDriverIDs(ctx, location.Location) locations := make([]*DriverLocation, len(driverIDs)) for i, driverID := range driverIDs { var drv Driver var err error for i := range 3 { drv, err = s.redis.GetDriver(ctx, driverID) if err == nil { break } s.logger.For(ctx).Error("Retrying GetDriver after error", zap.Int("retry_no", i+1), zap.Error(err)) } if err != nil { s.logger.For(ctx).Error("Failed to get driver after 3 attempts", zap.Error(err)) return nil, err } locations[i] = &DriverLocation{ DriverID: drv.DriverID, Location: drv.Location, } } s.logger.For(ctx).Info( "Search successful", zap.Int("driver_count", len(locations)), zap.String("locations", toJSON(locations)), ) return &DriverLocationResponse{Locations: locations}, nil } func toJSON(v any) string { str, err := json.Marshal(v) if err != nil { return err.Error() } return string(str) } ================================================ FILE: examples/hotrod/services/frontend/best_eta.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package frontend import ( "context" "errors" "math" "sync" "time" "go.opentelemetry.io/otel/baggage" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/pool" "github.com/jaegertracing/jaeger/examples/hotrod/services/config" "github.com/jaegertracing/jaeger/examples/hotrod/services/customer" "github.com/jaegertracing/jaeger/examples/hotrod/services/driver" "github.com/jaegertracing/jaeger/examples/hotrod/services/route" ) type bestETA struct { customer customer.Interface driver driver.Interface route route.Interface pool *pool.Pool logger log.Factory } // Response contains ETA for a trip. type Response struct { Driver string ETA time.Duration } func newBestETA(tracer trace.TracerProvider, logger log.Factory, options ConfigOptions) *bestETA { return &bestETA{ customer: customer.NewClient( tracer, logger.With(zap.String("component", "customer_client")), options.CustomerHostPort, ), driver: driver.NewClient( tracer, logger.With(zap.String("component", "driver_client")), options.DriverHostPort, ), route: route.NewClient( tracer, logger.With(zap.String("component", "route_client")), options.RouteHostPort, ), pool: pool.New(config.RouteWorkerPoolSize), logger: logger, } } func (eta *bestETA) Get(ctx context.Context, customerID int) (*Response, error) { cust, err := eta.customer.Get(ctx, customerID) if err != nil { return nil, err } eta.logger.For(ctx).Info("Found customer", zap.Any("customer", cust)) m, err := baggage.NewMember("customer", cust.Name) if err != nil { eta.logger.For(ctx).Error("cannot create baggage member", zap.Error(err)) } bag := baggage.FromContext(ctx) bag, err = bag.SetMember(m) if err != nil { eta.logger.For(ctx).Error("cannot set baggage member", zap.Error(err)) } ctx = baggage.ContextWithBaggage(ctx, bag) drivers, err := eta.driver.FindNearest(ctx, cust.Location) if err != nil { return nil, err } eta.logger.For(ctx).Info("Found drivers", zap.Any("drivers", drivers)) results := eta.getRoutes(ctx, cust, drivers) eta.logger.For(ctx).Info("Found routes", zap.Any("routes", results)) resp := &Response{ETA: math.MaxInt64} for _, result := range results { if result.err != nil { return nil, err } if result.route.ETA < resp.ETA { resp.ETA = result.route.ETA resp.Driver = result.driver } } if resp.Driver == "" { return nil, errors.New("no routes found") } eta.logger.For(ctx).Info("Dispatch successful", zap.String("driver", resp.Driver), zap.String("eta", resp.ETA.String())) return resp, nil } type routeResult struct { driver string route *route.Route err error } // getRoutes calls Route service for each (customer, driver) pair func (eta *bestETA) getRoutes(ctx context.Context, cust *customer.Customer, drivers []driver.Driver) []routeResult { results := make([]routeResult, 0, len(drivers)) wg := sync.WaitGroup{} routesLock := sync.Mutex{} for _, dd := range drivers { wg.Add(1) drv := dd // capture loop var // Use worker pool to (potentially) execute requests in parallel eta.pool.Execute(func() { route, err := eta.route.FindRoute(ctx, drv.Location, cust.Location) routesLock.Lock() results = append(results, routeResult{ driver: drv.DriverID, route: route, err: err, }) routesLock.Unlock() wg.Done() }) } wg.Wait() return results } ================================================ FILE: examples/hotrod/services/frontend/empty_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package frontend import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: examples/hotrod/services/frontend/server.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package frontend import ( "embed" "encoding/json" "expvar" "net/http" "path" "strconv" "time" "github.com/prometheus/client_golang/prometheus/promhttp" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/httperr" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" "github.com/jaegertracing/jaeger/internal/httpfs" ) //go:embed web_assets/* var assetFS embed.FS // Server implements jaeger-demo-frontend service type Server struct { hostPort string tracer trace.TracerProvider logger log.Factory bestETA *bestETA assetFS http.FileSystem basepath string jaegerUI string } // ConfigOptions used to make sure service clients // can find correct server ports type ConfigOptions struct { FrontendHostPort string DriverHostPort string CustomerHostPort string RouteHostPort string Basepath string JaegerUI string } // NewServer creates a new frontend.Server func NewServer(options ConfigOptions, tracer trace.TracerProvider, logger log.Factory) *Server { return &Server{ hostPort: options.FrontendHostPort, tracer: tracer, logger: logger, bestETA: newBestETA(tracer, logger, options), assetFS: httpfs.PrefixedFS("web_assets", http.FS(assetFS)), basepath: options.Basepath, jaegerUI: options.JaegerUI, } } // Run starts the frontend server func (s *Server) Run() error { mux := s.createServeMux() s.logger.Bg().Info("Starting", zap.String("address", "http://"+path.Join(s.hostPort, s.basepath))) server := &http.Server{ Addr: s.hostPort, Handler: mux, ReadHeaderTimeout: 3 * time.Second, } return server.ListenAndServe() } func (s *Server) createServeMux() http.Handler { mux := tracing.NewServeMux(true, s.tracer, s.logger) p := path.Join("/", s.basepath) mux.Handle(p, http.StripPrefix(p, http.FileServer(s.assetFS))) mux.Handle(path.Join(p, "/dispatch"), http.HandlerFunc(s.dispatch)) mux.Handle(path.Join(p, "/config"), http.HandlerFunc(s.config)) mux.Handle(path.Join(p, "/debug/vars"), expvar.Handler()) // expvar mux.Handle(path.Join(p, "/metrics"), promhttp.Handler()) // Prometheus return mux } func (s *Server) config(w http.ResponseWriter, r *http.Request) { config := map[string]string{ "jaeger": s.jaegerUI, } s.writeResponse(config, w, r) } func (s *Server) dispatch(w http.ResponseWriter, r *http.Request) { ctx := r.Context() s.logger.For(ctx).Info("HTTP request received", zap.String("method", r.Method), zap.Stringer("url", r.URL)) if err := r.ParseForm(); httperr.HandleError(w, err, http.StatusBadRequest) { s.logger.For(ctx).Error("bad request", zap.Error(err)) return } customer := r.Form.Get("customer") if customer == "" { http.Error(w, "Missing required 'customer' parameter", http.StatusBadRequest) return } customerID, err := strconv.Atoi(customer) if err != nil { http.Error(w, "Parameter 'customer' is not an integer", http.StatusBadRequest) return } // TODO distinguish between user errors (such as invalid customer ID) and server failures response, err := s.bestETA.Get(ctx, customerID) if httperr.HandleError(w, err, http.StatusInternalServerError) { s.logger.For(ctx).Error("request failed", zap.Error(err)) return } s.writeResponse(response, w, r) } func (s *Server) writeResponse(response any, w http.ResponseWriter, r *http.Request) { data, err := json.Marshal(response) if httperr.HandleError(w, err, http.StatusInternalServerError) { s.logger.For(r.Context()).Error("cannot marshal response", zap.Error(err)) return } w.Header().Set("Content-Type", "application/json") w.Write(data) //nolint:gosec // G705 - writing JSON response } ================================================ FILE: examples/hotrod/services/frontend/web_assets/index.html ================================================ HotROD - Rides On Demand

Hot R.O.D.

🚗 Rides On Demand 🚗

Rachel's Floral Designs
Trom Chocolatier
Japanese Desserts
Amazing Coffee Roasters
Click on customer name above to order a car.
================================================ FILE: examples/hotrod/services/route/client.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package route import ( "context" "net/url" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" ) // Client is a remote client that implements route.Interface type Client struct { logger log.Factory client *tracing.HTTPClient hostPort string } // NewClient creates a new route.Client func NewClient(tracer trace.TracerProvider, logger log.Factory, hostPort string) *Client { return &Client{ logger: logger, client: tracing.NewHTTPClient(tracer), hostPort: hostPort, } } // FindRoute implements route.Interface#FindRoute as an RPC func (c *Client) FindRoute(ctx context.Context, pickup, dropoff string) (*Route, error) { c.logger.For(ctx).Info("Finding route", zap.String("pickup", pickup), zap.String("dropoff", dropoff)) v := url.Values{} v.Set("pickup", pickup) v.Set("dropoff", dropoff) routeURL := "http://" + c.hostPort + "/route?" + v.Encode() var route Route if err := c.client.GetJSON(ctx, "/route", routeURL, &route); err != nil { c.logger.For(ctx).Error("Error getting route", zap.Error(err)) return nil, err } return &route, nil } ================================================ FILE: examples/hotrod/services/route/empty_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package route import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: examples/hotrod/services/route/interface.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package route import ( "context" "time" ) // Route describes a route between Pickup and Dropoff locations and expected time to arrival. type Route struct { Pickup string Dropoff string ETA time.Duration } // Interface exposed by the Driver service. type Interface interface { FindRoute(ctx context.Context, pickup, dropoff string) (*Route, error) } ================================================ FILE: examples/hotrod/services/route/server.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package route import ( "context" "encoding/json" "math" "math/rand" "net/http" "time" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/delay" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/httperr" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/log" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" "github.com/jaegertracing/jaeger/examples/hotrod/services/config" ) // Server implements Route service type Server struct { hostPort string tracer trace.TracerProvider logger log.Factory } // NewServer creates a new route.Server func NewServer(hostPort string, tracer trace.TracerProvider, logger log.Factory) *Server { return &Server{ hostPort: hostPort, tracer: tracer, logger: logger, } } // Run starts the Route server func (s *Server) Run() error { mux := s.createServeMux() s.logger.Bg().Info("Starting", zap.String("address", "http://"+s.hostPort)) server := &http.Server{ Addr: s.hostPort, Handler: mux, ReadHeaderTimeout: 3 * time.Second, } return server.ListenAndServe() } func (s *Server) createServeMux() http.Handler { mux := tracing.NewServeMux(false, s.tracer, s.logger) mux.Handle("/route", http.HandlerFunc(s.route)) mux.Handle("/debug/vars", http.HandlerFunc(movedToFrontend)) mux.Handle("/metrics", http.HandlerFunc(movedToFrontend)) return mux } func movedToFrontend(w http.ResponseWriter, _ *http.Request) { http.Error(w, "endpoint moved to the frontend service", http.StatusNotFound) } func (s *Server) route(w http.ResponseWriter, r *http.Request) { ctx := r.Context() s.logger.For(ctx).Info("HTTP request received", zap.String("method", r.Method), zap.Stringer("url", r.URL)) if err := r.ParseForm(); httperr.HandleError(w, err, http.StatusBadRequest) { s.logger.For(ctx).Error("bad request", zap.Error(err)) return } pickup := r.Form.Get("pickup") if pickup == "" { http.Error(w, "Missing required 'pickup' parameter", http.StatusBadRequest) return } dropoff := r.Form.Get("dropoff") if dropoff == "" { http.Error(w, "Missing required 'dropoff' parameter", http.StatusBadRequest) return } response := computeRoute(ctx, pickup, dropoff) data, err := json.Marshal(response) if httperr.HandleError(w, err, http.StatusInternalServerError) { s.logger.For(ctx).Error("cannot marshal response", zap.Error(err)) return } w.Header().Set("Content-Type", "application/json") w.Write(data) //nolint:gosec // G705 - writing JSON response } func computeRoute(ctx context.Context, pickup, dropoff string) *Route { start := time.Now() defer func() { updateCalcStats(ctx, time.Since(start)) }() // Simulate expensive calculation delay.Sleep(config.RouteCalcDelay, config.RouteCalcDelayStdDev) eta := math.Max(2, rand.NormFloat64()*3+5) return &Route{ Pickup: pickup, Dropoff: dropoff, ETA: time.Duration(eta) * time.Minute, } } ================================================ FILE: examples/hotrod/services/route/stats.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package route import ( "context" "expvar" "time" "github.com/jaegertracing/jaeger/examples/hotrod/pkg/tracing" ) var ( routeCalcByCustomer = expvar.NewMap("route.calc.by.customer.sec") routeCalcBySession = expvar.NewMap("route.calc.by.session.sec") ) var stats = []struct { expvar *expvar.Map baggageKey string }{ { expvar: routeCalcByCustomer, baggageKey: "customer", }, { expvar: routeCalcBySession, baggageKey: "session", }, } func updateCalcStats(ctx context.Context, delay time.Duration) { delaySec := float64(delay/time.Millisecond) / 1000.0 for _, s := range stats { key := tracing.BaggageItem(ctx, s.baggageKey) if key != "" { s.expvar.AddFloat(key, delaySec) } } } ================================================ FILE: examples/oci/README.md ================================================ # Jaeger + Prometheus + HotROD Demo Setup (Helm v2 Branch) This guide walks you through deploying **Jaeger** (using the v2 Helm chart), **Prometheus**, and the **HotROD demo app** on Kubernetes. ## Prerequisites Ensure the following tools are installed and configured: - A Kubernetes cluster (e.g., Minikube, kind, or cloud-based) - [`kubectl`](https://kubernetes.io/docs/tasks/tools/) - [`Helm 3`](https://helm.sh/docs/intro/install/) - `git` --- ## Deploy the Jaeger Demo Setup The following components are deployed as part of the Jaeger demo setup: - **Jaeger All-in-One**: Tracing backend (collector, query, UI, agent in one pod) - **HotROD Demo App**: Sample microservices application for tracing demonstration - **Prometheus Monitoring Stack**: Includes Prometheus, Grafana, and Alertmanager for metrics and dashboards - **Load Generator**: Continuously generates traces from the HotROD app To deploy the entire infrastructure with a single command, run: ```bash bash ./deploy-all.sh ``` This script will automatically install and configure all components on your Kubernetes , To deal with individual components refer to deploy-all.sh script . ## Access the Deployment After deploying, you can access each component locally using the following port-forward commands in separate terminals: ```bash # Jaeger UI kubectl port-forward svc/jaeger-query 16686:16686 # Prometheus UI kubectl port-forward svc/prometheus 9090:9090 # Grafana Dashboard kubectl port-forward svc/prometheus-grafana 9091:80 # HotROD UI kubectl port-forward svc/jaeger-hotrod 8080:80 ``` Then, open the following URLs in your browser: - **Jaeger UI:** [http://localhost:16686/jaeger](http://localhost:16686/jaeger) - **Prometheus:** [http://localhost:9090](http://localhost:9090) - **Grafana:** [http://localhost:9091](http://localhost:9091) - **HotROD Demo App:** [http://localhost:8080/hotrod](http://localhost:8080/hotrod) ## Deploying on Cloud Infrastructure (e.g., Oracle Cloud) To expose your services externally using a custom domain (e.g., `http://demo.jaegertracing.io/`), you need to set up an **Ingress Controller** and define an **Ingress resource**. ### Step 1: Deploy the NGINX Ingress Controller Apply the official NGINX Ingress Controller manifest for cloud environments: ```bash kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v/deploy/static/provider/cloud/deploy.yaml ``` > 🔁 Replace `` with the latest version from the [Ingress NGINX GitHub Releases](https://github.com/kubernetes/ingress-nginx/releases). --- ### Step 2: Verify the Ingress Controller After deployment, check that the ingress controller service is up and has an external IP: ```bash kubectl get svc -n ingress-nginx ``` You should see output like: ```bash NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE ingress-nginx-controller LoadBalancer 10.96.229.38 129.146.214.219 80:30756/TCP,443:30118/TCP 1h ``` > 🧠 Note: The `EXTERNAL-IP` is the public IP address your domain (e.g., `demo.jaegertracing.io`) should point to via DNS. --- ### Step 3: Apply the Ingress Resource Once your DNS is mapped and the ingress controller is ready, deploy your Ingress definition: ```bash kubectl apply -f ingress.yaml ``` This routes incoming HTTP traffic to the respective Kubernetes services based on the path or host rules defined in `ingress.yaml`. --- 🔧 Remarks 📌 The current configuration is set to run in the default namespace. You can use any custom namespace by making minor adjustments in: ``` bash Helm --namespace flags Kubernetes manifests (metadata.namespace) Prometheus scrape configs and service selectors if targeting Jaeger in a different namespace ``` 📌 The default credentials for Grafana dashboards are: - **Username:** `admin` - **Password:** `prom-operator` Once logged in, you can explore the pre-built dashboards or add your own tracing and metrics visualizations. ### Configure TLS/SSL with Cert-Manager To secure services with TLS/SSL, we use **Cert-Manager**. It provides the following features: - Automatic provisioning of TLS/SSL certificates. - Integration with Let's Encrypt for certificates. - Automatic renewal of certificates before expiration. - Integration with NGINX Ingress Controller. The issuer configuration YAML is located at `./tls-cert/issuer.yaml`. For detailed setup instructions, refer to the official Cert-Manager documentation: [Cert-Manager ACME Tutorial with NGINX Ingress](https://cert-manager.io/docs/tutorials/acme/nginx-ingress/) ================================================ FILE: examples/oci/config.yaml ================================================ service: extensions: [jaeger_storage, jaeger_query, healthcheckv2] pipelines: traces: receivers: [otlp] processors: [batch] exporters: [jaeger_storage_exporter, spanmetrics] metrics/spanmetrics: receivers: [spanmetrics] exporters: [prometheus] telemetry: resource: service.name: jaeger metrics: level: detailed readers: - pull: exporter: prometheus: host: 0.0.0.0 port: 8888 logs: level: DEBUG extensions: healthcheckv2: use_v2: true http: endpoint: 0.0.0.0:13133 jaeger_query: base_path: /jaeger ui: config_file: /etc/jaeger/ui-config/ui-config.json log_access: true storage: traces: some_storage metrics: some_metrics_storage jaeger_storage: backends: some_storage: memory: max_traces: 100000 metric_backends: some_metrics_storage: prometheus: endpoint: http://prometheus:9090 normalize_calls: true normalize_duration: true connectors: spanmetrics: receivers: otlp: protocols: grpc: http: endpoint: "0.0.0.0:4318" processors: batch: exporters: jaeger_storage_exporter: trace_storage: some_storage prometheus: endpoint: "0.0.0.0:8889" ================================================ FILE: examples/oci/deploy-all.sh ================================================ #!/bin/bash # Copyright (c) 2025 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 set -euo pipefail MODE="${1:-upgrade}" IMAGE_TAG="${2:-latest}" PROMETHEUS_STACK_CHART="prometheus-community/kube-prometheus-stack" PROMETHEUS_STACK_CHART_VERSION="${PROMETHEUS_STACK_CHART_VERSION:-82.10.4}" case "$MODE" in upgrade|clean|local) echo "🔵 Running in '$MODE' mode..." ;; *) echo "❌ Error: Invalid mode '$MODE'" echo "Usage: $0 [upgrade|clean|local] [image-tag]" echo "" echo "Modes:" echo " upgrade - Upgrade existing deployment or install if not present (default)" echo " clean - Clean install (removes existing deployment first)" echo " local - Deploy using local registry images (localhost:5000)" echo "" echo "Examples:" echo " $0 # Upgrade mode with latest tag" echo " $0 clean # Clean install" echo " $0 local # Local mode with specific image tag" exit 1 ;; esac if [[ "$MODE" == "upgrade" ]]; then HELM_JAEGER_CMD="upgrade --install --force" HELM_PROM_CMD="upgrade --install --force" else echo "🟣 Clean mode: Uninstalling Jaeger and Prometheus..." helm uninstall jaeger --ignore-not-found || true helm uninstall prometheus --ignore-not-found || true for name in jaeger prometheus; do while helm list --filter "^${name}$" | grep "$name" &>/dev/null; do echo "Waiting for Helm release $name to be deleted..." done done HELM_JAEGER_CMD="install" HELM_PROM_CMD="install" fi # Navigate to the script's directory (examples/oci) cd $(dirname $0) # Clone Jaeger Helm Charts if not already present if [ ! -d "helm-charts" ]; then echo "📥 Cloning Jaeger Helm Charts..." git clone https://github.com/jaegertracing/helm-charts.git cd helm-charts echo "Using v2 branch for Jaeger v2..." git checkout v2 echo "Adding required Helm repositories..." helm repo add bitnami https://charts.bitnami.com/bitnami helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo add incubator https://charts.helm.sh/incubator helm repo update helm dependency build ./charts/jaeger cd .. else echo "📁 Jaeger Helm Charts already exist. Skipping clone." fi # Set image repositories and deploy based on mode if [[ "$MODE" == "local" ]]; then echo "🟣 Deploying Jaeger with local registry images..." helm $HELM_JAEGER_CMD --timeout 10m0s jaeger ./helm-charts/charts/jaeger \ --set provisionDataStore.cassandra=false \ --set allInOne.enabled=true \ --set storage.type=memory \ --set hotrod.enabled=true \ --set global.imageRegistry="" \ --set allInOne.image.repository="localhost:5000/jaegertracing/jaeger" \ --set allInOne.image.tag="${IMAGE_TAG}" \ --set allInOne.image.pullPolicy="Never" \ --set hotrod.image.repository="localhost:5000/jaegertracing/example-hotrod" \ --set hotrod.image.tag="${IMAGE_TAG}" \ --set hotrod.image.pullPolicy="Never" \ --set-file userconfig="./config.yaml" \ --set-file uiconfig="./ui-config.json" \ -f ./jaeger-values.yaml else echo "🟣 Deploying Jaeger..." helm $HELM_JAEGER_CMD --timeout 10m0s jaeger ./helm-charts/charts/jaeger \ --set provisionDataStore.cassandra=false \ --set allInOne.enabled=true \ --set storage.type=memory \ --set allInOne.image.repository="jaegertracing/jaeger" \ --set-file userconfig="./config.yaml" \ --set-file uiconfig="./ui-config.json" \ -f ./jaeger-values.yaml fi echo "🟢 Deploying Prometheus..." kubectl apply -f prometheus-svc.yaml helm $HELM_PROM_CMD prometheus "$PROMETHEUS_STACK_CHART" \ --version "$PROMETHEUS_STACK_CHART_VERSION" \ --set crds.upgradeJob.enabled=true \ --set crds.upgradeJob.forceConflicts=true \ -f monitoring-values.yaml # Create ConfigMap for Trace Generator echo "🔵 Step 3: Creating ConfigMap for Trace Generator..." kubectl create configmap trace-script --from-file=./load-generator/generate_traces.py --dry-run=client -o yaml | kubectl apply -f - # Deploy Trace Generator Pod echo "🟡 Step 4: Deploying Trace Generator Pod..." kubectl apply -f ./load-generator/load-generator.yaml # Deploy ingress changes echo "🟡 Step 5: Deploying Ingress Resource..." kubectl apply -f ingress.yaml # Output Port-forward Instructions echo "✅ Deployment Complete!" echo "" echo "📡 Port-forward the following to access UIs locally:" echo "" echo "kubectl port-forward svc/jaeger-query 16686:16686 # Jaeger UI" echo "kubectl port-forward svc/prometheus 9090:9090 # Prometheus UI" echo "kubectl port-forward svc/prometheus-grafana 9091:80 # Grafana UI" echo "kubectl port-forward svc/jaeger-hotrod 8080:80 # HotROD UI" echo "" echo "Then open:" echo "🔍 Jaeger: http://localhost:16686/jaeger" echo "📈 Prometheus: http://localhost:9090" echo "📊 Grafana: http://localhost:9091" echo "🚕 HotROD: http://localhost:8080" echo "" echo "📝 Note: If you made changes to Jaeger configuration files (e.g., config.yaml, ui-config.json), you may need to run this script in clean mode:" echo " ./deploy-all.sh clean" echo "Or manually restart the CI workflow to ensure your changes are applied." ================================================ FILE: examples/oci/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: jaeger-demo-ingress annotations: cert-manager.io/issuer: letsencrypt-prod spec: ingressClassName: nginx tls: - hosts: - demo.jaegertracing.io secretName: demo-jaeger-tls rules: - host: demo.jaegertracing.io http: paths: - path: /jaeger pathType: Prefix backend: service: name: jaeger-query port: number: 16686 - path: /grafana pathType: Prefix backend: service: name: prometheus-grafana port: number: 80 - path: /hotrod pathType: Prefix backend: service: name: jaeger-hotrod port: number: 80 --- # Separate Ingress to redirect / -> external site apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: jaeger-root-redirect annotations: cert-manager.io/issuer: letsencrypt-prod nginx.ingress.kubernetes.io/permanent-redirect: https://www.jaegertracing.io/demo/ spec: ingressClassName: nginx tls: - hosts: - demo.jaegertracing.io secretName: demo-jaeger-tls rules: - host: demo.jaegertracing.io http: paths: - path: / pathType: Prefix backend: # Backend is ignored due to redirect annotation; must be valid service: name: jaeger-query port: number: 16686 ================================================ FILE: examples/oci/jaeger-values.yaml ================================================ global: imageRegistry: docker.io hotrod: enabled: true image: repository: jaegertracing/example-hotrod tag: "1.72.0" args: - all extraArgs: - --otel-exporter=otlp - --basepath=/hotrod - --jaeger-ui=https://demo.jaegertracing.io/jaeger livenessProbe: path: /hotrod readinessProbe: path: /hotrod extraEnv: - name: OTEL_EXPORTER_OTLP_ENDPOINT value: http://jaeger-collector:4318 - name: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT value: http://jaeger-collector:4318/v1/traces - name: OTEL_EXPORTER_OTLP_PROTOCOL value: http/protobuf - name: OTEL_SERVICE_NAME value: hotrod - name: OTEL_LOG_LEVEL value: debug ================================================ FILE: examples/oci/load-generator/generate_traces.py ================================================ # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 import requests import random import time url = "http://jaeger-hotrod.default.svc.cluster.local:80/hotrod/dispatch" # /hotrod is the basepath customer_ids = [123, 392, 731, 567] i = 0 print("Starting load generator script") while True: #Keep sending requests customer = random.choice(customer_ids) nonse = random.random() params = { "customer": customer, "nonse": nonse } try: res = requests.get(url, params=params, timeout=5) print(f"[{i}]th request Sent to {res.url} → Status: {res.status_code}") except Exception as e: print(f"[{i}]th request Error: {e}") i = i + 1 time.sleep(10) # Pause between requests to avoid overload ================================================ FILE: examples/oci/load-generator/load-generator.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: trace-generator spec: replicas: 1 selector: matchLabels: app: trace-generator template: metadata: labels: app: trace-generator spec: containers: - name: trace-generator image: python:3.11 command: - /bin/sh - -c - | pip install requests && python /app/generate_traces.py volumeMounts: - name: script-volume mountPath: /app restartPolicy: Always volumes: - name: script-volume configMap: name: trace-script ================================================ FILE: examples/oci/monitoring-values.yaml ================================================ # Configuration for Prometheus , Grafana , Alertmanager can be set from this configuration fullnameOverride: "" prometheus: prometheusSpec: enableAdminAPI: true additionalScrapeConfigs: | - job_name: aggregated-trace-metrics static_configs: - targets: ['jaeger-collector-prometheus.default.svc.cluster.local:8889'] scrape_interval: 15s grafana: grafana.ini: server: domain: demo.jaegertracing.io root_url: "%(protocol)s://%(domain)s/grafana/" serve_from_sub_path: true ================================================ FILE: examples/oci/prometheus-svc.yaml ================================================ apiVersion: v1 kind: Service metadata: name: prometheus labels: app.kubernetes.io/name: prometheus spec: selector: app.kubernetes.io/name: prometheus app.kubernetes.io/instance: prometheus-kube-prometheus-prometheus ports: - name: http port: 9090 targetPort: 9090 --- apiVersion: v1 kind: Service metadata: name: jaeger-collector-prometheus spec: selector: app.kubernetes.io/name: jaeger app.kubernetes.io/instance: jaeger ports: - name: prometheus port: 8889 targetPort: 8889 - name: metrics port: 8888 targetPort: 8888 ================================================ FILE: examples/oci/tls-cert/issuer.yaml ================================================ apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: letsencrypt-prod spec: acme: # The ACME server URL server: https://acme-v02.api.letsencrypt.org/directory # Email address used for ACME registration email: cncf-jaeger-maintainers@lists.cncf.io # The ACME certificate profile profile: tlsserver # Name of a secret used to store the ACME account private key privateKeySecretRef: name: letsencrypt-prod # Enable the HTTP-01 challenge provider solvers: - http01: ingress: ingressClassName: nginx ================================================ FILE: examples/oci/ui-config.json ================================================ { "tracking": { "gaID": "G-S88L6V684T", "trackErrors": true } } ================================================ FILE: examples/opentracing-tutorial/README.md ================================================ # OpenTracing Instrumentation Tutorial To learn how to instrument your own applications for distributed tracing with Jaeger and OpenTracing API, please see https://github.com/yurishkuro/opentracing-tutorial/. ================================================ FILE: examples/otel-demo/README.md ================================================ # OpenTelemetry Demo app + HotRODapp + Jaeger + OpenSearch This example provides a one-command deployment of a complete observability stack on Kubernetes: - Jaeger (all-in-one) for tracing - OpenSearch and OpenSearch Dashboards - OpenTelemetry Demo application (multi-service web store) - HotRod application It is driven by `deploy-all.sh`, which supports both clean installs and upgrades. ## Prerequisites - Kubernetes cluster reachable via `kubectl` - Installed CLIs: `bash`, `git`, `curl`, `kubectl`, `helm` - Network access to Helm repositories ## Quick start - Clean install (removes previous releases/namespaces, then installs everything): ```bash path=null start=null ./deploy-all.sh clean ``` - Upgrade (default) — installs if missing, upgrades if present: ```bash path=null start=null ./deploy-all.sh # or explicitly ./deploy-all.sh upgrade ``` - Specify Jaeger all-in-one image tag: ```bash path=null start=null ./deploy-all.sh upgrade # Example ./deploy-all.sh upgrade latest ``` Environment variables: - ROLLOUT_TIMEOUT: rollout wait timeout in seconds (default 600) ```bash path=null start=null ROLLOUT_TIMEOUT=900 ./deploy-all.sh clean ``` ## What gets deployed - Namespace `opensearch`: - OpenSearch (single node) StatefulSet - OpenSearch Dashboards Deployment - Namespace `jaeger`: - Jaeger all-in-one Deployment (storage=none) - HOTROD application - Jaeger Query ClusterIP service (jaeger-query-clusterip) - Namespace `otel-demo`: - OpenTelemetry Demo (frontend, load-generator, and supporting services) ## Verifying the deployment - Pods status: ```bash path=null start=null kubectl get pods -n opensearch kubectl get pods -n jaeger kubectl get pods -n otel-demo ``` - Services: ```bash path=null start=null kubectl get svc -n opensearch kubectl get svc -n jaeger kubectl get svc -n otel-demo ``` ## Automatic port-forward using scrpit - OpenSearch Dashboards: ```bash path=null start=null ./start-port-forward.sh ## Customization - Helm values provided in this directory: - `opensearch-values.yaml` - `opensearch-dashboard-values.yaml` - `jaeger-values.yaml` - `jaeger-config.yaml` - `otel-demo-values.yaml` - `jaeger-query-service.yaml` You can adjust these files and re-run `./deploy-all.sh upgrade` to apply changes. ## Clean-up - Clean uninstall using cleanup.sh : ```bash path=null start=null ./cleanup.sh ``` - Manual teardown: ```bash path=null start=null helm uninstall opensearch -n opensearch || true helm uninstall opensearch-dashboards -n opensearch || true helm uninstall jaeger -n jaeger || true helm uninstall otel-demo -n otel-demo || true kubectl delete namespace opensearch jaeger otel-demo --ignore-not-found=true ``` ================================================ FILE: examples/otel-demo/cleanup.sh ================================================ #!/bin/bash # Copyright (c) 2025 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # OpenSearch Observability Stack Cleanup Script main() { echo "Starting OpenSearch Observability Stack Cleanup" # Stop any existing port forwards echo "Stopping any existing port-forward processes..." pkill -f "kubectl port-forward" 2>/dev/null || true echo "✅ Port-forward processes stopped" # Uninstall OTEL Demo echo " Uninstalling OTEL Demo..." if helm list -n otel-demo | grep -q otel-demo; then helm uninstall otel-demo -n otel-demo echo "✅ OTEL Demo uninstalled" else echo "⚠️ OTEL Demo not found or already uninstalled" fi # Uninstall Jaeger echo "Uninstalling Jaeger..." if helm list -n jaeger | grep -q jaeger; then helm uninstall jaeger -n jaeger echo "✅ Jaeger uninstalled" else echo "⚠️ Jaeger not found or already uninstalled" fi # Uninstall OpenSearch Dashboards echo "Uninstalling OpenSearch Dashboards..." if helm list -n opensearch | grep -q opensearch-dashboards; then helm uninstall opensearch-dashboards -n opensearch echo "✅ OpenSearch Dashboards uninstalled" else echo "⚠️ OpenSearch Dashboards not found or already uninstalled" fi # Uninstall OpenSearch echo " Uninstalling OpenSearch..." if helm list -n opensearch | grep -q opensearch; then helm uninstall opensearch -n opensearch echo "✅ OpenSearch uninstalled" else echo "⚠️ OpenSearch not found or already uninstalled" fi # Wait for pods to terminate echo "Waiting for pods to terminate..." sleep 10 # Delete namespaces echo "Deleting namespaces..." for ns in otel-demo jaeger opensearch; do if kubectl get namespace "$ns" > /dev/null 2>&1; then kubectl delete namespace "$ns" --force --grace-period=0 2>/dev/null || true echo "✅ Namespace $ns deleted" else echo "⚠️ Namespace $ns not found or already deleted" fi done # Clean up any remaining resources (PVCs, etc.) echo "Cleaning up any remaining PVCs..." kubectl get pvc -A | grep -E "(opensearch|jaeger|otel-demo)" || echo "No remaining PVCs found" # Final verification echo "Performing final verification..." remaining_pods=$(kubectl get pods -A | grep -E "(opensearch|jaeger|otel-demo)" || true) if [ -z "$remaining_pods" ]; then echo "All components cleaned up successfully!" else echo "⚠️ Some pods may still be terminating:" echo "$remaining_pods" echo "This is normal and they should disappear shortly" fi echo "" echo "✅ Cleanup Complete!" echo "" echo " All OpenSearch observability stack components have been removed" echo "" } main "$@" ================================================ FILE: examples/otel-demo/deploy-all.sh ================================================ #!/usr/bin/env bash # Copyright (c) 2025 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROLLOUT_TIMEOUT="${ROLLOUT_TIMEOUT:-600}" MODE="${1:-upgrade}" IMAGE_TAG="${2:-latest}" case "$MODE" in upgrade|clean) echo " Running in '$MODE' mode..." ;; *) echo "Error: Invalid mode '$MODE'" echo "Usage: $0 [upgrade|clean] [image-tag]" echo "" echo "Modes:" echo " upgrade - Upgrade existing deployment or install if not present (default)" echo " clean - Clean install (removes existing deployment first)" echo "" echo "Examples:" echo " $0 # Upgrade mode with latest tag" echo " $0 clean # Clean install" exit 1 ;; esac if [[ "$MODE" == "upgrade" ]]; then HELM_JAEGER_CMD="upgrade --install --force" else # For clean mode, use install after cleanup HELM_JAEGER_CMD="install" fi log() { echo "[$(date +"%F %T")] $*"; } err() { echo "[$(date +"%F %T")] ERROR: $*" >&2; exit 1; } need() { if ! command -v "$1" >/dev/null 2>&1; then err "$1 is required but not installed" fi } check_cluster() { if ! kubectl cluster-info >/dev/null 2>&1; then err "Cannot reach a Kubernetes cluster with kubectl" fi } check_required_files() { local files=( "$SCRIPT_DIR/opensearch-values.yaml" "$SCRIPT_DIR/opensearch-dashboard-values.yaml" "$SCRIPT_DIR/jaeger-values.yaml" "$SCRIPT_DIR/jaeger-config.yaml" "$SCRIPT_DIR/otel-demo-values.yaml" "$SCRIPT_DIR/jaeger-query-service.yaml" ) for f in "${files[@]}"; do [[ -f "$f" ]] || err "Missing required file: $f" done } wait_for_deployment() { local namespace="$1" local deployment="$2" local timeout="${3:-${ROLLOUT_TIMEOUT}s}" log "Waiting for deployment $deployment in $namespace..." if ! kubectl rollout status "deployment/$deployment" -n "$namespace" --timeout="$timeout"; then kubectl -n "$namespace" get deploy "$deployment" -o wide || true kubectl -n "$namespace" describe deploy "$deployment" || true kubectl -n "$namespace" get pods -l app.kubernetes.io/name="$deployment" -o wide || true err "Deployment $deployment failed to become ready in $namespace" fi log "Deployment $deployment is ready" } wait_for_statefulset() { local namespace="$1" local sts="$2" local timeout="${3:-${ROLLOUT_TIMEOUT}s}" log "Waiting for statefulset $sts in $namespace..." if ! kubectl rollout status "statefulset/$sts" -n "$namespace" --timeout="$timeout"; then kubectl -n "$namespace" get statefulset "$sts" -o wide || true kubectl -n "$namespace" describe statefulset "$sts" || true kubectl -n "$namespace" get pods -l statefulset.kubernetes.io/pod-name -o wide || true err "StatefulSet $sts failed to become ready in $namespace" fi log "StatefulSet $sts is ready" } wait_for_service_endpoints() { local namespace="$1" local service="$2" local timeout_secs="${3:-120}" log "Waiting for service $service endpoints in $namespace..." for i in $(seq 1 "$timeout_secs"); do if kubectl get endpoints "$service" -n "$namespace" >/dev/null 2>&1; then local ready ready=$(kubectl get endpoints "$service" -n "$namespace" -o jsonpath='{.subsets[*].addresses[*].ip}' 2>/dev/null || true) if [[ -n "$ready" ]]; then log "Service $service has endpoints: $ready" return 0 fi fi sleep 1 done kubectl get svc "$service" -n "$namespace" -o wide || true kubectl get endpoints "$service" -n "$namespace" -o yaml || true err "Service $service in $namespace has no ready endpoints after ${timeout_secs}s" } cleanup() { log "Cleanup: uninstalling existing releases if present" helm uninstall opensearch -n opensearch >/dev/null 2>&1 || true helm uninstall opensearch-dashboards -n opensearch >/dev/null 2>&1 || true helm uninstall jaeger -n jaeger >/dev/null 2>&1 || true helm uninstall otel-demo -n otel-demo >/dev/null 2>&1 || true log "Cleanup: deleting ingress resources" kubectl delete ingress --all -n jaeger >/dev/null 2>&1 || true kubectl delete ingress --all -n opensearch >/dev/null 2>&1 || true kubectl delete ingress --all -n otel-demo >/dev/null 2>&1 || true log "Cleanup: deleting namespaces (may take time)" for ns in jaeger otel-demo opensearch; do kubectl delete namespace "$ns" --ignore-not-found=true >/dev/null 2>&1 || true done # Wait for namespaces to disappear for ns in jaeger otel-demo opensearch; do for i in {1..120}; do if ! kubectl get namespace "$ns" >/dev/null 2>&1; then break fi sleep 2 done done log "Cleanup complete" } # Deploy HTTPS ingress resources deploy_ingress() { log "Deploying HTTPS ingress resources..." # Check if ingress files exist if [[ ! -f "$SCRIPT_DIR/ingress/ingress-jaeger.yaml" ]]; then log " Ingress files not found in $SCRIPT_DIR/ingress/ - skipping HTTPS setup" return 0 fi # Apply ingress for each namespace if kubectl apply -f "$SCRIPT_DIR/ingress/ingress-jaeger.yaml" 2>&1 | grep -q "created\|configured\|unchanged"; then log "Jaeger ingress configured (jaeger.demo.jaegertracing.io, hotrod.demo.jaegertracing.io)" else log " Failed to apply Jaeger ingress " fi if kubectl apply -f "$SCRIPT_DIR/ingress/ingress-opensearch.yaml" 2>&1 | grep -q "created\|configured\|unchanged"; then log " OpenSearch ingress configured (opensearch.demo.jaegertracing.io)" else log " Failed to apply OpenSearch ingress " fi if kubectl apply -f "$SCRIPT_DIR/ingress/ingress-otel-demo.yaml" 2>&1 | grep -q "created\|configured\|unchanged"; then log " OTel Demo ingress configured (shop.demo.jaegertracing.io)" else log " Failed to apply OTel Demo ingress " fi log "Waiting for SSL certificates to be issued..." sleep 10 # Check certificate status local certs_ready=0 local certs_total=0 for ns in jaeger opensearch otel-demo; do if kubectl get namespace "$ns" >/dev/null 2>&1; then if kubectl get certificate -n "$ns" >/dev/null 2>&1; then certs_total=$((certs_total + 1)) if kubectl get certificate -n "$ns" -o jsonpath='{.items[*].status.conditions[?(@.type=="Ready")].status}' 2>/dev/null | grep -q "True"; then certs_ready=$((certs_ready + 1)) fi fi fi done if [[ $certs_total -eq 0 ]]; then log " No certificates found - cert-manager may not be installed" elif [[ $certs_ready -eq $certs_total ]]; then log "All SSL certificates ready ($certs_ready/$certs_total)" else log "Some certificates still pending ($certs_ready/$certs_total ready)" log "Certificates will be issued automatically by cert-manager" fi log "HTTPS endpoints:" log " • https://jaeger.demo.jaegertracing.io" log " • https://hotrod.demo.jaegertracing.io" log " • https://opensearch.demo.jaegertracing.io" log " • https://shop.demo.jaegertracing.io" } # Clone Jaeger Helm chart and prepare dependencies clone_jaeger_v2() { local dest="$SCRIPT_DIR/helm-charts" if [[ ! -d "$dest" ]]; then log "Cloning Jaeger Helm Charts..." git clone https://github.com/jaegertracing/helm-charts.git "$dest" ( cd "$dest" log "Using v2 branch for Jaeger v2..." git checkout v2 log "Adding required Helm repositories..." helm repo add bitnami https://charts.bitnami.com/bitnami >/dev/null 2>&1 || true helm repo add prometheus-community https://prometheus-community.github.io/helm-charts >/dev/null 2>&1 || true helm repo add incubator https://charts.helm.sh/incubator >/dev/null 2>&1 || true helm repo update >/dev/null helm dependency build ./charts/jaeger ) else log "Jaeger Helm Charts already exist. Skipping clone." # Ensure required repos exist even if charts folder already exists helm repo add bitnami https://charts.bitnami.com/bitnami >/dev/null 2>&1 || true helm repo add prometheus-community https://prometheus-community.github.io/helm-charts >/dev/null 2>&1 || true helm repo add incubator https://charts.helm.sh/incubator >/dev/null 2>&1 || true helm repo update >/dev/null fi } main() { log "Starting CI deploy (weekly refresh)" need bash need git need curl need kubectl need helm check_required_files check_cluster if [[ "$MODE" == "clean" ]]; then cleanup fi log "Adding/updating Helm repos" helm repo add opensearch https://opensearch-project.github.io/helm-charts >/dev/null 2>&1 || true helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts >/dev/null 2>&1 || true helm repo add jaegertracing https://jaegertracing.github.io/helm-charts >/dev/null 2>&1 || true helm repo update >/dev/null clone_jaeger_v2 log "Deploying OpenSearch" helm upgrade --install opensearch opensearch/opensearch \ --namespace opensearch --create-namespace \ --version 2.19.0 \ --set image.tag=2.11.0 \ -f "$SCRIPT_DIR/opensearch-values.yaml" \ --wait --timeout 10m wait_for_statefulset opensearch opensearch-cluster-single "${ROLLOUT_TIMEOUT}s" log "Deploying OpenSearch Dashboards" helm upgrade --install opensearch-dashboards opensearch/opensearch-dashboards \ --namespace opensearch \ -f "$SCRIPT_DIR/opensearch-dashboard-values.yaml" \ --wait --timeout 10m wait_for_deployment opensearch opensearch-dashboards "${ROLLOUT_TIMEOUT}s" log "Deploying Jaeger (all-in-one, no storage)" helm $HELM_JAEGER_CMD jaeger "$SCRIPT_DIR/helm-charts/charts/jaeger" \ --namespace jaeger --create-namespace \ --set allInOne.enabled=true \ --set storage.type=none \ --set allInOne.image.repository=jaegertracing/jaeger \ --set allInOne.image.tag="${IMAGE_TAG}" \ --set-file userconfig="$SCRIPT_DIR/jaeger-config.yaml" \ -f "$SCRIPT_DIR/jaeger-values.yaml" \ --wait --timeout 10m wait_for_deployment jaeger jaeger "${ROLLOUT_TIMEOUT}s" log "Creating Jaeger query ClusterIP service..." kubectl apply -n jaeger -f "$SCRIPT_DIR/jaeger-query-service.yaml" log "Jaeger query ClusterIP service created" log "Ensuring Jaeger Collector service endpoints are ready before deploying the demo" wait_for_service_endpoints jaeger jaeger-collector 180 log "Ensuring HotROD service endpoints are ready" wait_for_service_endpoints jaeger jaeger-hotrod 180 log "Deploying HotROD trace generator" kubectl -n jaeger create configmap trace-script --from-file="$SCRIPT_DIR/generate_traces.py" --dry-run=client -o yaml | kubectl apply -f - kubectl apply -n jaeger -f "$SCRIPT_DIR/load-generator.yaml" wait_for_deployment jaeger trace-generator "${ROLLOUT_TIMEOUT}s" log "Deploying OpenTelemetry Demo (with in-cluster Collector)" helm upgrade --install otel-demo open-telemetry/opentelemetry-demo \ -f "$SCRIPT_DIR/otel-demo-values.yaml" \ --namespace otel-demo --create-namespace \ --wait --timeout 15m wait_for_deployment otel-demo otel-collector "${ROLLOUT_TIMEOUT}s" wait_for_deployment otel-demo frontend "${ROLLOUT_TIMEOUT}s" wait_for_deployment otel-demo load-generator "${ROLLOUT_TIMEOUT}s" log "All components deployed successfully" # Deploy HTTPS ingress deploy_ingress log "🎉 Deployment complete! Stack is ready." } main ================================================ FILE: examples/otel-demo/generate_traces.py ================================================ # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 import os import requests import random import time TARGET_URL = os.getenv("TARGET_URL", "http://jaeger-hotrod.jaeger.svc.cluster.local/dispatch") SLEEP_SECONDS = float(os.getenv("SLEEP_SECONDS", "5")) CUSTOMER_IDS = [123, 392, 731, 567] print(f"Starting HotROD load generator → {TARGET_URL} (interval={SLEEP_SECONDS}s)") i = 0 session = requests.Session() while True: customer = random.choice(CUSTOMER_IDS) nonse = random.random() params = { "customer": customer, "nonse": nonse, } try: res = session.get(TARGET_URL, params=params, timeout=5) print(f"[{i}] Sent to {res.url} → {res.status_code}") except Exception as e: print(f"[{i}] Error: {e}") i += 1 time.sleep(SLEEP_SECONDS) ================================================ FILE: examples/otel-demo/ingress/README.md ================================================ # Ingress Configuration for OpenTelemetry Demo Stack This directory contains the HTTPS ingress configurations for exposing the observability stack services via NGINX ingress controller with Let's Encrypt SSL certificates. ## Files - **`clusterissuer-letsencrypt-prod.yaml`** - Let's Encrypt certificate issuer (already deployed) - **`ingress-jaeger.yaml`** - Exposes Jaeger UI and HotROD demo - **`ingress-opensearch.yaml`** - Exposes OpenSearch Dashboards - **`ingress-otel-demo.yaml`** - Exposes OTel Demo Shop (frontend-proxy) ## Exposed Services | Service | URL | Backend Service | Port | |---------|-----|-----------------|------| | Jaeger UI | https://jaeger.demo.jaegertracing.io | jaeger-query-clusterip | 16686 | | HotROD Demo | https://hotrod.demo.jaegertracing.io | jaeger-hotrod | 80 | | OpenSearch Dashboards | https://opensearch.demo.jaegertracing.io | opensearch-dashboards | 5601 | | OTel Demo Shop | https://shop.demo.jaegertracing.io | frontend-proxy | 8080 | | Load Generator | https://shop.demo.jaegertracing.io/loadgen/ | (via frontend-proxy) | 8080 | ## Certificate Management Certificates are automatically managed by cert-manager using the Let's Encrypt production issuer. ### View Certificate Status ```bash kubectl get certificates --all-namespaces ``` ### Certificate Secrets - `jaeger-demo-tls` (namespace: jaeger) - `opensearch-demo-tls` (namespace: opensearch) - `otel-demo-tls` (namespace: otel-demo) ### Force Certificate Renewal ```bash kubectl delete certificate -n # Certificate will be automatically recreated ``` ## Prerequisites - NGINX Ingress Controller (deployed) - cert-manager (deployed) - ClusterIssuer (letsencrypt-prod) configured - DNS records pointing to ingress controller IP (170.9.51.232) ## DNS Configuration All hostnames must resolve to the NGINX ingress controller external IP: ``` jaeger.demo.jaegertracing.io -> 170.9.51.232 hotrod.demo.jaegertracing.io -> 170.9.51.232 opensearch.demo.jaegertracing.io -> 170.9.51.232 shop.demo.jaegertracing.io -> 170.9.51.232 ``` Verify DNS: ```bash dig jaeger.demo.jaegertracing.io +short ``` ## Troubleshooting ### Ingress Not Working ```bash kubectl get ingress --all-namespaces kubectl describe ingress -n ``` ### Certificate Issues ```bash kubectl describe certificate -n kubectl get certificaterequest -n kubectl get challenge -n ``` ### Ingress Controller Logs ```bash kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx ``` ## Security Notes - Load generator is **not** directly exposed to the internet - Access load generator via frontend-proxy: https://shop.demo.jaegertracing.io/loadgen/ - All certificates are production Let's Encrypt certificates - Auto-renewal enabled (certificates valid for 90 days) ================================================ FILE: examples/otel-demo/ingress/clusterissuer-letsencrypt-prod.yaml ================================================ apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: email: cncf-jaeger-maintainers@lists.cncf.io server: https://acme-v02.api.letsencrypt.org/directory privateKeySecretRef: name: letsencrypt-prod solvers: - http01: ingress: ingressClassName: nginx ================================================ FILE: examples/otel-demo/ingress/ingress-jaeger.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: jaeger-demo-ingress namespace: jaeger annotations: cert-manager.io/cluster-issuer: letsencrypt-prod spec: ingressClassName: nginx rules: - host: jaeger.demo.jaegertracing.io http: paths: - path: / pathType: Prefix backend: service: name: jaeger-query-clusterip port: number: 16686 - host: hotrod.demo.jaegertracing.io http: paths: - path: / pathType: Prefix backend: service: name: jaeger-hotrod port: number: 80 tls: - hosts: - jaeger.demo.jaegertracing.io - hotrod.demo.jaegertracing.io secretName: jaeger-demo-tls ================================================ FILE: examples/otel-demo/ingress/ingress-opensearch.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: opensearch-demo-ingress namespace: opensearch annotations: cert-manager.io/cluster-issuer: letsencrypt-prod spec: ingressClassName: nginx rules: - host: opensearch.demo.jaegertracing.io http: paths: - path: / pathType: Prefix backend: service: name: opensearch-dashboards port: number: 5601 tls: - hosts: - opensearch.demo.jaegertracing.io secretName: opensearch-demo-tls ================================================ FILE: examples/otel-demo/ingress/ingress-otel-demo.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: otel-demo-ingress namespace: otel-demo annotations: cert-manager.io/cluster-issuer: letsencrypt-prod spec: ingressClassName: nginx rules: - host: shop.demo.jaegertracing.io http: paths: - path: / pathType: Prefix backend: service: name: frontend-proxy port: number: 8080 tls: - hosts: - shop.demo.jaegertracing.io secretName: otel-demo-tls ================================================ FILE: examples/otel-demo/jaeger-config.yaml ================================================ service: extensions: [jaeger_storage, jaeger_query, healthcheckv2] pipelines: traces: receivers: [otlp] processors: [batch] exporters: [jaeger_storage_exporter] extensions: healthcheckv2: use_v2: true http: endpoint: 0.0.0.0:13133 jaeger_query: storage: traces: some_storage traces_archive: another_storage metrics: some_storage # For SPM metrics jaeger_storage: backends: some_storage: &opensearch_config opensearch: server_urls: - http://opensearch-cluster-single.opensearch.svc.cluster.local:9200 indices: index_prefix: "jaeger-main" spans: date_layout: "2006-01-02" rollover_frequency: "day" shards: 1 replicas: 0 services: date_layout: "2006-01-02" rollover_frequency: "day" shards: 1 replicas: 0 dependencies: date_layout: "2006-01-02" rollover_frequency: "day" shards: 1 replicas: 0 sampling: date_layout: "2006-01-02" rollover_frequency: "day" shards: 1 replicas: 0 another_storage: opensearch: server_urls: - http://opensearch-cluster-single.opensearch.svc.cluster.local:9200 indices: index_prefix: "jaeger-archive" spans: date_layout: "2006-01-02" rollover_frequency: "day" shards: 1 replicas: 0 metric_backends: some_storage: *opensearch_config receivers: otlp: protocols: grpc: endpoint: "0.0.0.0:4317" http: endpoint: "0.0.0.0:4318" processors: batch: exporters: jaeger_storage_exporter: trace_storage: some_storage ================================================ FILE: examples/otel-demo/jaeger-query-service.yaml ================================================ apiVersion: v1 kind: Service metadata: name: jaeger-query-clusterip namespace: jaeger labels: app.kubernetes.io/name: jaeger app.kubernetes.io/component: query app.kubernetes.io/instance: jaeger spec: type: ClusterIP ports: - name: jaeger-query port: 16686 targetPort: 16686 protocol: TCP - name: jaeger-admin port: 16685 targetPort: 16685 protocol: TCP selector: app.kubernetes.io/name: jaeger app.kubernetes.io/component: all-in-one app.kubernetes.io/instance: jaeger ================================================ FILE: examples/otel-demo/jaeger-values.yaml ================================================ global: imageRegistry: docker.io allInOne: enabled: true extraEnv: [] # Enable HotROD demo application hotrod: enabled: true image: tag: "1.72.0" args: - all extraArgs: - --jaeger-ui=https://jaeger.demo.jaegertracing.io - --otel-exporter=otlp livenessProbe: path: / readinessProbe: path: / extraEnv: - name: OTEL_EXPORTER_OTLP_ENDPOINT value: http://jaeger-collector:4318 - name: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT value: http://jaeger-collector:4318/v1/traces - name: OTEL_EXPORTER_OTLP_PROTOCOL value: http/protobuf - name: OTEL_SERVICE_NAME value: hotrod - name: OTEL_LOG_LEVEL value: debug query: service: type: ClusterIP ================================================ FILE: examples/otel-demo/load-generator.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: trace-generator namespace: jaeger spec: replicas: 1 selector: matchLabels: app: trace-generator template: metadata: labels: app: trace-generator spec: restartPolicy: Always volumes: - name: script-volume configMap: name: trace-script items: - key: generate_traces.py path: generate_traces.py containers: - name: trace-generator image: python:3.11-slim command: - /bin/sh - -c - | pip install --no-cache-dir requests && python /app/generate_traces.py env: - name: TARGET_URL value: "http://jaeger-hotrod.jaeger.svc.cluster.local/dispatch" - name: SLEEP_SECONDS value: "5" volumeMounts: - name: script-volume mountPath: /app resources: requests: cpu: "50m" memory: "128Mi" ephemeral-storage: "200Mi" limits: cpu: "200m" memory: "256Mi" ephemeral-storage: "1Gi" ================================================ FILE: examples/otel-demo/opensearch-dashboard-values.yaml ================================================ image: repository: docker.io/opensearchproject/opensearch-dashboards tag: "2.11.0" opensearchHosts: "http://opensearch-cluster-single:9200" securityContext: runAsUser: 1000 runAsGroup: 1000 config: opensearch_dashboards.yml: | server.host: 0.0.0.0 opensearch.hosts: ["http://opensearch-cluster-single:9200"] opensearch.username: "admin" opensearch.password: "admin123" opensearch.ssl.verificationMode: none opensearch_security.enabled: false ================================================ FILE: examples/otel-demo/opensearch-values.yaml ================================================ clusterName: "opensearch-cluster" nodeGroup: "single" global: dockerRegistry: docker.io replicas: 1 persistence: enabled: true size: "10Gi" storageClass: "oci-bv" # Using Oracle Cloud Block Volume storage class opensearchJavaOpts: "-Xmx1g -Xms1g" securityConfig: enabled: false extraEnvs: - name: DISABLE_INSTALL_DEMO_CONFIG value: "true" - name: DISABLE_SECURITY_PLUGIN value: "true" config: opensearch.yml: | cluster.name: opensearch-cluster network.host: 0.0.0.0 bootstrap.memory_lock: false plugins.security.disabled: true service: type: ClusterIP port: 9200 ================================================ FILE: examples/otel-demo/otel-demo-values.yaml ================================================ # official schema for otel demo is at https://raw.githubusercontent.com/open-telemetry/opentelemetry-helm-charts/main/charts/opentelemetry-demo/values.yaml # Keep bundled infra disabled (we deploy Jaeger/OpenSearch separately) jaeger: enabled: false prometheus: enabled: false grafana: enabled: false opensearch: enabled: false # Preserve default.env (to keep OTEL_SERVICE_NAME and OTEL_COLLECTOR_NAME) and override only what we need default: envOverrides: # Narrower service namespace + explicit environment tag - name: OTEL_RESOURCE_ATTRIBUTES value: service.name=$(OTEL_SERVICE_NAME),service.namespace=otel-demo,deployment.environment=oke-dev # Send OTLP over HTTP by default and disable metrics/logs exporters (traces only) - name: OTEL_EXPORTER_OTLP_ENDPOINT value: http://otel-collector:4318 - name: OTEL_EXPORTER_OTLP_PROTOCOL value: http/protobuf - name: OTEL_EXPORTER_OTLP_TRACES_PROTOCOL value: http/protobuf - name: OTEL_LOGS_EXPORTER value: none - name: OTEL_METRICS_EXPORTER value: none - name: OTEL_TRACES_EXPORTER value: otlp components: accounting: initContainers: - name: wait-for-kafka image: docker.io/busybox:latest command: ["sh", "-c", "until nc -z -v -w30 kafka 9092; do echo waiting for kafka; sleep 2; done;"] cart: initContainers: - name: wait-for-valkey-cart image: docker.io/busybox:latest command: ["sh", "-c", "until nc -z -v -w30 valkey-cart 6379; do echo waiting for valkey-cart; sleep 2; done;"] checkout: initContainers: - name: wait-for-kafka image: docker.io/busybox:latest command: ["sh", "-c", "until nc -z -v -w30 kafka 9092; do echo waiting for kafka; sleep 2; done;"] fraud-detection: initContainers: - name: wait-for-kafka image: docker.io/busybox:latest command: ["sh", "-c", "until nc -z -v -w30 kafka 9092; do echo waiting for kafka; sleep 2; done;"] flagd: initContainers: - name: init-config image: docker.io/busybox:latest command: ["sh", "-c", "cp /config-ro/demo.flagd.json /config-rw/demo.flagd.json && cat /config-rw/demo.flagd.json"] volumeMounts: - mountPath: /config-ro name: config-ro - mountPath: /config-rw name: config-rw load-generator: envOverrides: - name: LOCUST_USERS value: "5" - name: LOCUST_SPAWN_RATE value: "2" valkey-cart: imageOverride: repository: docker.io/valkey/valkey tag: "8.1.3-alpine" # Override Collector config to export traces to Jaeger only and drop demo metrics/logs opentelemetry-collector: image: repository: docker.io/otel/opentelemetry-collector-contrib config: receivers: otlp: protocols: grpc: {} http: cors: allowed_origins: - http://* - https://* httpcheck/frontend-proxy: null redis: null processors: memory_limiter: {} k8sattributes: {} resource: attributes: - key: service.instance.id from_attribute: k8s.pod.uid action: insert transform: null batch: {} connectors: spanmetrics: null exporters: otlp/jaeger: endpoint: jaeger-collector.jaeger.svc.cluster.local:4317 tls: insecure: true opensearch: null otlphttp/prometheus: null debug: null otlp: null service: telemetry: null pipelines: traces: receivers: [otlp] processors: [memory_limiter, k8sattributes, resource, batch] exporters: [otlp/jaeger] metrics: null logs: null ================================================ FILE: examples/otel-demo/start-port-forward.sh ================================================ #!/bin/bash # Copyright (c) 2025 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # OpenSearch Observability Stack Port Forwarding Script # helper function to check if a service exists check_service() { local service=$1 local namespace=$2 if kubectl get svc "$service" -n "$namespace" > /dev/null 2>&1; then return 0 else return 1 fi } echo "Starting Port Forwarding for OpenSearch Observability Stack" # Check prerequisites if ! command -v kubectl > /dev/null 2>&1; then echo " kubectl is required but not installed" exit 1 fi if ! kubectl cluster-info > /dev/null 2>&1; then echo "🛑 Cannot connect to Kubernetes cluster. Please ensure minikube (or the cluster) is running" exit 1 fi # Stop any existing port forwards first echo " Stopping any existing port-forward processes..." pkill -f "kubectl port-forward" 2>/dev/null || true sleep 2 # Track results started_services=() failed_services=() echo " Starting port forwarding services..." # Jaeger Query UI if check_service "jaeger-query-clusterip" "jaeger"; then kubectl port-forward -n jaeger svc/jaeger-query-clusterip 16686:16686 & started_services+=("Jaeger UI (http://localhost:16686)") echo "Started: Jaeger UI on port 16686" else failed_services+=("Jaeger (service not found)") echo "⚠️ Jaeger service not found" fi # OpenSearch Dashboards if check_service "opensearch-dashboards" "opensearch"; then kubectl port-forward -n opensearch svc/opensearch-dashboards 5601:5601 & started_services+=("OpenSearch Dashboards (http://localhost:5601)") echo "Started: OpenSearch Dashboards on port 5601" else failed_services+=("OpenSearch Dashboards (service not found)") echo "⚠️ OpenSearch Dashboards service not found" fi # OpenSearch API if check_service "opensearch-cluster-single" "opensearch"; then kubectl port-forward -n opensearch svc/opensearch-cluster-single 9200:9200 & started_services+=("OpenSearch API (http://localhost:9200)") echo "Started: OpenSearch API on port 9200" else failed_services+=("OpenSearch API (service not found)") echo "⚠️ OpenSearch API service not found" fi # OTEL Demo Frontend if check_service "frontend-proxy" "otel-demo"; then kubectl port-forward -n otel-demo svc/frontend-proxy 8080:8080 & started_services+=("OTEL Demo Frontend (http://localhost:8080)") echo " Started: OTEL Demo Frontend on port 8080" else failed_services+=("OTEL Demo Frontend (service not found)") echo "⚠️ OTEL Demo Frontend service not found" fi # Load Generator if check_service "load-generator" "otel-demo"; then kubectl port-forward -n otel-demo svc/load-generator 8089:8089 & started_services+=("Load Generator (http://localhost:8089)") echo " Started: Load Generator on port 8089" else failed_services+=("Load Generator (service not found)") echo "⚠️ Load Generator service not found" fi # HotROD Demo App (from Jaeger Helm chart v2) if check_service "jaeger-hotrod" "jaeger"; then kubectl port-forward -n jaeger svc/jaeger-hotrod 8088:80 & started_services+=("HotROD Demo App (http://localhost:8088)") echo " Started: HotROD Demo App on port 8088" else failed_services+=("HotROD Demo App (service not found)") echo "⚠️ HotROD Demo App service not found" fi # Wait for services to start sleep 3 echo "" echo "✅ Port Forwarding Setup Complete!" echo "" if [ ${#started_services[@]} -gt 0 ]; then echo "Successfully started services:" for service in "${started_services[@]}"; do echo " • $service" done echo "" fi if [ ${#failed_services[@]} -gt 0 ]; then echo "Failed to start services:" for service in "${failed_services[@]}"; do echo " • $service" done echo "" echo "⚠️ Some services may not be deployed yet. Run the deployment script first." echo "" fi if [ ${#started_services[@]} -gt 0 ]; then echo "Management commands:" echo " • View all port-forwards: jobs" echo " • Stop all port-forwards: pkill -f 'kubectl port-forward'" echo " • Stop this script: Ctrl+C" echo "" echo " Port forwarding is active. Press Ctrl+C to stop all port-forwards." trap ' echo " Stopping all port-forwards..." pkill -f "kubectl port-forward" echo "✅ All port-forwards stopped." exit 0 ' INT # Keep script alive while true; do sleep 10 done else echo "🛑 No services were successfully started. Please check your deployment." exit 1 fi ================================================ FILE: examples/reverse-proxy/README.md ================================================ # reverse-proxy example This example illustrates how Jaeger UI can be run behind a reverse proxy under a different URL prefix. Start the servers: ```sh cd examples/reverse-proxy docker compose up ``` Jaeger UI can be accesssed at http://localhost:18080/jaeger/prefix . ================================================ FILE: examples/reverse-proxy/docker-compose.yml ================================================ services: jaeger: image: cr.jaegertracing.io/jaegertracing/jaeger:${JAEGER_VERSION:-2.5.0} ports: - "16686:16686" # Jaeger UI - "4317:4317" # Collector, OpenTelemetry gRPC - "4318:4318" # Collector, OpenTelemetry gRPC # We are using the build-in all-in-one configuration to avoid having # to provide an external configuration file for Jaeger. We use the # `--set` flag to override the default configuration of the `jaeger_query` # extension to tell Jaeger that it should run the UI with a given prefix. command: "--set extensions.jaeger_query.base_path=/jaeger/prefix" networks: - proxy-net httpd: image: httpd:latest ports: - "18080:80" volumes: - ./httpd.conf:/usr/local/apache2/conf/httpd.conf depends_on: - jaeger networks: - proxy-net networks: proxy-net: ================================================ FILE: examples/reverse-proxy/httpd.conf ================================================ ServerRoot "/usr/local/apache2" Listen 80 LoadModule mpm_event_module modules/mod_mpm_event.so LoadModule proxy_module modules/mod_proxy.so LoadModule proxy_http_module modules/mod_proxy_http.so # some of the modules below may not be needed LoadModule authn_file_module modules/mod_authn_file.so LoadModule authn_core_module modules/mod_authn_core.so LoadModule authz_host_module modules/mod_authz_host.so LoadModule authz_groupfile_module modules/mod_authz_groupfile.so LoadModule authz_user_module modules/mod_authz_user.so LoadModule authz_core_module modules/mod_authz_core.so LoadModule access_compat_module modules/mod_access_compat.so LoadModule auth_basic_module modules/mod_auth_basic.so LoadModule reqtimeout_module modules/mod_reqtimeout.so LoadModule filter_module modules/mod_filter.so LoadModule mime_module modules/mod_mime.so LoadModule log_config_module modules/mod_log_config.so LoadModule env_module modules/mod_env.so LoadModule headers_module modules/mod_headers.so LoadModule setenvif_module modules/mod_setenvif.so LoadModule version_module modules/mod_version.so LoadModule unixd_module modules/mod_unixd.so LoadModule status_module modules/mod_status.so LoadModule autoindex_module modules/mod_autoindex.so LoadModule dir_module modules/mod_dir.so LoadModule alias_module modules/mod_alias.so LoadModule rewrite_module modules/mod_rewrite.so # Note that the prefix is used in both the proxy and the backend URLs. ProxyPass "/jaeger/prefix" "http://jaeger:16686/jaeger/prefix" ProxyPassReverse "/jaeger/prefix" "http://jaeger:16686/jaeger/prefix" ErrorLog /proc/self/fd/2 LogLevel info # # The following directives define some format nicknames for use with # a CustomLog directive (see below). # LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined LogFormat "%h %l %u %t \"%r\" %>s %b" common # You need to enable mod_logio.c to use %I and %O LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio # # The location and format of the access logfile (Common Logfile Format). # If you do not define any access logfiles within a # container, they will be logged here. Contrariwise, if you *do* # define per- access logfiles, transactions will be # logged therein and *not* in this file. # CustomLog /proc/self/fd/1 common # # If you prefer a logfile with access, agent, and referer information # (Combined Logfile Format) you can use the following directive. # #CustomLog "logs/access_log" combined ================================================ FILE: examples/service-performance-monitoring/README.md ================================================ # Service Performance Monitoring example Please refer to [README](https://github.com/jaegertracing/jaeger/blob/main/docker-compose/monitor/README.md). ================================================ FILE: go.mod ================================================ module github.com/jaegertracing/jaeger go 1.26.0 require ( github.com/ClickHouse/ch-go v0.71.0 github.com/ClickHouse/clickhouse-go/v2 v2.43.0 github.com/apache/cassandra-gocql-driver/v2 v2.0.0 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/dgraph-io/badger/v4 v4.9.1 github.com/elastic/go-elasticsearch/v9 v9.3.1 github.com/fsnotify/fsnotify v1.9.0 github.com/go-logr/zapr v1.3.0 github.com/gogo/protobuf v1.3.2 github.com/gorilla/handlers v1.5.2 github.com/jaegertracing/jaeger-idl v0.6.0 github.com/kr/pretty v0.3.1 github.com/modelcontextprotocol/go-sdk v1.4.1 github.com/olivere/elastic/v7 v7.0.32 github.com/open-telemetry/opentelemetry-collector-contrib/connector/spanmetricsconnector v0.147.0 github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.147.0 github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusexporter v0.147.0 github.com/open-telemetry/opentelemetry-collector-contrib/extension/basicauthextension v0.147.0 github.com/open-telemetry/opentelemetry-collector-contrib/extension/healthcheckv2extension v0.147.0 github.com/open-telemetry/opentelemetry-collector-contrib/extension/pprofextension v0.147.0 github.com/open-telemetry/opentelemetry-collector-contrib/extension/sigv4authextension v0.147.0 github.com/open-telemetry/opentelemetry-collector-contrib/extension/storage v0.147.0 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.147.0 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.147.0 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.147.0 github.com/open-telemetry/opentelemetry-collector-contrib/processor/attributesprocessor v0.147.0 github.com/open-telemetry/opentelemetry-collector-contrib/processor/filterprocessor v0.147.0 github.com/open-telemetry/opentelemetry-collector-contrib/processor/tailsamplingprocessor v0.147.0 github.com/open-telemetry/opentelemetry-collector-contrib/receiver/jaegerreceiver v0.147.0 github.com/open-telemetry/opentelemetry-collector-contrib/receiver/kafkareceiver v0.147.0 github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.147.0 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 github.com/prometheus/common v0.67.5 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 go.opentelemetry.io/collector/client v1.54.0 go.opentelemetry.io/collector/component v1.54.0 go.opentelemetry.io/collector/component/componentstatus v0.148.0 go.opentelemetry.io/collector/component/componenttest v0.148.0 go.opentelemetry.io/collector/config/configauth v1.54.0 go.opentelemetry.io/collector/config/configgrpc v0.148.0 go.opentelemetry.io/collector/config/confighttp v0.148.0 go.opentelemetry.io/collector/config/confighttp/xconfighttp v0.148.0 go.opentelemetry.io/collector/config/confignet v1.54.0 go.opentelemetry.io/collector/config/configoptional v1.54.0 go.opentelemetry.io/collector/config/configretry v1.54.0 go.opentelemetry.io/collector/config/configtls v1.54.0 go.opentelemetry.io/collector/confmap v1.54.0 go.opentelemetry.io/collector/confmap/provider/envprovider v1.54.0 go.opentelemetry.io/collector/confmap/provider/fileprovider v1.54.0 go.opentelemetry.io/collector/confmap/provider/httpprovider v1.54.0 go.opentelemetry.io/collector/confmap/provider/httpsprovider v1.54.0 go.opentelemetry.io/collector/confmap/provider/yamlprovider v1.54.0 go.opentelemetry.io/collector/confmap/xconfmap v0.148.0 go.opentelemetry.io/collector/connector v0.148.0 go.opentelemetry.io/collector/connector/forwardconnector v0.148.0 go.opentelemetry.io/collector/consumer v1.54.0 go.opentelemetry.io/collector/consumer/consumertest v0.148.0 go.opentelemetry.io/collector/exporter v1.54.0 go.opentelemetry.io/collector/exporter/debugexporter v0.148.0 go.opentelemetry.io/collector/exporter/exporterhelper v0.148.0 go.opentelemetry.io/collector/exporter/exportertest v0.148.0 go.opentelemetry.io/collector/exporter/nopexporter v0.148.0 go.opentelemetry.io/collector/exporter/otlpexporter v0.148.0 go.opentelemetry.io/collector/exporter/otlphttpexporter v0.148.0 go.opentelemetry.io/collector/extension v1.54.0 go.opentelemetry.io/collector/extension/zpagesextension v0.148.0 go.opentelemetry.io/collector/featuregate v1.54.0 go.opentelemetry.io/collector/otelcol v0.148.0 go.opentelemetry.io/collector/pdata v1.54.0 go.opentelemetry.io/collector/processor v1.54.0 go.opentelemetry.io/collector/processor/batchprocessor v0.148.0 go.opentelemetry.io/collector/processor/memorylimiterprocessor v0.148.0 go.opentelemetry.io/collector/processor/processorhelper v0.148.0 go.opentelemetry.io/collector/processor/processortest v0.148.0 go.opentelemetry.io/collector/receiver v1.54.0 go.opentelemetry.io/collector/receiver/nopreceiver v0.148.0 go.opentelemetry.io/collector/receiver/otlpreceiver v0.148.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 go.opentelemetry.io/contrib/samplers/jaegerremote v0.36.0 go.opentelemetry.io/otel v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 go.opentelemetry.io/otel/exporters/prometheus v0.64.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 go.opentelemetry.io/otel/metric v1.42.0 go.opentelemetry.io/otel/sdk v1.42.0 go.opentelemetry.io/otel/sdk/metric v1.42.0 go.opentelemetry.io/otel/trace v1.42.0 go.uber.org/automaxprocs v1.6.0 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.27.1 golang.org/x/sys v0.42.0 google.golang.org/grpc v1.79.3 google.golang.org/protobuf v1.36.11 ) require ( cloud.google.com/go/auth v0.17.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/apache/thrift v0.22.0 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect github.com/dennwc/varint v1.0.0 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/open-telemetry/opentelemetry-collector-contrib/extension/internal/credentialsfile v0.147.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/internal/healthcheck v0.147.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/prometheus/client_golang/exp v0.0.0-20260101091701-2cd067eb23c9 // indirect github.com/prometheus/otlptranslator v1.0.0 // indirect github.com/prometheus/prometheus v0.309.2-0.20260113170727-c7bc56cf6c8f // indirect github.com/prometheus/sigv4 v0.3.0 // indirect github.com/segmentio/encoding v0.5.4 // indirect github.com/tg123/go-htpasswd v1.2.4 // indirect github.com/twmb/franz-go/pkg/kadm v1.17.2 // indirect github.com/xdg-go/scram v1.2.0 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/zeebo/xxh3 v1.1.0 // indirect go.opentelemetry.io/collector/config/configopaque v1.54.0 // indirect go.opentelemetry.io/collector/internal/componentalias v0.148.0 // indirect go.opentelemetry.io/collector/semconv v0.128.1-0.20250610090210-188191247685 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/api v0.258.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apimachinery v0.34.3 // indirect k8s.io/client-go v0.34.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect ) require ( github.com/IBM/sarama v1.46.3 // indirect github.com/alecthomas/participle/v2 v2.1.4 // indirect github.com/antchfx/xmlquery v1.5.0 // indirect github.com/antchfx/xpath v1.3.6 // indirect github.com/aws/aws-msk-iam-sasl-signer-go v1.0.4 // indirect github.com/aws/aws-sdk-go-v2 v1.41.2 // indirect github.com/aws/aws-sdk-go-v2/config v1.32.10 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.10 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect github.com/aws/smithy-go v1.24.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/eapache/go-resiliency v1.7.0 // indirect github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect github.com/eapache/queue v1.1.0 // indirect github.com/ebitengine/purego v0.10.0 // indirect github.com/elastic/elastic-transport-go/v8 v8.8.0 // indirect github.com/elastic/go-grok v0.3.1 // indirect github.com/elastic/lunes v0.2.0 // indirect github.com/expr-lang/expr v1.17.8 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/foxboron/go-tpm-keyfiles v0.0.0-20251226215517-609e4778396f // indirect github.com/go-faster/city v1.0.1 // indirect github.com/go-faster/errors v0.7.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/go-tpm v0.9.8 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/go-version v1.8.0 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/iancoleman/strcase v0.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect github.com/jcmturner/gofork v1.7.6 // indirect github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.4 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect github.com/knadh/koanf/providers/confmap v1.0.0 // indirect github.com/knadh/koanf/v2 v2.3.3 // indirect github.com/kr/text v0.2.0 // indirect github.com/lightstep/go-expohisto v1.0.0 // indirect github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect github.com/magefile/mage v1.15.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/mostynb/go-grpc-compression v1.2.3 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.147.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/internal/filter v0.147.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka v0.147.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/internal/pdatautil v0.147.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/batchpersignal v0.147.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/core/xidutils v0.147.0 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/configkafka v0.147.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/topic v0.147.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl v0.147.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/resourcetotelemetry v0.147.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/status v0.147.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/azure v0.147.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus v0.147.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/zipkin v0.147.0 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect github.com/paulmach/orb v0.12.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/procfs v0.20.1 // indirect github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect github.com/relvacode/iso8601 v1.7.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/cors v1.11.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/shirou/gopsutil/v4 v4.26.2 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect github.com/twmb/franz-go v1.20.7 // indirect github.com/twmb/franz-go/pkg/kmsg v1.12.0 // indirect github.com/twmb/franz-go/pkg/sasl/kerberos v1.1.0 // indirect github.com/twmb/franz-go/plugin/kzap v1.1.2 // indirect github.com/twmb/murmur3 v1.1.8 // indirect github.com/ua-parser/uap-go v0.0.0-20240611065828-3a4781585db6 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/collector v0.148.0 // indirect go.opentelemetry.io/collector/config/configcompression v1.54.0 // indirect go.opentelemetry.io/collector/config/configmiddleware v1.54.0 go.opentelemetry.io/collector/config/configtelemetry v0.148.0 // indirect go.opentelemetry.io/collector/connector/connectortest v0.148.0 // indirect go.opentelemetry.io/collector/connector/xconnector v0.148.0 // indirect go.opentelemetry.io/collector/consumer/consumererror v0.148.0 // indirect go.opentelemetry.io/collector/consumer/consumererror/xconsumererror v0.148.0 // indirect go.opentelemetry.io/collector/consumer/xconsumer v0.148.0 // indirect go.opentelemetry.io/collector/exporter/exporterhelper/xexporterhelper v0.148.0 // indirect go.opentelemetry.io/collector/exporter/xexporter v0.148.0 // indirect go.opentelemetry.io/collector/extension/extensionauth v1.54.0 go.opentelemetry.io/collector/extension/extensioncapabilities v0.148.0 go.opentelemetry.io/collector/extension/extensionmiddleware v0.148.0 // indirect go.opentelemetry.io/collector/extension/extensiontest v0.148.0 // indirect go.opentelemetry.io/collector/extension/xextension v0.148.0 // indirect go.opentelemetry.io/collector/internal/fanoutconsumer v0.148.0 // indirect go.opentelemetry.io/collector/internal/memorylimiter v0.148.0 // indirect go.opentelemetry.io/collector/internal/sharedcomponent v0.148.0 // indirect go.opentelemetry.io/collector/internal/telemetry v0.148.0 // indirect go.opentelemetry.io/collector/pdata/pprofile v0.148.0 // indirect go.opentelemetry.io/collector/pdata/testdata v0.148.0 // indirect go.opentelemetry.io/collector/pdata/xpdata v0.148.0 go.opentelemetry.io/collector/pipeline v1.54.0 // indirect go.opentelemetry.io/collector/pipeline/xpipeline v0.148.0 // indirect go.opentelemetry.io/collector/processor/processorhelper/xprocessorhelper v0.148.0 // indirect go.opentelemetry.io/collector/processor/xprocessor v0.148.0 // indirect go.opentelemetry.io/collector/receiver/receiverhelper v0.148.0 // indirect go.opentelemetry.io/collector/receiver/receivertest v0.148.0 // indirect go.opentelemetry.io/collector/receiver/xreceiver v0.148.0 // indirect go.opentelemetry.io/collector/service v0.148.0 go.opentelemetry.io/collector/service/hostcapabilities v0.148.0 // indirect go.opentelemetry.io/contrib/bridges/otelzap v0.17.0 // indirect go.opentelemetry.io/contrib/otelconf v0.22.0 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.42.0 // indirect go.opentelemetry.io/contrib/zpages v0.67.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 // indirect go.opentelemetry.io/otel/log v0.18.0 // indirect go.opentelemetry.io/otel/sdk/log v0.18.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa golang.org/x/text v0.34.0 // indirect gonum.org/v1/gonum v0.17.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) ================================================ FILE: go.sum ================================================ cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0 h1:LkHbJbgF3YyvC53aqYGR+wWQDn2Rdp9AQdGndf9QvY4= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0/go.mod h1:QyiQdW4f4/BIfB8ZutZ2s+28RAgfa/pT+zS++ZHyM1I= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0 h1:bXwSugBiSbgtz7rOtbfGf+woewp4f06orW9OP5BjHLA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0/go.mod h1:Y/HgrePTmGy9HjdSGTqZNa+apUpTVIEVKXJyARP2lrk= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM= github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw= github.com/ClickHouse/clickhouse-go/v2 v2.43.0 h1:fUR05TrF1GyvLDa/mAQjkx7KbgwdLRffs2n9O3WobtE= github.com/ClickHouse/clickhouse-go/v2 v2.43.0/go.mod h1:o6jf7JM/zveWC/PP277BLxjHy5KjnGX/jfljhM4s34g= github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U+0KMqAA0KcU= github.com/Code-Hex/go-generics-cache v1.5.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4= github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI= github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec= github.com/IBM/sarama v1.46.3 h1:njRsX6jNlnR+ClJ8XmkO+CM4unbrNr/2vB5KK6UA+IE= github.com/IBM/sarama v1.46.3/go.mod h1:GTUYiF9DMOZVe3FwyGT+dtSPceGFIgA+sPc5u6CBwko= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/participle/v2 v2.1.4 h1:W/H79S8Sat/krZ3el6sQMvMaahJ+XcM9WSI2naI7w2U= github.com/alecthomas/participle/v2 v2.1.4/go.mod h1:8tqVbpTX20Ru4NfYQgZf4mP18eXPTBViyMWiArNEgGI= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/antchfx/xmlquery v1.5.0 h1:uAi+mO40ZWfyU6mlUBxRVvL6uBNZ6LMU4M3+mQIBV4c= github.com/antchfx/xmlquery v1.5.0/go.mod h1:lJfWRXzYMK1ss32zm1GQV3gMIW/HFey3xDZmkP1SuNc= github.com/antchfx/xpath v1.3.5/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/antchfx/xpath v1.3.6 h1:s0y+ElRRtTQdfHP609qFu0+c6bglDv20pqOViQjjdPI= github.com/antchfx/xpath v1.3.6/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/apache/cassandra-gocql-driver/v2 v2.0.0 h1:Omnzb1Z/P90Dr2TbVNu54ICQL7TKVIIsJO231w484HU= github.com/apache/cassandra-gocql-driver/v2 v2.0.0/go.mod h1:QH/asJjB3mHvY6Dot6ZKMMpTcOrWJ8i9GhsvG1g0PK4= github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc= github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g= github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.4 h1:2jAwFwA0Xgcx94dUId+K24yFabsKYDtAhCgyMit6OqE= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.4/go.mod h1:MVYeeOhILFFemC/XlYTClvBjYZrg/EPd3ts885KrNTI= github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls= github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4= github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI= github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw= github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8= github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/service/ec2 v1.279.0 h1:o7eJKe6VYAnqERPlLAvDW5VKXV6eTKv1oxTpMoDP378= github.com/aws/aws-sdk-go-v2/service/ec2 v1.279.0/go.mod h1:Wg68QRgy2gEGGdmTPU/UbVpdv8sM14bUZmF64KFwAsY= github.com/aws/aws-sdk-go-v2/service/ecs v1.70.0 h1:IZpZatHsscdOKjwmDXC6idsCXmm3F/obutAUNjnX+OM= github.com/aws/aws-sdk-go-v2/service/ecs v1.70.0/go.mod h1:LQMlcWBoiFVD3vUVEz42ST0yTiaDujv2dRE6sXt1yPE= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk= github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.10 h1:MQuZZ6Tq1qQabPlkVxrCMdyVl70Ogl4AERZKo+y9Wzo= github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.10/go.mod h1:U5C3JME1ibKESmpzBAqlRpTYZfVbTqrb5ICJm+sVVd8= github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ= github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g= github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o= github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU= github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c= github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs= github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0= github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= github.com/dgraph-io/badger/v4 v4.9.1 h1:DocZXZkg5JJHJPtUErA0ibyHxOVUDVoXLSCV6t8NC8w= github.com/dgraph-io/badger/v4 v4.9.1/go.mod h1:5/MEx97uzdPUHR4KtkNt8asfI2T4JiEiQlV7kWUo8c0= github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/digitalocean/godo v1.171.0 h1:QwpkwWKr3v7yxc8D4NQG973NoR9APCEWjYnLOQeXVpQ= github.com/digitalocean/godo v1.171.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA= github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84= github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/elastic/elastic-transport-go/v8 v8.8.0 h1:7k1Ua+qluFr6p1jfJjGDl97ssJS/P7cHNInzfxgBQAo= github.com/elastic/elastic-transport-go/v8 v8.8.0/go.mod h1:YLHer5cj0csTzNFXoNQ8qhtGY1GTvSqPnKWKaqQE3Hk= github.com/elastic/go-elasticsearch/v9 v9.3.1 h1:v5A9uFw0nLFA0luD3xAqliBXbscfuhch409HIinfhKY= github.com/elastic/go-elasticsearch/v9 v9.3.1/go.mod h1:B5u4H2jo2/v0+PrgbmIUdEyHdenFyavWtjciAFl7TA0= github.com/elastic/go-grok v0.3.1 h1:WEhUxe2KrwycMnlvMimJXvzRa7DoByJB4PVUIE1ZD/U= github.com/elastic/go-grok v0.3.1/go.mod h1:n38ls8ZgOboZRgKcjMY8eFeZFMmcL9n2lP0iHhIDk64= github.com/elastic/lunes v0.2.0 h1:WI3bsdOTuaYXVe2DS1KbqA7u7FOHN4o8qJw80ZyZoQs= github.com/elastic/lunes v0.2.0/go.mod h1:u3W/BdONWTrh0JjNZ21C907dDc+cUZttZrGa625nf2k= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM= github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM= github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/foxboron/go-tpm-keyfiles v0.0.0-20251226215517-609e4778396f h1:RJ+BDPLSHQO7cSjKBqjPJSbi1qfk9WcsjQDtZiw3dZw= github.com/foxboron/go-tpm-keyfiles v0.0.0-20251226215517-609e4778396f/go.mod h1:VHbbch/X4roIY22jL1s3qRbZhCiRIgUAF/PdSUcx2io= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/analysis v0.24.1 h1:Xp+7Yn/KOnVWYG8d+hPksOYnCYImE3TieBa7rBOesYM= github.com/go-openapi/analysis v0.24.1/go.mod h1:dU+qxX7QGU1rl7IYhBC8bIfmWQdX4Buoea4TGtxXY84= github.com/go-openapi/errors v0.22.4 h1:oi2K9mHTOb5DPW2Zjdzs/NIvwi2N3fARKaTJLdNabaM= github.com/go-openapi/errors v0.22.4/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk= github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4= github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4= github.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY= github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k= github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA= github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ= github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8= github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= github.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw= github.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc= github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4= github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-zookeeper/zk v1.0.4 h1:DPzxraQx7OrPyXq2phlGlNSIyWEsAox0RJmjTseMV6I= github.com/go-zookeeper/zk v1.0.4/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/go-tpm-tools v0.4.7 h1:J3ycC8umYxM9A4eF73EofRZu4BxY0jjQnUnkhIBbvws= github.com/google/go-tpm-tools v0.4.7/go.mod h1:gSyXTZHe3fgbzb6WEGd90QucmsnT1SRdlye82gH8QjQ= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/gophercloud/gophercloud/v2 v2.9.0 h1:Y9OMrwKF9EDERcHFSOTpf/6XGoAI0yOxmsLmQki4LPM= github.com/gophercloud/gophercloud/v2 v2.9.0/go.mod h1:Ki/ILhYZr/5EPebrPL9Ej+tUg4lqx71/YH2JWVeU+Qk= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM= github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE= github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4= github.com/hashicorp/cronexpr v1.1.3 h1:rl5IkxXN2m681EfivTlccqIryzYJSXRGRNa0xeG7NA4= github.com/hashicorp/cronexpr v1.1.3/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/nomad/api v0.0.0-20260106084653-e8f2200c7039 h1:77URO0yPjlPjRc00KbjoBTG2dqHXFKA7Fv3s98w16kM= github.com/hashicorp/nomad/api v0.0.0-20260106084653-e8f2200c7039/go.mod h1:sldFTIgs+FsUeKU3LwVjviAIuksxD8TzDOn02MYwslE= github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= github.com/hetznercloud/hcloud-go/v2 v2.33.0 h1:g9hwuo60IXbupXJCYMlO4xDXgxxMPuFk31iOpLXDCV4= github.com/hetznercloud/hcloud-go/v2 v2.33.0/go.mod h1:GzYEl7slIGKc6Ttt08hjiJvGj8/PbWzcQf6IUi02dIs= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/ionos-cloud/sdk-go/v6 v6.3.6 h1:l/TtKgdQ1wUH3DDe2SfFD78AW+TJWdEbDpQhHkWd6CM= github.com/ionos-cloud/sdk-go/v6 v6.3.6/go.mod h1:nUGHP4kZHAZngCVr4v6C8nuargFrtvt7GrzH/hqn7c4= github.com/jaegertracing/jaeger-idl v0.6.0 h1:LOVQfVby9ywdMPI9n3hMwKbyLVV3BL1XH2QqsP5KTMk= github.com/jaegertracing/jaeger-idl v0.6.0/go.mod h1:mpW0lZfG907/+o5w5OlnNnig7nHJGT3SfKmRqC42HGQ= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= github.com/jcmturner/gokrb5/v8 v8.4.3/go.mod h1:dqRwJGXznQrzw6cWmyo6kH+E7jksEQG/CyVWsJEsJO0= github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 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/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= 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.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5zwp8Aebir+/EaRE= github.com/knadh/koanf/providers/confmap v1.0.0/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A= github.com/knadh/koanf/v2 v2.3.3 h1:jLJC8XCRfLC7n4F+ZKKdBsbq1bfXTpuFhf4L7t94D94= github.com/knadh/koanf/v2 v2.3.3/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= 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= 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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lightstep/go-expohisto v1.0.0 h1:UPtTS1rGdtehbbAF7o/dhkWLTDI73UifG8LbfQI7cA4= github.com/lightstep/go-expohisto v1.0.0/go.mod h1:xDXD0++Mu2FOaItXtdDfksfgxfV0z1TMPa+e/EUd0cs= github.com/linode/linodego v1.63.0 h1:MdjizfXNJDVJU6ggoJmMO5O9h4KGPGivNX0fzrAnstk= github.com/linode/linodego v1.63.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc= github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/mostynb/go-grpc-compression v1.2.3 h1:42/BKWMy0KEJGSdWvzqIyOZ95YcR9mLPqKctH7Uo//I= github.com/mostynb/go-grpc-compression v1.2.3/go.mod h1:AghIxF3P57umzqM9yz795+y1Vjs47Km/Y2FE6ouQ7Lg= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E= github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k= github.com/open-telemetry/opentelemetry-collector-contrib/connector/spanmetricsconnector v0.147.0 h1:sAjARhiJxByhuUz0JTUPthqetNp6rxACW6KMEDd6K3c= github.com/open-telemetry/opentelemetry-collector-contrib/connector/spanmetricsconnector v0.147.0/go.mod h1:nAEpAXAz41JmUxHcHeXpg6tMwe9JL5RxpX+pGa9yJP0= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.147.0 h1:8DF9hSx66jk3eWjZjUH3+aokuUQPdCQkw9z+NqJ1oYY= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.147.0/go.mod h1:CXjSPfbi7uiP9HcOscN5ultxy9YZ4RrSAgarO9tMnAM= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusexporter v0.147.0 h1:8k93l6lIa1W4QQOU0/OH9BEKciBIJWPmrXW9n/esXJs= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusexporter v0.147.0/go.mod h1:dRxp6Gk8ngODlTz+Cayv3dxyfPPiE55bdV7hwBG6XwY= github.com/open-telemetry/opentelemetry-collector-contrib/extension/basicauthextension v0.147.0 h1:5PjhFOIEALESZolVAfTC4Sg53RcIgGW/Ke5AYFQ9l5M= github.com/open-telemetry/opentelemetry-collector-contrib/extension/basicauthextension v0.147.0/go.mod h1:47js3Z256jOh+XpOtCH+cQZtz9jW2wpcq6cWe5IVr6Q= github.com/open-telemetry/opentelemetry-collector-contrib/extension/healthcheckv2extension v0.147.0 h1:RneYo1jyTJMqNT0HHYRHLb2LYf7f4NtwjETmG73NGS8= github.com/open-telemetry/opentelemetry-collector-contrib/extension/healthcheckv2extension v0.147.0/go.mod h1:69PRbxCJeu2l+d+Gayubu53RFxUuuqcLU8XtmA+kLfE= github.com/open-telemetry/opentelemetry-collector-contrib/extension/internal/credentialsfile v0.147.0 h1:BlP9xxHe4BAbABEIS8TuHr3qDfpyv81n7Bu/sQuVlps= github.com/open-telemetry/opentelemetry-collector-contrib/extension/internal/credentialsfile v0.147.0/go.mod h1:neAzGFqd93Rg4m1DDJm9rAWg0+0Jk76fHXHgzAfczgM= github.com/open-telemetry/opentelemetry-collector-contrib/extension/pprofextension v0.147.0 h1:UJnSG8N2l9t+Pr0fY/EboDFzZYB2HsMLdL6uj+OzvCo= github.com/open-telemetry/opentelemetry-collector-contrib/extension/pprofextension v0.147.0/go.mod h1:ClUcsFAUK0PXWazIpiZdJORKRnEk+Mp4vtdxrP2yELA= github.com/open-telemetry/opentelemetry-collector-contrib/extension/sigv4authextension v0.147.0 h1:Y7xcLh9OGqYTgR7ylUR1Ht3Y1YArcM3uQXMcBlkrNus= github.com/open-telemetry/opentelemetry-collector-contrib/extension/sigv4authextension v0.147.0/go.mod h1:8JIXqozseUSNyHgBd6EFT6vWuIg23RXWNMio0aD1DHc= github.com/open-telemetry/opentelemetry-collector-contrib/extension/storage v0.147.0 h1:esfK3tYYNEea3jfmcOKJyR7Ezz9+KmI3bgF3+TwOOiQ= github.com/open-telemetry/opentelemetry-collector-contrib/extension/storage v0.147.0/go.mod h1:+Y4GA0ade48Tu2jJ6BbsP8ZCyv7FqSvsHR+F+aUKJHY= github.com/open-telemetry/opentelemetry-collector-contrib/internal/common v0.147.0 h1:T7yBzSHaI7kKMe8skPtnqQvp4hp8sAmLZdQav2MDrW0= github.com/open-telemetry/opentelemetry-collector-contrib/internal/common v0.147.0/go.mod h1:ZsCmntCwaHQFhjYdn1CnT6+wadDNDbHqCSWjRwpr63U= github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.147.0 h1:DVadnM6h9kbXkpCQLbr12Z+fuQPmAlY51iHWs5ut3fw= github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.147.0/go.mod h1:aF8IuTH/4RSG3znojA0KFavdtGfykAqExX1YjOaIa5M= github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.147.0 h1:rUPX31MpXODJqkrZWSyS8cnxf3lNgA69PxvV7HzBLCs= github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.147.0/go.mod h1:CvU3E/36YEVyReqRvgJqW3IQXhN8Odhf6tC1yev2PHw= github.com/open-telemetry/opentelemetry-collector-contrib/internal/filter v0.147.0 h1:YPe60sJMIoKxQotdUClpgiIPcb7Knb/OM2qpoKgRb/c= github.com/open-telemetry/opentelemetry-collector-contrib/internal/filter v0.147.0/go.mod h1:vbExcaw9Q47djcw4ILpnaTZTp8n3QRJFI4xFXcSHmig= github.com/open-telemetry/opentelemetry-collector-contrib/internal/healthcheck v0.147.0 h1:NkLpgOdYnTCs6hUiqvr8U9UxbUM1WUkfjgvGG8MJTQo= github.com/open-telemetry/opentelemetry-collector-contrib/internal/healthcheck v0.147.0/go.mod h1:AkFZzL012QPWCLtLvXrQh1xTZVqBn8iIIt5zbgWdPf4= github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka v0.147.0 h1:/TPCPFE/1WR3zOfLropPqsQ8NSVOmst5q3C8pHPABe0= github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka v0.147.0/go.mod h1:ywtB5h2kNqFqSyVShjFJic0BC8sO5r2HQZ77atu+2VQ= github.com/open-telemetry/opentelemetry-collector-contrib/internal/pdatautil v0.147.0 h1:ROhVuI04U0/jRv3zcW9u5OTDlqyfAcg+/QsKwPbRR1s= github.com/open-telemetry/opentelemetry-collector-contrib/internal/pdatautil v0.147.0/go.mod h1:k0+eJUJNXUoKjF2XFjZnWfBmAHRd+Rb24uI2A72amYc= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/batchpersignal v0.147.0 h1:TUbXDTIjnZpXCAA2MCXrRCVcsT6kwPflPd8DLzcWzsI= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/batchpersignal v0.147.0/go.mod h1:oHKqyWra+9FjEIJPR7W9D8QUEXrRwzE+uKgwqeOO2SE= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/core/xidutils v0.147.0 h1:e2UW/ZBN3Um+zHIRmCJl9rAOtIynPzTVAUQfpAt5Dtk= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/core/xidutils v0.147.0/go.mod h1:tCfzrCKv5wNnJxZvi/2xJqFptOLMj1tz5PYiKqdAqHE= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.147.0 h1:nxCNHHUItl2j0sjknI/mRbBBcQCxu0yv3baii9GNB1U= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.147.0/go.mod h1:LrW8KarPjlu+1VdP2t6kjJeOTF+y3/n2wCZAdc/NWg0= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/configkafka v0.147.0 h1:eH8MrShWyTuAE/mxtE8T0nExCg8tZzoS1vwDUEl2U0I= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/configkafka v0.147.0/go.mod h1:urjJHvpoRTtDExH/qoRRu1lJiTcYoK5HrInQfUjyZRE= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/topic v0.147.0 h1:MZ2IvmAgWJRQyt1enQJpK5j3VUw5D7nXTr8vCQH33w8= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/topic v0.147.0/go.mod h1:CnkW4xd+Xa6MvVtuY5CKkQtEdb2Sazq7E4rRI/F7nJY= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl v0.147.0 h1:wVGrNsA8QvEzXvR6vQOv7S94EGs/L8mUUsV70Qate4g= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl v0.147.0/go.mod h1:FSh8OidZ963lNDB1SeEphUhK/HKj983+5gv9eVwh9gk= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.147.0 h1:jgmHvcC3WCrkA49VBm/Tay6O5OEaLvevlqd+OEoPI3M= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.147.0/go.mod h1:NMXNbNZ4aEhyW0Oe4BfbGLLOI8Y9FmB/unZp01HUlKU= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.147.0 h1:EYy8gmyjGLS0iYV7ksOOHrjZgiTjbWU26vziBAt4jKI= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.147.0/go.mod h1:VDqy65biIJI9iYN9rrVi2nm9KAvSfq+6Fzrm8WyL4Qw= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/resourcetotelemetry v0.147.0 h1:7dvd0FwpRWTWp66fbCsGf/JhYYMPSYcu9XF0cmPKc2Y= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/resourcetotelemetry v0.147.0/go.mod h1:WJUHOQIEa2VG73eBj0Zqo3On4gVkCgOrtSRq7CssD1w= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/status v0.147.0 h1:frHEmZtGSx+aahM6RVW6LTKXdqrPetGpB1nix9QQ//Y= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/status v0.147.0/go.mod h1:d/AjbYIhXKqX9Rr92+iCvGMPQNIu/FE0I8N5A2cJBqo= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/azure v0.147.0 h1:tVhO/op0pgRN8o2kOUvKzZ1e37DKHCW/3MpegUkuJHE= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/azure v0.147.0/go.mod h1:7mql5p8gvpK+nlHy+BiidRnMXe49QTNEcy1WOkd2fcM= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.147.0 h1:CgI9ucLeKNrA/vpTgdb3p2/WHW/MarIQKntc84xpFJE= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.147.0/go.mod h1:+xw9tDl9F3N0y+ppHR8f1owl18A8d5UNKJzwrHIGQR8= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus v0.147.0 h1:ohA/GdgjyijjdpELoGnInmkrg4fxsUYQTWs6nUdJWJ0= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus v0.147.0/go.mod h1:wpArXVy+Y3ZvgIebJYjywfPRyltpSqrPHWJDfEfsUas= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/zipkin v0.147.0 h1:ybOAetZoXyUx/afmDveMR0sd5SHt66lH7sVQ848PgqU= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/zipkin v0.147.0/go.mod h1:lHN3dsMIZOY4txzU8BMcLsF33AAWc2nKGfFOW6pq+SQ= github.com/open-telemetry/opentelemetry-collector-contrib/processor/attributesprocessor v0.147.0 h1:evdMn3nTOwmR/77/3yT5cpehkFO2RMEdcU7NNI7DOKg= github.com/open-telemetry/opentelemetry-collector-contrib/processor/attributesprocessor v0.147.0/go.mod h1:SNgCq3hgH+QCdzeUFH70cQwsDKNpa/1OieJQUwCK7Ps= github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.147.0 h1:P0wATwpMJthz0KYvJ7/IGQphirAtL+xoEf9l9Tjbl6w= github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.147.0/go.mod h1:eybr2AGrtxlJBytZscy93gu0+7pMFMjtZVAQOu7djvk= github.com/open-telemetry/opentelemetry-collector-contrib/processor/filterprocessor v0.147.0 h1:EispIh+opDvbpIVZ2qCTnlowz4NPFH01cAybzZZYshs= github.com/open-telemetry/opentelemetry-collector-contrib/processor/filterprocessor v0.147.0/go.mod h1:1Gc7xmntFDFqZUfrLBgtMTfUGCEttfovDm5D9phnSlM= github.com/open-telemetry/opentelemetry-collector-contrib/processor/tailsamplingprocessor v0.147.0 h1:vOuSxeHYCNc2Hi1Zq+nulZ/nRGLImtZvo3iP5zKzuRg= github.com/open-telemetry/opentelemetry-collector-contrib/processor/tailsamplingprocessor v0.147.0/go.mod h1:UsDob5QlnmXNT00UNNA+e2OJPCH84oLTm6crfoIugi0= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/jaegerreceiver v0.147.0 h1:4Fxs1MC5uedffj72Cbsp4PGYlS2QwYzzFMsnWqKrvXI= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/jaegerreceiver v0.147.0/go.mod h1:KPxcGM9KYv+4l3fTYpVA/tAX0Tmh3e9LFVvMRXaCcwk= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/kafkareceiver v0.147.0 h1:F8jGKEu7VRz/tFE1onoklGtfcPcYLzoTTc1yqRZdwBA= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/kafkareceiver v0.147.0/go.mod h1:YHzzAESLZ+ecM23UheTEaZdP67xHyQxGzQ1u1AHcbKo= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/prometheusreceiver v0.147.0 h1:+UKAkzgVjdYbeI/hErs0R20LOlzqCYIXcWuiHBcZLpY= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/prometheusreceiver v0.147.0/go.mod h1:GIyJzng/QxPqTl+2zlXCtZflF9O6EXM4gSjHYtUgrK8= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.147.0 h1:99RtE1pkTjNkRlcwTdCOI9miK+q7DWPZ/3anBfRN2xE= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.147.0/go.mod h1:hBGGRz7YKS4GBUmk/SvKenPX+qVDIiRl6d6AsGWR4nY= 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.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE= github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= github.com/paulmach/orb v0.12.0 h1:z+zOwjmG3MyEEqzv92UN49Lg1JFYx0L9GpGKNVDKk1s= github.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/alertmanager v0.30.0 h1:E4dnxSFXK8V2Bb8iqudlisTmaIrF3hRJSWnliG08tBM= github.com/prometheus/alertmanager v0.30.0/go.mod h1:93PBumcTLr/gNtNtM0m7BcCffbvYP5bKuLBWiOnISaA= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_golang/exp v0.0.0-20260101091701-2cd067eb23c9 h1:al1B/YzHmaXhacIFkrZSDSUpnPHV4ZPMfENQpvk3PZQ= github.com/prometheus/client_golang/exp v0.0.0-20260101091701-2cd067eb23c9/go.mod h1:PmAYDB13uBFBG9qE1qxZZgZWhg7Rg6SfKM5DMK7hjyI= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM= github.com/prometheus/common/assets v0.2.0/go.mod h1:D17UVUE12bHbim7HzwUvtqm6gwBEaDQ0F+hIGbFbccI= github.com/prometheus/exporter-toolkit v0.15.1 h1:XrGGr/qWl8Gd+pqJqTkNLww9eG8vR/CoRk0FubOKfLE= github.com/prometheus/exporter-toolkit v0.15.1/go.mod h1:P/NR9qFRGbCFgpklyhix9F6v6fFr/VQB/CVsrMDGKo4= github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/prometheus/prometheus v0.309.2-0.20260113170727-c7bc56cf6c8f h1:jdB0ldVZ0vDSopY9ya4h/XPGlRYVEDsWH5x7+vIx4R8= github.com/prometheus/prometheus v0.309.2-0.20260113170727-c7bc56cf6c8f/go.mod h1:wSFyaZQ1ioryO2X47s2wvQEWypS+Mwf9IQ1ABDEo2Sk= github.com/prometheus/sigv4 v0.3.0 h1:QIG7nTbu0JTnNidGI1Uwl5AGVIChWUACxn2B/BQ1kms= github.com/prometheus/sigv4 v0.3.0/go.mod h1:fKtFYDus2M43CWKMNtGvFNHGXnAJJEGZbiYCmVp/F8I= github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo= github.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/relvacode/iso8601 v1.7.0 h1:BXy+V60stMP6cpswc+a93Mq3e65PfXCgDFfhvNNGrdo= github.com/relvacode/iso8601 v1.7.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI= github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+YgDpBcK1ITf3o96N/K7/wsRXQnUTEs= github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stackitcloud/stackit-sdk-go/core v0.20.1 h1:odiuhhRXmxvEvnVTeZSN9u98edvw2Cd3DcnkepncP3M= github.com/stackitcloud/stackit-sdk-go/core v0.20.1/go.mod h1:fqto7M82ynGhEnpZU6VkQKYWYoFG5goC076JWXTUPRQ= github.com/stretchr/objx v0.1.0/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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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.6.1/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= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tg123/go-htpasswd v1.2.4 h1:HgH8KKCjdmo7jjXWN9k1nefPBd7Be3tFCTjc2jPraPU= github.com/tg123/go-htpasswd v1.2.4/go.mod h1:EKThQok9xHkun6NBMynNv6Jmu24A33XdZzzl4Q7H1+0= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/twmb/franz-go v1.7.0/go.mod h1:PMze0jNfNghhih2XHbkmTFykbMF5sJqmNJB31DOOzro= github.com/twmb/franz-go v1.20.7 h1:P4MGSXJjjAPP3NRGPCks/Lrq+j+twWMVl1qYCVgNmWY= github.com/twmb/franz-go v1.20.7/go.mod h1:0bRX9HZVaoueqFWhPZNi2ODnJL7DNa6mK0HeCrC2bNU= github.com/twmb/franz-go/pkg/kadm v1.17.2 h1:g5f1sAxnTkYC6G96pV5u715HWhxd66hWaDZUAQ8xHY8= github.com/twmb/franz-go/pkg/kadm v1.17.2/go.mod h1:ST55zUB+sUS+0y+GcKY/Tf1XxgVilaFpB9I19UubLmU= github.com/twmb/franz-go/pkg/kfake v0.0.0-20251021233722-4ca18825d8c0 h1:2ldj0Fktzd8IhnSZWyCnz/xulcW7zGvTLMOXTDqm7wA= github.com/twmb/franz-go/pkg/kfake v0.0.0-20251021233722-4ca18825d8c0/go.mod h1:UmQGDzMTYkAMr3CtNNYz1n0bD6KBI+cSnfQx70vP+c8= github.com/twmb/franz-go/pkg/kmsg v1.2.0/go.mod h1:SxG/xJKhgPu25SamAq0rrucfp7lbzCpEXOC+vH/ELrY= github.com/twmb/franz-go/pkg/kmsg v1.12.0 h1:CbatD7ers1KzDNgJqPbKOq0Bz/WLBdsTH75wgzeVaPc= github.com/twmb/franz-go/pkg/kmsg v1.12.0/go.mod h1:+DPt4NC8RmI6hqb8G09+3giKObE6uD2Eya6CfqBpeJY= github.com/twmb/franz-go/pkg/sasl/kerberos v1.1.0 h1:alKdbddkPw3rDh+AwmUEwh6HNYgTvDSFIe/GWYRR9RM= github.com/twmb/franz-go/pkg/sasl/kerberos v1.1.0/go.mod h1:k8BoBjyUbFj34f0rRbn+Ky12sZFAPbmShrg0karAIMo= github.com/twmb/franz-go/plugin/kzap v1.1.2 h1:0arX5xJ0soUPX1LlDay6ZZoxuWkWk1lggQ5M/IgRXAE= github.com/twmb/franz-go/plugin/kzap v1.1.2/go.mod h1:53Cl9Uz1pbdOPDvUISIxLrZIWSa2jCuY1bTMauRMBmo= github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= github.com/ua-parser/uap-go v0.0.0-20240611065828-3a4781585db6 h1:SIKIoA4e/5Y9ZOl0DCe3eVMLPOQzJxgZpfdHHeauNTM= github.com/ua-parser/uap-go v0.0.0-20240611065828-3a4781585db6/go.mod h1:BUbeWZiieNxAuuADTBNb3/aeje6on3DhU3rpWsQSB1E= github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs= github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/collector v0.148.0 h1:v/MudgCZ7n0LfOxtMIJjYdA8R073vjUllhhsaBtiTro= go.opentelemetry.io/collector v0.148.0/go.mod h1:EoAnQknwq/wemQGw89xl6+IITdNwenPWl0ARNhgWwPA= go.opentelemetry.io/collector/client v1.54.0 h1:JDpDdc67n2LGVcDzMKN7fSsmmB7333g6d38LshTuXR0= go.opentelemetry.io/collector/client v1.54.0/go.mod h1:4ODFLlgYmMEA+GNy96Qsn6Gi2PwFQFNUScvv5vVTyfE= go.opentelemetry.io/collector/component v1.54.0 h1:LvtX0Tzz18n44OrUFVk77N1FNsejfWJqztB28hrmDM8= go.opentelemetry.io/collector/component v1.54.0/go.mod h1:yUMBYsySY/sDcXm8kOzEoZxt+JLdala6hxzSW0npOxY= go.opentelemetry.io/collector/component/componentstatus v0.148.0 h1:sCGRaXNQolHFhPjrNJEwQ1WZOf96iL99tzm9GxuZsvg= go.opentelemetry.io/collector/component/componentstatus v0.148.0/go.mod h1:yqg3SpGQc22W3wGICdnb+2kZVW9daBr3+LrGUCHkKfc= go.opentelemetry.io/collector/component/componenttest v0.148.0 h1:tBXJWmy2X6KD8S0QU2YZa2zYBqP+IycSM4iOtwDD2pA= go.opentelemetry.io/collector/component/componenttest v0.148.0/go.mod h1:1c1+6mZOmI0raoya5vA/X0F+fawEjNS6tCEs5xLATtA= go.opentelemetry.io/collector/config/configauth v1.54.0 h1:qJ3JdalSJmKWa59kkJoD/nElPlxWvyGf3xZAVnp1TrI= go.opentelemetry.io/collector/config/configauth v1.54.0/go.mod h1:vyp8mZJ793H82GV4eVuuoL+sG6n32SQgG/6jGGfOf+o= go.opentelemetry.io/collector/config/configcompression v1.54.0 h1:YDnrdNSEXqam0OQWRAE+arMsvm/fxQb3oNhgcWhAZ5k= go.opentelemetry.io/collector/config/configcompression v1.54.0/go.mod h1:SEcE2uFLHHPc/Vi8WCkW5MhOMUwaT321HBdZ3P8x8D0= go.opentelemetry.io/collector/config/configgrpc v0.148.0 h1:UxGCpYGw55RVsNkhesRF4im6TJvB9ImaJenpwEgOWco= go.opentelemetry.io/collector/config/configgrpc v0.148.0/go.mod h1:wdDhkiKYPLEfU/8dcHQaJWQiY0nbI/Z75q0YBEQ3f3Q= go.opentelemetry.io/collector/config/confighttp v0.148.0 h1:1OYlN1pK0IlJrZTLiNxQNPD90AnrEjJ72HXd77w5Xqs= go.opentelemetry.io/collector/config/confighttp v0.148.0/go.mod h1:bXmmkVH3L4E2XZwKOQuy/5EbOzhX97e0iuv9iMlFbXQ= go.opentelemetry.io/collector/config/confighttp/xconfighttp v0.148.0 h1:P7W7AMzuf9G7Nf4S61DaPabDVECqK1oihmdxbGTaI2E= go.opentelemetry.io/collector/config/confighttp/xconfighttp v0.148.0/go.mod h1:QxCETpKso9gowySOslD9Fv5lnkgzc9P9RVHxCLpu7CM= go.opentelemetry.io/collector/config/configmiddleware v1.54.0 h1:FPMNDPumiZ7FhfzRggn5PR0AnPZQOVB7VWua11VGAUU= go.opentelemetry.io/collector/config/configmiddleware v1.54.0/go.mod h1:6PYzhcC5402GuSjIs6Q14O2HjH2ZE+A60wWdQuI7ZhY= go.opentelemetry.io/collector/config/confignet v1.54.0 h1:Led1uZQkFDSRIaO9GyZjvpIfuMBAADou7MvhtZkV/Pc= go.opentelemetry.io/collector/config/confignet v1.54.0/go.mod h1:okpHzgIUQW9ga1P9PXzUsggmG1woR1rYsfZGDWKAC6c= go.opentelemetry.io/collector/config/configopaque v1.54.0 h1:DsVlBIk3RDbRz48GxkrKFN5uNet8EaGXU39C6VsUjZQ= go.opentelemetry.io/collector/config/configopaque v1.54.0/go.mod h1:beDuR48blgodzbJkUgMFu9vg0qxjU04tcBtb/rVEP/A= go.opentelemetry.io/collector/config/configoptional v1.54.0 h1:W6MHMrVEbjw/5boxN+VXGZmMBi62IF/lf41vhuNGebU= go.opentelemetry.io/collector/config/configoptional v1.54.0/go.mod h1:c8cFSCUN/A6U00janThFC64ZpyKV1viq/chPOoaqe3I= go.opentelemetry.io/collector/config/configretry v1.54.0 h1:v0G/FxIkkcZzaM/1JrHN5sWBoUWWvb3c+UEgvo5iFs4= go.opentelemetry.io/collector/config/configretry v1.54.0/go.mod h1:1BoQ5SvJT751bqP/5g0VTPLkNgMtvifAr2QqMCVOv2o= go.opentelemetry.io/collector/config/configtelemetry v0.148.0 h1:TZPiz6T6AOvEHmKzU0cPF+CcRbJVR0c3DCqP8Orylx8= go.opentelemetry.io/collector/config/configtelemetry v0.148.0/go.mod h1:vLUthxDJbDk0ZE9MXPvmSslNESDdGblIXWoDMov3UOE= go.opentelemetry.io/collector/config/configtls v1.54.0 h1:Xt5oEs+q7Y6l7mdFYqSrqr0lwJilGcg9EBlCBd/jiBw= go.opentelemetry.io/collector/config/configtls v1.54.0/go.mod h1:ikruZqHoIlR+MaqUgDKotQiuN64uAdlH6zxsTjSKjSM= go.opentelemetry.io/collector/confmap v1.54.0 h1:RUoxQ4uAYHTI57GfHh61D00tTQsXm9T88ozrAiicByc= go.opentelemetry.io/collector/confmap v1.54.0/go.mod h1:mQxG8bk0IWIt9gbWMvzE+cRkOuCuzbzkNGBq2YJ4wNM= go.opentelemetry.io/collector/confmap/provider/envprovider v1.54.0 h1:oFzvQvVf6w/H6niYNkp/I3FJpjD3bG7Phw8pad/mO60= go.opentelemetry.io/collector/confmap/provider/envprovider v1.54.0/go.mod h1:NxRpjioeOYqRaqg83REYYUAIaraZapbhWsvIrDVhQ8k= go.opentelemetry.io/collector/confmap/provider/fileprovider v1.54.0 h1:oypNOydhUDKyg2GBhchpwofKQbgnGrLmXkldrXD8T3Q= go.opentelemetry.io/collector/confmap/provider/fileprovider v1.54.0/go.mod h1:x2HycFHWpfplIjjMERFOO9byCLLMCnuoxZ87TYwvPF4= go.opentelemetry.io/collector/confmap/provider/httpprovider v1.54.0 h1:AScV+fx6izn08GRlxlZ0KZBCqv6/Q89oV0i3xqlg9zs= go.opentelemetry.io/collector/confmap/provider/httpprovider v1.54.0/go.mod h1:laleeicMLVT96TNDXQcZbs0YiZnszi9xvXQFrbiS7iY= go.opentelemetry.io/collector/confmap/provider/httpsprovider v1.54.0 h1:/HUPm/8GW5IUc1J0wSJGh3Sc1mlDMHFcdNFL8u+ujic= go.opentelemetry.io/collector/confmap/provider/httpsprovider v1.54.0/go.mod h1:qLgxp5Csz0D7v2LWM1lOwnfsc/7yN1Jx/egeuhrE3oA= go.opentelemetry.io/collector/confmap/provider/yamlprovider v1.54.0 h1:CjTo0rLNhcmcdk42OxC15yBz4JAUpUrEAXj/F3W3yP8= go.opentelemetry.io/collector/confmap/provider/yamlprovider v1.54.0/go.mod h1:CxAzf/ESn11aY/wCUeGQg1QBEtW+KVwQZi5T50y+RCU= go.opentelemetry.io/collector/confmap/xconfmap v0.148.0 h1:UW8MX5VlKJf67x4Et7J9kPwP9Rv4VSmJ+UUpgRcb//c= go.opentelemetry.io/collector/confmap/xconfmap v0.148.0/go.mod h1:4qTMr3V0uSXXac9wVs/UD5fIqRKw5yIl58+Vjsc6RHM= go.opentelemetry.io/collector/connector v0.148.0 h1:nJOvqm57ab4xRDxF0C+PQdptOF/x6NU9MAaqQJqOq7A= go.opentelemetry.io/collector/connector v0.148.0/go.mod h1:Evipn8SpEed4NSynwcef3s/VihyutpAzv9aFh2KvtJA= go.opentelemetry.io/collector/connector/connectortest v0.148.0 h1:LPrjLF9UbGOtZkG/PfA2Lh94Aouxf0FeqtL4TLvKXvY= go.opentelemetry.io/collector/connector/connectortest v0.148.0/go.mod h1:y9S8I7FLfb8+nyqugOFiExv/ZlGi/BIcINUEdowX4eQ= go.opentelemetry.io/collector/connector/forwardconnector v0.148.0 h1:16am6caOX+Sd0D0k9z8+9EH0inFVlVIrM0O5MW5IFKg= go.opentelemetry.io/collector/connector/forwardconnector v0.148.0/go.mod h1:G4TRuamriPMNElN02qbh5AE9+fwMe8TUkEU/4DZPSNs= go.opentelemetry.io/collector/connector/xconnector v0.148.0 h1:O6GOSkFezdCovPWIlcx0ZkymLGBlmMIoBrRzMLUV8ho= go.opentelemetry.io/collector/connector/xconnector v0.148.0/go.mod h1:FMtp0iuWWmv2wY30QQyMbetNLn0MJfFgVXZDAViUwKs= go.opentelemetry.io/collector/consumer v1.54.0 h1:RGGtUN+GbkV1px3T6XdUHmgJ+ldJ1hAHdesFzW/wgL0= go.opentelemetry.io/collector/consumer v1.54.0/go.mod h1:1PC6XINTL9DdT1bwvfMdHE72EB4RWU/WcPemUrhqKN8= go.opentelemetry.io/collector/consumer/consumererror v0.148.0 h1:lKVkNWBeRXG41lHBf5KzA9oErRZifx6qTd9erAFfEkE= go.opentelemetry.io/collector/consumer/consumererror v0.148.0/go.mod h1:N/UppmtknIdzpEiy3xirH1EiBEBOqKqD77NCyNi2Rbc= go.opentelemetry.io/collector/consumer/consumererror/xconsumererror v0.148.0 h1:61RfzjtvnATQEahTN/Enwz0QFEBK9M9eNcxHh5Etzm0= go.opentelemetry.io/collector/consumer/consumererror/xconsumererror v0.148.0/go.mod h1:vJSXpbjZelXtXdV3AjdGC2WjoVQkNLpzxy+5MUl3Xd8= go.opentelemetry.io/collector/consumer/consumertest v0.148.0 h1:ms0HtWMj17tI1Yds0hSuUI5QYpNEqd11AAhwIoUY2HE= go.opentelemetry.io/collector/consumer/consumertest v0.148.0/go.mod h1:wScw/OzKkf/ZzJn4ToI30OoI1kJiY16WNrcFToXSzK0= go.opentelemetry.io/collector/consumer/xconsumer v0.148.0 h1:m3b9rY7CLD5Pcge6sSKHIT3OlcPN6xqYsdtVs9oJ528= go.opentelemetry.io/collector/consumer/xconsumer v0.148.0/go.mod h1:bG+Wz6xmIBl/gHzq1sqvksWXqTLuTX17Wo//zIsdZpw= go.opentelemetry.io/collector/exporter v1.54.0 h1:SSkEc9VGCf4OJaf+spj4euZ/FcswzOwLm8zR9an5Fxc= go.opentelemetry.io/collector/exporter v1.54.0/go.mod h1:thsNaoV7xRq91sXkKsyFXHj0l2c/ZDM88Mdwe2/QP40= go.opentelemetry.io/collector/exporter/debugexporter v0.148.0 h1:wIKpbnB9YJCYHGwL6gm7Yb45QW31H/ii2RZxoJBmD1E= go.opentelemetry.io/collector/exporter/debugexporter v0.148.0/go.mod h1:szSrW/yxBwNJUleotXfkuESh/uII62YOS7JxDS2iwU0= go.opentelemetry.io/collector/exporter/exporterhelper v0.148.0 h1:mZXGdleKMaEF0jSOcCoOVRWwt3AcgSTAnIZmAqdDYNs= go.opentelemetry.io/collector/exporter/exporterhelper v0.148.0/go.mod h1:+EZCJ6vlgQiozHvUoeEJHnIaV6Ez7HHOLdNWNpo+CUc= go.opentelemetry.io/collector/exporter/exporterhelper/xexporterhelper v0.148.0 h1:LpTc/OsKXy/MBIdpCJ0VC9BCJreH5JUE8DIaNRlw488= go.opentelemetry.io/collector/exporter/exporterhelper/xexporterhelper v0.148.0/go.mod h1:bc03Yf1kCAG4LwkztlOR9NXKBZ6dMfnJLV6SL77zAzQ= go.opentelemetry.io/collector/exporter/exportertest v0.148.0 h1:joLVWwfWDk7idnikGPeOWOa7nJG1pG1+jGvuuOOB1/E= go.opentelemetry.io/collector/exporter/exportertest v0.148.0/go.mod h1:R202E9bjYU4R+2jiDt+aiZSwsIZI3slL6M8y1MeuqkM= go.opentelemetry.io/collector/exporter/nopexporter v0.148.0 h1:d6uRdmAIM+I4SZkRao7aXy3esy4Ki19AYoi1hPXvrnY= go.opentelemetry.io/collector/exporter/nopexporter v0.148.0/go.mod h1:MJC9zsIZcKmJZGWIvqyxyq2MaGDWt5vir278opk8BrY= go.opentelemetry.io/collector/exporter/otlpexporter v0.148.0 h1:oBcYpuuRdK0gZNXO/zCQViSZTn/JH/Z9j/tuqpmd+q4= go.opentelemetry.io/collector/exporter/otlpexporter v0.148.0/go.mod h1:Eavhwd/38JzeDeiDRsO8WFY2+FPmw0pSJt8k1tMz2d0= go.opentelemetry.io/collector/exporter/otlphttpexporter v0.148.0 h1:hFUKoEOzOS0yJpLX9ucLjl0paXqFRArhbplHTVEb7y8= go.opentelemetry.io/collector/exporter/otlphttpexporter v0.148.0/go.mod h1:YY2DVennR8C1uPeldeuPNLj8rUcabF2Pb+SDOpKvFwA= go.opentelemetry.io/collector/exporter/xexporter v0.148.0 h1:QKMwwrUe4snzB9B97NaBtf9qFEeIjx4/oBSwv8EZbJc= go.opentelemetry.io/collector/exporter/xexporter v0.148.0/go.mod h1:rZ60Z9Ny4H+IX5dsn+RiJEJQRNEXAEYZ6XFwE2EWxGU= go.opentelemetry.io/collector/extension v1.54.0 h1:nF+pPfXWcWXjauX0+E1gsWUlUdAe2+26VKIb9hKZJAk= go.opentelemetry.io/collector/extension v1.54.0/go.mod h1:hqjEnkrjjxLXjzyDnLsOJnWMLWkfEjbqm8CHj1ud5pY= go.opentelemetry.io/collector/extension/extensionauth v1.54.0 h1:IglgKxygOcGCCCB31bBxOYwtB8h1oQ2MXVGWKV0k1C0= go.opentelemetry.io/collector/extension/extensionauth v1.54.0/go.mod h1:5SXF5D0r+uhrHU50xCXAnJ1HNmSDDuXamD+fZdcYRLs= go.opentelemetry.io/collector/extension/extensionauth/extensionauthtest v0.148.0 h1:k2Hk5VhnWkn5C79tkZ554KAydyf0awfaW6Ku/bttS6s= go.opentelemetry.io/collector/extension/extensionauth/extensionauthtest v0.148.0/go.mod h1:LhiPIqE7pIDo0+Njo9gPtrAbpnx4tjzqVCP8C0UFBvQ= go.opentelemetry.io/collector/extension/extensioncapabilities v0.148.0 h1:nhIKJyE5YDy0KkI1mrULLBxMwLsq/EyeXQJJZDSRXHI= go.opentelemetry.io/collector/extension/extensioncapabilities v0.148.0/go.mod h1:WQEEnK/GdM4n5EwEUL5PYimT4JFYgGzUrT7yAe8yaxI= go.opentelemetry.io/collector/extension/extensionmiddleware v0.148.0 h1:GdlmwwQ1IxExKL27Ou5YRCs91Z8QYzlENUOBax252bc= go.opentelemetry.io/collector/extension/extensionmiddleware v0.148.0/go.mod h1:ySiHSkCzMcgphWdZiGYIPrFgaEGO2tPY3D0MipGsYpo= go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest v0.148.0 h1:SgNl5DswPxs+gDH5Ojg8xyorogbxTqXoayLGZkvdB/A= go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest v0.148.0/go.mod h1:WMKYe+WIhgCnXAmOtn69yY7tTZZqgSkx+lh1Y5tk3OI= go.opentelemetry.io/collector/extension/extensiontest v0.148.0 h1:ZBrYWe+7oQQVqChXrq4PL1P/Febuo6un2/g7oQP9zfQ= go.opentelemetry.io/collector/extension/extensiontest v0.148.0/go.mod h1:wLxKb/SkoqbStm6zv+9MAhzhySI49oGw2aszPaw9No4= go.opentelemetry.io/collector/extension/xextension v0.148.0 h1:LoSXaI3jd7fhQbPdIDpXy0HC2j4ftsG7LlVrUrghtwA= go.opentelemetry.io/collector/extension/xextension v0.148.0/go.mod h1:dlMQsSTo8Jgd+u8/ssdg0oIItQptkUIfX4zO9xy+hiE= go.opentelemetry.io/collector/extension/zpagesextension v0.148.0 h1:s2M8HLykiRB7Ub5qyTsYoeJ5hR9MdsG9FJR9wricyVM= go.opentelemetry.io/collector/extension/zpagesextension v0.148.0/go.mod h1:+x2vb3TFNtE32qCv6ScG//RpAYdAZYE6ok4Ua++DWkU= go.opentelemetry.io/collector/featuregate v1.54.0 h1:ufo5Hy4Co9pcHVg24hyanm8qFG3TkkYbVyQXPVAbwDc= go.opentelemetry.io/collector/featuregate v1.54.0/go.mod h1:PS7zY/zaCb28EqciePVwRHVhc3oKortTFXsi3I6ee4g= go.opentelemetry.io/collector/internal/componentalias v0.148.0 h1:Y6MftNIZSzOr47TTj6A2z2UR3IwbeG46sAQshicGtDg= go.opentelemetry.io/collector/internal/componentalias v0.148.0/go.mod h1:uwKzfehzwRgHxdHgFXYSBHNBeWSSqsqQYGWr5fk08G0= go.opentelemetry.io/collector/internal/fanoutconsumer v0.148.0 h1:Vy5HOsm6IODqbg7ZHaGizcs0mXXU7yZYFTH9Be0u4mM= go.opentelemetry.io/collector/internal/fanoutconsumer v0.148.0/go.mod h1:0wG5wD4+XPIrrS69j1DnUvCbfAvnhMqcrxPvQkWzdpo= go.opentelemetry.io/collector/internal/memorylimiter v0.148.0 h1:5BPGr7lLc0jRneMIF4YUCeuz47ZrOkWFZ8JT0MWc15s= go.opentelemetry.io/collector/internal/memorylimiter v0.148.0/go.mod h1:jWgo0uP5lLTyLVwoso+958eCodbUAJQ57GHXXL52P2E= go.opentelemetry.io/collector/internal/sharedcomponent v0.148.0 h1:6SB7YuKaBvUzQOiZzT7MxbiMm5KzwNDiml/T4Thzogs= go.opentelemetry.io/collector/internal/sharedcomponent v0.148.0/go.mod h1:ofyvdfavSSSD/AN49eoIxg6HskpOGfYXBQKpLfVxisI= go.opentelemetry.io/collector/internal/telemetry v0.148.0 h1:7U/be+11agYLb67lzoRzsCBoDpaGy8vDFhgI1gGYcco= go.opentelemetry.io/collector/internal/telemetry v0.148.0/go.mod h1:pvflQkIAaj5UwURlkaB8BNTaYw6OjmXTbiWQ75PnYqc= go.opentelemetry.io/collector/internal/testutil v0.148.0 h1:3Z9hperte3vSmbBTYeNndoEUICICrNz8hzx+v0FYXBQ= go.opentelemetry.io/collector/internal/testutil v0.148.0/go.mod h1:Jkjs6rkqs973LqgZ0Fe3zrokQRKULYXPIf4HuqStiEE= go.opentelemetry.io/collector/otelcol v0.148.0 h1:MFhR9u5SMJG3WcT+ON0aV8CV7lIuBWo0o7DQM1TXWtE= go.opentelemetry.io/collector/otelcol v0.148.0/go.mod h1:ocDXLyaKKJOPyb7A5Mr0VuIJWgqUtiL6qpispV/xVv8= go.opentelemetry.io/collector/pdata v1.54.0 h1:3LharKb792cQ3VrUGxd3IcpWwfu3ST+GSTU382jVz1s= go.opentelemetry.io/collector/pdata v1.54.0/go.mod h1:+MqC3VVOv/EX9YVFUo+mI4F0YmwJ+fXBYwjmu+mRiZ8= go.opentelemetry.io/collector/pdata/pprofile v0.148.0 h1:MgrNZmqwhZGfiYwcKKtM/iXgTZqqvG5dUphriRXMZHU= go.opentelemetry.io/collector/pdata/pprofile v0.148.0/go.mod h1:MTTMnZPqWX1S/rBDatU0W19udlycBkWuzVV5qnemHdc= go.opentelemetry.io/collector/pdata/testdata v0.148.0 h1:yzakPuFgoKK8WcrlhyYHLMLA/kLScQKGsXkIgwieAQ8= go.opentelemetry.io/collector/pdata/testdata v0.148.0/go.mod h1:2rFvxm8qwd3nlO90FtJw6ZGAjt+bLndxmQuJaMO9kfQ= go.opentelemetry.io/collector/pdata/xpdata v0.148.0 h1:pTXz872QDl5oHByjlIEkQhIFvv0oeX/5cKNWsUg9KeY= go.opentelemetry.io/collector/pdata/xpdata v0.148.0/go.mod h1:4iL8wugmu589aQNx0dFVT3Ecui/d3TEvVgMlAu8S//0= go.opentelemetry.io/collector/pipeline v1.54.0 h1:jYlCkdFLITVBdeB+IGS07zXWywEgvT3Ky46vdKKT+Ks= go.opentelemetry.io/collector/pipeline v1.54.0/go.mod h1:RD90NG3Jbk965Xaqym3JyHkuol4uZJjQVUkD9ddXJIs= go.opentelemetry.io/collector/pipeline/xpipeline v0.148.0 h1:WTgUC/QXYxhWEwPQ0ezOMbkh4p4DzsRdCxdYLBqNz+U= go.opentelemetry.io/collector/pipeline/xpipeline v0.148.0/go.mod h1:ECXG1qs+H1pUnK0Wu0MUlAbsUlzJOKhV9z4wqep6KWQ= go.opentelemetry.io/collector/processor v1.54.0 h1:zmHBFiEFmU9ZYuHhVP3lHIkbfy+ueapzGpTdXVMcWBg= go.opentelemetry.io/collector/processor v1.54.0/go.mod h1:L0lA6DZ0VbrtQBg44cmYfSpRlgm4zxW1I6QfBnRizPw= go.opentelemetry.io/collector/processor/batchprocessor v0.148.0 h1:RN/NU7giTuTCeWsbFmtk27rBprzJv4xfj4KDYzROEyc= go.opentelemetry.io/collector/processor/batchprocessor v0.148.0/go.mod h1:i79zRqG29xhLEX0rpOLo7dqdEAIiVZPcV4l3eRsLCJM= go.opentelemetry.io/collector/processor/memorylimiterprocessor v0.148.0 h1:sfYYAKBp5kwnCSwIyAxK+3ShENfBkOgTGDG+ioMF2AQ= go.opentelemetry.io/collector/processor/memorylimiterprocessor v0.148.0/go.mod h1:x8q27G9adfJ3rJTjNyoIHyuCpuJZI6pMbSIipJLb0sE= go.opentelemetry.io/collector/processor/processorhelper v0.148.0 h1:qbX7EJ7QJAP47PIdo9Jzxj39VP5Nke48uwP52HYs/O0= go.opentelemetry.io/collector/processor/processorhelper v0.148.0/go.mod h1:ZuNKoLZ3jMZQ+hsA6XFz3GmH6l23eLsYSgHB0qdAf/U= go.opentelemetry.io/collector/processor/processorhelper/xprocessorhelper v0.148.0 h1:8KTlVM8PGGuLwOTMhfeAaH2ok+aEccdbPe5Z2JPf/xw= go.opentelemetry.io/collector/processor/processorhelper/xprocessorhelper v0.148.0/go.mod h1:xojdMQqttMu9kh6xi/oG81gnslB3rSHSMZkKfhmiB8c= go.opentelemetry.io/collector/processor/processortest v0.148.0 h1:p0k59frZxy/Z4fXe82i5eOJv/UyOH75XhI8nFD1ZWCE= go.opentelemetry.io/collector/processor/processortest v0.148.0/go.mod h1:E2Li2gnkUXgvApvGyEtn3Eq5KyzV05ljfbFRsZ7sTC4= go.opentelemetry.io/collector/processor/xprocessor v0.148.0 h1:v7Qv6k2b2cvgGWuTO5KN5QYDLl1r5sznt7Le4Fhpa4c= go.opentelemetry.io/collector/processor/xprocessor v0.148.0/go.mod h1:r7ADpSX2nf0rZR9STxh956Qw1740QOWMXLnEM/ZiaF8= go.opentelemetry.io/collector/receiver v1.54.0 h1:2e9o+eihZ/nJnzVj5JAcJ+VQ653HcZRiT127qBZRqa8= go.opentelemetry.io/collector/receiver v1.54.0/go.mod h1:xFZnvYTBjdi9iS/d/UUXzss4h311mLsZliQFQXk4o/k= go.opentelemetry.io/collector/receiver/nopreceiver v0.148.0 h1:vV0RbFwSwW0hzM/6Y4fNnGXePmbc8D3TiLo/eV5irTA= go.opentelemetry.io/collector/receiver/nopreceiver v0.148.0/go.mod h1:Pb7uEA+VjQwy6nVsC7zd/Bnf40UGyO2za/+I2ikwDs4= go.opentelemetry.io/collector/receiver/otlpreceiver v0.148.0 h1:npsN3tAw4941EJAdSD9DRPSvyc9uCr9r07rOO3WVd6E= go.opentelemetry.io/collector/receiver/otlpreceiver v0.148.0/go.mod h1:5Yumcgp457+ki1/1vnWt+U+tJnauJkO411xKseL/6jw= go.opentelemetry.io/collector/receiver/receiverhelper v0.148.0 h1:B1JOFfdv1dj4WhxSSt3KL1+BOV7Zkf27KisTWdhiFLs= go.opentelemetry.io/collector/receiver/receiverhelper v0.148.0/go.mod h1:jBJbrMZ1dUn/gKr9vEDmU+MPsrz9RhRFWooG72qhUkU= go.opentelemetry.io/collector/receiver/receivertest v0.148.0 h1:Fu+B4jCqgZVZmhsKBz3tcgimFryR6TRAK2D5VGLD2Xc= go.opentelemetry.io/collector/receiver/receivertest v0.148.0/go.mod h1:K8dMDMEggEg6jB688VOHutivOGEEZ20FJGe4jV9RtWU= go.opentelemetry.io/collector/receiver/xreceiver v0.148.0 h1:u66Zi3udD9RMRiNOsZzsVcUjRwqJEK+5LV76Ry9l3K0= go.opentelemetry.io/collector/receiver/xreceiver v0.148.0/go.mod h1:jyHxf8SOfH48ZXb32IS3vPbVYDinsLlZYQddyrveqMg= go.opentelemetry.io/collector/semconv v0.128.1-0.20250610090210-188191247685 h1:XCN7qkZRNzRYfn6chsMZkbFZxoFcW6fZIsZs2aCzcbc= go.opentelemetry.io/collector/semconv v0.128.1-0.20250610090210-188191247685/go.mod h1:OPXer4l43X23cnjLXIZnRj/qQOjSuq4TgBLI76P9hns= go.opentelemetry.io/collector/service v0.148.0 h1:GsAx4nkGTB21QRK9hOTFmLcATN/mugLWsb3iQwt91nY= go.opentelemetry.io/collector/service v0.148.0/go.mod h1:f7oBS9IdX0nLRtyPOIgPj0Q1HCqbxepgWJfEpyVNAfE= go.opentelemetry.io/collector/service/hostcapabilities v0.148.0 h1:BHQV7Fa1y8fQ87V1ieXNpP4+7UGOAj66xWryXSSj27I= go.opentelemetry.io/collector/service/hostcapabilities v0.148.0/go.mod h1:UwHkux+xSVl7k5PEl+qYi8VSONv538rgZeHhfYqBwmE= go.opentelemetry.io/collector/service/telemetry/telemetrytest v0.148.0 h1:0KKY0VHy8y+6LRkW/jE7a2G96tK7rfpl/6hKCt3mHD4= go.opentelemetry.io/collector/service/telemetry/telemetrytest v0.148.0/go.mod h1:uhEy3Ez2aJjGpAIYuy1C0NFO5yr86EJlJGoKFucXQFE= go.opentelemetry.io/contrib/bridges/otelzap v0.17.0 h1:oCltVHJcblcth2z9B9dRTeZIZTe2Sf9Ad9h8bcc+s8M= go.opentelemetry.io/contrib/bridges/otelzap v0.17.0/go.mod h1:G/VE1A/hRn6mEWdfC8rMvSdQVGM64KUPi4XilLkwcQw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.64.0 h1:OXSUzgmIFkcC4An+mv+lqqZSndTffXpjAyoR+1f8k/A= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.64.0/go.mod h1:1A4GVLFIm54HFqVdOpWmukap7rgb0frrE3zWXohLPdM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= go.opentelemetry.io/contrib/otelconf v0.22.0 h1:+kpcfczGOFM85zDZyqQCzWefhovegfn24D0WwmQz0n4= go.opentelemetry.io/contrib/otelconf v0.22.0/go.mod h1:ojdbOukO+JRDJQmJY2PRIZEg0UYVzcOuZR59hp7xffc= go.opentelemetry.io/contrib/propagators/b3 v1.42.0 h1:B2Pew5ufEtgkjLF+tSkXjgYZXQr9m7aCm1wLKB0URbU= go.opentelemetry.io/contrib/propagators/b3 v1.42.0/go.mod h1:iPgUcSEF5DORW6+yNbdw/YevUy+QqJ508ncjhrRSCjc= go.opentelemetry.io/contrib/samplers/jaegerremote v0.36.0 h1:h8kHGv9+VIiJbQ2Qx6BbORZwcvVnd0le/SFK8Vom0bA= go.opentelemetry.io/contrib/samplers/jaegerremote v0.36.0/go.mod h1:tjrgaYHDx+1CmTk5YzNAUCbLX1ZrjrsogXBQHaVf7rI= go.opentelemetry.io/contrib/zpages v0.67.0 h1:cIUwWSVDovuLEbDIKreptjdxMuIhGiqwq0uL8YNaq1c= go.opentelemetry.io/contrib/zpages v0.67.0/go.mod h1:vK8fsYHgPYg4Z/XDbFSEvItSGZDbjWTvjBOu8+AiDhc= go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 h1:deI9UQMoGFgrg5iLPgzueqFPHevDl+28YKfSpPTI6rY= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0/go.mod h1:PFx9NgpNUKXdf7J4Q3agRxMs3Y07QhTCVipKmLsMKnU= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 h1:icqq3Z34UrEFk2u+HMhTtRsvo7Ues+eiJVjaJt62njs= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0/go.mod h1:W2m8P+d5Wn5kipj4/xmbt9uMqezEKfBjzVJadfABSBE= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 h1:H7O6RlGOMTizyl3R08Kn5pdM06bnH8oscSj7o11tmLA= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0/go.mod h1:mBFWu/WOVDkWWsR7Tx7h6EpQB8wsv7P0Yrh0Pb7othc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc= go.opentelemetry.io/otel/exporters/prometheus v0.64.0 h1:g0LRDXMX/G1SEZtK8zl8Chm4K6GBwRkjPKE36LxiTYs= go.opentelemetry.io/otel/exporters/prometheus v0.64.0/go.mod h1:UrgcjnarfdlBDP3GjDIJWe6HTprwSazNjwsI+Ru6hro= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 h1:KJVjPD3rcPb98rIs3HznyJlrfx9ge5oJvxxlGR+P/7s= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0/go.mod h1:K3kRa2ckmHWQaTWQdPRHc7qGXASuVuoEQXzrvlA98Ws= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 h1:lSZHgNHfbmQTPfuTmWVkEu8J8qXaQwuV30pjCcAUvP8= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0/go.mod h1:so9ounLcuoRDu033MW/E0AD4hhUjVqswrMF5FoZlBcw= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs= go.opentelemetry.io/otel/log v0.18.0 h1:XgeQIIBjZZrliksMEbcwMZefoOSMI1hdjiLEiiB0bAg= go.opentelemetry.io/otel/log v0.18.0/go.mod h1:KEV1kad0NofR3ycsiDH4Yjcoj0+8206I6Ox2QYFSNgI= go.opentelemetry.io/otel/log/logtest v0.18.0 h1:2QeyoKJdIgK2LJhG1yn78o/zmpXx1EditeyRDREqVS8= go.opentelemetry.io/otel/log/logtest v0.18.0/go.mod h1:v1vh3PYR9zIa5MK6HwkH2lMrLBg/Y9Of6Qc+krlesX0= go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= go.opentelemetry.io/otel/sdk/log v0.18.0 h1:n8OyZr7t7otkeTnPTbDNom6rW16TBYGtvyy2Gk6buQw= go.opentelemetry.io/otel/sdk/log v0.18.0/go.mod h1:C0+wxkTwKpOCZLrlJ3pewPiiQwpzycPI/u6W0Z9fuYk= go.opentelemetry.io/otel/sdk/log/logtest v0.18.0 h1:l3mYuPsuBx6UKE47BVcPrZoZ0q/KER57vbj2qkgDLXA= go.opentelemetry.io/otel/sdk/log/logtest v0.18.0/go.mod h1:7cHtiVJpZebB3wybTa4NG+FUo5NPe3PROz1FqB0+qdw= go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.opentelemetry.io/proto/slim/otlp v1.10.0 h1:iR97Vs/ZDR+y9TfuP9b1XBtdPWeC+OMslIBmhcLU7jM= go.opentelemetry.io/proto/slim/otlp v1.10.0/go.mod h1:lV9250stpjYLPNA5viFabIgP2QlUGRT1GdTgAf8SIUk= go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0 h1:RUF5rO0hAlgiJt1fzQVzcVs3vZVNHIcMLgOgG4rWNcQ= go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0/go.mod h1:I89cynRj8y+383o7tEQVg2SVA6SRgDVIouWPUVXjx0U= go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.3.0 h1:CQvJSldHRUN6Z8jsUeYv8J0lXRvygALXIzsmAeCcZE0= go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.3.0/go.mod h1:xSQ+mEfJe/GjK1LXEyVOoSI1N9JV9ZI923X5kup43W4= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.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/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= 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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= 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-20200114155413-6afb5195e5aa/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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM= golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc= google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww= google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-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/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= ================================================ FILE: internal/auth/apikey/apikey-context.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package apikey import "context" // apiKeyContextKey is the type used as a key for storing API keys in context. type apiKeyContextKey struct{} // GetAPIKey retrieves the API key from the context. func GetAPIKey(ctx context.Context) (string, bool) { val := ctx.Value(apiKeyContextKey{}) if val == nil { return "", false } if apiKey, ok := val.(string); ok { return apiKey, true } return "", false } // ContextWithAPIKey sets the API key in the context if the key is non-empty. func ContextWithAPIKey(ctx context.Context, apiKey string) context.Context { if apiKey == "" { return ctx } return context.WithValue(ctx, apiKeyContextKey{}, apiKey) } ================================================ FILE: internal/auth/apikey/apikey-context_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package apikey import ( "context" "testing" "github.com/stretchr/testify/assert" ) func TestGetAPIKey(t *testing.T) { // No value in context emptyCtx := context.Background() apiKey, ok := GetAPIKey(emptyCtx) assert.Empty(t, apiKey) assert.False(t, ok) // Correct string value in context (via ContextWithAPIKey) expectedApiKey := "test-api-key" ctxWithApiKey := ContextWithAPIKey(context.Background(), expectedApiKey) apiKey, ok = GetAPIKey(ctxWithApiKey) assert.Equal(t, expectedApiKey, apiKey) assert.True(t, ok) // Non-string value in context (simulate misuse) ctxWithNonString := context.WithValue(context.Background(), apiKeyContextKey{}, 123) apiKey, ok = GetAPIKey(ctxWithNonString) assert.Empty(t, apiKey) assert.False(t, ok) // No API key when empty string passed to ContextWithAPIKey emptyStringCtx := ContextWithAPIKey(context.Background(), "") apiKey, ok = GetAPIKey(emptyStringCtx) assert.Empty(t, apiKey) assert.False(t, ok) } func TestContextWithAPIKey(t *testing.T) { baseCtx := context.Background() // Non-empty apiKey: should set value in context apiKey := "my-secret-key" ctxWithKey := ContextWithAPIKey(baseCtx, apiKey) val, ok := GetAPIKey(ctxWithKey) assert.True(t, ok, "apiKey should be present in context") assert.Equal(t, apiKey, val) // Empty apiKey: should return original context, no value set emptyCtx := ContextWithAPIKey(baseCtx, "") val, ok = GetAPIKey(emptyCtx) assert.False(t, ok, "apiKey should not be present for empty string") assert.Empty(t, val) // Should not mutate original context val, ok = GetAPIKey(baseCtx) assert.False(t, ok, "original context should remain unchanged") assert.Empty(t, val) } ================================================ FILE: internal/auth/apikey/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package apikey import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/auth/bearertoken/context.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package bearertoken import "context" type contextKeyType int const contextKey = contextKeyType(iota) // StoragePropagationKey is a key for viper configuration to pass this option to storage plugins. const StoragePropagationKey = "storage.propagate.token" // ContextWithBearerToken set bearer token in context. func ContextWithBearerToken(ctx context.Context, token string) context.Context { if token == "" { return ctx } return context.WithValue(ctx, contextKey, token) } // GetBearerToken from context, or empty string if there is no token. func GetBearerToken(ctx context.Context) (string, bool) { val, ok := ctx.Value(contextKey).(string) return val, ok } ================================================ FILE: internal/auth/bearertoken/context_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package bearertoken import ( "context" "testing" "github.com/stretchr/testify/assert" ) func Test_GetBearerToken(t *testing.T) { const token = "blah" ctx := context.Background() ctx = ContextWithBearerToken(ctx, token) contextToken, ok := GetBearerToken(ctx) assert.True(t, ok) assert.Equal(t, token, contextToken) } ================================================ FILE: internal/auth/bearertoken/grpc.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package bearertoken import ( "context" "errors" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) const Key = "bearer.token" type tokenatedServerStream struct { grpc.ServerStream context context.Context } func (tss *tokenatedServerStream) Context() context.Context { return tss.context } // extract bearer token from the metadata func ValidTokenFromGRPCMetadata(ctx context.Context, bearerHeader string) (string, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { return "", nil } tokens := md.Get(bearerHeader) if len(tokens) < 1 { return "", nil } if len(tokens) > 1 { return "", errors.New("malformed token: multiple tokens found") } return tokens[0], nil } // NewStreamServerInterceptor creates a new stream interceptor that injects the bearer token into the context if available. func NewStreamServerInterceptor() grpc.StreamServerInterceptor { return func(srv any, ss grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error { if token, _ := GetBearerToken(ss.Context()); token != "" { return handler(srv, ss) } bearerToken, err := ValidTokenFromGRPCMetadata(ss.Context(), Key) if err != nil { return err } return handler(srv, &tokenatedServerStream{ ServerStream: ss, context: ContextWithBearerToken(ss.Context(), bearerToken), }) } } // NewUnaryServerInterceptor creates a new unary interceptor that injects the bearer token into the context if available. func NewUnaryServerInterceptor() grpc.UnaryServerInterceptor { return func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { if token, _ := GetBearerToken(ctx); token != "" { return handler(ctx, req) } bearerToken, err := ValidTokenFromGRPCMetadata(ctx, Key) if err != nil { return nil, err } return handler(ContextWithBearerToken(ctx, bearerToken), req) } } // NewUnaryClientInterceptor injects the bearer token header into gRPC request metadata. func NewUnaryClientInterceptor() grpc.UnaryClientInterceptor { return grpc.UnaryClientInterceptor(func( ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption, ) error { var token string token, err := ValidTokenFromGRPCMetadata(ctx, Key) if err != nil { return err } if token == "" { bearerToken, ok := GetBearerToken(ctx) if ok && bearerToken != "" { token = bearerToken } } if token != "" { ctx = metadata.AppendToOutgoingContext(ctx, Key, token) } return invoker(ctx, method, req, reply, cc, opts...) }) } // NewStreamClientInterceptor injects the bearer token header into gRPC request metadata. func NewStreamClientInterceptor() grpc.StreamClientInterceptor { return grpc.StreamClientInterceptor(func( ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption, ) (grpc.ClientStream, error) { var token string token, err := ValidTokenFromGRPCMetadata(ctx, Key) if err != nil { return nil, err } if token == "" { bearerToken, ok := GetBearerToken(ctx) if ok && bearerToken != "" { token = bearerToken } } if token != "" { ctx = metadata.AppendToOutgoingContext(ctx, Key, token) } return streamer(ctx, desc, cc, method, opts...) }) } ================================================ FILE: internal/auth/bearertoken/grpc_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package bearertoken import ( "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) type mockServerStream struct { ctx context.Context grpc.ServerStream } func (s *mockServerStream) Context() context.Context { return s.ctx } func TestClientInterceptors(t *testing.T) { tests := []struct { name string ctx context.Context expectedErr string expectedMD metadata.MD }{ { name: "no token in context", ctx: context.Background(), expectedErr: "", expectedMD: nil, // Expecting no metadata }, { name: "token in context", ctx: ContextWithBearerToken(context.Background(), "test-token"), expectedErr: "", expectedMD: metadata.MD{Key: []string{"test-token"}}, }, { name: "multiple tokens in metadata", ctx: metadata.NewIncomingContext(context.Background(), metadata.MD{Key: []string{"token1", "token2"}}), expectedErr: "malformed token: multiple tokens found", }, { name: "valid token in metadata", ctx: metadata.NewIncomingContext(context.Background(), metadata.MD{Key: []string{"valid-token"}}), expectedErr: "", expectedMD: metadata.MD{Key: []string{"valid-token"}}, // Valid token setup }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { // Unary interceptor test verifyMetadata := func(ctx context.Context) error { md, ok := metadata.FromOutgoingContext(ctx) if test.expectedMD == nil { require.False(t, ok, "metadata should not be present") } else { require.True(t, ok, "metadata should be present") assert.Equal(t, test.expectedMD, md) } return nil } unaryInterceptor := NewUnaryClientInterceptor() unaryInvoker := func(ctx context.Context, _ string, _, _ any, _ *grpc.ClientConn, _ ...grpc.CallOption) error { return verifyMetadata(ctx) } err := unaryInterceptor(test.ctx, "method", nil, nil, nil, unaryInvoker) if test.expectedErr == "" { require.NoError(t, err) } else { require.ErrorContains(t, err, test.expectedErr) } // Stream interceptor test streamInterceptor := NewStreamClientInterceptor() streamInvoker := func(ctx context.Context, _ *grpc.StreamDesc, _ *grpc.ClientConn, _ string, _ ...grpc.CallOption) (grpc.ClientStream, error) { if err := verifyMetadata(ctx); err != nil { return nil, err } return nil, nil } _, err = streamInterceptor(test.ctx, &grpc.StreamDesc{}, nil, "method", streamInvoker) if test.expectedErr == "" { require.NoError(t, err) } else { assert.ErrorContains(t, err, test.expectedErr) } }) } } func TestServerInterceptors(t *testing.T) { tests := []struct { name string ctx context.Context expectedErr string wantToken string }{ { name: "no token in context", ctx: context.Background(), expectedErr: "", wantToken: "", }, { name: "token in context", ctx: ContextWithBearerToken(context.Background(), "test-token"), expectedErr: "", wantToken: "test-token", }, { name: "multiple tokens in metadata", ctx: metadata.NewIncomingContext(context.Background(), metadata.MD{ Key: []string{"token1", "token2"}, }), expectedErr: "malformed token: multiple tokens found", wantToken: "", }, { name: "valid token in metadata", ctx: metadata.NewIncomingContext(context.Background(), metadata.MD{ Key: []string{"valid-token"}, }), expectedErr: "", wantToken: "valid-token", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { verifyToken := func(ctx context.Context) error { token, ok := GetBearerToken(ctx) if test.wantToken == "" { assert.False(t, ok, "expected no token") } else { assert.True(t, ok, "expected token to be present") assert.Equal(t, test.wantToken, token) } return nil } // Test unary server interceptor unaryInterceptor := NewUnaryServerInterceptor() unaryHandler := func(ctx context.Context, _ any) (any, error) { return nil, verifyToken(ctx) } _, err := unaryInterceptor(test.ctx, nil, &grpc.UnaryServerInfo{}, unaryHandler) if test.expectedErr == "" { require.NoError(t, err) } else { require.ErrorContains(t, err, test.expectedErr) } // Test stream server interceptor streamInterceptor := NewStreamServerInterceptor() mockStream := &mockServerStream{ctx: test.ctx} streamHandler := func(_ any, stream grpc.ServerStream) error { return verifyToken(stream.Context()) } err = streamInterceptor(nil, mockStream, &grpc.StreamServerInfo{}, streamHandler) if test.expectedErr == "" { require.NoError(t, err) } else { assert.ErrorContains(t, err, test.expectedErr) } }) } } func TestTokenatedServerStream(t *testing.T) { originalCtx := context.Background() testToken := "test-token" newCtx := ContextWithBearerToken(originalCtx, testToken) stream := &tokenatedServerStream{ ServerStream: &mockServerStream{ctx: originalCtx}, context: newCtx, } // Verify that Context() returns the modified context token, ok := GetBearerToken(stream.Context()) require.True(t, ok) assert.Equal(t, testToken, token) } ================================================ FILE: internal/auth/bearertoken/http.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package bearertoken import ( "net/http" "strings" "go.uber.org/zap" ) // PropagationHandler returns a http.Handler containing the logic to extract // the Bearer token from the Authorization header of the http.Request and insert it into request.Context // for propagation. The token can be accessed via GetBearerToken. func PropagationHandler(logger *zap.Logger, h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() authHeaderValue := r.Header.Get("Authorization") // If no Authorization header is present, try with X-Forwarded-Access-Token if authHeaderValue == "" { authHeaderValue = r.Header.Get("X-Forwarded-Access-Token") } if authHeaderValue != "" { headerValue := strings.Split(authHeaderValue, " ") token := "" switch { case len(headerValue) == 2: // Make sure we only capture bearer token , not other types like Basic auth. if headerValue[0] == "Bearer" { token = headerValue[1] } case len(headerValue) == 1: // Treat the entire value as a token. token = authHeaderValue default: logger.Warn("Invalid authorization header value, skipping token propagation") } h.ServeHTTP(w, r.WithContext(ContextWithBearerToken(ctx, token))) } else { h.ServeHTTP(w, r.WithContext(ctx)) } }) } ================================================ FILE: internal/auth/bearertoken/http_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package bearertoken import ( "net/http" "net/http/httptest" "sync" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" ) func Test_PropagationHandler(t *testing.T) { httpClient := &http.Client{ Timeout: 2 * time.Second, } logger := zap.NewNop() const bearerToken = "blah" validTokenHandler := func(stop *sync.WaitGroup) http.HandlerFunc { return func(_ http.ResponseWriter, r *http.Request) { ctx := r.Context() token, ok := GetBearerToken(ctx) assert.Equal(t, bearerToken, token) assert.True(t, ok) stop.Done() } } emptyHandler := func(stop *sync.WaitGroup) http.HandlerFunc { return func(_ http.ResponseWriter, r *http.Request) { ctx := r.Context() token, _ := GetBearerToken(ctx) assert.Empty(t, token, bearerToken) stop.Done() } } testCases := []struct { name string sendHeader bool headerValue string headerName string handler func(stop *sync.WaitGroup) http.HandlerFunc }{ {name: "Bearer token", sendHeader: true, headerName: "Authorization", headerValue: "Bearer " + bearerToken, handler: validTokenHandler}, {name: "Raw bearer token", sendHeader: true, headerName: "Authorization", headerValue: bearerToken, handler: validTokenHandler}, {name: "No headerValue", sendHeader: false, headerName: "Authorization", handler: emptyHandler}, {name: "Basic Auth", sendHeader: true, headerName: "Authorization", headerValue: "Basic " + bearerToken, handler: emptyHandler}, {name: "X-Forwarded-Access-Token", headerName: "X-Forwarded-Access-Token", sendHeader: true, headerValue: "Bearer " + bearerToken, handler: validTokenHandler}, {name: "Invalid header", headerName: "X-Forwarded-Access-Token", sendHeader: true, headerValue: "Bearer " + bearerToken + " another stuff", handler: emptyHandler}, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { stop := sync.WaitGroup{} stop.Add(1) r := PropagationHandler(logger, testCase.handler(&stop)) server := httptest.NewServer(r) defer server.Close() req, err := http.NewRequest(http.MethodGet, server.URL, http.NoBody) require.NoError(t, err) if testCase.sendHeader { req.Header.Add(testCase.headerName, testCase.headerValue) } _, err = httpClient.Do(req) require.NoError(t, err) stop.Wait() }) } } ================================================ FILE: internal/auth/bearertoken/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package bearertoken import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/auth/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package auth import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/auth/tokenloader.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package auth import ( "fmt" "os" "path/filepath" "strings" "sync" "time" "go.uber.org/zap" ) // CachedFileTokenLoader returns a function that loads a token from the given file path, // caches it for the specified interval, and reloads after the interval expires. // disable Reloading by setting interval to 0 func cachedFileTokenLoader(path string, interval time.Duration, timeFn func() time.Time) func() (string, error) { var ( mu sync.Mutex cachedToken string lastRead time.Time ) return func() (string, error) { mu.Lock() defer mu.Unlock() now := timeFn() // Special case: interval = 0 means "never reload after first load" // Otherwise reload only if `interval` time has passed since last load. if !lastRead.IsZero() && (interval == 0 || now.Sub(lastRead) < interval) { return cachedToken, nil } // Read from file b, err := os.ReadFile(filepath.Clean(path)) if err != nil { return "", fmt.Errorf("failed to read token file: %w", err) } cachedToken = strings.TrimRight(string(b), "\r\n") lastRead = now return cachedToken, nil } } // TokenProvider creates a token provider that handles file loading and error handling consistently. func TokenProvider(path string, interval time.Duration, logger *zap.Logger) (func() string, error) { return TokenProviderWithTime(path, interval, logger, time.Now) // Use real time.Now in production } // TokenProviderWithTime creates a token provider with injectable time (for testing) func TokenProviderWithTime(path string, interval time.Duration, logger *zap.Logger, timeFn func() time.Time) (func() string, error) { loader := cachedFileTokenLoader(path, interval, timeFn) // current token load currentToken, err := loader() if err != nil { return nil, fmt.Errorf("failed to get token from file: %w", err) } return func() string { newToken, err := loader() if err != nil { logger.Warn("Token reload failed", zap.Error(err)) return currentToken } // save it in case the load fails later (e.g. if file is removed) currentToken = newToken return currentToken }, nil } ================================================ FILE: internal/auth/tokenloader_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package auth import ( "fmt" "os" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest/observer" ) // TestCachedFileTokenLoader_Deterministic covers basic cache and reload logic with mock time func TestCachedFileTokenLoader_Deterministic(t *testing.T) { currentTime := time.Unix(0, 0) timeFn := func() time.Time { return currentTime } tokenFile := createTempTokenFile(t, "my-secret-token\n") loader := cachedFileTokenLoader(tokenFile, 100*time.Millisecond, timeFn) // T=0: First load - should read from file token1, err := loader() require.NoError(t, err) assert.Equal(t, "my-secret-token", token1) // Change the file content updateTokenFile(t, tokenFile, "new-token\n") // T=50ms: Still within cache interval (< 100ms) currentTime = currentTime.Add(50 * time.Millisecond) token2, err := loader() require.NoError(t, err) assert.Equal(t, "my-secret-token", token2, "Should return cached token within interval") // T=150ms: Beyond cache interval (> 100ms) currentTime = currentTime.Add(100 * time.Millisecond) // Total: 150ms token3, err := loader() require.NoError(t, err) assert.Equal(t, "new-token", token3, "Should return refreshed token after cache expires") } // TestCachedFileTokenLoader_ExactBoundaries tests exact cache boundary conditions func TestCachedFileTokenLoader_ExactBoundaries(t *testing.T) { currentTime := time.Unix(0, 0) timeFn := func() time.Time { return currentTime } tokenFile := createTempTokenFile(t, "boundary-token\n") cacheInterval := 200 * time.Millisecond loader := cachedFileTokenLoader(tokenFile, cacheInterval, timeFn) // Load initial token token, err := loader() require.NoError(t, err) assert.Equal(t, "boundary-token", token) // Update file updateTokenFile(t, tokenFile, "boundary-updated\n") // Test exact boundary conditions testCases := []struct { timeAdvance time.Duration expectedToken string description string }{ {199 * time.Millisecond, "boundary-token", "1ms before cache expires"}, {1 * time.Millisecond, "boundary-updated", "exactly at cache expiry"}, {50 * time.Millisecond, "boundary-updated", "well past cache expiry"}, } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { currentTime = currentTime.Add(tc.timeAdvance) token, err := loader() require.NoError(t, err) assert.Equal(t, tc.expectedToken, token, tc.description) }) } } // TestCachedFileTokenLoader_ZeroInterval tests disabled reloading func TestCachedFileTokenLoader_ZeroInterval(t *testing.T) { currentTime := time.Unix(0, 0) timeFn := func() time.Time { return currentTime } tokenFile := createTempTokenFile(t, "initial-token\n") loader := cachedFileTokenLoader(tokenFile, 0, timeFn) // Zero interval = no reloading // T=0: Initial load token, err := loader() require.NoError(t, err) assert.Equal(t, "initial-token", token) // Update file content updateTokenFile(t, tokenFile, "updated-token\n") // Should still return cached token (no reloading) currentTime = currentTime.Add(1 * time.Hour) // Advance time significantly token, err = loader() require.NoError(t, err) assert.Equal(t, "initial-token", token, "Zero interval should disable reloading completely") // Multiple calls should continue returning cached token for i := range 5 { // Generate different content for each iteration newContent := fmt.Sprintf("different-token-%d-%d\n", i, currentTime.Unix()) updateTokenFile(t, tokenFile, newContent) currentTime = currentTime.Add(1 * time.Hour) token, err = loader() require.NoError(t, err) assert.Equal(t, "initial-token", token, "Should always return initially cached token despite file change to %s at time %v", strings.TrimSpace(newContent), currentTime) } } // TestNewTokenProvider_InitialLoad covers initial load and fail-fast scenarios func TestNewTokenProvider_InitialLoad(t *testing.T) { // Test successful initial load tokenFile := createTempTokenFile(t, "initial-token\n") tokenFn, err := TokenProvider(tokenFile, 100*time.Millisecond, nil) require.NoError(t, err, "TokenProvider should not fail with valid token file") assert.Equal(t, "initial-token", tokenFn(), "Token should match file contents") // Test fail-fast on invalid file _, err = TokenProvider("/nonexistent/file", 100*time.Millisecond, nil) require.Error(t, err, "TokenProvider should fail fast on missing file") assert.Contains(t, err.Error(), "failed to get token from file", "Error message should indicate token loading failure") // Test empty file emptyFile := createTempTokenFile(t, "") tokenFn, err = TokenProvider(emptyFile, 100*time.Millisecond, nil) require.NoError(t, err) assert.Empty(t, tokenFn(), "Empty file should return empty token") // Test file with trailing whitespace - properly trimmed whitespaceFile := createTempTokenFile(t, "my-secret-token\r\n") tokenFn, err = TokenProvider(whitespaceFile, 100*time.Millisecond, nil) require.NoError(t, err) assert.Equal(t, "my-secret-token", tokenFn(), "\\r\\n should be properly trimmed from end") } // TestNewTokenProvider_ReloadErrors_Deterministic ensures reload errors log and return cached token func TestNewTokenProvider_ReloadErrors_Deterministic(t *testing.T) { currentTime := time.Unix(0, 0) timeFn := func() time.Time { return currentTime } tokenFile := createTempTokenFile(t, "initial-token\n") // Create an observed zap logger core, logs := observer.New(zapcore.InfoLevel) logger := zap.New(core) // Initialize token provider with mock time tokenFn, err := TokenProviderWithTime(tokenFile, 10*time.Millisecond, logger, timeFn) require.NoError(t, err) // Initial call should succeed token := tokenFn() assert.Equal(t, "initial-token", token) // Remove the file to force reload error os.Remove(tokenFile) // Advance time beyond cache interval currentTime = currentTime.Add(15 * time.Millisecond) // Call should return last cached token and log error token = tokenFn() assert.Equal(t, "initial-token", token, "Should return cached token even after file deletion") // Verify the error was logged require.Equal(t, 1, logs.Len(), "Expected one log message") logEntry := logs.All()[0] assert.Equal(t, "Token reload failed", logEntry.Message, "Expected log message to match") assert.Equal(t, zapcore.WarnLevel, logEntry.Level, "Expected warning level log") } // TestTokenProviderWithTime_DirectCall tests the time-injectable function directly func TestTokenProviderWithTime_DirectCall(t *testing.T) { currentTime := time.Unix(0, 0) timeFn := func() time.Time { return currentTime } tokenFile := createTempTokenFile(t, "time-test-token\n") logger := zap.NewNop() // Create token provider with mock time tokenFn, err := TokenProviderWithTime(tokenFile, 200*time.Millisecond, logger, timeFn) require.NoError(t, err) require.NotNil(t, tokenFn) // Test initial token token := tokenFn() assert.Equal(t, "time-test-token", token) // Update file updateTokenFile(t, tokenFile, "time-updated\n") // Should still return cached token currentTime = currentTime.Add(100 * time.Millisecond) token = tokenFn() assert.Equal(t, "time-test-token", token) // Should return updated token after cache expires currentTime = currentTime.Add(150 * time.Millisecond) token = tokenFn() assert.Equal(t, "time-updated", token) } // TestNewTokenProvider_WithZapLogger ensures zap logger is used properly func TestNewTokenProvider_WithZapLogger(t *testing.T) { currentTime := time.Unix(0, 0) timeFn := func() time.Time { return currentTime } tokenFile := createTempTokenFile(t, "initial-token\n") // Create an observed zap logger core, logs := observer.New(zapcore.InfoLevel) logger := zap.New(core) // Initialize token provider with structured logger tokenFn, err := TokenProviderWithTime(tokenFile, 10*time.Millisecond, logger, timeFn) require.NoError(t, err) // Initial call should succeed token := tokenFn() assert.Equal(t, "initial-token", token) // No logs yet assert.Equal(t, 0, logs.Len(), "No logs should be emitted for successful loads") // Remove the file to force reload error os.Remove(tokenFile) // Advance time to trigger reload currentTime = currentTime.Add(15 * time.Millisecond) // Call should log using zap logger tokenFn() assert.Equal(t, 1, logs.Len(), "Error should be logged using zap") logEntry := logs.All()[0] assert.Equal(t, "Token reload failed", logEntry.Message, "Log message should match") assert.NotNil(t, logEntry.Context[0].Interface, "Error should be attached to log") } // TestCachedFileTokenLoader_FilePermissions tests file permission errors func TestCachedFileTokenLoader_FilePermissions(t *testing.T) { if os.Getuid() == 0 { t.Skip("Running as root - file permission tests not meaningful") } currentTime := time.Unix(0, 0) timeFn := func() time.Time { return currentTime } // Create a file and make it unreadable tokenFile := createTempTokenFile(t, "permission-test\n") err := os.Chmod(tokenFile, 0o000) // No permissions require.NoError(t, err) // Cleanup with restored permissions t.Cleanup(func() { os.Chmod(tokenFile, 0o644) os.Remove(tokenFile) }) loader := cachedFileTokenLoader(tokenFile, 100*time.Millisecond, timeFn) // Should return permission error _, err = loader() require.Error(t, err) assert.Contains(t, err.Error(), "permission denied") } func TestTokenProvider_Wrapper(t *testing.T) { tokenFile := createTempTokenFile(t, "production-test-token\n") logger := zap.NewNop() // Test that TokenProvider uses real time tokenFn, err := TokenProvider(tokenFile, 100*time.Millisecond, logger) require.NoError(t, err) token := tokenFn() assert.Equal(t, "production-test-token", token) } // Helper functions // createTempTokenFile creates a temp file with the given content and returns its name func createTempTokenFile(t *testing.T, content string) string { t.Helper() tmpFile, err := os.CreateTemp(t.TempDir(), "token-*.txt") require.NoError(t, err, "Failed to create temp file") _, err = tmpFile.WriteString(content) require.NoError(t, err, "Failed to write to temp file") err = tmpFile.Close() require.NoError(t, err, "Failed to close temp file") // Cleanup happens automatically with t.TempDir() return tmpFile.Name() } // updateTokenFile updates an existing token file with new content func updateTokenFile(t *testing.T, filename, content string) { t.Helper() err := os.WriteFile(filename, []byte(content), 0o600) require.NoError(t, err, "Failed to update token file") } ================================================ FILE: internal/auth/transport.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package auth import ( "context" "errors" "fmt" "net/http" ) // Method represents a single authentication method configuration type Method struct { // Scheme is the authentication scheme (e.g., "Bearer") Scheme string // TokenFn returns the authentication token TokenFn func() string // FromCtx extracts token from context FromCtx func(context.Context) (string, bool) } // RoundTripper wraps another http.RoundTripper and injects // an authentication header with token into requests. type RoundTripper struct { // Transport is the underlying http.RoundTripper being wrapped. Required. Transport http.RoundTripper Auths []Method } // RoundTrip injects the outbound Authorization header with the // token provided in the inbound request. func (tr RoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { if tr.Transport == nil { return nil, errors.New("no http.RoundTripper provided") } req := r.Clone(r.Context()) for _, auth := range tr.Auths { token := "" // Get token from context if available if auth.FromCtx != nil { if t, ok := auth.FromCtx(r.Context()); ok { token = t } } // Fall back to TokenFn if no token from context if token == "" && auth.TokenFn != nil { token = auth.TokenFn() } // Add Authorization header if we have a token if token != "" { req.Header.Add("Authorization", fmt.Sprintf("%s %s", auth.Scheme, token)) } } return tr.Transport.RoundTrip(req) } ================================================ FILE: internal/auth/transport_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package auth import ( "context" "encoding/base64" "net/http" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/auth/bearertoken" ) type roundTripFunc func(r *http.Request) (*http.Response, error) func (s roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return s(r) } func TestRoundTripper(t *testing.T) { tests := []struct { name string auths []Method requestContext context.Context expectedHeaders []string // Expected Authorization headers expectError bool expectNoAuthHeader bool }{ { name: "No auth configs - no headers added", auths: []Method{}, requestContext: context.Background(), expectNoAuthHeader: true, }, { name: "Single Bearer auth with static token", auths: []Method{ { Scheme: "Bearer", TokenFn: func() string { return "static-token" }, }, }, requestContext: context.Background(), expectedHeaders: []string{"Bearer static-token"}, }, { name: "Bearer auth with context override - context token used", auths: []Method{ { Scheme: "Bearer", TokenFn: func() string { return "static-token" }, FromCtx: bearertoken.GetBearerToken, }, }, requestContext: bearertoken.ContextWithBearerToken(context.Background(), "context-token"), expectedHeaders: []string{"Bearer context-token"}, }, { name: "Multiple auth methods - both tokens added", auths: []Method{ { Scheme: "Bearer", TokenFn: func() string { return "bearer-token" }, }, { Scheme: "ApiKey", TokenFn: func() string { return "api-key-value" }, }, }, requestContext: context.Background(), expectedHeaders: []string{"Bearer bearer-token", "ApiKey api-key-value"}, }, { name: "Auth config with empty token - no header added", auths: []Method{ { Scheme: "Bearer", TokenFn: func() string { return "" }, // Empty token }, { Scheme: "ApiKey", TokenFn: func() string { return "valid-key" }, }, }, requestContext: context.Background(), expectedHeaders: []string{"ApiKey valid-key"}, // Only valid token }, { name: "Only context extraction, no context token - no header added", auths: []Method{ { Scheme: "Bearer", FromCtx: bearertoken.GetBearerToken, }, }, requestContext: context.Background(), expectNoAuthHeader: true, }, { name: "Nil transport - should return error", auths: []Method{}, requestContext: context.Background(), expectError: true, }, { name: "Basic auth with pre-encoded credentials", auths: []Method{ { Scheme: "Basic", TokenFn: func() string { // Pre-encoded "user:pass" return base64.StdEncoding.EncodeToString([]byte("user:pass")) }, }, }, requestContext: context.Background(), expectedHeaders: []string{"Basic dXNlcjpwYXNz"}, // base64("user:pass") }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() // Enable parallel execution for faster tests // Each test gets its own isolated capturedRequest and transport var capturedRequest *http.Request wrappedTransport := roundTripFunc(func(r *http.Request) (*http.Response, error) { capturedRequest = r return &http.Response{StatusCode: http.StatusOK}, nil }) req, err := http.NewRequestWithContext(tc.requestContext, http.MethodGet, "http://fake.example.com/api", http.NoBody) require.NoError(t, err) var transport http.RoundTripper = wrappedTransport if tc.expectError { transport = nil // Force nil transport for error test } tr := RoundTripper{ Transport: transport, Auths: tc.auths, } resp, err := tr.RoundTrip(req) if tc.expectError { assert.Nil(t, resp) require.Error(t, err) assert.Contains(t, err.Error(), "no http.RoundTripper provided") return } require.NoError(t, err) assert.NotNil(t, resp) require.NotNil(t, capturedRequest, "Request should have been captured") // Perform assertions on captured request if tc.expectNoAuthHeader { assert.Empty(t, capturedRequest.Header.Get("Authorization")) } else if len(tc.expectedHeaders) > 0 { authHeaders := capturedRequest.Header["Authorization"] assert.ElementsMatch(t, tc.expectedHeaders, authHeaders) } }) } } ================================================ FILE: internal/cache/cache.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package cache import ( "time" ) // A Cache is a generalized interface to a cache. See cache.LRU for a specific // implementation (bounded cache with LRU eviction) type Cache interface { // Get retrieves an element based on a key, returning nil if the element // does not exist Get(key string) any // Put adds an element to the cache, returning the previous element Put(key string, value any) any // Delete deletes an element in the cache Delete(key string) // Size returns the number of entries currently stored in the Cache Size() int // CompareAndSwap adds an element to the cache if the existing entry matches the old value. // It returns the element in cache after function is executed and true if the element was replaced, false otherwise. CompareAndSwap(key string, oldEntry, newEntry any) (any, bool) } // Options control the behavior of the cache type Options struct { // TTL controls the time-to-live for a given cache entry. Cache entries that // are older than the TTL will not be returned TTL time.Duration // InitialCapacity controls the initial capacity of the cache InitialCapacity int // OnEvict is an optional function called when an element is evicted. OnEvict EvictCallback // TimeNow is used to override the behavior of default time.Now(), e.g. in tests. TimeNow func() time.Time } // EvictCallback is a type for notifying applications when an item is // scheduled for eviction from the Cache. type EvictCallback func(key string, value any) ================================================ FILE: internal/cache/lru.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package cache import ( "container/list" "sync" "time" ) // LRU is a concurrent fixed size cache that evicts elements in LRU order as well as by TTL. type LRU struct { mux sync.Mutex byAccess *list.List byKey map[string]*list.Element maxSize int ttl time.Duration TimeNow func() time.Time onEvict EvictCallback } // NewLRUWithOptions creates a new LRU cache with the given options. func NewLRUWithOptions(maxSize int, opts *Options) *LRU { if opts == nil { opts = &Options{} } if opts.TimeNow == nil { opts.TimeNow = time.Now } return &LRU{ byAccess: list.New(), byKey: make(map[string]*list.Element, opts.InitialCapacity), ttl: opts.TTL, maxSize: maxSize, TimeNow: opts.TimeNow, onEvict: opts.OnEvict, } } // Get retrieves the value stored under the given key func (c *LRU) Get(key string) any { c.mux.Lock() defer c.mux.Unlock() elt := c.byKey[key] if elt == nil { return nil } cacheEntry := elt.Value.(*cacheEntry) if !cacheEntry.expiration.IsZero() && c.TimeNow().After(cacheEntry.expiration) { // Entry has expired if c.onEvict != nil { c.onEvict(cacheEntry.key, cacheEntry.value) } c.byAccess.Remove(elt) delete(c.byKey, cacheEntry.key) return nil } c.byAccess.MoveToFront(elt) return cacheEntry.value } // Put puts a new value associated with a given key, returning the existing value (if present) func (c *LRU) Put(key string, value any) any { c.mux.Lock() defer c.mux.Unlock() elt := c.byKey[key] return c.putWithMutexHold(key, value, elt) } // CompareAndSwap puts a new value associated with a given key if existing value matches oldValue. // It returns itemInCache as the element in cache after the function is executed and replaced as true if value is replaced, false otherwise. func (c *LRU) CompareAndSwap(key string, oldValue, newValue any) (itemInCache any, replaced bool) { c.mux.Lock() defer c.mux.Unlock() elt := c.byKey[key] // If entry not found, old value should be nil if elt == nil && oldValue != nil { return nil, false } if elt != nil { // Entry found, compare it with that you expect. entry := elt.Value.(*cacheEntry) if entry.value != oldValue { return entry.value, false } } c.putWithMutexHold(key, newValue, elt) return newValue, true } // putWithMutexHold populates the cache and returns the inserted value. // Caller is expected to hold the c.mut mutex before calling. func (c *LRU) putWithMutexHold(key string, value any, elt *list.Element) any { if elt != nil { entry := elt.Value.(*cacheEntry) existing := entry.value entry.value = value if c.ttl != 0 { entry.expiration = c.TimeNow().Add(c.ttl) } c.byAccess.MoveToFront(elt) return existing } entry := &cacheEntry{ key: key, value: value, } if c.ttl != 0 { entry.expiration = c.TimeNow().Add(c.ttl) } c.byKey[key] = c.byAccess.PushFront(entry) for len(c.byKey) > c.maxSize { oldest := c.byAccess.Remove(c.byAccess.Back()).(*cacheEntry) if c.onEvict != nil { c.onEvict(oldest.key, oldest.value) } delete(c.byKey, oldest.key) } return nil } // Delete deletes a key, value pair associated with a key func (c *LRU) Delete(key string) { c.mux.Lock() defer c.mux.Unlock() elt := c.byKey[key] if elt != nil { entry := c.byAccess.Remove(elt).(*cacheEntry) if c.onEvict != nil { c.onEvict(entry.key, entry.value) } delete(c.byKey, key) } } // Size returns the number of entries currently in the lru, useful if cache is not full func (c *LRU) Size() int { c.mux.Lock() defer c.mux.Unlock() return len(c.byKey) } type cacheEntry struct { key string expiration time.Time value any } ================================================ FILE: internal/cache/lru_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package cache import ( "sync" "testing" "time" "github.com/stretchr/testify/assert" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestLRU(t *testing.T) { cache := NewLRUWithOptions(4, &Options{ OnEvict: func(_ string, _ any) { // do nothing, just for code coverage }, }) cache.Put("A", "Foo") assert.Equal(t, "Foo", cache.Get("A")) assert.Nil(t, cache.Get("B")) assert.Equal(t, 1, cache.Size()) cache.Put("B", "Bar") cache.Put("C", "Cid") cache.Put("D", "Delt") assert.Equal(t, 4, cache.Size()) assert.Equal(t, "Bar", cache.Get("B")) assert.Equal(t, "Cid", cache.Get("C")) assert.Equal(t, "Delt", cache.Get("D")) cache.Put("A", "Foo2") assert.Equal(t, "Foo2", cache.Get("A")) cache.Put("E", "Epsi") assert.Equal(t, "Epsi", cache.Get("E")) assert.Equal(t, "Foo2", cache.Get("A")) assert.Nil(t, cache.Get("B")) // Oldest, should be evicted // Access C, D is now LRU cache.Get("C") cache.Put("F", "Felp") assert.Nil(t, cache.Get("D")) cache.Delete("A") assert.Nil(t, cache.Get("A")) } func TestCompareAndSwap(t *testing.T) { cache := NewLRUWithOptions(2, nil) item, ok := cache.CompareAndSwap("A", nil, "Foo") assert.True(t, ok) assert.Equal(t, "Foo", item) assert.Equal(t, "Foo", cache.Get("A")) assert.Nil(t, cache.Get("B")) assert.Equal(t, 1, cache.Size()) item, ok = cache.CompareAndSwap("B", nil, "Bar") assert.True(t, ok) assert.Equal(t, 2, cache.Size()) assert.Equal(t, "Bar", item) assert.Equal(t, "Bar", cache.Get("B")) item, ok = cache.CompareAndSwap("A", "Foo", "Foo2") assert.True(t, ok) assert.Equal(t, "Foo2", item) assert.Equal(t, "Foo2", cache.Get("A")) item, ok = cache.CompareAndSwap("A", nil, "Foo3") assert.False(t, ok) assert.Equal(t, "Foo2", item) assert.Equal(t, "Foo2", cache.Get("A")) item, ok = cache.CompareAndSwap("A", "Foo", "Foo3") assert.False(t, ok) assert.Equal(t, "Foo2", item) assert.Equal(t, "Foo2", cache.Get("A")) item, ok = cache.CompareAndSwap("F", "foo", "Foo3") assert.False(t, ok) assert.Nil(t, item) assert.Nil(t, cache.Get("F")) // Evict the oldest entry item, ok = cache.CompareAndSwap("E", nil, "Epsi") assert.True(t, ok) assert.Equal(t, "Epsi", item) assert.Equal(t, "Foo2", cache.Get("A")) assert.Nil(t, cache.Get("B")) // Oldest, should be evicted } func TestLRUWithTTL(t *testing.T) { clk := &simulatedClock{} cache := NewLRUWithOptions(5, &Options{ TTL: time.Millisecond * 100, TimeNow: clk.Now, }) cache.Put("A", "Foo") assert.Equal(t, "Foo", cache.Get("A")) item, _ := cache.CompareAndSwap("A", "Foo", "Foo2") assert.Equal(t, "Foo2", item) assert.Equal(t, "Foo2", cache.Get("A")) clk.Elapse(time.Millisecond * 50) assert.Equal(t, "Foo2", cache.Get("A")) clk.Elapse(time.Millisecond * 100) assert.Nil(t, cache.Get("A")) assert.Equal(t, 0, cache.Size()) } func TestDefaultClock(t *testing.T) { cache := NewLRUWithOptions(5, &Options{ TTL: time.Millisecond * 1, }) cache.Put("A", "foo") assert.Equal(t, "foo", cache.Get("A")) time.Sleep(time.Millisecond * 3) assert.Nil(t, cache.Get("A")) assert.Equal(t, 0, cache.Size()) } func TestLRUCacheConcurrentAccess(*testing.T) { cache := NewLRUWithOptions(5, nil) values := map[string]string{ "A": "foo", "B": "bar", "C": "zed", "D": "dank", "E": "ezpz", } for k, v := range values { cache.Put(k, v) } start := make(chan struct{}) var wg sync.WaitGroup for range 20 { wg.Go(func() { <-start for range 1000 { cache.Get("A") } }) } close(start) wg.Wait() } func TestRemoveFunc(t *testing.T) { ch := make(chan bool) cache := NewLRUWithOptions(5, &Options{ OnEvict: func(_ string, i any) { go func() { _, ok := i.(*testing.T) assert.True(t, ok) ch <- true }() }, }) cache.Put("testing", t) cache.Delete("testing") assert.Nil(t, cache.Get("testing")) timeout := time.NewTimer(time.Millisecond * 300) select { case b := <-ch: assert.True(t, b) case <-timeout.C: t.Error("RemovedFunc did not send true on channel ch") } } func TestRemovedFuncWithTTL(t *testing.T) { ch := make(chan bool) cache := NewLRUWithOptions(5, &Options{ TTL: time.Millisecond * 5, OnEvict: func(_ string, i any) { go func() { _, ok := i.(*testing.T) assert.True(t, ok) ch <- true }() }, }) cache.Put("A", t) assert.Equal(t, t, cache.Get("A")) time.Sleep(time.Millisecond * 10) assert.Nil(t, cache.Get("A")) timeout := time.NewTimer(time.Millisecond * 30) select { case b := <-ch: assert.True(t, b) case <-timeout.C: t.Error("RemovedFunc did not send true on channel ch") } } type simulatedClock struct { sync.Mutex currTime time.Time } func (c *simulatedClock) Now() time.Time { c.Lock() defer c.Unlock() return c.currTime } func (c *simulatedClock) Elapse(d time.Duration) time.Time { c.Lock() defer c.Unlock() c.currTime = c.currTime.Add(d) return c.currTime } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/config/config.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package config import ( "flag" "strings" "github.com/spf13/cobra" "github.com/spf13/viper" ) // Viperize creates new Viper and command add passes flags to command // Viper is initialized with flags from command and configured to accept flags as environmental variables. // Characters `.-` in environmental variables are changed to `_` func Viperize(inits ...func(*flag.FlagSet)) (*viper.Viper, *cobra.Command) { return AddFlags(viper.New(), &cobra.Command{}, inits...) } // AddFlags adds flags to command and viper and configures func AddFlags(v *viper.Viper, command *cobra.Command, inits ...func(*flag.FlagSet)) (*viper.Viper, *cobra.Command) { flagSet := new(flag.FlagSet) for i := range inits { inits[i](flagSet) } command.Flags().AddGoFlagSet(flagSet) configureViper(v) v.BindPFlags(command.Flags()) return v, command } func configureViper(v *viper.Viper) { v.AutomaticEnv() v.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_")) } ================================================ FILE: internal/config/config_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package config import ( "flag" "fmt" "testing" "time" "github.com/stretchr/testify/assert" ) func TestViperize(t *testing.T) { intFlag := "intFlag" stringFlag := "stringFlag" durationFlag := "durationFlag" expectedInt := 5 expectedString := "string" expectedDuration := 13 * time.Second addFlags := func(flagSet *flag.FlagSet) { flagSet.Int(intFlag, 0, "") flagSet.String(stringFlag, "", "") flagSet.Duration(durationFlag, 0, "") } v, command := Viperize(addFlags) command.ParseFlags([]string{ fmt.Sprintf("--%s=%d", intFlag, expectedInt), fmt.Sprintf("--%s=%s", stringFlag, expectedString), fmt.Sprintf("--%s=%s", durationFlag, expectedDuration.String()), }) assert.Equal(t, expectedInt, v.GetInt(intFlag)) assert.Equal(t, expectedString, v.GetString(stringFlag)) assert.Equal(t, expectedDuration, v.GetDuration(durationFlag)) } func TestEnv(t *testing.T) { envFlag := "jaeger.test-flag" actualEnvFlag := "JAEGER_TEST_FLAG" addFlags := func(flagSet *flag.FlagSet) { flagSet.String(envFlag, "", "") } expectedString := "string" t.Setenv(actualEnvFlag, expectedString) v, _ := Viperize(addFlags) assert.Equal(t, expectedString, v.GetString(envFlag)) } ================================================ FILE: internal/config/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package config import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/config/promcfg/config.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package promcfg import ( "time" "github.com/asaskevich/govalidator" "go.opentelemetry.io/collector/config/configtls" ) // Configuration describes the options to customize the storage behavior. type Configuration struct { ServerURL string `valid:"required" mapstructure:"endpoint"` ConnectTimeout time.Duration `mapstructure:"connect_timeout"` // TLS contains the TLS configuration for the connection to the Prometheus clusters. TLS configtls.ClientConfig `mapstructure:"tls"` TokenFilePath string `mapstructure:"token_file_path"` TokenOverrideFromContext bool `mapstructure:"token_override_from_context"` MetricNamespace string `mapstructure:"metric_namespace"` LatencyUnit string `mapstructure:"latency_unit"` NormalizeCalls bool `mapstructure:"normalize_calls"` NormalizeDuration bool `mapstructure:"normalize_duration"` // ExtraQueryParams is used to provide extra parameters to be appended // to the URL of queries going out to the metrics backend. ExtraQueryParams map[string]string `mapstructure:"extra_query_parameters"` } func (c *Configuration) Validate() error { _, err := govalidator.ValidateStruct(c) return err } ================================================ FILE: internal/config/promcfg/config_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package promcfg import ( "testing" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestValidate(t *testing.T) { cfg := Configuration{ ServerURL: "localhost:1234", } err := cfg.Validate() require.NoError(t, err) cfg = Configuration{} err = cfg.Validate() require.Error(t, err) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/config/string_slice.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package config import "strings" // StringSlice implements the pflag.Value interface and allows for parsing multiple // config values with the same name. It purposefully mimics pFlag.stringSliceValue // (https://github.com/spf13/pflag/blob/master/string_slice.go) in order to be // treated like a string slice by both viper and pflag cleanly. type StringSlice []string // String implements pflag.Value func (l *StringSlice) String() string { if len(*l) == 0 { return "[]" } return `["` + strings.Join(*l, `","`) + `"]` } // Set implements pflag.Value func (l *StringSlice) Set(value string) error { *l = append(*l, value) return nil } // Type implements pflag.Value func (*StringSlice) Type() string { // this type string needs to match pflag.stringSliceValue's Type return "stringSlice" } ================================================ FILE: internal/config/string_slice_test.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package config import ( "flag" "testing" "github.com/spf13/pflag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestStringSlice(t *testing.T) { f := &StringSlice{} assert.Equal(t, "[]", f.String()) assert.Equal(t, "stringSlice", f.Type()) f.Set("test") assert.Equal(t, `["test"]`, f.String()) f.Set("test2") assert.Equal(t, `["test","test2"]`, f.String()) f.Set("test3,test4") assert.Equal(t, `["test","test2","test3,test4"]`, f.String()) } func TestStringSliceTreatedAsStringSlice(t *testing.T) { f := &StringSlice{} // create and add flags/values to a go flag set flagset := flag.NewFlagSet("test", flag.ContinueOnError) flagset.Var(f, "test", "test") err := flagset.Set("test", "asdf") require.NoError(t, err) err = flagset.Set("test", "blerg") require.NoError(t, err) err = flagset.Set("test", "other,thing") require.NoError(t, err) // add go flag set to pflag pflagset := pflag.FlagSet{} pflagset.AddGoFlagSet(flagset) actual, err := pflagset.GetStringSlice("test") require.NoError(t, err) assert.Equal(t, []string{"asdf", "blerg", "other,thing"}, actual) } ================================================ FILE: internal/config/tlscfg/flags.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tlscfg import ( "flag" "fmt" "reflect" "strings" "github.com/spf13/viper" "go.opentelemetry.io/collector/config/configoptional" "go.opentelemetry.io/collector/config/configtls" ) const ( tlsPrefix = ".tls" tlsEnabled = tlsPrefix + ".enabled" tlsCA = tlsPrefix + ".ca" tlsCert = tlsPrefix + ".cert" tlsKey = tlsPrefix + ".key" tlsServerName = tlsPrefix + ".server-name" tlsClientCA = tlsPrefix + ".client-ca" tlsSkipHostVerify = tlsPrefix + ".skip-host-verify" tlsCipherSuites = tlsPrefix + ".cipher-suites" tlsMinVersion = tlsPrefix + ".min-version" tlsMaxVersion = tlsPrefix + ".max-version" tlsReloadInterval = tlsPrefix + ".reload-interval" ) // ClientFlagsConfig describes which CLI flags for TLS client should be generated. type ClientFlagsConfig struct { Prefix string } // ServerFlagsConfig describes which CLI flags for TLS server should be generated. type ServerFlagsConfig struct { Prefix string } // AddFlags adds flags for TLS to the FlagSet. func (c ClientFlagsConfig) AddFlags(flags *flag.FlagSet) { flags.Bool(c.Prefix+tlsEnabled, false, "Enable TLS when talking to the remote server(s)") flags.String(c.Prefix+tlsCA, "", "Path to a TLS CA (Certification Authority) file used to verify the remote server(s) (by default will use the system truststore)") flags.String(c.Prefix+tlsCert, "", "Path to a TLS Certificate file, used to identify this process to the remote server(s)") flags.String(c.Prefix+tlsKey, "", "Path to a TLS Private Key file, used to identify this process to the remote server(s)") flags.String(c.Prefix+tlsServerName, "", "Override the TLS server name we expect in the certificate of the remote server(s)") flags.Bool(c.Prefix+tlsSkipHostVerify, false, "(insecure) Skip server's certificate chain and host name verification") } // AddFlags adds flags for TLS to the FlagSet. func (c ServerFlagsConfig) AddFlags(flags *flag.FlagSet) { flags.Bool(c.Prefix+tlsEnabled, false, "Enable TLS on the server") flags.String(c.Prefix+tlsCert, "", "Path to a TLS Certificate file, used to identify this server to clients") flags.String(c.Prefix+tlsKey, "", "Path to a TLS Private Key file, used to identify this server to clients") flags.String(c.Prefix+tlsClientCA, "", "Path to a TLS CA (Certification Authority) file used to verify certificates presented by clients (if unset, all clients are permitted)") flags.String(c.Prefix+tlsCipherSuites, "", "Comma-separated list of cipher suites for the server, values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants).") flags.String(c.Prefix+tlsMinVersion, "", "Minimum TLS version supported (Possible values: 1.0, 1.1, 1.2, 1.3)") flags.String(c.Prefix+tlsMaxVersion, "", "Maximum TLS version supported (Possible values: 1.0, 1.1, 1.2, 1.3)") flags.Duration(c.Prefix+tlsReloadInterval, 0, "The duration after which the certificate will be reloaded (0s means will not be reloaded)") } // InitFromViper creates tls.Config populated with values retrieved from Viper. func (c ClientFlagsConfig) InitFromViper(v *viper.Viper) (configtls.ClientConfig, error) { var p options p.Enabled = v.GetBool(c.Prefix + tlsEnabled) p.CAPath = v.GetString(c.Prefix + tlsCA) p.CertPath = v.GetString(c.Prefix + tlsCert) p.KeyPath = v.GetString(c.Prefix + tlsKey) p.ServerName = v.GetString(c.Prefix + tlsServerName) p.SkipHostVerify = v.GetBool(c.Prefix + tlsSkipHostVerify) if !p.Enabled { var empty options if !reflect.DeepEqual(&p, &empty) { return configtls.ClientConfig{}, fmt.Errorf("%s.tls.* options cannot be used when %s is false", c.Prefix, c.Prefix+tlsEnabled) } } return p.ToOtelClientConfig(), nil } // InitFromViper creates tls.Config populated with values retrieved from Viper. func (c ServerFlagsConfig) InitFromViper(v *viper.Viper) (configoptional.Optional[configtls.ServerConfig], error) { var p options p.Enabled = v.GetBool(c.Prefix + tlsEnabled) p.CertPath = v.GetString(c.Prefix + tlsCert) p.KeyPath = v.GetString(c.Prefix + tlsKey) p.ClientCAPath = v.GetString(c.Prefix + tlsClientCA) if s := v.GetString(c.Prefix + tlsCipherSuites); s != "" { p.CipherSuites = strings.Split(stripWhiteSpace(v.GetString(c.Prefix+tlsCipherSuites)), ",") } p.MinVersion = v.GetString(c.Prefix + tlsMinVersion) p.MaxVersion = v.GetString(c.Prefix + tlsMaxVersion) p.ReloadInterval = v.GetDuration(c.Prefix + tlsReloadInterval) if !p.Enabled { var empty options if !reflect.DeepEqual(&p, &empty) { return configoptional.None[configtls.ServerConfig](), fmt.Errorf("%s.tls.* options cannot be used when %s is false", c.Prefix, c.Prefix+tlsEnabled) } } return p.ToOtelServerConfig(), nil } // stripWhiteSpace removes all whitespace characters from a string func stripWhiteSpace(str string) string { return strings.ReplaceAll(str, " ", "") } ================================================ FILE: internal/config/tlscfg/flags_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tlscfg import ( "flag" "testing" "time" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/config/configoptional" "go.opentelemetry.io/collector/config/configtls" "github.com/jaegertracing/jaeger/internal/config" ) func TestClientFlags(t *testing.T) { cmdLine := []string{ "--prefix.tls.ca=ca-file", "--prefix.tls.cert=cert-file", "--prefix.tls.key=key-file", "--prefix.tls.server-name=HAL1", "--prefix.tls.skip-host-verify=true", } tests := []struct { option string }{ { option: "--prefix.tls.enabled=true", }, } for _, test := range tests { t.Run(test.option, func(t *testing.T) { v := viper.New() command := cobra.Command{} flagSet := &flag.FlagSet{} flagCfg := ClientFlagsConfig{ Prefix: "prefix", } flagCfg.AddFlags(flagSet) command.PersistentFlags().AddGoFlagSet(flagSet) v.BindPFlags(command.PersistentFlags()) err := command.ParseFlags(append(cmdLine, test.option)) require.NoError(t, err) clientCfg, err := flagCfg.InitFromViper(v) require.NoError(t, err) expectedCfg := configtls.ClientConfig{ Config: configtls.Config{ CAFile: "ca-file", CertFile: "cert-file", KeyFile: "key-file", IncludeSystemCACertsPool: false, CipherSuites: nil, ReloadInterval: 0, }, Insecure: false, InsecureSkipVerify: true, ServerName: "HAL1", } assert.Equal(t, expectedCfg, clientCfg) }) } } func TestServerFlags(t *testing.T) { cmdLine := []string{ "##placeholder##", // replaced in each test below "--prefix.tls.enabled=true", "--prefix.tls.cert=cert-file", "--prefix.tls.key=key-file", "--prefix.tls.cipher-suites=TLS_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", "--prefix.tls.min-version=1.2", "--prefix.tls.max-version=1.3", } tests := []struct { option string file string }{ { option: "--prefix.tls.client-ca=client-ca-file", file: "client-ca-file", }, } for _, test := range tests { t.Run(test.file, func(t *testing.T) { v := viper.New() command := cobra.Command{} flagSet := &flag.FlagSet{} flagCfg := ServerFlagsConfig{ Prefix: "prefix", } flagCfg.AddFlags(flagSet) command.PersistentFlags().AddGoFlagSet(flagSet) v.BindPFlags(command.PersistentFlags()) cmdLine[0] = test.option err := command.ParseFlags(cmdLine) require.NoError(t, err) serverConfig, err := flagCfg.InitFromViper(v) require.NoError(t, err) expectedConfig := configoptional.Some(configtls.ServerConfig{ ClientCAFile: "client-ca-file", Config: configtls.Config{ CertFile: "cert-file", KeyFile: "key-file", MinVersion: "1.2", MaxVersion: "1.3", CipherSuites: []string{"TLS_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA"}, }, }) assert.Equal(t, expectedConfig, serverConfig) }) } } func TestServerCertReloadInterval(t *testing.T) { cfg := ServerFlagsConfig{ Prefix: "prefix", } v, command := config.Viperize(cfg.AddFlags) err := command.ParseFlags([]string{ "--prefix.tls.enabled=true", "--prefix.tls.reload-interval=24h", }) require.NoError(t, err) tlscfg, err := cfg.InitFromViper(v) require.NoError(t, err) require.True(t, tlscfg.HasValue()) assert.Equal(t, 24*time.Hour, tlscfg.Get().ReloadInterval) } // TestFailedTLSFlags verifies that TLS options cannot be used when tls.enabled=false func TestFailedTLSFlags(t *testing.T) { clientTests := []string{ ".ca=blah", ".cert=blah", ".key=blah", ".server-name=blah", ".skip-host-verify=true", } serverTests := []string{ ".cert=blah", ".key=blah", ".client-ca=blah", ".cipher-suites=blah", ".min-version=1.1", ".max-version=1.3", } allTests := []struct { side string tests []string }{ {side: "client", tests: clientTests}, {side: "server", tests: serverTests}, } for _, metaTest := range allTests { t.Run(metaTest.side, func(t *testing.T) { for _, test := range metaTest.tests { t.Run(test, func(t *testing.T) { var ( addFlags func(*flag.FlagSet) initFromViper func(*viper.Viper) (any, error) ) if metaTest.side == "client" { clientConfig := &ClientFlagsConfig{Prefix: "prefix"} addFlags = clientConfig.AddFlags initFromViper = func(v *viper.Viper) (any, error) { return clientConfig.InitFromViper(v) } } else { serverConfig := &ServerFlagsConfig{Prefix: "prefix"} addFlags = serverConfig.AddFlags initFromViper = func(v *viper.Viper) (any, error) { return serverConfig.InitFromViper(v) } } v, command := config.Viperize(addFlags) cmdLine := []string{ "--prefix.tls.enabled=true", "--prefix.tls" + test, } err := command.ParseFlags(cmdLine) require.NoError(t, err) result, err := initFromViper(v) require.NoError(t, err) switch metaTest.side { case "client": require.IsType(t, configtls.ClientConfig{}, result, "result should be of type configtls.ClientConfig") case "server": exp := configoptional.Some(configtls.ServerConfig{}) require.IsType(t, exp, result, "result should be of type *configtls.ServerConfig") default: t.Errorf("Unexpected side value: %s", metaTest.side) } cmdLine[0] = "--prefix.tls.enabled=false" err = command.ParseFlags(cmdLine) require.NoError(t, err) _, err = initFromViper(v) require.Error(t, err) require.EqualError(t, err, "prefix.tls.* options cannot be used when prefix.tls.enabled is false") }) } }) } } ================================================ FILE: internal/config/tlscfg/options.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tlscfg import ( "time" "go.opentelemetry.io/collector/config/configoptional" "go.opentelemetry.io/collector/config/configtls" ) // options describes the configuration properties for TLS Connections. type options struct { Enabled bool CAPath string CertPath string KeyPath string ServerName string // only for client-side TLS config ClientCAPath string // only for server-side TLS config for client auth CipherSuites []string MinVersion string MaxVersion string SkipHostVerify bool ReloadInterval time.Duration } func (o *options) ToOtelClientConfig() configtls.ClientConfig { return configtls.ClientConfig{ Insecure: !o.Enabled, InsecureSkipVerify: o.SkipHostVerify, ServerName: o.ServerName, Config: configtls.Config{ CAFile: o.CAPath, CertFile: o.CertPath, KeyFile: o.KeyPath, CipherSuites: o.CipherSuites, MinVersion: o.MinVersion, MaxVersion: o.MaxVersion, ReloadInterval: o.ReloadInterval, // when no truststore given, use SystemCertPool // https://github.com/jaegertracing/jaeger/issues/6334 IncludeSystemCACertsPool: o.Enabled && (o.CAPath == ""), }, } } // ToOtelServerConfig provides a mapping between from Options to OTEL's TLS Server Configuration. func (o *options) ToOtelServerConfig() configoptional.Optional[configtls.ServerConfig] { if !o.Enabled { return configoptional.None[configtls.ServerConfig]() } cfg := configtls.ServerConfig{ ClientCAFile: o.ClientCAPath, Config: configtls.Config{ CAFile: o.CAPath, CertFile: o.CertPath, KeyFile: o.KeyPath, CipherSuites: o.CipherSuites, MinVersion: o.MinVersion, MaxVersion: o.MaxVersion, ReloadInterval: o.ReloadInterval, }, } if o.ReloadInterval > 0 { cfg.ReloadClientCAFile = true } return configoptional.Some(cfg) } ================================================ FILE: internal/config/tlscfg/options_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tlscfg import ( "testing" "time" "github.com/stretchr/testify/assert" "go.opentelemetry.io/collector/config/configoptional" "go.opentelemetry.io/collector/config/configtls" ) func TestToOtelClientConfig(t *testing.T) { testCases := []struct { name string options options expected configtls.ClientConfig }{ { name: "insecure", options: options{ Enabled: false, }, expected: configtls.ClientConfig{ Insecure: true, }, }, { name: "secure with skip host verify", options: options{ Enabled: true, SkipHostVerify: true, ServerName: "example.com", CAPath: "path/to/ca.pem", CertPath: "path/to/cert.pem", KeyPath: "path/to/key.pem", CipherSuites: []string{"TLS_RSA_WITH_AES_128_CBC_SHA"}, MinVersion: "1.2", MaxVersion: "1.3", ReloadInterval: 24 * time.Hour, }, expected: configtls.ClientConfig{ Insecure: false, InsecureSkipVerify: true, ServerName: "example.com", Config: configtls.Config{ CAFile: "path/to/ca.pem", CertFile: "path/to/cert.pem", KeyFile: "path/to/key.pem", CipherSuites: []string{"TLS_RSA_WITH_AES_128_CBC_SHA"}, MinVersion: "1.2", MaxVersion: "1.3", ReloadInterval: 24 * time.Hour, }, }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { actual := tc.options.ToOtelClientConfig() assert.Equal(t, tc.expected, actual) }) } } func TestToOtelServerConfig(t *testing.T) { testCases := []struct { name string options options expected configoptional.Optional[configtls.ServerConfig] }{ { name: "not enabled", options: options{ Enabled: false, }, expected: configoptional.None[configtls.ServerConfig](), }, { name: "default mapping", options: options{ Enabled: true, ClientCAPath: "path/to/client/ca.pem", CAPath: "path/to/ca.pem", CertPath: "path/to/cert.pem", KeyPath: "path/to/key.pem", CipherSuites: []string{"TLS_RSA_WITH_AES_128_CBC_SHA"}, MinVersion: "1.2", MaxVersion: "1.3", }, expected: configoptional.Some(configtls.ServerConfig{ ClientCAFile: "path/to/client/ca.pem", Config: configtls.Config{ CAFile: "path/to/ca.pem", CertFile: "path/to/cert.pem", KeyFile: "path/to/key.pem", CipherSuites: []string{"TLS_RSA_WITH_AES_128_CBC_SHA"}, MinVersion: "1.2", MaxVersion: "1.3", }, }), }, { name: "with reload interval", options: options{ Enabled: true, ClientCAPath: "path/to/client/ca.pem", CAPath: "path/to/ca.pem", CertPath: "path/to/cert.pem", KeyPath: "path/to/key.pem", CipherSuites: []string{"TLS_RSA_WITH_AES_128_CBC_SHA"}, MinVersion: "1.2", MaxVersion: "1.3", ReloadInterval: 24 * time.Hour, }, expected: configoptional.Some(configtls.ServerConfig{ ClientCAFile: "path/to/client/ca.pem", ReloadClientCAFile: true, Config: configtls.Config{ CAFile: "path/to/ca.pem", CertFile: "path/to/cert.pem", KeyFile: "path/to/key.pem", CipherSuites: []string{"TLS_RSA_WITH_AES_128_CBC_SHA"}, MinVersion: "1.2", MaxVersion: "1.3", ReloadInterval: 24 * time.Hour, }, }), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { actual := tc.options.ToOtelServerConfig() assert.Equal(t, tc.expected, actual) }) } } ================================================ FILE: internal/config/tlscfg/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tlscfg import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/config/tlscfg/testdata/README.md ================================================ # Example Certificate Authority and Certificate creation for testing The PEM files located in this directory are used by unit tests in this package. To generate and update the PEM files in this directory, run the following from the project root: make certs To only generate the PEM files without copying them to this directory: make certs-dryrun The location of the generated PEM files will be printed to STDOUT like so: # Dry-run complete. Generated files can be found in /var/folders/3p/yms48z2s6v7c8fy2m_1481g00000gn/T/certificates.p7pFHXpy ================================================ FILE: internal/config/tlscfg/testdata/bad-CA-cert.txt ================================================ -----BEGIN CERTIFICATE----- bad certificate -----END CERTIFICATE----- ================================================ FILE: internal/config/tlscfg/testdata/example-CA-cert.pem ================================================ -----BEGIN CERTIFICATE----- MIIDMTCCAhkCFCi65dSe1JONpNGghyam61+4gTL7MA0GCSqGSIb3DQEBCwUAMFUx CzAJBgNVBAYTAkFVMRIwEAYDVQQIDAlBdXN0cmFsaWExDzANBgNVBAcMBlN5ZG5l eTEQMA4GA1UECgwHTG9nei5pbzEPMA0GA1UEAwwGSmFlZ2VyMB4XDTIyMDkxMDAw MjE0NFoXDTMyMDkwNzAwMjE0NFowVTELMAkGA1UEBhMCQVUxEjAQBgNVBAgMCUF1 c3RyYWxpYTEPMA0GA1UEBwwGU3lkbmV5MRAwDgYDVQQKDAdMb2d6LmlvMQ8wDQYD VQQDDAZKYWVnZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLlEq/ DF2pkhfSedvAd5h6BXCjpC/mUA6BN3RyMHUjTWr9hhBtaIYv68O12GMVf//ST/Fs CjRrjOcqrz2QQn3P8UelGRd2vJfcMhJElQ/lnKmZZlAHEOMF8TC7nQfsReLCwcpj T6bXqvDcfHjDye+45F2rPDpRGLzyysg7pgdINp0Duph0Z16ggrBgz7RVNBmWsYVe sGD3VOR3hLd8GTDzJ5amRpkq8nfliJ+U3JLGcDG/7Wkuvl/YZZxf21v9f4yYVEZZ aLAcKsHIUoFRDJtdrBeaPZRJjL/I9B1M6En+Styxb5wJw42h9BXtJd2IeQPp15pP KfPbkmOj+X+2s9n1AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAJbm7WXgQirWQbaa E304K8tvdpC2E1ewxTTrUEN8jUONER4KC+epRnsTgkEpVlj7sehiAgSMnbT4E3ve GjmsUrZiJcKPaf+ogn49Cj0weD99wbJtUNgbH4HiqR1ePOHIRDQ7GD5G0zdFq7oO Il09eHAbbWM61x04I3XDQ0OwXyeVXIEWJcR1R6wnuNMJm54czbXvn6SrIuoMCvs6 oSkVm43Q+plk0hlDZnA/KiOxqFRLVHBuX/SgRf5NBg8m7id3fNzIJnWWK+zqoDoZ ryja7dFIJnLqEXJxJkc5ubT1/j9PDE51WbM5MyPB6lnuQKdZTbDziyKiVXg0au3E QK5K/Ow= -----END CERTIFICATE----- ================================================ FILE: internal/config/tlscfg/testdata/example-client-cert.pem ================================================ -----BEGIN CERTIFICATE----- MIIDUjCCAjqgAwIBAgIUE56RLVss9rH/ojHQlVqysg6vJQYwDQYJKoZIhvcNAQEL BQAwVTELMAkGA1UEBhMCQVUxEjAQBgNVBAgMCUF1c3RyYWxpYTEPMA0GA1UEBwwG U3lkbmV5MRAwDgYDVQQKDAdMb2d6LmlvMQ8wDQYDVQQDDAZKYWVnZXIwHhcNMjIw OTEwMDAyMTQ0WhcNMzIwOTA3MDAyMTQ0WjBVMQswCQYDVQQGEwJBVTESMBAGA1UE CAwJQXVzdHJhbGlhMQ8wDQYDVQQHDAZTeWRuZXkxEDAOBgNVBAoMB0xvZ3ouaW8x DzANBgNVBAMMBkphZWdlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB ALwEHBIe2nffXpJtYRUQ2GzuIwzPhI6fwT/KYXc/ao6mhHQ/oGyMZJFBxxpB0Inz GuurMtJBIgAlrhKmX5Dg5tB05iMpqA1Hbxa4fQS34iw9bBEvH/7SkuQ7gox6ht/n ZX9UuyAw751B/KlGQlVInGzySAgR9T7RdT7YOAGoaQtXNsE6b3/Jm6z/uRW3Buqp 1jzqL9VHzUdC7k8nRRdTTivcDUiZ+ocp4j2lRVP4hOylU0DSAG7mfwR8YQ/Xt1cU kn+2pe+D4tcx23lQcQFFWeJ2CKVx+Gx2BwJNoqPJ0LJLLSAQyY+S2wGSjoc8nqvM 8mhgykFU9dW+GEwJzhLqRRMCAwEAAaMaMBgwFgYDVR0RBA8wDYILZXhhbXBsZS5j b20wDQYJKoZIhvcNAQELBQADggEBABqjQPg5voqMNnBBtnAKDnuTF4hOBNAo0Wq/ KzD5QqvaWPPspx+oIahSIwq+8aL+NzptfYwbke10Q5qmOzq/ZgVTela+k/hgbjn6 I/nOTCg5/v7m0AN3HgIGdgh5TOBiZMEsNpS+Lr2DangjaBKwpe4sucsgevJpggg1 m/FT8rL7X5AjNx+mgsjdzQaboe6SkaGSSzByN8jEO03ceYpLvfqMGdAJpF4MEGiZ BlAAMHn3m5NLuBsHM/SiewTEmLBa6AEo33/XI0rOjDlYOj7A0xj2NLz0EwfRf3AG UpDuAB9O5n3iVXrHtaHDMRihGjBbeDEVaf68uodz7nH/UIWc2Rs= -----END CERTIFICATE----- ================================================ FILE: internal/config/tlscfg/testdata/example-client-key.pem ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAvAQcEh7ad99ekm1hFRDYbO4jDM+Ejp/BP8phdz9qjqaEdD+g bIxkkUHHGkHQifMa66sy0kEiACWuEqZfkODm0HTmIymoDUdvFrh9BLfiLD1sES8f /tKS5DuCjHqG3+dlf1S7IDDvnUH8qUZCVUicbPJICBH1PtF1Ptg4AahpC1c2wTpv f8mbrP+5FbcG6qnWPOov1UfNR0LuTydFF1NOK9wNSJn6hyniPaVFU/iE7KVTQNIA buZ/BHxhD9e3VxSSf7al74Pi1zHbeVBxAUVZ4nYIpXH4bHYHAk2io8nQskstIBDJ j5LbAZKOhzyeq8zyaGDKQVT11b4YTAnOEupFEwIDAQABAoIBAEWCa3JTj8dDgG44 G+0y1iCnhbPFwKcN7t8Lji8M9fMZItzrbP7UhJWjMN3HOTbW9rvsBhTvWYeeZpWk hq5ER3EH1tFnJCcMoshOmoG1Ddv3NU3BE14dMYtJaQFQhy6eGMsTYz8KeHu2Gpfm Tr3C43nvtKuvH/ECdQsv2rzaK0OybxwN0GQGPPeKFhGJ8/v+s2NGQvfuaDFqPZ3u 9e/gZ8DHAPrj6kf87j1SzDAytuaDItPwQJv01Kc9gVPajuoWMTkz1IcGB5KhRohO CO8h33PSA1gq7d7yn4fCqtwWORJTqFc7Bq1512bM4dh+lqhm+LAYPI7s899x2KI3 A837xLkCgYEA6O/6nN2npwhPEROBL7T5KGKOwWrgKLO1XL/zwtrXAXFO4pN0Pvcf K5Aqrm1TjhDqwvMUmtjFVrB6FkejGw/22NlKNrsBQ5HQvHv8RjLu7kC5VxysUei1 S7IlS8HjA6APap9cgELWocivrFIOijXXvRmO0EgIqWYl2TF60Cyx6sUCgYEAzqGO TR1IE8s2bJGxwF3toSR69Q3i5GgUnELATNEqIg0i/j9kYDNl8oJmyJ0DEsKlIe6X 72JSDMLX0mwzBHit7LXvBUYAXCu/i91Rnkn7ME7KTPIJaKnZHNT1KZQi0vClv9St gQeAl1YGHSlEh98lEhHwykchmqaVFiOo0zlNzfcCgYEAwDHRvEB/JiiK5HINc4mE 0zeOxjQixDKS//Y5cJsUL9KH3hcAITvRciY/sS/vcxauPTBH3gPhv0dZVKzC/X9M k1umCkZ+InxbmElMu7cmwVqSEjhMTkEN5WkVsM5HOySD09utfP6pDVAC8tG5wXvv h81gsqXcz7jCndRfmwhlvGkCgYEAhVOLDUj6jAMQX+d2aShyPwrZ56sJHtXljpon mKlR5VzSmnju3H/tpRftGD7vj7hWctmP4Z9wT9mdBqJYHOd9WgJecumjK9Xyp12r 31XfJWGBeTqnRYhqlgb3FdgGzFMIsAmb1miv2XZhRYmuNXmPYuR+mRZioXYhNoLV 2UzdXisCgYAofFoRrtFAUZn1AY7no1MSXrOZ0fAwnRm6va73aSOFHHJG9swI2GOi hGUuABh6TbpU6G7FIDD/E9zjXoz43j6muN9RUqynVK4x+fEUQDGUtHXavtL97vWg eHbzNbxx46DlaGj2VQkQZ8iFsIMXbeAp5vPGbdau5dCFEgL+DfC87w== -----END RSA PRIVATE KEY----- ================================================ FILE: internal/config/tlscfg/testdata/example-server-cert.pem ================================================ -----BEGIN CERTIFICATE----- MIIDUjCCAjqgAwIBAgIUE56RLVss9rH/ojHQlVqysg6vJQUwDQYJKoZIhvcNAQEL BQAwVTELMAkGA1UEBhMCQVUxEjAQBgNVBAgMCUF1c3RyYWxpYTEPMA0GA1UEBwwG U3lkbmV5MRAwDgYDVQQKDAdMb2d6LmlvMQ8wDQYDVQQDDAZKYWVnZXIwHhcNMjIw OTEwMDAyMTQ0WhcNMzIwOTA3MDAyMTQ0WjBVMQswCQYDVQQGEwJBVTESMBAGA1UE CAwJQXVzdHJhbGlhMQ8wDQYDVQQHDAZTeWRuZXkxEDAOBgNVBAoMB0xvZ3ouaW8x DzANBgNVBAMMBkphZWdlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB AN17nlVlHzFoEDnAA7kvrjzuKiZQZ70znDW5TrqtwXqHr5XG0m7rdQlt9xyr3HFg DbXbkg7wBidqUySWZ7N/cxiqB/oMnfbntapwmBP77Ss8KLLQx17Geb8pryIHrhcE a/E556epv3WRkoz3j8ph3DY7g+ghQWNtWI3UvBdaIkmPaS+wVfH6hwzpT4rbdVSF 1n7SnMcJccKPEPgqASiEsYZeQgnZUedayKzHRnJeQD3lOPXLHAOIGHajGvyQFMqE fG9dJfWNVxH/+GxMNul9jsUfJMc99mG/vy3B1WROOl2EiTi8FzfM64lo8SvEs3Db jcAFItI7BcyM/MJxqYtYFQ0CAwEAAaMaMBgwFgYDVR0RBA8wDYILZXhhbXBsZS5j b20wDQYJKoZIhvcNAQELBQADggEBAFjZrgLJiezjX2enrh1pJDRrj9NClTKM8Vck dnpI4OFmViqSyUkyY28PO9omoXUPAbcVuXcGQ/f4PR7tlKmv1lGH/4vGGgmvLjus Mm0vYZoBos/KPN92RIUkpO1Lvt3es96CFI0k6G0JmstXn4EShQibm1424jTWU3tF praOAsaTVWO/ukVPbULJ8dWzKoQVTyb/cNQiPiL0IXx7XYc/cqCB2yqzELtMOmIe kQuyCmUNzK1qQaezxwkMl2P+121QdOvKkxcu7XlAEo0SRNNNkpOkyRqLvC2iou39 SHxqc/Vbf+Pj9N6oC0twI7KAJELHMi9qhlQsNssxUMjYe7BRYmQ= -----END CERTIFICATE----- ================================================ FILE: internal/config/tlscfg/testdata/example-server-key.pem ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEA3XueVWUfMWgQOcADuS+uPO4qJlBnvTOcNblOuq3BeoevlcbS but1CW33HKvccWANtduSDvAGJ2pTJJZns39zGKoH+gyd9ue1qnCYE/vtKzwostDH XsZ5vymvIgeuFwRr8Tnnp6m/dZGSjPePymHcNjuD6CFBY21YjdS8F1oiSY9pL7BV 8fqHDOlPitt1VIXWftKcxwlxwo8Q+CoBKISxhl5CCdlR51rIrMdGcl5APeU49csc A4gYdqMa/JAUyoR8b10l9Y1XEf/4bEw26X2OxR8kxz32Yb+/LcHVZE46XYSJOLwX N8zriWjxK8SzcNuNwAUi0jsFzIz8wnGpi1gVDQIDAQABAoIBAQCAE55J74IMRgsr +hetHR966JbDNTfoN1Ib1x7p4NTDkHc++4xwzAQQAeEmWVPO1CbZhTF/JdnJLTkL LVamfAsItjqKpIUsZG2vNBEdbU+G8vDuBsFj0w5QN0CpQxuu/8WT51JIqGapDBdd IUOrWs/HJL9wmtp/LppI2j4ymtK9Cffce8AVTazfHspVF2e05b8GEeBjoMmvpPgw bvHPLdCVoWPGsYOFUWG9V1eCo2CFtvspsa8CYghpaXg7EOElF73W1gEoEd5SdMx9 svHeH4bJAzrWoqDrC5kOJUZRip9YjF8WXRudVmSaRPHptwN6qRvF8HWiGrYrdTtJ j1seb87BAoGBAPUrxCI64EN/6YYNziM49RpORLVrZGLaZQCf0IJkoH3DcsBcrtF8 hqJC73z75kj1Y+oOzulYPBlhQr+4hvbMSzHwsffi5nepPXSSGK2+D1O5rASou7b3 Re/OiJNex7IrDAy354PV4B/7iFmgGOVUn+sXIKoprqnor7f3mALAa/yZAoGBAOdE AMKktCQYIHweKPF0mYDsOnoJ8TEAydxShOan5r5gkVTnZhDHa3fD9eGh19Mfi9qC cDro5Sq1+8OLoX6Ta/Ju3PNfI2Qn4KLF9CZrEQhrV90HmXluCflZXyL71SB8pGVo 5ybr8UtalUXVPXKi+inK7CXaJBZaboJWnqmaqJCVAoGAdIX0lgA9jldA+gGds4fi ljoU1dTQxVrfHkjWpOKGlL9Lzrk+LTpuEriVcmWWsZ5PenLHTIgvKDDdtJlTLAE0 y+uF6jbhKoY5OyokqI7oYfahFyXK8c7cYnla2A/4AWoMNA9D7Zi9CPZXe6Fns7dg ui8nyzg8V2zL9zep+8TQjiECgYEAm2zTif0BaGSiqGfoomX3qHKa1lwKMiHSiHUZ Bp9+7yGdas9dhBdSPZqAjJSlpSlFZ6RUYvMU2UCXJJOaBKR1XuhtLE8bTPuT+DFL 5en894iU82JhHf/7Sg5rZuqTERNTtSfsefcGItuNCPLIKlwn/qB3VvUlXbSHIqeu WFQtx4UCgYEA1FIVEc4BjRE6jH80X7RSSOLJ6PwPglZzM8JEVyiYHAHE65zdORF1 iCiuI+pRQc3yHkm2gbB+hY5HSrCmyJrJc0tcUd4QoMqOHV8UEGLVwxtr/4DPMsl4 JIEmzmgvs56TJeKX0YlXnD612zjDWCPV6q+LWlUUzd8qLwk6L1+EFhE= -----END RSA PRIVATE KEY----- ================================================ FILE: internal/config/tlscfg/testdata/gen-certs.sh ================================================ #!/usr/bin/env bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # The following commands were used to create the CA, server and client's certificates and keys in this directory used by unit tests. # These certificates use the Subject Alternative Name extension rather than the Common Name, which will be unsupported in Go 1.15. usage() { echo "Usage: $0 [-d]" echo echo "-d Dry-run mode. PEM files will not be modified." exit 1 } dry_run=false while getopts "d" o; do case "${o}" in d) dry_run=true ;; *) usage ;; esac done shift $((OPTIND-1)) set -ex # Create temp dir for generated files. tmp_dir=$(mktemp -d -t certificates) clean_up() { ARG=$? if [ $dry_run = true ]; then echo "Dry-run complete. Generated files can be found in $tmp_dir" else rm -rf "$tmp_dir" fi exit $ARG } trap clean_up EXIT gen_ssl_conf() { domain_name=$1 output_file=$2 cat << EOF > "$output_file" [ req ] prompt = no default_bits = 2048 distinguished_name = req_distinguished_name req_extensions = req_ext [ req_distinguished_name ] countryName = AU stateOrProvinceName = Australia localityName = Sydney organizationName = Logz.io commonName = Jaeger [ req_ext ] subjectAltName = @alt_names [alt_names] DNS.1 = $domain_name EOF } # Generate config files. # The server name (under alt_names in the ssl.conf) is `example.com`. (in accordance to [RFC 2006](https://tools.ietf.org/html/rfc2606)) gen_ssl_conf example.com "$tmp_dir/ssl.conf" gen_ssl_conf wrong.com "$tmp_dir/wrong-ssl.conf" # Create CA (accept defaults from prompts). openssl genrsa -out "$tmp_dir/example-CA-key.pem" 2048 openssl req -new -key "$tmp_dir/example-CA-key.pem" -x509 -days 3650 -out "$tmp_dir/example-CA-cert.pem" -config "$tmp_dir/ssl.conf" # Create Wrong CA (a dummy CA which doesn't provide any certificate; accept defaults from prompts). openssl genrsa -out "$tmp_dir/wrong-CA-key.pem" 2048 openssl req -new -key "$tmp_dir/wrong-CA-key.pem" -x509 -days 3650 -out "$tmp_dir/wrong-CA-cert.pem" -config "$tmp_dir/wrong-ssl.conf" # Create client and server keys. openssl genrsa -out "$tmp_dir/example-server-key.pem" 2048 openssl genrsa -out "$tmp_dir/example-client-key.pem" 2048 # Create certificate sign request using the above created keys and configuration given and commandline arguments. openssl req -new -nodes -key "$tmp_dir/example-server-key.pem" -out "$tmp_dir/example-server.csr" -config "$tmp_dir/ssl.conf" openssl req -new -nodes -key "$tmp_dir/example-client-key.pem" -out "$tmp_dir/example-client.csr" -config "$tmp_dir/ssl.conf" # Creating the client and server certificate. openssl x509 -req \ -sha256 \ -days 3650 \ -in "$tmp_dir/example-server.csr" \ -out "$tmp_dir/example-server-cert.pem" \ -extensions req_ext \ -CA "$tmp_dir/example-CA-cert.pem" \ -CAkey "$tmp_dir/example-CA-key.pem" \ -CAcreateserial \ -extfile "$tmp_dir/ssl.conf" openssl x509 -req \ -sha256 \ -days 3650 \ -in "$tmp_dir/example-client.csr" \ -out "$tmp_dir/example-client-cert.pem" \ -extensions req_ext \ -CA "$tmp_dir/example-CA-cert.pem" \ -CAkey "$tmp_dir/example-CA-key.pem" \ -CAcreateserial \ -extfile "$tmp_dir/ssl.conf" # Copy PEM files. if [ $dry_run = false ]; then cp "$tmp_dir/example-CA-cert.pem" \ "$tmp_dir/example-client-cert.pem" \ "$tmp_dir/example-client-key.pem" \ "$tmp_dir/example-server-cert.pem" \ "$tmp_dir/example-server-key.pem" \ "$tmp_dir/wrong-CA-cert.pem" . fi ================================================ FILE: internal/config/tlscfg/testdata/wrong-CA-cert.pem ================================================ -----BEGIN CERTIFICATE----- MIIDMTCCAhkCFHHHf3IkwJYPKija3g7MR53ttN+QMA0GCSqGSIb3DQEBCwUAMFUx CzAJBgNVBAYTAkFVMRIwEAYDVQQIDAlBdXN0cmFsaWExDzANBgNVBAcMBlN5ZG5l eTEQMA4GA1UECgwHTG9nei5pbzEPMA0GA1UEAwwGSmFlZ2VyMB4XDTIyMDkxMDAw MjE0NFoXDTMyMDkwNzAwMjE0NFowVTELMAkGA1UEBhMCQVUxEjAQBgNVBAgMCUF1 c3RyYWxpYTEPMA0GA1UEBwwGU3lkbmV5MRAwDgYDVQQKDAdMb2d6LmlvMQ8wDQYD VQQDDAZKYWVnZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDDWRKQ VcJGGiQVrAreflpWMaurA+dPBhKwrfiHZ1glFEevEyOcgXVP6pSvRsUYJ1x36yGq 8EV2bEAmpkBx0QEnPOiQIRfA8nuFmy9bE6RWl2amXoEg81E7LhW0uC9qGkF9eLG6 o9F/knd0WGNCcEpfk9NdXH1HtNJJvjNNi6no/9KHHfT2pg/3OSzt3dCPOHwXZEdL 72rLs+NoV0sM1oP1MFZmYHzSNhquOKGWDEPTk58YvA5uRe06nacLt30ZCZh3oBso vJxS8Cs7mAGZMnZrMbTcNd7iYN850lTlcYFmv/3zlzF7fO84mPBzWYSY90bJ2AwQ AdJFrZl5sol/j9STAgMBAAEwDQYJKoZIhvcNAQELBQADggEBALYxUQPwRg+i3yVe 6Q4LuaJdTTJMF/ABHHffAZqAEjkfnzODimRyroGN6l5ixCRokbJ9q2Az7JLSj/Tv y+5O4Tu8P3Z5nFkTPE70JDEw+sFKxHCGdQ8eNf+DlL7Ado+75a8Yug2RM4wHODVh TKfHwj7c7vHnPkv8o8a4DiCQBNZmH6qXwCqLAYdeScrrhUzX04+3ZEq0boIYUTFn FYqsdpKYFwwrQ1njlUu4VDGl02QXD3HYnkbzBzEI7HW7lxfcb2py8xrdIw9bh1fl r+ou4bKctTJ4NOf3TezWT01MEGCgLgI8vdsyVGRUbPrfuHHVyuYARPXmEe89Q34+ fHP/TLM= -----END CERTIFICATE----- ================================================ FILE: internal/converter/doc.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 // Package converter contains various utilities for converting model.Trace // to/from other data modes, like Thrift, or UI JSON. package converter ================================================ FILE: internal/converter/empty_test.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package converter import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestDummy(*testing.T) { // This is a dummy test in the root package. // Without it `go test -v .` prints "testing: warning: no tests to run". } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/converter/thrift/doc.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 // Package thrift allows converting model.Trace to/from various thrift models. package thrift ================================================ FILE: internal/converter/thrift/empty_test.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package thrift import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestDummy(*testing.T) { // This is a dummy test in the root package. // Without it `go test -v .` prints "testing: warning: no tests to run". } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/converter/thrift/jaeger/doc.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 // Package jaeger allows converting model.Trace to/from jaeger.thrift model. package jaeger ================================================ FILE: internal/converter/thrift/jaeger/fixtures/domain_01.json ================================================ { "spans": [ { "traceId": "AAAAAAAAAABSlpqJVVcaPw==", "spanId": "AAAAAABkfZg=", "operationName": "get", "references": [ { "refType": "CHILD_OF", "traceId": "AAAAAAAAAABSlpqJVVcaPw==", "spanId": "AAAAAABoxOM=" } ], "startTime": "2017-01-26T16:46:31.639875-05:00", "duration": "22938000ns", "tags": [ { "key": "http.url", "vType": "STRING", "vStr": "http://127.0.0.1:15598/client_transactions" }, { "key": "span.kind", "vType": "STRING", "vStr": "server" }, { "key": "peer.port", "vType": "INT64", "vInt64": 53931 }, { "key": "someBool", "vType": "BOOL", "vBool": true }, { "key": "someDouble", "vType": "FLOAT64", "vFloat64": 129.8 }, { "key": "peer.service", "vType": "STRING", "vStr": "rtapi" }, { "key": "peer.ipv4", "vType": "INT64", "vInt64": 3224716605 } ], "process": { "serviceName": "api", "tags": [ { "key": "hostname", "vType": "STRING", "vStr": "api246-sjc1" }, { "key": "ip", "vType": "STRING", "vStr": "10.53.69.61" }, { "key": "jaeger.version", "vType": "STRING", "vStr": "Python-3.1.0" } ] }, "logs": [ { "timestamp": "2017-01-26T16:46:31.639875-05:00", "fields": [ { "key": "key1", "vType": "STRING", "vStr": "value1" }, { "key": "key2", "vType": "STRING", "vStr": "value2" } ] }, { "timestamp": "2017-01-26T16:46:31.639875-05:00", "fields": [ { "key": "event", "vType": "STRING", "vStr": "nothing" } ] } ] } ] } ================================================ FILE: internal/converter/thrift/jaeger/fixtures/domain_02.json ================================================ { "spans": [ { "traceId": "AAAAAAAAAABSlpqJVVcaPw==", "spanId": "AAAAAABkfZg=", "operationName": "get", "references": [ { "refType": "CHILD_OF", "traceId": "AAAAAAAAAABSlpqJVVcaPw==", "spanId": "AAAAAABoxOM=" } ], "startTime": "2017-01-26T16:46:31.639875-05:00", "duration": "22938000ns", "tags": [ { "key": "peer.service", "vType": "BINARY", "vBinary": "AAAAAAAAMDk=" } ], "process": { "serviceName": "api" } }, { "traceId": "AAAAAAAAAABSlpqJVVcaPw==", "spanId": "AAAAAABkfZk=", "operationName": "get", "references": [ { "refType": "FOLLOWS_FROM", "traceId": "AAAAAAAAAABSlpqJVVcaPw==", "spanId": "AAAAAABoxOM=" } ], "startTime": "2017-01-26T16:46:31.639875-05:00", "duration": "22938000ns", "process": { "serviceName": "api" } } ] } ================================================ FILE: internal/converter/thrift/jaeger/fixtures/domain_03.json ================================================ { "spans": [ { "traceId": "AAAAAAAAAABSlpqJVVcaPw==", "spanId": "AAAAAABkfZg=", "operationName": "get", "startTime": "2017-01-26T16:46:31.639875-05:00", "duration": "22938000ns", "tags": [ { "key": "http.url", "vType": "STRING", "vStr": "http://127.0.0.1:15598/client_transactions" }, { "key": "span.kind", "vType": "STRING", "vStr": "server" }, { "key": "peer.port", "vType": "INT64", "vInt64": 53931 }, { "key": "someBool", "vType": "BOOL", "vBool": true }, { "key": "someDouble", "vType": "FLOAT64", "vFloat64": 129.8 }, { "key": "peer.service", "vType": "STRING", "vStr": "rtapi" }, { "key": "peer.ipv4", "vType": "INT64", "vInt64": 3224716605 } ], "process": { "serviceName": "api", "tags": [ { "key": "hostname", "vType": "STRING", "vStr": "api246-sjc1" }, { "key": "ip", "vType": "STRING", "vStr": "10.53.69.61" }, { "key": "jaeger.version", "vType": "STRING", "vStr": "Python-3.1.0" } ] }, "logs": [ { "timestamp": "2017-01-26T16:46:31.639875-05:00", "fields": [ { "key": "key1", "vType": "STRING", "vStr": "value1" }, { "key": "key2", "vType": "STRING", "vStr": "value2" } ] }, { "timestamp": "2017-01-26T16:46:31.639875-05:00", "fields": [ { "key": "event", "vType": "STRING", "vStr": "nothing" } ] } ] }, { "traceId": "AAAAAAAAAABSlpqJVVcaPw==", "spanId": "AAAAAABSlHs=", "operationName": "get", "startTime": "2017-01-26T16:46:31.639875-05:00", "duration": "22938000ns", "references": [ { "refType": "CHILD_OF", "traceId": "AAAAAAAAAABSlpqJVVcaPw==", "spanId": "AAAAAABSlHs=" } ], "tags": [ { "key": "http.url", "vType": "STRING", "vStr": "http://127.0.0.1:15598/client_transactions" }, { "key": "span.kind", "vType": "STRING", "vStr": "server" }, { "key": "peer.port", "vType": "INT64", "vInt64": 53931 }, { "key": "someBool", "vType": "BOOL", "vBool": true }, { "key": "someDouble", "vType": "FLOAT64", "vFloat64": 4638770948061370000 }, { "key": "peer.service", "vType": "STRING", "vStr": "rtapi" }, { "key": "peer.ipv4", "vType": "INT64", "vInt64": 3224716605 }, { "key": "some.binary.data", "vType": "BINARY", "vBinary": "c29tZS1iaW5hcnktZGF0YQ==" } ], "process": { "serviceName": "api", "tags": [ { "key": "hostname", "vType": "STRING", "vStr": "api246-sjc1" }, { "key": "ip", "vType": "STRING", "vStr": "10.53.69.61" }, { "key": "jaeger.version", "vType": "STRING", "vStr": "Python-3.1.0" } ] }, "logs": [ { "timestamp": "2017-01-26T16:46:31.639875-05:00", "fields": [ { "key": "key1", "vType": "STRING", "vStr": "value1" }, { "key": "key2", "vType": "STRING", "vStr": "value2" } ] }, { "timestamp": "2017-01-26T16:46:31.639875-05:00", "fields": [ { "key": "event", "vType": "STRING", "vStr": "nothing" } ] } ] } ] } ================================================ FILE: internal/converter/thrift/jaeger/fixtures/thrift_batch_01.json ================================================ { "process": { "serviceName": "api", "tags": [ { "key": "hostname", "vType": "STRING", "vStr": "api246-sjc1" }, { "key": "ip", "vType": "STRING", "vStr": "10.53.69.61" }, { "key": "jaeger.version", "vType": "STRING", "vStr": "Python-3.1.0" } ] }, "spans": [ { "traceIdLow": 5951113872249657919, "spanId": 6585752, "parentSpanId": 6866147, "operationName": "get", "startTime": 1485467191639875, "duration": 22938, "tags": [ { "key": "http.url", "vType": "STRING", "vStr": "http://127.0.0.1:15598/client_transactions" }, { "key": "span.kind", "vType": "STRING", "vStr": "server" }, { "key": "peer.port", "vType": "LONG", "vLong": 53931 }, { "key": "someBool", "vType": "BOOL", "vBool": true }, { "key": "someDouble", "vType": "DOUBLE", "vDouble": 129.8 }, { "key": "peer.service", "vType": "STRING", "vStr": "rtapi" }, { "key": "peer.ipv4", "vType": "LONG", "vLong": 3224716605 } ], "logs": [ { "timestamp": 1485467191639875, "fields": [ { "key": "key1", "vType": "STRING", "vStr": "value1" }, { "key": "key2", "vType": "STRING", "vStr": "value2" } ] }, { "timestamp": 1485467191639875, "fields": [ { "key": "event", "vType": "STRING", "vStr": "nothing" } ] } ] } ] } ================================================ FILE: internal/converter/thrift/jaeger/fixtures/thrift_batch_02.json ================================================ { "process": { "serviceName": "api", "tags": [] }, "spans": [ { "traceIdLow": 5951113872249657919, "spanId": 6585752, "parentSpanId": 6866147, "operationName": "get", "startTime": 1485467191639875, "duration": 22938, "tags": [ { "key": "peer.service", "vType": "BINARY", "vBinary": "AAAAAAAAMDk=" } ] }, { "traceIdLow": 5951113872249657919, "spanId": 6585753, "parentSpanId": 6866147, "operationName": "get", "references": [ { "refType": "FOLLOWS_FROM", "traceIdLow": 5951113872249657919, "spanId": 6866147 } ], "startTime": 1485467191639875, "duration": 22938 } ] } ================================================ FILE: internal/converter/thrift/jaeger/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jaeger import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/converter/thrift/jaeger/sampling_from_domain.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jaeger import ( "errors" "math" "github.com/jaegertracing/jaeger-idl/proto-gen/api_v2" "github.com/jaegertracing/jaeger-idl/thrift-gen/sampling" ) // ConvertSamplingResponseFromDomain converts proto sampling response to its thrift representation. func ConvertSamplingResponseFromDomain(r *api_v2.SamplingStrategyResponse) (*sampling.SamplingStrategyResponse, error) { typ, err := convertStrategyTypeFromDomain(r.GetStrategyType()) if err != nil { return nil, err } rl, err := convertRateLimitingFromDomain(r.GetRateLimitingSampling()) if err != nil { return nil, err } thriftResp := &sampling.SamplingStrategyResponse{ StrategyType: typ, ProbabilisticSampling: convertProbabilisticFromDomain(r.GetProbabilisticSampling()), RateLimitingSampling: rl, OperationSampling: convertPerOperationFromDomain(r.GetOperationSampling()), } return thriftResp, nil } func convertProbabilisticFromDomain(s *api_v2.ProbabilisticSamplingStrategy) *sampling.ProbabilisticSamplingStrategy { if s == nil { return nil } return &sampling.ProbabilisticSamplingStrategy{SamplingRate: s.GetSamplingRate()} } func convertRateLimitingFromDomain(s *api_v2.RateLimitingSamplingStrategy) (*sampling.RateLimitingSamplingStrategy, error) { if s == nil { return nil, nil } if s.MaxTracesPerSecond > math.MaxInt16 { return nil, errors.New("maxTracesPerSecond is higher than int16") } return &sampling.RateLimitingSamplingStrategy{ //nolint:gosec // G115 MaxTracesPerSecond: int16(s.GetMaxTracesPerSecond()), }, nil } func convertPerOperationFromDomain(s *api_v2.PerOperationSamplingStrategies) *sampling.PerOperationSamplingStrategies { if s == nil { return nil } r := &sampling.PerOperationSamplingStrategies{ DefaultSamplingProbability: s.GetDefaultSamplingProbability(), DefaultLowerBoundTracesPerSecond: s.GetDefaultLowerBoundTracesPerSecond(), DefaultUpperBoundTracesPerSecond: &s.DefaultUpperBoundTracesPerSecond, } perOp := s.GetPerOperationStrategies() // Default to empty array so that json.Marshal returns [] instead of null (Issue #3891). r.PerOperationStrategies = make([]*sampling.OperationSamplingStrategy, len(perOp)) for i, k := range perOp { r.PerOperationStrategies[i] = convertOperationFromDomain(k) } return r } func convertOperationFromDomain(s *api_v2.OperationSamplingStrategy) *sampling.OperationSamplingStrategy { if s == nil { return nil } return &sampling.OperationSamplingStrategy{ Operation: s.GetOperation(), ProbabilisticSampling: convertProbabilisticFromDomain(s.GetProbabilisticSampling()), } } func convertStrategyTypeFromDomain(s api_v2.SamplingStrategyType) (sampling.SamplingStrategyType, error) { switch s { case api_v2.SamplingStrategyType_PROBABILISTIC: return sampling.SamplingStrategyType_PROBABILISTIC, nil case api_v2.SamplingStrategyType_RATE_LIMITING: return sampling.SamplingStrategyType_RATE_LIMITING, nil default: return sampling.SamplingStrategyType_PROBABILISTIC, errors.New("could not convert sampling strategy type") } } ================================================ FILE: internal/converter/thrift/jaeger/sampling_from_domain_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jaeger import ( "math" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger-idl/proto-gen/api_v2" "github.com/jaegertracing/jaeger-idl/thrift-gen/sampling" ) func TestConvertStrategyTypeFromDomain(t *testing.T) { tests := []struct { expected sampling.SamplingStrategyType in api_v2.SamplingStrategyType err string }{ {expected: sampling.SamplingStrategyType_PROBABILISTIC, in: api_v2.SamplingStrategyType_PROBABILISTIC}, {expected: sampling.SamplingStrategyType_RATE_LIMITING, in: api_v2.SamplingStrategyType_RATE_LIMITING}, {in: 44, err: "could not convert sampling strategy type"}, } for _, test := range tests { st, err := convertStrategyTypeFromDomain(test.in) if test.err != "" { require.EqualError(t, err, test.err) } else { require.NoError(t, err) assert.Equal(t, test.expected, st) } } } func TestConvertProbabilisticFromDomain(t *testing.T) { tests := []struct { in *api_v2.ProbabilisticSamplingStrategy expected *sampling.ProbabilisticSamplingStrategy }{ {in: &api_v2.ProbabilisticSamplingStrategy{SamplingRate: 21}, expected: &sampling.ProbabilisticSamplingStrategy{SamplingRate: 21}}, {}, } for _, test := range tests { st := convertProbabilisticFromDomain(test.in) assert.Equal(t, test.expected, st) } } func TestConvertRateLimitingFromDomain(t *testing.T) { tests := []struct { in *api_v2.RateLimitingSamplingStrategy expected *sampling.RateLimitingSamplingStrategy err string }{ {in: &api_v2.RateLimitingSamplingStrategy{MaxTracesPerSecond: 21}, expected: &sampling.RateLimitingSamplingStrategy{MaxTracesPerSecond: 21}}, {in: &api_v2.RateLimitingSamplingStrategy{MaxTracesPerSecond: math.MaxInt32}, err: "maxTracesPerSecond is higher than int16"}, {}, } for _, test := range tests { st, err := convertRateLimitingFromDomain(test.in) if test.err != "" { require.EqualError(t, err, test.err) require.Nil(t, st) } else { require.NoError(t, err) assert.Equal(t, test.expected, st) } } } func TestConvertOperationStrategyFromDomain(t *testing.T) { tests := []struct { in *api_v2.OperationSamplingStrategy expected *sampling.OperationSamplingStrategy }{ {in: &api_v2.OperationSamplingStrategy{Operation: "foo"}, expected: &sampling.OperationSamplingStrategy{Operation: "foo"}}, { in: &api_v2.OperationSamplingStrategy{Operation: "foo", ProbabilisticSampling: &api_v2.ProbabilisticSamplingStrategy{SamplingRate: 2}}, expected: &sampling.OperationSamplingStrategy{Operation: "foo", ProbabilisticSampling: &sampling.ProbabilisticSamplingStrategy{SamplingRate: 2}}, }, {}, } for _, test := range tests { o := convertOperationFromDomain(test.in) assert.Equal(t, test.expected, o) } } func TestConvertPerOperationStrategyFromDomain(t *testing.T) { a := 11.2 tests := []struct { in *api_v2.PerOperationSamplingStrategies expected *sampling.PerOperationSamplingStrategies }{ { in: &api_v2.PerOperationSamplingStrategies{ DefaultSamplingProbability: 15.2, DefaultUpperBoundTracesPerSecond: a, DefaultLowerBoundTracesPerSecond: 2, PerOperationStrategies: []*api_v2.OperationSamplingStrategy{{Operation: "fao"}}, }, expected: &sampling.PerOperationSamplingStrategies{ DefaultSamplingProbability: 15.2, DefaultUpperBoundTracesPerSecond: &a, DefaultLowerBoundTracesPerSecond: 2, PerOperationStrategies: []*sampling.OperationSamplingStrategy{{Operation: "fao"}}, }, }, { in: &api_v2.PerOperationSamplingStrategies{DefaultSamplingProbability: 15.2, DefaultUpperBoundTracesPerSecond: a, DefaultLowerBoundTracesPerSecond: 2}, expected: &sampling.PerOperationSamplingStrategies{ DefaultSamplingProbability: 15.2, DefaultUpperBoundTracesPerSecond: &a, DefaultLowerBoundTracesPerSecond: 2, PerOperationStrategies: []*sampling.OperationSamplingStrategy{}, }, }, } for _, test := range tests { o := convertPerOperationFromDomain(test.in) assert.Equal(t, test.expected, o) } } func TestConvertSamplingResponseFromDomain(t *testing.T) { tests := []struct { in *api_v2.SamplingStrategyResponse expected *sampling.SamplingStrategyResponse err string }{ {in: &api_v2.SamplingStrategyResponse{StrategyType: 55}, err: "could not convert sampling strategy type"}, { in: &api_v2.SamplingStrategyResponse{StrategyType: api_v2.SamplingStrategyType_PROBABILISTIC, RateLimitingSampling: &api_v2.RateLimitingSamplingStrategy{MaxTracesPerSecond: math.MaxInt32}}, err: "maxTracesPerSecond is higher than int16", }, {in: &api_v2.SamplingStrategyResponse{StrategyType: api_v2.SamplingStrategyType_PROBABILISTIC}, expected: &sampling.SamplingStrategyResponse{StrategyType: sampling.SamplingStrategyType_PROBABILISTIC}}, } for _, test := range tests { r, err := ConvertSamplingResponseFromDomain(test.in) if test.err != "" { require.EqualError(t, err, test.err) require.Nil(t, r) } else { require.NoError(t, err) assert.Equal(t, test.expected, r) } } } ================================================ FILE: internal/converter/thrift/jaeger/sampling_to_domain.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jaeger import ( "errors" "github.com/jaegertracing/jaeger-idl/proto-gen/api_v2" "github.com/jaegertracing/jaeger-idl/thrift-gen/sampling" ) // ConvertSamplingResponseToDomain converts thrift sampling response to its proto representation. func ConvertSamplingResponseToDomain(r *sampling.SamplingStrategyResponse) (*api_v2.SamplingStrategyResponse, error) { if r == nil { return nil, nil } t, err := convertStrategyTypeToDomain(r.GetStrategyType()) if err != nil { return nil, err } response := &api_v2.SamplingStrategyResponse{ StrategyType: t, ProbabilisticSampling: convertProbabilisticToDomain(r.GetProbabilisticSampling()), RateLimitingSampling: convertRateLimitingToDomain(r.GetRateLimitingSampling()), OperationSampling: convertPerOperationToDomain(r.GetOperationSampling()), } return response, nil } func convertRateLimitingToDomain(s *sampling.RateLimitingSamplingStrategy) *api_v2.RateLimitingSamplingStrategy { if s == nil { return nil } return &api_v2.RateLimitingSamplingStrategy{MaxTracesPerSecond: int32(s.GetMaxTracesPerSecond())} } func convertProbabilisticToDomain(s *sampling.ProbabilisticSamplingStrategy) *api_v2.ProbabilisticSamplingStrategy { if s == nil { return nil } return &api_v2.ProbabilisticSamplingStrategy{SamplingRate: s.GetSamplingRate()} } func convertPerOperationToDomain(s *sampling.PerOperationSamplingStrategies) *api_v2.PerOperationSamplingStrategies { if s == nil { return nil } poss := make([]*api_v2.OperationSamplingStrategy, len(s.PerOperationStrategies)) for i, pos := range s.PerOperationStrategies { poss[i] = &api_v2.OperationSamplingStrategy{ Operation: pos.Operation, ProbabilisticSampling: convertProbabilisticToDomain(pos.GetProbabilisticSampling()), } } return &api_v2.PerOperationSamplingStrategies{ DefaultSamplingProbability: s.GetDefaultSamplingProbability(), DefaultUpperBoundTracesPerSecond: s.GetDefaultUpperBoundTracesPerSecond(), DefaultLowerBoundTracesPerSecond: s.GetDefaultLowerBoundTracesPerSecond(), PerOperationStrategies: poss, } } func convertStrategyTypeToDomain(t sampling.SamplingStrategyType) (api_v2.SamplingStrategyType, error) { switch t { case sampling.SamplingStrategyType_PROBABILISTIC: return api_v2.SamplingStrategyType_PROBABILISTIC, nil case sampling.SamplingStrategyType_RATE_LIMITING: return api_v2.SamplingStrategyType_RATE_LIMITING, nil default: return api_v2.SamplingStrategyType_PROBABILISTIC, errors.New("could not convert sampling strategy type") } } ================================================ FILE: internal/converter/thrift/jaeger/sampling_to_domain_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jaeger import ( "errors" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger-idl/proto-gen/api_v2" "github.com/jaegertracing/jaeger-idl/thrift-gen/sampling" ) func TestConvertStrategyTypeToDomain(t *testing.T) { tests := []struct { in sampling.SamplingStrategyType expected api_v2.SamplingStrategyType err error }{ {in: sampling.SamplingStrategyType_PROBABILISTIC, expected: api_v2.SamplingStrategyType_PROBABILISTIC}, {in: sampling.SamplingStrategyType_RATE_LIMITING, expected: api_v2.SamplingStrategyType_RATE_LIMITING}, {in: 44, err: errors.New("could not convert sampling strategy type")}, } for _, test := range tests { st, err := convertStrategyTypeToDomain(test.in) if test.err != nil { require.EqualError(t, test.err, err.Error()) } else { require.NoError(t, err) assert.Equal(t, test.expected, st) } } } func TestConvertProbabilisticToDomain(t *testing.T) { tests := []struct { expected *api_v2.ProbabilisticSamplingStrategy in *sampling.ProbabilisticSamplingStrategy }{ {expected: &api_v2.ProbabilisticSamplingStrategy{SamplingRate: 21}, in: &sampling.ProbabilisticSamplingStrategy{SamplingRate: 21}}, {}, } for _, test := range tests { st := convertProbabilisticToDomain(test.in) assert.Equal(t, test.expected, st) } } func TestConvertRateLimitingToDomain(t *testing.T) { tests := []struct { expected *api_v2.RateLimitingSamplingStrategy in *sampling.RateLimitingSamplingStrategy }{ {expected: &api_v2.RateLimitingSamplingStrategy{MaxTracesPerSecond: 21}, in: &sampling.RateLimitingSamplingStrategy{MaxTracesPerSecond: 21}}, {}, } for _, test := range tests { st := convertRateLimitingToDomain(test.in) assert.Equal(t, test.expected, st) } } func TestConvertPerOperationStrategyToDomain(t *testing.T) { a := 11.2 tests := []struct { expected *api_v2.PerOperationSamplingStrategies in *sampling.PerOperationSamplingStrategies }{ { expected: &api_v2.PerOperationSamplingStrategies{ DefaultSamplingProbability: 15.2, DefaultUpperBoundTracesPerSecond: a, DefaultLowerBoundTracesPerSecond: 2, PerOperationStrategies: []*api_v2.OperationSamplingStrategy{{Operation: "fao"}}, }, in: &sampling.PerOperationSamplingStrategies{ DefaultSamplingProbability: 15.2, DefaultUpperBoundTracesPerSecond: &a, DefaultLowerBoundTracesPerSecond: 2, PerOperationStrategies: []*sampling.OperationSamplingStrategy{{Operation: "fao"}}, }, }, {}, } for _, test := range tests { o := convertPerOperationToDomain(test.in) assert.Equal(t, test.expected, o) } } func TestConvertSamplingResponseToDomain(t *testing.T) { tests := []struct { expected *api_v2.SamplingStrategyResponse in *sampling.SamplingStrategyResponse err string }{ {in: &sampling.SamplingStrategyResponse{StrategyType: 55}, err: "could not convert sampling strategy type"}, {expected: &api_v2.SamplingStrategyResponse{StrategyType: api_v2.SamplingStrategyType_PROBABILISTIC}, in: &sampling.SamplingStrategyResponse{StrategyType: sampling.SamplingStrategyType_PROBABILISTIC}}, {}, } for _, test := range tests { r, err := ConvertSamplingResponseToDomain(test.in) if test.err != "" { require.EqualError(t, err, test.err) require.Nil(t, r) } else { require.NoError(t, err) assert.Equal(t, test.expected, r) } } } ================================================ FILE: internal/converter/thrift/jaeger/to_domain.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package jaeger import ( "fmt" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger-idl/thrift-gen/jaeger" ) // ToDomain transforms a set of spans and a process in jaeger.thrift format into a slice of model.Span. // A valid []*model.Span is always returned, even when there are errors. // Errors are presented as tags on spans func ToDomain(jSpans []*jaeger.Span, jProcess *jaeger.Process) []*model.Span { return toDomain{}.ToDomain(jSpans, jProcess) } // ToDomainSpan transforms a span in jaeger.thrift format into model.Span. // A valid model.Span is always returned, even when there are errors. // Errors are presented as tags on spans func ToDomainSpan(jSpan *jaeger.Span, jProcess *jaeger.Process) *model.Span { return toDomain{}.ToDomainSpan(jSpan, jProcess) } // ToDomainProcess transforms a process in jaeger.thrift format to model.Span. func ToDomainProcess(jProcess *jaeger.Process) *model.Process { return toDomain{}.getProcess(jProcess) } type toDomain struct{} func (td toDomain) ToDomain(jSpans []*jaeger.Span, jProcess *jaeger.Process) []*model.Span { spans := make([]*model.Span, len(jSpans)) mProcess := td.getProcess(jProcess) for i, jSpan := range jSpans { spans[i] = td.transformSpan(jSpan, mProcess) } return spans } func (td toDomain) ToDomainSpan(jSpan *jaeger.Span, jProcess *jaeger.Process) *model.Span { mProcess := td.getProcess(jProcess) return td.transformSpan(jSpan, mProcess) } func (td toDomain) transformSpan(jSpan *jaeger.Span, mProcess *model.Process) *model.Span { //nolint:gosec // G115 traceID := model.NewTraceID(uint64(jSpan.TraceIdHigh), uint64(jSpan.TraceIdLow)) // allocate extra space for future append operation tags := td.getTags(jSpan.Tags, 1) refs := td.getReferences(jSpan.References) // We no longer store ParentSpanID in the domain model, but the data in Thrift model // might still have these IDs without representing them in the References, so we // convert it back into child-of reference. if jSpan.ParentSpanId != 0 { //nolint:gosec // G115 parentSpanID := model.NewSpanID(uint64(jSpan.ParentSpanId)) refs = model.MaybeAddParentSpanID(traceID, parentSpanID, refs) } return &model.Span{ TraceID: traceID, //nolint:gosec // G115 SpanID: model.NewSpanID(uint64(jSpan.SpanId)), OperationName: jSpan.OperationName, References: refs, //nolint:gosec // G115 Flags: model.Flags(jSpan.Flags), //nolint:gosec // G115 StartTime: model.EpochMicrosecondsAsTime(uint64(jSpan.StartTime)), //nolint:gosec // G115 Duration: model.MicrosecondsAsDuration(uint64(jSpan.Duration)), Tags: tags, Logs: td.getLogs(jSpan.Logs), Process: mProcess, } } func (toDomain) getReferences(jRefs []*jaeger.SpanRef) []model.SpanRef { if len(jRefs) == 0 { return nil } mRefs := make([]model.SpanRef, len(jRefs)) for idx, jRef := range jRefs { mRefs[idx] = model.SpanRef{ //nolint:gosec // G115 RefType: model.SpanRefType(int(jRef.RefType)), //nolint:gosec // G115 TraceID: model.NewTraceID(uint64(jRef.TraceIdHigh), uint64(jRef.TraceIdLow)), //nolint:gosec // G115 SpanID: model.NewSpanID(uint64(jRef.SpanId)), } } return mRefs } // getProcess takes a jaeger.thrift process and produces a model.Process. // Any errors are presented as tags func (td toDomain) getProcess(jProcess *jaeger.Process) *model.Process { if jProcess == nil { return nil } tags := td.getTags(jProcess.Tags, 0) return &model.Process{ Tags: tags, ServiceName: jProcess.ServiceName, } } // convert the jaeger.Tag slice to domain KeyValue slice // zipkin/to_domain.go does not give a default slice size since it has to filter annotations, jaeger conversion is more predictable // thus to avoid future full array copy when using append, pre-allocate extra space as an optimization func (td toDomain) getTags(tags []*jaeger.Tag, extraSpace int) model.KeyValues { if len(tags) == 0 { return nil } retMe := make(model.KeyValues, len(tags), len(tags)+extraSpace) for i, tag := range tags { retMe[i] = td.getTag(tag) } return retMe } func (toDomain) getTag(tag *jaeger.Tag) model.KeyValue { switch tag.VType { case jaeger.TagType_BOOL: return model.Bool(tag.Key, tag.GetVBool()) case jaeger.TagType_BINARY: return model.Binary(tag.Key, tag.GetVBinary()) case jaeger.TagType_DOUBLE: return model.Float64(tag.Key, tag.GetVDouble()) case jaeger.TagType_LONG: return model.Int64(tag.Key, tag.GetVLong()) case jaeger.TagType_STRING: return model.String(tag.Key, tag.GetVStr()) default: return model.String(tag.Key, fmt.Sprintf("Unknown VType: %+v", tag)) } } func (td toDomain) getLogs(logs []*jaeger.Log) []model.Log { if len(logs) == 0 { return nil } retMe := make([]model.Log, len(logs)) for i, log := range logs { retMe[i] = model.Log{ //nolint:gosec // G115 Timestamp: model.EpochMicrosecondsAsTime(uint64(log.Timestamp)), Fields: td.getTags(log.Fields, 0), } } return retMe } ================================================ FILE: internal/converter/thrift/jaeger/to_domain_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package jaeger import ( "encoding/json" "fmt" "os" "testing" "time" "github.com/gogo/protobuf/jsonpb" "github.com/gogo/protobuf/proto" "github.com/kr/pretty" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger-idl/thrift-gen/jaeger" ) const NumberOfFixtures = 2 func TestToDomain(t *testing.T) { for i := 1; i <= NumberOfFixtures; i++ { in := fmt.Sprintf("fixtures/thrift_batch_%02d.json", i) out := fmt.Sprintf("fixtures/domain_%02d.json", i) mSpans := loadSpans(t, out) for _, s := range mSpans { s.NormalizeTimestamps() } jBatch := loadBatch(t, in) name := in + " -> " + out + " : " + jBatch.Process.ServiceName t.Run(name, func(t *testing.T) { actualSpans := ToDomain(jBatch.Spans, jBatch.Process) for _, s := range actualSpans { s.NormalizeTimestamps() } if !assert.Equal(t, mSpans, actualSpans) { for _, err := range pretty.Diff(mSpans, actualSpans) { t.Log(err) } out, err := json.Marshal(actualSpans) require.NoError(t, err) t.Logf("Actual trace %v: %s", i, string(out)) } }) if i == 1 { t.Run("ToDomainSpan", func(t *testing.T) { jSpan := jBatch.Spans[0] mSpan := ToDomainSpan(jSpan, jBatch.Process) mSpan.NormalizeTimestamps() assert.Equal(t, mSpans[0], mSpan) }) } } } func loadSpans(t *testing.T, file string) []*model.Span { var trace model.Trace loadJSONPB(t, file, &trace) return trace.Spans } func loadJSONPB(t *testing.T, fileName string, obj proto.Message) { jsonFile, err := os.Open(fileName) require.NoError(t, err, "Failed to open json fixture file %s", fileName) require.NoError(t, jsonpb.Unmarshal(jsonFile, obj), fileName) } func loadBatch(t *testing.T, file string) *jaeger.Batch { var batch jaeger.Batch loadJSON(t, file, &batch) return &batch } func loadJSON(t *testing.T, fileName string, obj any) { jsonFile, err := os.Open(fileName) require.NoError(t, err, "Failed to load json fixture file %s", fileName) jsonParser := json.NewDecoder(jsonFile) err = jsonParser.Decode(obj) require.NoError(t, err, "Failed to parse json fixture file %s", fileName) } func TestUnknownJaegerType(t *testing.T) { mkv := toDomain{}.getTag(&jaeger.Tag{ VType: 999, Key: "sneh", }) expected := model.String("sneh", "Unknown VType: Tag({Key:sneh VType: VStr: VDouble: VBool: VLong: VBinary:[]})") assert.Equal(t, expected, mkv) } func TestToDomain_ToDomainProcess(t *testing.T) { p := ToDomainProcess(&jaeger.Process{ServiceName: "foo", Tags: []*jaeger.Tag{{Key: "foo", VType: jaeger.TagType_BOOL}}}) assert.Equal(t, &model.Process{ServiceName: "foo", Tags: []model.KeyValue{{Key: "foo", VType: model.BoolType}}}, p) } func TestToDomain_ToDomainSpanProcessNull(t *testing.T) { tm := time.Unix(158, 0) s := ToDomainSpan(&jaeger.Span{OperationName: "foo", StartTime: int64(model.TimeAsEpochMicroseconds(tm))}, nil) assert.Equal(t, &model.Span{OperationName: "foo", StartTime: tm.UTC()}, s) } ================================================ FILE: internal/distributedlock/empty_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package distributedlock import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/distributedlock/interface.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package distributedlock import ( "time" ) // Lock uses distributed lock for control of a resource. type Lock interface { // Acquire acquires a lease of duration ttl around a given resource. In case of an error, // acquired is meaningless. Acquire(resource string, ttl time.Duration) (acquired bool, err error) // Forfeit forfeits a lease around a given resource. In case of an error, // forfeited is meaningless. Forfeit(resource string) (forfeited bool, err error) } ================================================ FILE: internal/distributedlock/mocks/mocks.go ================================================ // Copyright (c) The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // // Run 'make generate-mocks' to regenerate. // Code generated by mockery; DO NOT EDIT. // github.com/vektra/mockery // template: testify package mocks import ( "time" mock "github.com/stretchr/testify/mock" ) // NewLock creates a new instance of Lock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewLock(t interface { mock.TestingT Cleanup(func()) }) *Lock { mock := &Lock{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // Lock is an autogenerated mock type for the Lock type type Lock struct { mock.Mock } type Lock_Expecter struct { mock *mock.Mock } func (_m *Lock) EXPECT() *Lock_Expecter { return &Lock_Expecter{mock: &_m.Mock} } // Acquire provides a mock function for the type Lock func (_mock *Lock) Acquire(resource string, ttl time.Duration) (bool, error) { ret := _mock.Called(resource, ttl) if len(ret) == 0 { panic("no return value specified for Acquire") } var r0 bool var r1 error if returnFunc, ok := ret.Get(0).(func(string, time.Duration) (bool, error)); ok { return returnFunc(resource, ttl) } if returnFunc, ok := ret.Get(0).(func(string, time.Duration) bool); ok { r0 = returnFunc(resource, ttl) } else { r0 = ret.Get(0).(bool) } if returnFunc, ok := ret.Get(1).(func(string, time.Duration) error); ok { r1 = returnFunc(resource, ttl) } else { r1 = ret.Error(1) } return r0, r1 } // Lock_Acquire_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Acquire' type Lock_Acquire_Call struct { *mock.Call } // Acquire is a helper method to define mock.On call // - resource string // - ttl time.Duration func (_e *Lock_Expecter) Acquire(resource interface{}, ttl interface{}) *Lock_Acquire_Call { return &Lock_Acquire_Call{Call: _e.mock.On("Acquire", resource, ttl)} } func (_c *Lock_Acquire_Call) Run(run func(resource string, ttl time.Duration)) *Lock_Acquire_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } var arg1 time.Duration if args[1] != nil { arg1 = args[1].(time.Duration) } run( arg0, arg1, ) }) return _c } func (_c *Lock_Acquire_Call) Return(acquired bool, err error) *Lock_Acquire_Call { _c.Call.Return(acquired, err) return _c } func (_c *Lock_Acquire_Call) RunAndReturn(run func(resource string, ttl time.Duration) (bool, error)) *Lock_Acquire_Call { _c.Call.Return(run) return _c } // Forfeit provides a mock function for the type Lock func (_mock *Lock) Forfeit(resource string) (bool, error) { ret := _mock.Called(resource) if len(ret) == 0 { panic("no return value specified for Forfeit") } var r0 bool var r1 error if returnFunc, ok := ret.Get(0).(func(string) (bool, error)); ok { return returnFunc(resource) } if returnFunc, ok := ret.Get(0).(func(string) bool); ok { r0 = returnFunc(resource) } else { r0 = ret.Get(0).(bool) } if returnFunc, ok := ret.Get(1).(func(string) error); ok { r1 = returnFunc(resource) } else { r1 = ret.Error(1) } return r0, r1 } // Lock_Forfeit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Forfeit' type Lock_Forfeit_Call struct { *mock.Call } // Forfeit is a helper method to define mock.On call // - resource string func (_e *Lock_Expecter) Forfeit(resource interface{}) *Lock_Forfeit_Call { return &Lock_Forfeit_Call{Call: _e.mock.On("Forfeit", resource)} } func (_c *Lock_Forfeit_Call) Run(run func(resource string)) *Lock_Forfeit_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *Lock_Forfeit_Call) Return(forfeited bool, err error) *Lock_Forfeit_Call { _c.Call.Return(forfeited, err) return _c } func (_c *Lock_Forfeit_Call) RunAndReturn(run func(resource string) (bool, error)) *Lock_Forfeit_Call { _c.Call.Return(run) return _c } ================================================ FILE: internal/fswatcher/fswatcher.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package fswatcher import ( "crypto/sha256" "encoding/hex" "errors" "io" "os" "path" "path/filepath" "sync" "github.com/fsnotify/fsnotify" "go.uber.org/zap" ) type FSWatcher struct { watcher *fsnotify.Watcher logger *zap.Logger fileHashContentMap map[string]string onChange func() mu sync.RWMutex } // FSWatcher waits for notifications of changes in the watched directories // and attempts to reload all files that changed. // // Write and Rename events indicate that some files might have changed and reload might be necessary. // Remove event indicates that the file was deleted and we should write a warn to log. // // Reasoning: // // Write event is sent if the file content is rewritten. // // Usually files are not rewritten, but they are updated by swapping them with new // ones by calling Rename. That avoids files being read while they are not yet // completely written but it also means that inotify on file level will not work: // watch is invalidated when the old file is deleted. // // If reading from Kubernetes Secret volumes the target files are symbolic links // to files in a different directory. That directory is swapped with a new one, // while the symbolic links remain the same. This guarantees atomic swap for all // files at once, but it also means any Rename event in the directory might // indicate that the files were replaced, even if event.Name is not any of the // files we are monitoring. We check the hashes of the files to detect if they // were really changed. func New(filepaths []string, onChange func(), logger *zap.Logger) (*FSWatcher, error) { watcher, err := fsnotify.NewWatcher() if err != nil { return nil, err } w := &FSWatcher{ watcher: watcher, logger: logger, fileHashContentMap: make(map[string]string), onChange: onChange, } if err = w.setupWatchedPaths(filepaths); err != nil { err = errors.Join(err, w.Close()) return nil, err } go w.watch() return w, nil } func (w *FSWatcher) setupWatchedPaths(filepaths []string) error { uniqueDirs := make(map[string]bool) for _, p := range filepaths { if p == "" { continue } h, err := hashFile(p) if err != nil { return err } w.fileHashContentMap[p] = h dir := path.Dir(p) if _, ok := uniqueDirs[dir]; !ok { if err := w.watcher.Add(dir); err != nil { return err } uniqueDirs[dir] = true } } return nil } // Watch watches for Events and Errors of files. // Each time an Event happen, all the files are checked for content change. // If a file's content changes, its hashed content is updated and // onChange is invoked after all file checks. func (w *FSWatcher) watch() { for { select { case event, ok := <-w.watcher.Events: if !ok { return } w.logger.Info("Received event", zap.String("event", event.String())) var changed bool w.mu.Lock() for file, hash := range w.fileHashContentMap { fileChanged, newHash := w.isModified(file, hash) if fileChanged { changed = fileChanged w.fileHashContentMap[file] = newHash } } w.mu.Unlock() if changed { w.onChange() } case err, ok := <-w.watcher.Errors: if !ok { return } w.logger.Error("fsnotifier reported an error", zap.Error(err)) } } } // Close closes the watcher. func (w *FSWatcher) Close() error { return w.watcher.Close() } // isModified returns true if the file has been modified since the last check. func (w *FSWatcher) isModified(filePathName string, previousHash string) (bool, string) { hash, err := hashFile(filePathName) if err != nil { w.logger.Warn("Unable to read the file", zap.Error(err)) return true, "" } return previousHash != hash, hash } // hashFile returns the SHA256 hash of the file. func hashFile(file string) (string, error) { f, err := os.Open(filepath.Clean(file)) //nolint:gosec // G703 if err != nil { return "", err } defer f.Close() h := sha256.New() if _, err := io.Copy(h, f); err != nil { return "", err } return hex.EncodeToString(h.Sum(nil)), nil } ================================================ FILE: internal/fswatcher/fswatcher_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package fswatcher import ( "fmt" "os" "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest/observer" "github.com/jaegertracing/jaeger/internal/testutils" ) func createTestFiles(t *testing.T) (file1 string, file2 string, file3 string) { testDir1 := t.TempDir() file1 = filepath.Join(testDir1, "test1.doc") err := os.WriteFile(file1, []byte("test data"), 0o600) require.NoError(t, err) file2 = filepath.Join(testDir1, "test2.doc") err = os.WriteFile(file2, []byte("test data"), 0o600) require.NoError(t, err) testDir2 := t.TempDir() file3 = filepath.Join(testDir2, "test3.doc") err = os.WriteFile(file3, []byte("test data"), 0o600) require.NoError(t, err) return file1, file2, file3 } func TestFSWatcherAddFiles(t *testing.T) { file1, file2, file3 := createTestFiles(t) // Add one unreadable file _, err := New([]string{"invalid-file-name"}, nil, nil) require.Error(t, err) // Add one readable file w, err := New([]string{file1}, nil, nil) require.NoError(t, err) assert.IsType(t, &FSWatcher{}, w) require.NoError(t, w.Close()) // Add one empty-name file and one readable file w, err = New([]string{"", file1}, nil, nil) require.NoError(t, err) assert.IsType(t, &FSWatcher{}, w) require.NoError(t, w.Close()) // Add one readable file and one unreadable file _, err = New([]string{file1, "invalid-file-name"}, nil, nil) require.Error(t, err) // Add two readable files from one dir w, err = New([]string{file1, file2}, nil, nil) require.NoError(t, err) assert.IsType(t, &FSWatcher{}, w) require.NoError(t, w.Close()) // Add two readable files from two different repos w, err = New([]string{file1, file3}, nil, nil) require.NoError(t, err) assert.IsType(t, &FSWatcher{}, w) require.NoError(t, w.Close()) } func TestFSWatcherWithMultipleFiles(t *testing.T) { tempDir := t.TempDir() testFile1, err := os.Create(tempDir + "test-file-1") require.NoError(t, err) defer testFile1.Close() testFile2, err := os.Create(tempDir + "test-file-2") require.NoError(t, err) defer testFile2.Close() _, err = testFile1.WriteString("test content 1") require.NoError(t, err) _, err = testFile2.WriteString("test content 2") require.NoError(t, err) zcore, logObserver := observer.New(zapcore.InfoLevel) logger := zap.New(zcore) onChange := func() { logger.Info("Change happens") } w, err := New([]string{testFile1.Name(), testFile2.Name()}, onChange, logger) require.NoError(t, err) require.IsType(t, &FSWatcher{}, w) defer w.Close() // Test Write event testFile1.WriteString(" changed") testFile2.WriteString(" changed") assertLogs(t, func() bool { return logObserver.FilterMessage("Received event").Len() > 0 }, "Unable to locate 'Received event' in log. All logs: %v", logObserver) assertLogs(t, func() bool { return logObserver.FilterMessage("Change happens").Len() > 0 }, "Unable to locate 'Change happens' in log. All logs: %v", logObserver) // Wait until the watcher has processed events for BOTH files, not just one. // The watcher may see the first file's event before the second write completes, // storing a stale hash for the second file until a subsequent event arrives. assert.Eventuallyf(t, func() bool { h1, _ := hashFile(testFile1.Name()) h2, _ := hashFile(testFile2.Name()) w.mu.RLock() defer w.mu.RUnlock() return w.fileHashContentMap[testFile1.Name()] == h1 && w.fileHashContentMap[testFile2.Name()] == h2 }, 10*time.Second, 10*time.Millisecond, "watcher hashes not updated for both files") // Test Remove event os.Remove(testFile1.Name()) os.Remove(testFile2.Name()) assertLogs(t, func() bool { return logObserver.FilterMessage("Received event").Len() > 0 }, "Unable to locate 'Received event' in log. All logs: %v", logObserver) assertLogs(t, func() bool { return logObserver.FilterMessage("Unable to read the file").Len() >= 2 // Check for multiple occurrences }, "Unable to locate expected 'Unable to read the file' entries in log. All logs: %v", logObserver) } func TestFSWatcherWithSymlinkAndRepoChanges(t *testing.T) { testDir := t.TempDir() err := os.Symlink("..timestamp-1", filepath.Join(testDir, "..data")) require.NoError(t, err) err = os.Symlink(filepath.Join("..data", "test.doc"), filepath.Join(testDir, "test.doc")) require.NoError(t, err) timestamp1Dir := filepath.Join(testDir, "..timestamp-1") createTimestampDir(t, timestamp1Dir) zcore, logObserver := observer.New(zapcore.InfoLevel) logger := zap.New(zcore) onChange := func() {} w, err := New([]string{filepath.Join(testDir, "test.doc")}, onChange, logger) require.NoError(t, err) require.IsType(t, &FSWatcher{}, w) defer w.Close() timestamp2Dir := filepath.Join(testDir, "..timestamp-2") createTimestampDir(t, timestamp2Dir) err = os.Symlink("..timestamp-2", filepath.Join(testDir, "..data_tmp")) require.NoError(t, err) os.Rename(filepath.Join(testDir, "..data_tmp"), filepath.Join(testDir, "..data")) require.NoError(t, err) err = os.RemoveAll(timestamp1Dir) require.NoError(t, err) assertLogs(t, func() bool { return logObserver.FilterMessage("Received event").Len() > 0 }, "Unable to locate 'Received event' in log. All logs: %v", logObserver) byteData, err := os.ReadFile(filepath.Join(testDir, "test.doc")) require.NoError(t, err) assert.Equal(t, byteData, []byte("test data")) timestamp3Dir := filepath.Join(testDir, "..timestamp-3") createTimestampDir(t, timestamp3Dir) err = os.Symlink("..timestamp-3", filepath.Join(testDir, "..data_tmp")) require.NoError(t, err) os.Rename(filepath.Join(testDir, "..data_tmp"), filepath.Join(testDir, "..data")) require.NoError(t, err) err = os.RemoveAll(timestamp2Dir) require.NoError(t, err) assertLogs(t, func() bool { return logObserver.FilterMessage("Received event").Len() > 0 }, "Unable to locate 'Received event' in log. All logs: %v", logObserver) byteData, err = os.ReadFile(filepath.Join(testDir, "test.doc")) require.NoError(t, err) assert.Equal(t, byteData, []byte("test data")) } func createTimestampDir(t *testing.T, dir string) { t.Helper() err := os.MkdirAll(dir, 0o700) require.NoError(t, err) err = os.WriteFile(filepath.Join(dir, "test.doc"), []byte("test data"), 0o600) require.NoError(t, err) } type delayedFormat struct { fn func() any } func (df delayedFormat) String() string { return fmt.Sprintf("%v", df.fn()) } func assertLogs(t *testing.T, f func() bool, errorMsg string, logObserver *observer.ObservedLogs) { assert.Eventuallyf(t, f, 10*time.Second, 10*time.Millisecond, errorMsg, delayedFormat{ fn: func() any { return logObserver.All() }, }, ) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/gogocodec/codec.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package gogocodec import ( "reflect" "strings" "github.com/gogo/protobuf/jsonpb" gogoproto "github.com/gogo/protobuf/proto" "google.golang.org/grpc/encoding" "google.golang.org/grpc/encoding/proto" "google.golang.org/grpc/mem" ) const ( jaegerProtoGenPkgPath = "github.com/jaegertracing/jaeger-idl/proto-gen" jaegerModelPkgPath = "github.com/jaegertracing/jaeger-idl/model/v1" jaegerProtoGenPkgPathOld = "github.com/jaegertracing/jaeger/internal/proto-gen" jaegerModelPkgPathOld = "github.com/jaegertracing/jaeger-idl/model/v1" ) var defaultCodec encoding.CodecV2 // CustomType is an interface that Gogo expects custom types to implement. // https://github.com/gogo/protobuf/blob/master/custom_types.md type CustomType interface { Marshal() ([]byte, error) MarshalTo(data []byte) (n int, err error) Unmarshal(data []byte) error gogoproto.Sizer jsonpb.JSONPBMarshaler jsonpb.JSONPBUnmarshaler } func init() { defaultCodec = encoding.GetCodecV2(proto.Name) defaultCodec.Name() // ensure it's not nil encoding.RegisterCodecV2(newCodec()) } // gogoCodec forces the use of gogo proto marshalling/unmarshalling for // Jaeger proto types (package jaeger/gen-proto). type gogoCodec struct{} var _ encoding.CodecV2 = (*gogoCodec)(nil) func newCodec() *gogoCodec { return &gogoCodec{} } // Name implements encoding.Codec func (*gogoCodec) Name() string { return proto.Name } // Marshal implements encoding.Codec func (*gogoCodec) Marshal(v any) (mem.BufferSlice, error) { t := reflect.TypeOf(v) elem := t.Elem() // use gogo proto only for Jaeger types if useGogo(elem) { bytes, err := gogoproto.Marshal(v.(gogoproto.Message)) return mem.BufferSlice{mem.SliceBuffer(bytes)}, err } return defaultCodec.Marshal(v) } // Unmarshal implements encoding.Codec func (*gogoCodec) Unmarshal(data mem.BufferSlice, v any) error { t := reflect.TypeOf(v) elem := t.Elem() // only for collections // use gogo proto only for Jaeger types if useGogo(elem) { return gogoproto.Unmarshal(data.Materialize(), v.(gogoproto.Message)) } return defaultCodec.Unmarshal(data, v) } func useGogo(t reflect.Type) bool { if t == nil { return false } pkg := t.PkgPath() if strings.HasPrefix(pkg, jaegerProtoGenPkgPath) { return true } if strings.HasPrefix(pkg, jaegerModelPkgPath) { return true } if strings.HasPrefix(pkg, jaegerProtoGenPkgPathOld) { return true } if strings.HasPrefix(pkg, jaegerModelPkgPathOld) { return true } return false } ================================================ FILE: internal/gogocodec/codec_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package gogocodec import ( "os" "reflect" "testing" "github.com/gogo/protobuf/jsonpb" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc/mem" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestCodecMarshallAndUnmarshall_jaeger_type(t *testing.T) { c := newCodec() s1 := &model.Span{OperationName: "foo", TraceID: model.NewTraceID(1, 2)} data, err := c.Marshal(s1) require.NoError(t, err) s2 := &model.Span{} err = c.Unmarshal(data, s2) require.NoError(t, err) assert.Equal(t, s1, s2) } func TestCodecMarshallAndUnmarshall_no_jaeger_type(t *testing.T) { c := newCodec() msg1 := ×tamppb.Timestamp{Seconds: 42, Nanos: 24} data, err := c.Marshal(msg1) require.NoError(t, err) msg2 := ×tamppb.Timestamp{} err = c.Unmarshal(data, msg2) require.NoError(t, err) // Marshal function initializes some internal fields in msg1, like sizeCache. // To ensure the final assert.Equal, do a dummy marshal call on msg2. _, err = c.Marshal(msg2) require.NoError(t, err) assert.Equal(t, msg1, msg2) } func TestWireCompatibility(t *testing.T) { c := newCodec() s1 := &model.Span{OperationName: "foo", TraceID: model.NewTraceID(1, 2)} data, err := c.Marshal(s1) require.NoError(t, err) var goprotoMessage emptypb.Empty err = proto.Unmarshal(data.Materialize(), &goprotoMessage) require.NoError(t, err) data2, err := proto.Marshal(&goprotoMessage) require.NoError(t, err) s2 := &model.Span{} err = c.Unmarshal(mem.BufferSlice{mem.SliceBuffer(data2)}, s2) require.NoError(t, err) assert.Equal(t, s1, s2) } func TestUseGogo(t *testing.T) { assert.False(t, useGogo(nil)) assert.True(t, useGogo(reflect.TypeFor[model.Span]())) } func BenchmarkCodecUnmarshal25Spans(b *testing.B) { const fileName = "../../internal/converter/thrift/jaeger/fixtures/domain_01.json" jsonFile, err := os.Open(fileName) require.NoError(b, err, "Failed to open json fixture file %s", fileName) var trace model.Trace require.NoError(b, jsonpb.Unmarshal(jsonFile, &trace), fileName) require.NotEmpty(b, trace.Spans) spans := make([]*model.Span, 25) for i := range spans { spans[i] = trace.Spans[0] } trace.Spans = spans c := newCodec() bytes, err := c.Marshal(&trace) require.NoError(b, err) for b.Loop() { var trace model.Trace err := c.Unmarshal(bytes, &trace) require.NoError(b, err) } } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/grpctest/reflection.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package grpctest import ( "context" "testing" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" grpcreflection "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" ) // ReflectionServiceValidator verifies that a gRPC service at a given address // supports reflection service. Called must invoke Execute func. type ReflectionServiceValidator struct { HostPort string ExpectedServices []string } // Execute performs validation. func (v ReflectionServiceValidator) Execute(t *testing.T) { conn, err := grpc.NewClient( v.HostPort, grpc.WithTransportCredentials(insecure.NewCredentials())) require.NoError(t, err) defer conn.Close() client := grpcreflection.NewServerReflectionClient(conn) r, err := client.ServerReflectionInfo(context.Background()) require.NoError(t, err) require.NotNil(t, r) err = r.Send(&grpcreflection.ServerReflectionRequest{ MessageRequest: &grpcreflection.ServerReflectionRequest_ListServices{}, }) require.NoError(t, err) m, err := r.Recv() require.NoError(t, err) require.IsType(t, new(grpcreflection.ServerReflectionResponse_ListServicesResponse), m.MessageResponse) resp := m.MessageResponse.(*grpcreflection.ServerReflectionResponse_ListServicesResponse) for _, svc := range v.ExpectedServices { var found string for _, s := range resp.ListServicesResponse.Service { if svc == s.Name { found = s.Name break } } require.Equalf(t, svc, found, "service not found, got '%+v'", resp.ListServicesResponse.Service) } } ================================================ FILE: internal/grpctest/reflection_test.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package grpctest import ( "net" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/reflection" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestReflectionServiceValidator(t *testing.T) { server := grpc.NewServer() reflection.Register(server) listener, err := net.Listen("tcp", ":0") require.NoError(t, err) defer listener.Close() go func() { err := server.Serve(listener) assert.NoError(t, err) }() defer server.Stop() ReflectionServiceValidator{ HostPort: listener.Addr().String(), ExpectedServices: []string{"grpc.reflection.v1alpha.ServerReflection"}, }.Execute(t) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/gzipfs/gzip.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // Copyright 2021 The Prometheus Authors. // SPDX-License-Identifier: Apache-2.0 package gzipfs import ( "compress/gzip" "io" "io/fs" "time" ) const suffix = ".gz" type file struct { file fs.File content []byte offset int } type fileInfo struct { info fs.FileInfo size int64 } type fileSystem struct { fs fs.FS } func (f file) Stat() (fs.FileInfo, error) { stat, err := f.file.Stat() if err != nil { return nil, err } return fileInfo{ info: stat, size: int64(len(f.content)), }, nil } func (f *file) Read(buf []byte) (int, error) { if len(buf) > len(f.content)-f.offset { buf = buf[0:len(f.content[f.offset:])] } n := copy(buf, f.content[f.offset:]) if n == len(f.content)-f.offset { return n, io.EOF } f.offset += n return n, nil } func (f file) Close() error { return f.file.Close() } func (fi fileInfo) Name() string { name := fi.info.Name() return name[:len(name)-len(suffix)] } func (fi fileInfo) Size() int64 { return fi.size } func (fi fileInfo) Mode() fs.FileMode { return fi.info.Mode() } func (fi fileInfo) ModTime() time.Time { return fi.info.ModTime() } func (fi fileInfo) IsDir() bool { return fi.info.IsDir() } func (fileInfo) Sys() any { return nil } // New wraps underlying fs that is expected to contain gzipped files // and presents an unzipped view of it. func New(fileSys fs.FS) fs.FS { return fileSystem{fileSys} } func (cfs fileSystem) Open(path string) (fs.File, error) { var f fs.File f, err := cfs.fs.Open(path) if err == nil { return f, nil } f, err = cfs.fs.Open(path + suffix) if err != nil { return f, err } gr, err := gzip.NewReader(f) if err != nil { return f, err } defer gr.Close() c, err := io.ReadAll(gr) if err != nil { return f, err } return &file{ file: f, content: c, }, nil } ================================================ FILE: internal/gzipfs/gzip_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // Copyright 2021 The Prometheus Authors. // SPDX-License-Identifier: Apache-2.0 package gzipfs import ( "embed" "io" "io/fs" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/testutils" ) //go:embed testdata var EmbedFS embed.FS var testFS = New(EmbedFS) type mockFile struct { err error } func (f *mockFile) Stat() (fs.FileInfo, error) { return nil, f.err } func (f *mockFile) Read([]byte) (int, error) { return 0, f.err } func (f *mockFile) Close() error { return f.err } func TestFS(t *testing.T) { cases := []struct { name string path string expectedErr string expectedName string expectedMode fs.FileMode expectedSize int64 expectedContent string expectedModTime time.Time }{ { name: "uncompressed file", path: "testdata/foobar", expectedMode: 0o444, expectedName: "foobar", expectedSize: 11, expectedContent: "hello world", expectedModTime: time.Date(1, 1, 1, 0, 0, 0, 0 /* nanos */, time.UTC), }, { name: "compressed file", path: "testdata/foobar.gz", expectedMode: 0o444, expectedName: "foobar.gz", expectedSize: 38, expectedContent: "", // actual gzipped payload is returned expectedModTime: time.Date(1, 1, 1, 0, 0, 0, 0 /* nanos */, time.UTC), }, { name: "compressed file accessed without gz extension", path: "testdata/foobaz", expectedMode: 0o444, expectedName: "foobaz", expectedSize: 11, expectedContent: "hello world", expectedModTime: time.Date(1, 1, 1, 0, 0, 0, 0 /* nanos */, time.UTC), }, { name: "non-existing file", path: "testdata/non-existing-file", expectedErr: "file does not exist", }, { name: "not gzipped file", path: "testdata/not_archive", expectedErr: "invalid header", }, { // To provide coverage of the error from io.ReadAll function, we use a file // that is a copy of proper gzipped file testdata/foobaz.gz but truncated // to 36 bytes with: // perl -e "truncate 'internal/gzipfs/testdata/foobaz_truncated.gz', 36" // This allows gzip.NewReader() to succeed because the file has a proper gz // header, but subsequent read fails with unexpected EOF. name: "compressed but truncated file accessed without gz extension", path: "testdata/foobaz_truncated", expectedErr: "unexpected EOF", }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { f, err := testFS.Open(c.path) if c.expectedErr != "" { require.ErrorContains(t, err, c.expectedErr) return } require.NoError(t, err) defer f.Close() stat, err := f.Stat() require.NoError(t, err) assert.Equal(t, c.expectedName, stat.Name()) assert.Equal(t, c.expectedMode, stat.Mode()) assert.Equal(t, c.expectedSize, stat.Size()) assert.Equal(t, c.expectedModTime, stat.ModTime()) assert.False(t, stat.IsDir()) assert.Nil(t, stat.Sys()) content, err := io.ReadAll(f) require.NoError(t, err) if c.expectedContent != "" { assert.Equal(t, c.expectedContent, string(content)) } }) } } func TestFileStatError(t *testing.T) { f := &file{file: &mockFile{assert.AnError}} _, err := f.Stat() assert.Equal(t, assert.AnError, err) } func TestFileRead(t *testing.T) { f := &file{content: []byte("long content")} buf := make([]byte, 5) // shorter buffer n, err := f.Read(buf) require.NoError(t, err) assert.Equal(t, 5, n) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/gzipfs/testdata/foobar ================================================ hello world ================================================ FILE: internal/hostname/hostname.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package hostname import ( "crypto/rand" "fmt" "os" "sync" ) type hostname struct { once sync.Once err error hostname string } var h hostname // AsIdentifier uses the hostname of the os and postfixes a short random string to guarantee uniqueness // The returned value is appropriate to use as a convenient unique identifier and will always be equal // when called from within the same process. func AsIdentifier() (string, error) { h.once.Do(func() { h.hostname, h.err = os.Hostname() if h.err != nil { return } buff := make([]byte, 8) _, h.err = rand.Read(buff) if h.err != nil { return } h.hostname = h.hostname + "-" + fmt.Sprintf("%2x", buff) }) return h.hostname, h.err } ================================================ FILE: internal/hostname/hostname_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package hostname import ( "os" "strings" "sync" "testing" "github.com/stretchr/testify/assert" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestAsIdentifier(t *testing.T) { var hostname1 string var hostname2 string var wg sync.WaitGroup wg.Go(func() { var err error hostname1, err = AsIdentifier() assert.NoError(t, err) }) wg.Go(func() { var err error hostname2, err = AsIdentifier() assert.NoError(t, err) }) wg.Wait() actualHostname, _ := os.Hostname() assert.Equal(t, hostname1, hostname2) assert.True(t, strings.HasPrefix(hostname1, actualHostname)) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/httpfs/prefixed.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package httpfs import ( "net/http" ) // PrefixedFS returns a FileSystem that adds a path prefix to all files // before delegating to the underlying fs. func PrefixedFS(prefix string, fs http.FileSystem) http.FileSystem { return &prefixedFS{ prefix: prefix, fs: fs, } } type prefixedFS struct { prefix string fs http.FileSystem } func (fs *prefixedFS) Open(name string) (http.File, error) { prefixedName := fs.prefix + name if name == "/" { // Return the dir itself when asked for the root. // This is what http.FS() also does to allow redirects // from `/`` to `/index.html`. prefixedName = fs.prefix } return fs.fs.Open(prefixedName) } ================================================ FILE: internal/httpfs/prefixed_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package httpfs import ( "embed" "net/http" "testing" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/testutils" ) //go:embed test_assets/* var assetFS embed.FS func TestPrefixedFS(t *testing.T) { fs := PrefixedFS("test_assets", http.FS(assetFS)) tests := []struct { file string isDir bool }{ {file: "/", isDir: true}, {file: "/somefile.txt", isDir: false}, } for _, tt := range tests { t.Run(tt.file, func(t *testing.T) { file, err := fs.Open(tt.file) require.NoError(t, err) require.NotNil(t, file) stat, err := file.Stat() require.NoError(t, err) require.Equal(t, tt.isDir, stat.IsDir()) }) } } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/httpfs/test_assets/somefile.txt ================================================ ================================================ FILE: internal/jaegerclientenv2otel/envvars.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jaegerclientenv2otel import ( "os" "go.uber.org/zap" ) //nolint:gosec // G101 - env var names, not credentials var envVars = map[string]string{ "JAEGER_SERVICE_NAME": "", "JAEGER_AGENT_HOST": "OTEL_EXPORTER_JAEGER_AGENT_HOST", "JAEGER_AGENT_PORT": "OTEL_EXPORTER_JAEGER_AGENT_PORT", "JAEGER_ENDPOINT": "OTEL_EXPORTER_JAEGER_ENDPOINT", "JAEGER_USER": "OTEL_EXPORTER_JAEGER_USER", "JAEGER_PASSWORD": "OTEL_EXPORTER_JAEGER_PASSWORD", "JAEGER_REPORTER_LOG_SPANS": "", "JAEGER_REPORTER_MAX_QUEUE_SIZE": "", "JAEGER_REPORTER_FLUSH_INTERVAL": "", "JAEGER_REPORTER_ATTEMPT_RECONNECTING_DISABLED": "", "JAEGER_REPORTER_ATTEMPT_RECONNECT_INTERVAL": "", "JAEGER_SAMPLER_TYPE": "", "JAEGER_SAMPLER_PARAM": "", "JAEGER_SAMPLER_MANAGER_HOST_PORT": "", "JAEGER_SAMPLING_ENDPOINT": "", "JAEGER_SAMPLER_MAX_OPERATIONS": "", "JAEGER_SAMPLER_REFRESH_INTERVAL": "", "JAEGER_TAGS": "", "JAEGER_TRACEID_128BIT": "", "JAEGER_DISABLED": "", "JAEGER_RPC_METRICS": "", } func MapJaegerToOtelEnvVars(logger *zap.Logger) { for jname, otelname := range envVars { val := os.Getenv(jname) if val == "" { continue } if otelname == "" { logger.Sugar().Infof("Ignoring deprecated Jaeger SDK env var %s, as there is no equivalent in OpenTelemetry", jname) } else { os.Setenv(otelname, val) logger.Sugar().Infof("Replacing deprecated Jaeger SDK env var %s with OpenTelemetry env var %s", jname, otelname) } } } ================================================ FILE: internal/jaegerclientenv2otel/envvars_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jaegerclientenv2otel import ( "os" "testing" "github.com/stretchr/testify/assert" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMapJaegerToOtelEnvVars(t *testing.T) { t.Setenv("JAEGER_TAGS", "tags") t.Setenv("JAEGER_USER", "user") logger, buffer := testutils.NewLogger() MapJaegerToOtelEnvVars(logger) assert.Equal(t, "user", os.Getenv("OTEL_EXPORTER_JAEGER_USER")) assert.Contains(t, buffer.String(), "Replacing deprecated Jaeger SDK env var JAEGER_USER with OpenTelemetry env var OTEL_EXPORTER_JAEGER_USER") assert.Contains(t, buffer.String(), "Ignoring deprecated Jaeger SDK env var JAEGER_TAGS") } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/jiter/iter.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // Package iter is a backport of Go 1.23 official "iter" package, until we upgrade. package jiter import ( "iter" ) func CollectWithErrors[V any](seq iter.Seq2[V, error]) ([]V, error) { var result []V for v, err := range seq { if err != nil { return nil, err } result = append(result, v) } return result, nil } func FlattenWithErrors[V any](seq iter.Seq2[[]V, error]) ([]V, error) { var result []V for v, err := range seq { if err != nil { return nil, err } result = append(result, v...) } return result, nil } ================================================ FILE: internal/jiter/iter_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jiter import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestCollectWithErrors(t *testing.T) { type item struct { str string err error } tests := []struct { name string items []item expected []string err error }{ { name: "no errors", items: []item{ {str: "a", err: nil}, {str: "b", err: nil}, {str: "c", err: nil}, }, expected: []string{"a", "b", "c"}, }, { name: "first error", items: []item{ {str: "a", err: nil}, {str: "b", err: nil}, {str: "c", err: assert.AnError}, }, err: assert.AnError, }, { name: "second error", items: []item{ {str: "a", err: nil}, {str: "b", err: assert.AnError}, {str: "c", err: nil}, }, err: assert.AnError, }, { name: "third error", items: []item{ {str: "a", err: nil}, {str: "b", err: nil}, {str: "c", err: assert.AnError}, }, err: assert.AnError, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { seq := func(yield func(string, error) bool) { for _, item := range test.items { if !yield(item.str, item.err) { return } } } result, err := CollectWithErrors(seq) if test.err != nil { require.ErrorIs(t, err, test.err) } else { require.NoError(t, err) assert.Equal(t, test.expected, result) } }) } } func TestFlattenWithErrors(t *testing.T) { type item struct { strs []string err error } tests := []struct { name string items []item expected []string err error }{ { name: "no errors", items: []item{ {strs: []string{"a", "b", "c"}, err: nil}, {strs: []string{"d", "e", "f"}, err: nil}, {strs: []string{"g", "h", "i"}, err: nil}, }, expected: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"}, }, { name: "first error", items: []item{ {strs: []string{"a", "b", "c"}, err: nil}, {strs: []string{"d", "e", "f"}, err: assert.AnError}, {strs: []string{"g", "h", "i"}, err: nil}, }, err: assert.AnError, }, { name: "second error", items: []item{ {strs: []string{"a", "b", "c"}, err: nil}, {strs: []string{"d", "e", "f"}, err: nil}, {strs: []string{"g", "h", "i"}, err: assert.AnError}, }, err: assert.AnError, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { seq := func(yield func([]string, error) bool) { for _, item := range test.items { if !yield(item.strs, item.err) { return } } } result, err := FlattenWithErrors(seq) if test.err != nil { require.ErrorIs(t, err, test.err) } else { require.NoError(t, err) assert.Equal(t, test.expected, result) } }) } } ================================================ FILE: internal/jiter/package_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jiter import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/jptrace/aggregator.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jptrace import ( "fmt" "iter" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" ) // AggregateTraces aggregates a sequence of trace batches into individual traces. // // The `tracesSeq` input must adhere to the chunking requirements of tracestore.Reader.GetTraces. func AggregateTraces(tracesSeq iter.Seq2[[]ptrace.Traces, error]) iter.Seq2[ptrace.Traces, error] { return AggregateTracesWithLimit(tracesSeq, 0) } // AggregateTracesWithLimit aggregates a sequence of trace batches into individual traces // but limits each trace size to maxSize spans. If maxSize is 0 or negative, there is no // limit and all spans will be included in the aggregated trace. // // The `tracesSeq` input must adhere to the chunking requirements of tracestore.Reader.GetTraces. func AggregateTracesWithLimit(tracesSeq iter.Seq2[[]ptrace.Traces, error], maxSize int) iter.Seq2[ptrace.Traces, error] { return func(yield func(trace ptrace.Traces, err error) bool) { currentTrace := ptrace.NewTraces() currentTraceID := pcommon.NewTraceIDEmpty() currentSpanCount := 0 currentTruncated := false cont := true tracesSeq(func(traces []ptrace.Traces, err error) bool { if err != nil { cont = yield(ptrace.NewTraces(), err) return false } for _, trace := range traces { incomingSpanCount := trace.SpanCount() if incomingSpanCount == 0 { continue } traceID := GetTraceID(trace) if currentTraceID == traceID { // same trace as current, merge it into the current trace, respecting the maxSize limit var truncated bool currentSpanCount, truncated = mergeTracesWithLimit(currentTrace, currentSpanCount, trace, incomingSpanCount, maxSize) if truncated && !currentTruncated { markTraceTruncated(currentTrace, maxSize) currentTruncated = true } } else { if currentSpanCount > 0 { if !yield(currentTrace, nil) { cont = false return false } } currentTraceID = traceID currentTruncated = false if maxSize > 0 && incomingSpanCount > maxSize { currentTrace = ptrace.NewTraces() copySpansUpToLimit(currentTrace, trace, maxSize) currentSpanCount = maxSize markTraceTruncated(currentTrace, maxSize) currentTruncated = true } else { // Optimization: when incoming trace fits within the limit (or there is no limit), // we can skip the copy and use it directly as the current trace. currentTrace = trace currentSpanCount = incomingSpanCount } } } return true }) // Emit the last accumulated trace if non-empty. // `cont` guards against calling yield after consumer already returned false. if cont && currentSpanCount > 0 { yield(currentTrace, nil) } } } // mergeTracesWithLimit merges src into dest, respecting the maxSize span limit. // destCount and srcCount are the pre-computed span counts for dest and src respectively. // Returns the updated span count of dest and whether the trace was truncated (true if // src spans were dropped due to the limit). If maxSize <= 0, all spans are merged without limit. func mergeTracesWithLimit(dest ptrace.Traces, destCount int, src ptrace.Traces, srcCount int, maxSize int) (int, bool) { // early exit if already at max if maxSize > 0 && destCount >= maxSize { return destCount, true } // check if we can merge all spans without exceeding limit if maxSize <= 0 || destCount+srcCount <= maxSize { MergeTraces(dest, src) return destCount + srcCount, false } // partial copy: only copy the spans that fit copySpansUpToLimit(dest, src, maxSize-destCount) return maxSize, true } func copySpansUpToLimit(dest, src ptrace.Traces, limit int) { copied := 0 for _, srcResource := range src.ResourceSpans().All() { if copied >= limit { return } var destResource ptrace.ResourceSpans resourceAdded := false for _, srcScope := range srcResource.ScopeSpans().All() { if copied >= limit { return } var destScope ptrace.ScopeSpans scopeAdded := false for _, span := range srcScope.Spans().All() { if copied >= limit { return } // Lazily create resource and scope containers only when a span is actually copied, // to avoid leaving empty container artifacts in dest. if !resourceAdded { destResource = dest.ResourceSpans().AppendEmpty() srcResource.Resource().CopyTo(destResource.Resource()) destResource.SetSchemaUrl(srcResource.SchemaUrl()) resourceAdded = true } if !scopeAdded { destScope = destResource.ScopeSpans().AppendEmpty() srcScope.Scope().CopyTo(destScope.Scope()) destScope.SetSchemaUrl(srcScope.SchemaUrl()) scopeAdded = true } span.CopyTo(destScope.Spans().AppendEmpty()) copied++ } } } } // MergeTraces merges src trace into dest trace. // This is useful when multiple iterations return parts of the same trace. func MergeTraces(dest, src ptrace.Traces) { resources := src.ResourceSpans() for i := 0; i < resources.Len(); i++ { resource := resources.At(i) resource.CopyTo(dest.ResourceSpans().AppendEmpty()) } } func markTraceTruncated(trace ptrace.Traces, maxSize int) { for _, span := range SpanIter(trace) { AddWarnings( span, fmt.Sprintf("trace has more than %d spans, showing first %d spans only", maxSize, maxSize), ) return // stop after first span } } ================================================ FILE: internal/jptrace/aggregator_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jptrace import ( "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" ) func TestAggregateTraces_AggregatesSpansWithSameTraceID(t *testing.T) { trace1 := ptrace.NewTraces() resource1 := trace1.ResourceSpans().AppendEmpty() scope1 := resource1.ScopeSpans().AppendEmpty() span1 := scope1.Spans().AppendEmpty() span1.SetTraceID(pcommon.TraceID([16]byte{1})) span1.SetName("span1") trace1Continued := ptrace.NewTraces() resource2 := trace1Continued.ResourceSpans().AppendEmpty() scope2 := resource2.ScopeSpans().AppendEmpty() span2 := scope2.Spans().AppendEmpty() span2.SetTraceID(pcommon.TraceID([16]byte{1})) span2.SetName("span2") trace2 := ptrace.NewTraces() resource3 := trace2.ResourceSpans().AppendEmpty() scope3 := resource3.ScopeSpans().AppendEmpty() span3 := scope3.Spans().AppendEmpty() span3.SetTraceID(pcommon.TraceID([16]byte{2})) span3.SetName("span3") trace3 := ptrace.NewTraces() resource4 := trace3.ResourceSpans().AppendEmpty() scope4 := resource4.ScopeSpans().AppendEmpty() span4 := scope4.Spans().AppendEmpty() span4.SetTraceID(pcommon.TraceID([16]byte{3})) span4.SetName("span4") tracesSeq := func(yield func([]ptrace.Traces, error) bool) { yield([]ptrace.Traces{trace1, trace1Continued, trace2}, nil) yield([]ptrace.Traces{trace3}, nil) } var result []ptrace.Traces AggregateTraces(tracesSeq)(func(trace ptrace.Traces, _ error) bool { result = append(result, trace) return true }) require.Len(t, result, 3) require.Equal(t, 2, result[0].ResourceSpans().Len()) require.Equal(t, 1, result[1].ResourceSpans().Len()) require.Equal(t, 1, result[2].ResourceSpans().Len()) gotSpan1 := result[0].ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0) require.Equal(t, gotSpan1.TraceID(), pcommon.TraceID([16]byte{1})) require.Equal(t, "span1", gotSpan1.Name()) gotSpan2 := result[0].ResourceSpans().At(1).ScopeSpans().At(0).Spans().At(0) require.Equal(t, gotSpan2.TraceID(), pcommon.TraceID([16]byte{1})) require.Equal(t, "span2", gotSpan2.Name()) gotSpan3 := result[1].ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0) require.Equal(t, gotSpan3.TraceID(), pcommon.TraceID([16]byte{2})) require.Equal(t, "span3", gotSpan3.Name()) gotSpan4 := result[2].ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0) require.Equal(t, gotSpan4.TraceID(), pcommon.TraceID([16]byte{3})) require.Equal(t, "span4", gotSpan4.Name()) } func TestAggregateTraces_YieldsErrorFromTracesSeq(t *testing.T) { trace1 := ptrace.NewTraces() resource1 := trace1.ResourceSpans().AppendEmpty() scope1 := resource1.ScopeSpans().AppendEmpty() span1 := scope1.Spans().AppendEmpty() span1.SetTraceID(pcommon.TraceID([16]byte{1})) span1.SetName("span1") tracesSeq := func(yield func([]ptrace.Traces, error) bool) { if !yield(nil, assert.AnError) { return } yield([]ptrace.Traces{trace1}, nil) // should not get here } aggregatedSeq := AggregateTraces(tracesSeq) var lastResult ptrace.Traces var lastErr error aggregatedSeq(func(trace ptrace.Traces, e error) bool { lastResult = trace if e != nil { lastErr = e } return true }) require.ErrorIs(t, lastErr, assert.AnError) require.Equal(t, ptrace.NewTraces(), lastResult) } func TestAggregateTraces_RespectsEarlyReturn(t *testing.T) { trace1 := ptrace.NewTraces() resource1 := trace1.ResourceSpans().AppendEmpty() scope1 := resource1.ScopeSpans().AppendEmpty() span1 := scope1.Spans().AppendEmpty() span1.SetTraceID(pcommon.TraceID([16]byte{1})) span1.SetName("span1") trace2 := ptrace.NewTraces() resource2 := trace2.ResourceSpans().AppendEmpty() scope2 := resource2.ScopeSpans().AppendEmpty() span2 := scope2.Spans().AppendEmpty() span2.SetTraceID(pcommon.TraceID([16]byte{2})) span2.SetName("span2") tracesSeq := func(yield func([]ptrace.Traces, error) bool) { yield([]ptrace.Traces{trace1}, nil) yield([]ptrace.Traces{trace2}, nil) } aggregatedSeq := AggregateTraces(tracesSeq) var lastResult ptrace.Traces aggregatedSeq(func(trace ptrace.Traces, _ error) bool { lastResult = trace return false }) require.Equal(t, trace1, lastResult) } func TestAggregateTracesWithLimit(t *testing.T) { createTrace := func(traceID byte, spanCount int) ptrace.Traces { trace := ptrace.NewTraces() spans := trace.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans() for i := 0; i < spanCount; i++ { span := spans.AppendEmpty() span.SetTraceID(pcommon.TraceID([16]byte{traceID})) } return trace } tests := []struct { name string maxSize int inputSpans int expectedSpans int expectTruncate bool }{ {"no_limit", 0, 5, 5, false}, {"under_limit", 10, 5, 5, false}, {"over_limit", 3, 5, 3, true}, {"exact_limit", 5, 5, 5, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tracesSeq := func(yield func([]ptrace.Traces, error) bool) { yield([]ptrace.Traces{createTrace(1, tt.inputSpans)}, nil) } var result []ptrace.Traces AggregateTracesWithLimit(tracesSeq, tt.maxSize)(func(trace ptrace.Traces, _ error) bool { result = append(result, trace) return true }) require.Len(t, result, 1) assert.Equal(t, tt.expectedSpans, result[0].SpanCount()) // Check for truncation warning if tt.expectTruncate { firstSpan := result[0].ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0) warnings := GetWarnings(firstSpan) assert.NotEmpty(t, warnings, "expected truncation warning") assert.Contains(t, warnings[len(warnings)-1], fmt.Sprintf("trace has more than %d spans", tt.maxSize)) } }) } } func TestCopySpansUpToLimit(t *testing.T) { src := ptrace.NewTraces() spans := src.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans() for i := 0; i < 5; i++ { spans.AppendEmpty().SetName("span") } dest := ptrace.NewTraces() copySpansUpToLimit(dest, src, 3) assert.Equal(t, 3, dest.SpanCount()) } func TestCopySpansUpToLimit_MultipleResourceSpans(t *testing.T) { src := ptrace.NewTraces() rs0 := src.ResourceSpans().AppendEmpty() ss0 := rs0.ScopeSpans().AppendEmpty() ss0.Spans().AppendEmpty().SetName("rs0-span0") ss0.Spans().AppendEmpty().SetName("rs0-span1") rs1 := src.ResourceSpans().AppendEmpty() ss1 := rs1.ScopeSpans().AppendEmpty() ss1.Spans().AppendEmpty().SetName("rs1-span0") ss1.Spans().AppendEmpty().SetName("rs1-span1") dest := ptrace.NewTraces() copySpansUpToLimit(dest, src, 3) require.Equal(t, 3, dest.SpanCount()) require.Equal(t, 2, dest.ResourceSpans().Len()) assert.Equal(t, 2, dest.ResourceSpans().At(0).ScopeSpans().At(0).Spans().Len()) assert.Equal(t, 1, dest.ResourceSpans().At(1).ScopeSpans().At(0).Spans().Len()) } func TestCopySpansUpToLimit_MultipleScopeSpans(t *testing.T) { src := ptrace.NewTraces() rs := src.ResourceSpans().AppendEmpty() ss0 := rs.ScopeSpans().AppendEmpty() ss0.Spans().AppendEmpty().SetName("ss0-span0") ss0.Spans().AppendEmpty().SetName("ss0-span1") ss1 := rs.ScopeSpans().AppendEmpty() ss1.Spans().AppendEmpty().SetName("ss1-span0") ss1.Spans().AppendEmpty().SetName("ss1-span1") dest := ptrace.NewTraces() copySpansUpToLimit(dest, src, 3) require.Equal(t, 3, dest.SpanCount()) require.Equal(t, 1, dest.ResourceSpans().Len()) destScopes := dest.ResourceSpans().At(0).ScopeSpans() require.Equal(t, 2, destScopes.Len()) assert.Equal(t, 2, destScopes.At(0).Spans().Len()) assert.Equal(t, 1, destScopes.At(1).Spans().Len()) } func TestCopySpansUpToLimit_NoEmptyContainers(t *testing.T) { // src has two resources: the first has no scopes, the second has spans. // copySpansUpToLimit should not create an empty ResourceSpans for the first resource. src := ptrace.NewTraces() src.ResourceSpans().AppendEmpty() // empty resource, no scopes spans := src.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans() for i := 0; i < 3; i++ { spans.AppendEmpty().SetName("span") } dest := ptrace.NewTraces() copySpansUpToLimit(dest, src, 2) assert.Equal(t, 2, dest.SpanCount()) assert.Equal(t, 1, dest.ResourceSpans().Len(), "empty resource should not be copied") } func TestAggregateTracesWithLimit_MultiBatch(t *testing.T) { // A trace that arrives in three batches should produce exactly one truncation // warning even when subsequent batches arrive after the limit is already reached. createBatch := func(traceID byte, spanCount int) ptrace.Traces { trace := ptrace.NewTraces() spans := trace.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans() for i := 0; i < spanCount; i++ { span := spans.AppendEmpty() span.SetTraceID(pcommon.TraceID([16]byte{traceID})) } return trace } // Limit is 3. Batch 1: 2 spans (under limit). Batch 2: 2 spans (partial copy, hits limit). // Batch 3: 2 spans (already at limit, ignored). tracesSeq := func(yield func([]ptrace.Traces, error) bool) { if !yield([]ptrace.Traces{createBatch(1, 2)}, nil) { return } if !yield([]ptrace.Traces{createBatch(1, 2)}, nil) { return } yield([]ptrace.Traces{createBatch(1, 2)}, nil) } var result []ptrace.Traces AggregateTracesWithLimit(tracesSeq, 3)(func(trace ptrace.Traces, _ error) bool { result = append(result, trace) return true }) require.Len(t, result, 1) assert.Equal(t, 3, result[0].SpanCount()) firstSpan := result[0].ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0) warnings := GetWarnings(firstSpan) assert.Len(t, warnings, 1, "should have exactly one truncation warning, not one per extra batch") assert.Contains(t, warnings[0], fmt.Sprintf("trace has more than %d spans", 3)) } // TestAggregateTracesWithLimit_ExactLimitThenOverflow specifically tests the scenario // where the first batch fills the trace to exactly maxSize (no warning yet), and a // subsequent batch then causes the first overflow and must trigger the truncation warning. func TestAggregateTracesWithLimit_ExactLimitThenOverflow(t *testing.T) { createBatch := func(traceID byte, spanCount int) ptrace.Traces { trace := ptrace.NewTraces() spans := trace.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans() for i := 0; i < spanCount; i++ { span := spans.AppendEmpty() span.SetTraceID(pcommon.TraceID([16]byte{traceID})) } return trace } // Batch 1 has exactly maxSize spans — fits without truncation, no warning added yet. // Batch 2 has 1 more span — must be dropped AND must trigger the warning. tracesSeq := func(yield func([]ptrace.Traces, error) bool) { if !yield([]ptrace.Traces{createBatch(1, 3)}, nil) { return } yield([]ptrace.Traces{createBatch(1, 1)}, nil) } var result []ptrace.Traces AggregateTracesWithLimit(tracesSeq, 3)(func(trace ptrace.Traces, _ error) bool { result = append(result, trace) return true }) require.Len(t, result, 1) assert.Equal(t, 3, result[0].SpanCount()) firstSpan := result[0].ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0) warnings := GetWarnings(firstSpan) assert.Len(t, warnings, 1, "overflow after exact-limit batch must produce exactly one truncation warning") assert.Contains(t, warnings[0], fmt.Sprintf("trace has more than %d spans", 3)) } func TestMarkAndCheckTruncated(t *testing.T) { trace := ptrace.NewTraces() firstSpan := trace.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty() assert.Empty(t, GetWarnings(firstSpan)) markTraceTruncated(trace, 10) // Now should have truncation warning warnings := GetWarnings(firstSpan) assert.NotEmpty(t, warnings) assert.Contains(t, warnings[0], "trace has more than 10 spans") } func TestAggregateTraces_HandlesEmptyTraces(t *testing.T) { emptyTrace := ptrace.NewTraces() // No resource spans traceWithNoSpans := ptrace.NewTraces() traceWithNoSpans.ResourceSpans().AppendEmpty() // Has resource spans but no scope spans traceWithNoSpans2 := ptrace.NewTraces() rs := traceWithNoSpans2.ResourceSpans().AppendEmpty() rs.ScopeSpans().AppendEmpty() // Has scope spans but no spans trace1 := ptrace.NewTraces() rs1 := trace1.ResourceSpans().AppendEmpty() ss1 := rs1.ScopeSpans().AppendEmpty() span1 := ss1.Spans().AppendEmpty() span1.SetTraceID(pcommon.TraceID([16]byte{1})) tracesSeq := func(yield func([]ptrace.Traces, error) bool) { yield([]ptrace.Traces{emptyTrace, traceWithNoSpans, traceWithNoSpans2, trace1}, nil) } var result []ptrace.Traces AggregateTraces(tracesSeq)(func(trace ptrace.Traces, _ error) bool { result = append(result, trace) return true }) require.Len(t, result, 1) require.Equal(t, trace1, result[0]) } func TestAggregateTraces_DoesNotYieldAfterConsumerStops(t *testing.T) { // This test demonstrates why the `cont` variable is needed in AggregateTraces. // Without it, the function would violate the iterator protocol by calling yield // after the consumer has returned false. // // Setup: Create two separate traces with different IDs that will be yielded // from separate batches. The consumer will stop after the first trace. trace1 := ptrace.NewTraces() resource1 := trace1.ResourceSpans().AppendEmpty() scope1 := resource1.ScopeSpans().AppendEmpty() span1 := scope1.Spans().AppendEmpty() span1.SetTraceID(pcommon.TraceID([16]byte{1})) span1.SetName("span1") trace2 := ptrace.NewTraces() resource2 := trace2.ResourceSpans().AppendEmpty() scope2 := resource2.ScopeSpans().AppendEmpty() span2 := scope2.Spans().AppendEmpty() span2.SetTraceID(pcommon.TraceID([16]byte{2})) span2.SetName("span2") // Yield traces in separate batches - this ensures the final yield happens // after the iterator completes, which is where the bug would manifest. tracesSeq := func(yield func([]ptrace.Traces, error) bool) { if !yield([]ptrace.Traces{trace1}, nil) { return } yield([]ptrace.Traces{trace2}, nil) } var yieldCount int aggregatedSeq := AggregateTraces(tracesSeq) // Consumer stops after first yield aggregatedSeq(func(_ ptrace.Traces, _ error) bool { yieldCount++ return false // Stop iteration after first trace }) // Without the `cont` variable, this would panic with: // "runtime error: range function continued iteration after function for loop body returned false" // The cont variable prevents the final yield (line 48-50 in aggregator.go) from // being called after the consumer has already returned false. require.Equal(t, 1, yieldCount, "yield should only be called once since consumer returned false") } ================================================ FILE: internal/jptrace/attributes.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jptrace import ( "go.opentelemetry.io/collector/pdata/pcommon" ) const ( // WarningsAttribute is the name of the span attribute where we can // store various warnings produced from transformations, // such as inbound sanitizers and outbound adjusters. // The value type of the attribute is a string slice. WarningsAttribute = "@jaeger@warnings" // FormatAttribute is a key for span attribute that records the original // wire format in which the span was received by Jaeger, // e.g. proto, thrift, json. FormatAttribute = "@jaeger@format" ) func PcommonMapToPlainMap(attributes pcommon.Map) map[string]string { mapAttributes := make(map[string]string) attributes.Range(func(k string, v pcommon.Value) bool { mapAttributes[k] = v.AsString() return true }) return mapAttributes } func PlainMapToPcommonMap(attributesMap map[string]string) pcommon.Map { attributes := pcommon.NewMap() for k, v := range attributesMap { attributes.PutStr(k, v) } return attributes } ================================================ FILE: internal/jptrace/attributes_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jptrace import ( "testing" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" ) func TestPcommonMapToPlainMap(t *testing.T) { tests := []struct { name string attributes pcommon.Map expected map[string]string }{ { name: "empty attributes", attributes: pcommon.NewMap(), expected: map[string]string{}, }, { name: "single attribute", attributes: func() pcommon.Map { m := pcommon.NewMap() m.PutStr("key1", "value1") return m }(), expected: map[string]string{"key1": "value1"}, }, { name: "multiple attributes", attributes: func() pcommon.Map { m := pcommon.NewMap() m.PutStr("key1", "value1") m.PutStr("key2", "value2") return m }(), expected: map[string]string{"key1": "value1", "key2": "value2"}, }, { name: "non-string attributes", attributes: func() pcommon.Map { m := pcommon.NewMap() m.PutInt("key1", 1) m.PutDouble("key2", 3.14) return m }(), expected: map[string]string{"key1": "1", "key2": "3.14"}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { result := PcommonMapToPlainMap(test.attributes) require.Equal(t, test.expected, result) }) } } func TestPlainMapToPcommonMap(t *testing.T) { tests := []struct { name string expected map[string]string }{ { name: "empty map", expected: map[string]string{}, }, { name: "single attribute", expected: map[string]string{"key1": "value1"}, }, { name: "multiple attributes", expected: map[string]string{"key1": "value1", "key2": "value2"}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { result := PlainMapToPcommonMap(test.expected) require.Equal(t, test.expected, PcommonMapToPlainMap(result)) }) } } ================================================ FILE: internal/jptrace/package_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jptrace import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/jptrace/sanitizer/emptyservicename.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package sanitizer import ( "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) const ( emptyServiceName = "empty-service-name" serviceNameWrongType = "service-name-wrong-type" missingServiceName = "missing-service-name" ) // NewEmptyServiceNameSanitizer returns a sanitizer function that replaces // empty and missing service names with placeholder strings. func NewEmptyServiceNameSanitizer() Func { return sanitizeEmptyServiceName } func sanitizeEmptyServiceName(traces ptrace.Traces) ptrace.Traces { needsModification := false for _, resourceSpan := range traces.ResourceSpans().All() { attributes := resourceSpan.Resource().Attributes() serviceName, ok := attributes.Get(string(otelsemconv.ServiceNameKey)) switch { case !ok: needsModification = true case serviceName.Type() != pcommon.ValueTypeStr: needsModification = true case serviceName.Str() == "": needsModification = true default: // Service name is valid, no modification needed } if needsModification { break } } if !needsModification { return traces } var workingTraces ptrace.Traces if traces.IsReadOnly() { workingTraces = ptrace.NewTraces() traces.CopyTo(workingTraces) } else { workingTraces = traces } for _, resourceSpan := range workingTraces.ResourceSpans().All() { attributes := resourceSpan.Resource().Attributes() serviceName, ok := attributes.Get(string(otelsemconv.ServiceNameKey)) switch { case !ok: attributes.PutStr(string(otelsemconv.ServiceNameKey), missingServiceName) case serviceName.Type() != pcommon.ValueTypeStr: attributes.PutStr(string(otelsemconv.ServiceNameKey), serviceNameWrongType) case serviceName.Str() == "": attributes.PutStr(string(otelsemconv.ServiceNameKey), emptyServiceName) default: // Service name is valid, no action needed } } return workingTraces } ================================================ FILE: internal/jptrace/sanitizer/emptyservicename_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package sanitizer import ( "testing" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/ptrace" ) func TestEmptyServiceNameSanitizer_SubstitutesCorrectlyForStrings(t *testing.T) { emptyServiceName := "" nonEmptyServiceName := "hello" tests := []struct { name string serviceName *string expectedServiceName string }{ { name: "no service name", expectedServiceName: "missing-service-name", }, { name: "empty service name", serviceName: &emptyServiceName, expectedServiceName: "empty-service-name", }, { name: "non-empty service name", serviceName: &nonEmptyServiceName, expectedServiceName: "hello", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { traces := ptrace.NewTraces() attributes := traces. ResourceSpans(). AppendEmpty(). Resource(). Attributes() if test.serviceName != nil { attributes.PutStr("service.name", *test.serviceName) } sanitizer := NewEmptyServiceNameSanitizer() sanitized := sanitizer(traces) serviceName, ok := sanitized. ResourceSpans(). At(0). Resource(). Attributes(). Get("service.name") require.True(t, ok) require.Equal(t, test.expectedServiceName, serviceName.Str()) }) } } func TestEmptyServiceNameSanitizer_SubstitutesCorrectlyForNonStringType(t *testing.T) { traces := ptrace.NewTraces() traces. ResourceSpans(). AppendEmpty(). Resource(). Attributes(). PutInt("service.name", 1) sanitizer := NewEmptyServiceNameSanitizer() sanitized := sanitizer(traces) serviceName, ok := sanitized. ResourceSpans(). At(0). Resource(). Attributes(). Get("service.name") require.True(t, ok) require.Equal(t, "service-name-wrong-type", serviceName.Str()) } func TestEmptyServiceNameSanitizer_DefaultCases(t *testing.T) { validServiceName := "valid-service" traces := ptrace.NewTraces() attributes := traces. ResourceSpans(). AppendEmpty(). Resource(). Attributes() attributes.PutStr("service.name", validServiceName) sanitizer := NewEmptyServiceNameSanitizer() sanitized := sanitizer(traces) serviceName, ok := sanitized. ResourceSpans(). At(0). Resource(). Attributes(). Get("service.name") require.True(t, ok) require.Equal(t, validServiceName, serviceName.Str()) attributes2 := traces. ResourceSpans(). AppendEmpty(). Resource(). Attributes() attributes2.PutStr("service.name", "") sanitized = sanitizer(traces) serviceName, ok = sanitized. ResourceSpans(). At(0). Resource(). Attributes(). Get("service.name") require.True(t, ok) require.Equal(t, validServiceName, serviceName.Str()) serviceName2, ok := sanitized. ResourceSpans(). At(1). Resource(). Attributes(). Get("service.name") require.True(t, ok) require.Equal(t, "empty-service-name", serviceName2.Str()) } ================================================ FILE: internal/jptrace/sanitizer/emptyspanname.go ================================================ // Copyright (c) 2026 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package sanitizer import ( "go.opentelemetry.io/collector/pdata/ptrace" ) const ( emptySpanName = "empty-span-name" ) // NewEmptySpanNameSanitizer returns a sanitizer function that replaces // empty span names with a placeholder string. func NewEmptySpanNameSanitizer() Func { return sanitizeEmptySpanName } func sanitizeEmptySpanName(traces ptrace.Traces) ptrace.Traces { if !tracesNeedSpanNameSanitization(traces) { return traces } var workingTraces ptrace.Traces if traces.IsReadOnly() { workingTraces = ptrace.NewTraces() traces.CopyTo(workingTraces) } else { workingTraces = traces } for _, resourceSpan := range workingTraces.ResourceSpans().All() { for _, scopeSpan := range resourceSpan.ScopeSpans().All() { for _, span := range scopeSpan.Spans().All() { if span.Name() == "" { span.SetName(emptySpanName) } } } } return workingTraces } func tracesNeedSpanNameSanitization(traces ptrace.Traces) bool { for _, resourceSpan := range traces.ResourceSpans().All() { for _, scopeSpan := range resourceSpan.ScopeSpans().All() { for _, span := range scopeSpan.Spans().All() { if span.Name() == "" { return true } } } } return false } ================================================ FILE: internal/jptrace/sanitizer/emptyspanname_test.go ================================================ // Copyright (c) 2026 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package sanitizer import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/ptrace" ) func TestEmptySpanNameSanitizer(t *testing.T) { tests := []struct { name string spanName string expectedSpanName string }{ { name: "empty span name", spanName: "", expectedSpanName: "empty-span-name", }, { name: "non-empty span name", spanName: "my-operation", expectedSpanName: "my-operation", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { traces := ptrace.NewTraces() span := traces. ResourceSpans(). AppendEmpty(). ScopeSpans(). AppendEmpty(). Spans(). AppendEmpty() span.SetName(test.spanName) sanitizer := NewEmptySpanNameSanitizer() sanitized := sanitizer(traces) actualSpan := sanitized. ResourceSpans(). At(0). ScopeSpans(). At(0). Spans(). At(0) require.Equal(t, test.expectedSpanName, actualSpan.Name()) }) } } func TestEmptySpanNameSanitizer_ReadOnly(t *testing.T) { traces := ptrace.NewTraces() span := traces. ResourceSpans(). AppendEmpty(). ScopeSpans(). AppendEmpty(). Spans(). AppendEmpty() span.SetName("") traces.MarkReadOnly() sanitizer := NewEmptySpanNameSanitizer() result := sanitizer(traces) // The original read-only traces should still have an empty span name. assert.Empty(t, traces.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0).Name()) // The returned traces should have the placeholder span name. assert.Equal(t, "empty-span-name", result.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0).Name()) } func TestEmptySpanNameSanitizer_NoModificationNeeded(t *testing.T) { traces := ptrace.NewTraces() span := traces. ResourceSpans(). AppendEmpty(). ScopeSpans(). AppendEmpty(). Spans(). AppendEmpty() span.SetName("valid-span") traces.MarkReadOnly() sanitizer := NewEmptySpanNameSanitizer() result := sanitizer(traces) assert.Equal(t, "valid-span", result.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0).Name()) assert.True(t, result.IsReadOnly()) } ================================================ FILE: internal/jptrace/sanitizer/negative_duration_santizer.go ================================================ // Copyright (c) 2025 The Jaeger Authors. package sanitizer import ( "fmt" "time" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger/internal/jptrace" ) const ( minDuration = time.Duration(1) ) // NewNegativeDurationSanitizer returns a sanitizer function that performs the // following sanitizationson the trace data: // - Checks all spans in the ResourceSpans to ensure that the start timestamp // is less than the end timestamp. If not, it sets the end timestamp to // start timestamp plus a minimum duration of 1 nanosecond. func NewNegativeDurationSanitizer() Func { return sanitizeNegativeDuration } func sanitizeNegativeDuration(traces ptrace.Traces) ptrace.Traces { if !tracesNeedDurationSanitization(traces) { return traces } var workingTraces ptrace.Traces if traces.IsReadOnly() { workingTraces = ptrace.NewTraces() traces.CopyTo(workingTraces) } else { workingTraces = traces } for _, resourceSpan := range workingTraces.ResourceSpans().All() { for _, scopeSpan := range resourceSpan.ScopeSpans().All() { for _, span := range scopeSpan.Spans().All() { sanitizeDuration(&span) } } } return workingTraces } func tracesNeedDurationSanitization(traces ptrace.Traces) bool { for _, resourceSpan := range traces.ResourceSpans().All() { for _, scopeSpan := range resourceSpan.ScopeSpans().All() { for _, span := range scopeSpan.Spans().All() { if spanNeedsDurationSanitization(span) { return true } } } } return false } func spanNeedsDurationSanitization(span ptrace.Span) bool { start := span.StartTimestamp().AsTime() end := span.EndTimestamp().AsTime() return !start.Before(end) } func sanitizeDuration(span *ptrace.Span) { start := span.StartTimestamp().AsTime() end := span.EndTimestamp().AsTime() if start.Before(end) { return } newEnd := start.Add(minDuration) jptrace.AddWarnings( *span, fmt.Sprintf( "Negative duration detected, sanitizing end timestamp. Original end timestamp: %s, adjusted to: %s", end.String(), newEnd.String(), ), ) span.SetEndTimestamp(pcommon.NewTimestampFromTime(newEnd)) } ================================================ FILE: internal/jptrace/sanitizer/negative_duration_santizer_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package sanitizer import ( "fmt" "testing" "time" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger/internal/jptrace" ) func TestNegativeDurationSanitizer(t *testing.T) { now := time.Date(2025, 5, 25, 0, 0, 0, 0, time.UTC) tests := []struct { name string start time.Time end time.Time delta time.Duration // end - start duration expectWarning bool }{ { name: "valid duration", start: now, end: now.Add(5 * time.Second), delta: 5 * time.Second, expectWarning: false, }, { name: "negative duration", start: now, end: now.Add(-2 * time.Second), delta: 1 * time.Nanosecond, expectWarning: true, }, { name: "zero duration", start: now, end: now, delta: 1 * time.Nanosecond, expectWarning: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { traces := ptrace.NewTraces() span := traces. ResourceSpans().AppendEmpty(). ScopeSpans().AppendEmpty(). Spans().AppendEmpty() span.SetStartTimestamp(pcommon.NewTimestampFromTime(test.start)) span.SetEndTimestamp(pcommon.NewTimestampFromTime(test.end)) sanitizer := NewNegativeDurationSanitizer() sanitized := sanitizer(traces) sanitizedSpan := sanitized. ResourceSpans().At(0). ScopeSpans().At(0). Spans().At(0) gotStart := sanitizedSpan.StartTimestamp().AsTime() gotEnd := sanitizedSpan.EndTimestamp().AsTime() require.Equal(t, test.start, gotStart) require.Equal(t, test.start.Add(test.delta), gotEnd) if test.expectWarning { warnings := jptrace.GetWarnings(sanitizedSpan) require.Equal( t, fmt.Sprintf( "Negative duration detected, sanitizing end timestamp. Original end timestamp: %s, adjusted to: %s", test.end.String(), gotEnd.String(), ), warnings[0], ) } }) } } ================================================ FILE: internal/jptrace/sanitizer/package_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package sanitizer import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/jptrace/sanitizer/readonly_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package sanitizer import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) // Tests that all sanitizers handle read-only traces correctly // This reproduces the panic scenario described in the GitHub issue #7221 func TestSanitizersWithReadOnlyTraces(t *testing.T) { tests := []struct { name string sanitizer Func setupFunc func(ptrace.Traces) }{ { name: "EmptyServiceNameSanitizer", sanitizer: NewEmptyServiceNameSanitizer(), setupFunc: func(traces ptrace.Traces) { // Create a trace without service.name to trigger sanitization rSpans := traces.ResourceSpans().AppendEmpty() sSpans := rSpans.ScopeSpans().AppendEmpty() span := sSpans.Spans().AppendEmpty() span.SetName("test-span") // Intentionally not setting service.name to trigger sanitization }, }, { name: "EmptySpanNameSanitizer", sanitizer: NewEmptySpanNameSanitizer(), setupFunc: func(traces ptrace.Traces) { // Create a trace with an empty span name to trigger sanitization rSpans := traces.ResourceSpans().AppendEmpty() sSpans := rSpans.ScopeSpans().AppendEmpty() span := sSpans.Spans().AppendEmpty() span.SetName("") }, }, { name: "UTF8Sanitizer", sanitizer: NewUTF8Sanitizer(), setupFunc: func(traces ptrace.Traces) { // Create a trace with invalid UTF-8 to trigger sanitization rSpans := traces.ResourceSpans().AppendEmpty() sSpans := rSpans.ScopeSpans().AppendEmpty() span := sSpans.Spans().AppendEmpty() span.SetName("test-span") // Add an attribute with invalid UTF-8 invalidUTF8 := string([]byte{0xff, 0xfe, 0xfd}) span.Attributes().PutStr("invalid-utf8", invalidUTF8) }, }, { name: "NegativeDurationSanitizer", sanitizer: NewNegativeDurationSanitizer(), setupFunc: func(traces ptrace.Traces) { // Create a trace with negative duration to trigger sanitization rSpans := traces.ResourceSpans().AppendEmpty() sSpans := rSpans.ScopeSpans().AppendEmpty() span := sSpans.Spans().AppendEmpty() span.SetName("test-span") // Set end time before start time to create negative duration span.SetStartTimestamp(pcommon.NewTimestampFromTime(pcommon.Timestamp(2000).AsTime())) span.SetEndTimestamp(pcommon.NewTimestampFromTime(pcommon.Timestamp(1000).AsTime())) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { traces := ptrace.NewTraces() tt.setupFunc(traces) // Mark traces as read-only to simulate multiple exporters scenario traces.MarkReadOnly() result := tt.sanitizer(traces) require.NotNil(t, result) assert.NotSame(t, &traces, &result, "Expected a copy to be made for read-only traces that need modification") }) } } // Tests that sanitizers return the original // traces when no modification is needed, even if they are read-only func TestSanitizersWithReadOnlyTracesNoModification(t *testing.T) { tests := []struct { name string sanitizer Func setupFunc func(ptrace.Traces) }{ { name: "EmptyServiceNameSanitizer_NoModification", sanitizer: NewEmptyServiceNameSanitizer(), setupFunc: func(traces ptrace.Traces) { rSpans := traces.ResourceSpans().AppendEmpty() rSpans.Resource().Attributes().PutStr(string(otelsemconv.ServiceNameKey), "valid-service") sSpans := rSpans.ScopeSpans().AppendEmpty() span := sSpans.Spans().AppendEmpty() span.SetName("test-span") }, }, { name: "EmptySpanNameSanitizer_NoModification", sanitizer: NewEmptySpanNameSanitizer(), setupFunc: func(traces ptrace.Traces) { rSpans := traces.ResourceSpans().AppendEmpty() sSpans := rSpans.ScopeSpans().AppendEmpty() span := sSpans.Spans().AppendEmpty() span.SetName("valid-span") }, }, { name: "UTF8Sanitizer_NoModification", sanitizer: NewUTF8Sanitizer(), setupFunc: func(traces ptrace.Traces) { rSpans := traces.ResourceSpans().AppendEmpty() sSpans := rSpans.ScopeSpans().AppendEmpty() span := sSpans.Spans().AppendEmpty() span.SetName("test-span") span.Attributes().PutStr("valid-key", "valid-value") }, }, { name: "NegativeDurationSanitizer_NoModification", sanitizer: NewNegativeDurationSanitizer(), setupFunc: func(traces ptrace.Traces) { rSpans := traces.ResourceSpans().AppendEmpty() sSpans := rSpans.ScopeSpans().AppendEmpty() span := sSpans.Spans().AppendEmpty() span.SetName("test-span") span.SetStartTimestamp(pcommon.NewTimestampFromTime(pcommon.Timestamp(1000).AsTime())) span.SetEndTimestamp(pcommon.NewTimestampFromTime(pcommon.Timestamp(2000).AsTime())) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { traces := ptrace.NewTraces() tt.setupFunc(traces) traces.MarkReadOnly() result := tt.sanitizer(traces) require.NotNil(t, result) assert.True(t, result.IsReadOnly(), "Expected the result to remain read-only when no modification is needed") }) } } func TestChainedSanitizerWithReadOnlyTraces(t *testing.T) { // Create traces that need multiple sanitizations traces := ptrace.NewTraces() rSpans := traces.ResourceSpans().AppendEmpty() sSpans := rSpans.ScopeSpans().AppendEmpty() span := sSpans.Spans().AppendEmpty() span.SetName("test-span") // No service.name (triggers empty service name sanitizer) // Invalid UTF-8 attribute (triggers UTF8 sanitizer) invalidUTF8 := string([]byte{0xff, 0xfe, 0xfd}) span.Attributes().PutStr("invalid-utf8", invalidUTF8) // Negative duration (triggers negative duration sanitizer) span.SetStartTimestamp(pcommon.NewTimestampFromTime(pcommon.Timestamp(2000).AsTime())) span.SetEndTimestamp(pcommon.NewTimestampFromTime(pcommon.Timestamp(1000).AsTime())) traces.MarkReadOnly() chainedSanitizer := Sanitize result := chainedSanitizer(traces) require.NotNil(t, result) // Verify that sanitization occurred by checking the service name was added resultResourceSpans := result.ResourceSpans() require.Positive(t, resultResourceSpans.Len()) serviceName, ok := resultResourceSpans.At(0).Resource().Attributes().Get(string(otelsemconv.ServiceNameKey)) require.True(t, ok) assert.Equal(t, "missing-service-name", serviceName.Str()) } ================================================ FILE: internal/jptrace/sanitizer/sanitizer.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package sanitizer import ( "go.opentelemetry.io/collector/pdata/ptrace" ) // Func is a function that performs enrichment, clean-up, or normalization of trace data. type Func func(traces ptrace.Traces) ptrace.Traces // Sanitize is a function that applies all sanitizers to the given trace data. var Sanitize = NewChainedSanitizer(NewStandardSanitizers()...) // NewStandardSanitizers returns a list of all the sanitizers that are used by the // storage exporter. func NewStandardSanitizers() []Func { return []Func{ NewEmptyServiceNameSanitizer(), NewEmptySpanNameSanitizer(), NewUTF8Sanitizer(), NewNegativeDurationSanitizer(), } } // NewChainedSanitizer creates a Sanitizer from the variadic list of passed Sanitizers. // If the list only has one element, it is returned directly to minimize indirection. func NewChainedSanitizer(sanitizers ...Func) Func { if len(sanitizers) == 1 { return sanitizers[0] } return func(traces ptrace.Traces) ptrace.Traces { for _, s := range sanitizers { traces = s(traces) } return traces } } ================================================ FILE: internal/jptrace/sanitizer/sanitizer_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package sanitizer import ( "testing" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/ptrace" ) func TestNewStandardSanitizers(t *testing.T) { sanitizers := NewStandardSanitizers() require.Len(t, sanitizers, 4) } func TestNewChainedSanitizer(t *testing.T) { var s1 Func = func(traces ptrace.Traces) ptrace.Traces { traces. ResourceSpans(). AppendEmpty(). Resource(). Attributes(). PutStr("hello", "world") return traces } var s2 Func = func(traces ptrace.Traces) ptrace.Traces { traces. ResourceSpans(). At(0). Resource(). Attributes(). PutStr("hello", "goodbye") return traces } c1 := NewChainedSanitizer(s1) t1 := c1(ptrace.NewTraces()) hello, ok := t1. ResourceSpans(). At(0). Resource(). Attributes(). Get("hello") require.True(t, ok) require.Equal(t, "world", hello.Str()) c2 := NewChainedSanitizer(s1, s2) t2 := c2(ptrace.NewTraces()) hello, ok = t2. ResourceSpans(). At(0). Resource(). Attributes(). Get("hello") require.True(t, ok) require.Equal(t, "goodbye", hello.Str()) } ================================================ FILE: internal/jptrace/sanitizer/utf8.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package sanitizer import ( "fmt" "unicode/utf8" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" ) const ( invalidSpanName = "invalid-span-name" invalidTagKey = "invalid-tag-key" ) // NewUTF8Sanitizer returns a sanitizer function that performs the following sanitizations // on the trace data: // - Checks all attributes of the span to ensure that the keys are valid UTF-8 strings // and all string typed values are valid UTF-8 strings. If any keys or values are invalid, // they are replaced with a valid UTF-8 string containing debugging data related to the invalidations. // - Explicitly checks that all span names are valid UTF-8 strings. If they are not, they are replaced // with a valid UTF-8 string and debugging information is put into the attributes of the span. func NewUTF8Sanitizer() Func { return sanitizeUTF8 } func sanitizeUTF8(traces ptrace.Traces) ptrace.Traces { if !tracesNeedUTF8Sanitization(traces) { return traces } var workingTraces ptrace.Traces if traces.IsReadOnly() { workingTraces = ptrace.NewTraces() traces.CopyTo(workingTraces) } else { workingTraces = traces } workingResourceSpans := workingTraces.ResourceSpans() for i := 0; i < workingResourceSpans.Len(); i++ { resourceSpan := workingResourceSpans.At(i) sanitizeAttributes(resourceSpan.Resource().Attributes()) scopeSpans := resourceSpan.ScopeSpans() for j := 0; j < scopeSpans.Len(); j++ { scopeSpan := scopeSpans.At(j) sanitizeAttributes(scopeSpan.Scope().Attributes()) spans := scopeSpan.Spans() for k := 0; k < spans.Len(); k++ { span := spans.At(k) if !utf8.ValidString(span.Name()) { sanitized := []byte(span.Name()) newVal := span.Attributes().PutEmptyBytes(invalidSpanName) newVal.Append(sanitized...) span.SetName(invalidSpanName) } sanitizeAttributes(span.Attributes()) } } } return workingTraces } func tracesNeedUTF8Sanitization(traces ptrace.Traces) bool { for _, resourceSpan := range traces.ResourceSpans().All() { if attributesNeedUTF8Sanitization(resourceSpan.Resource().Attributes()) { return true } for _, scopeSpan := range resourceSpan.ScopeSpans().All() { if attributesNeedUTF8Sanitization(scopeSpan.Scope().Attributes()) { return true } for _, span := range scopeSpan.Spans().All() { if !utf8.ValidString(span.Name()) || attributesNeedUTF8Sanitization(span.Attributes()) { return true } } } } return false } func attributesNeedUTF8Sanitization(attributes pcommon.Map) bool { needsSanitization := false for k, v := range attributes.All() { if !utf8.ValidString(k) { needsSanitization = true break } if v.Type() == pcommon.ValueTypeStr && !utf8.ValidString(v.Str()) { needsSanitization = true break } } return needsSanitization } func sanitizeAttributes(attributes pcommon.Map) { // collect invalid keys during iteration to avoid changing the keys of the map // while iterating over the map using Range invalidKeys := make(map[string]pcommon.Value) attributes.Range(func(k string, v pcommon.Value) bool { if !utf8.ValidString(k) { invalidKeys[k] = v } if v.Type() == pcommon.ValueTypeStr && !utf8.ValidString(v.Str()) { sanitized := []byte(v.Str()) newVal := attributes.PutEmptyBytes(k) newVal.Append(sanitized...) } return true }) i := 1 for k, v := range invalidKeys { sanitized := []byte(k + ":") switch v.Type() { case pcommon.ValueTypeBytes: sanitized = append(sanitized, v.Bytes().AsRaw()...) default: sanitized = append(sanitized, []byte(v.AsString())...) } newKey := fmt.Sprintf("%s-%d", invalidTagKey, i) newVal := attributes.PutEmptyBytes(newKey) newVal.Append(sanitized...) i++ attributes.Remove(k) } } ================================================ FILE: internal/jptrace/sanitizer/utf8_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package sanitizer import ( "encoding/hex" "fmt" "testing" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" ) func invalidUTF8() string { s, _ := hex.DecodeString("fefeffff") return string(s) } func getBytesValueFromString(s string) pcommon.Value { b := pcommon.NewValueBytes() b.Bytes().Append([]byte(s)...) return b } var utf8EncodingTests = []struct { name string key string value string expectedKey string expectedValue pcommon.Value }{ { name: "valid key + valid value", key: "key", value: "value", expectedKey: "key", expectedValue: pcommon.NewValueStr("value"), }, { name: "invalid key + valid value", key: invalidUTF8(), value: "value", expectedKey: "invalid-tag-key-1", expectedValue: getBytesValueFromString(invalidUTF8() + ":value"), }, { name: "valid key + invalid value", key: "key", value: invalidUTF8(), expectedKey: "key", expectedValue: getBytesValueFromString(invalidUTF8()), }, { name: "invalid key + invalid value", key: invalidUTF8(), value: invalidUTF8(), expectedKey: "invalid-tag-key-1", expectedValue: getBytesValueFromString(fmt.Sprintf("%s:%s", invalidUTF8(), invalidUTF8())), }, } func TestUTF8Sanitizer_SanitizesResourceSpanAttributes(t *testing.T) { tests := utf8EncodingTests for _, test := range tests { t.Run(test.name, func(t *testing.T) { traces := ptrace.NewTraces() traces. ResourceSpans(). AppendEmpty(). Resource(). Attributes(). PutStr(test.key, test.value) sanitizer := NewUTF8Sanitizer() sanitized := sanitizer(traces) value, ok := sanitized. ResourceSpans(). At(0). Resource(). Attributes(). Get(test.expectedKey) require.True(t, ok) require.Equal(t, test.expectedValue, value) }) } } func TestUTF8Sanitizer_SanitizesScopeSpanAttributes(t *testing.T) { tests := utf8EncodingTests for _, test := range tests { t.Run(test.name, func(t *testing.T) { traces := ptrace.NewTraces() traces. ResourceSpans(). AppendEmpty(). ScopeSpans(). AppendEmpty(). Scope(). Attributes(). PutStr(test.key, test.value) sanitizer := NewUTF8Sanitizer() sanitized := sanitizer(traces) value, ok := sanitized. ResourceSpans(). At(0). ScopeSpans(). At(0). Scope(). Attributes(). Get(test.expectedKey) require.True(t, ok) require.Equal(t, test.expectedValue, value) }) } } func TestUTF8Sanitizer_SanitizesSpanAttributes(t *testing.T) { tests := utf8EncodingTests for _, test := range tests { t.Run(test.name, func(t *testing.T) { traces := ptrace.NewTraces() traces. ResourceSpans(). AppendEmpty(). ScopeSpans(). AppendEmpty(). Spans(). AppendEmpty(). Attributes(). PutStr(test.key, test.value) sanitizer := NewUTF8Sanitizer() sanitized := sanitizer(traces) value, ok := sanitized. ResourceSpans(). At(0). ScopeSpans(). At(0). Spans(). At(0). Attributes(). Get(test.expectedKey) require.True(t, ok) require.Equal(t, test.expectedValue, value) }) } } func TestUTF8Sanitizer_SanitizesInvalidSpanName(t *testing.T) { traces := ptrace.NewTraces() traces. ResourceSpans(). AppendEmpty(). ScopeSpans(). AppendEmpty(). Spans(). AppendEmpty(). SetName(invalidUTF8()) sanitizer := NewUTF8Sanitizer() sanitized := sanitizer(traces) name := sanitized. ResourceSpans(). At(0). ScopeSpans(). At(0). Spans(). At(0). Name() require.Equal(t, "invalid-span-name", name) } func TestUTF8Sanitizer_DoesNotSanitizeValidSpanName(t *testing.T) { traces := ptrace.NewTraces() traces. ResourceSpans(). AppendEmpty(). ScopeSpans(). AppendEmpty(). Spans(). AppendEmpty(). SetName("name") sanitizer := NewUTF8Sanitizer() sanitized := sanitizer(traces) name := sanitized. ResourceSpans(). At(0). ScopeSpans(). At(0). Spans(). At(0). Name() require.Equal(t, "name", name) } func TestUTF8Sanitizer_RemovesInvalidKeys(t *testing.T) { k1 := fmt.Sprintf("%s-%d", invalidUTF8(), 1) k2 := fmt.Sprintf("%s-%d", invalidUTF8(), 2) traces := ptrace.NewTraces() attributes := traces. ResourceSpans(). AppendEmpty(). Resource(). Attributes() attributes.PutStr(k1, "v1") attributes.PutStr(k2, "v2") sanitizer := NewUTF8Sanitizer() sanitized := sanitizer(traces) _, ok := sanitized. ResourceSpans(). At(0). Resource(). Attributes(). Get(k1) require.False(t, ok) sanitizer = NewUTF8Sanitizer() sanitized = sanitizer(traces) _, ok = sanitized. ResourceSpans(). At(0). Resource(). Attributes(). Get(k2) require.False(t, ok) } func TestUTF8Sanitizer_DoesNotSanitizeNonStringAttributeValue(t *testing.T) { traces := ptrace.NewTraces() traces. ResourceSpans(). AppendEmpty(). Resource(). Attributes(). PutInt("key", 99) sanitizer := NewUTF8Sanitizer() sanitized := sanitizer(traces) value, ok := sanitized. ResourceSpans(). At(0). Resource(). Attributes(). Get("key") require.True(t, ok) require.EqualValues(t, 99, value.Int()) } func TestUTF8Sanitizer_SanitizesNonStringAttributeValueWithInvalidKey(t *testing.T) { traces := ptrace.NewTraces() traces. ResourceSpans(). AppendEmpty(). Resource(). Attributes(). PutInt(invalidUTF8(), 99) sanitizer := NewUTF8Sanitizer() sanitized := sanitizer(traces) value, ok := sanitized. ResourceSpans(). At(0). Resource(). Attributes(). Get("invalid-tag-key-1") require.True(t, ok) require.Equal(t, getBytesValueFromString(invalidUTF8()+":99"), value) } func TestUTF8Sanitizer_SanitizesMultipleAttributesWithInvalidKeys(t *testing.T) { k1 := fmt.Sprintf("%s-%d", invalidUTF8(), 1) k2 := fmt.Sprintf("%s-%d", invalidUTF8(), 2) traces := ptrace.NewTraces() attributes := traces. ResourceSpans(). AppendEmpty(). Resource(). Attributes() attributes.PutStr(k1, "v1") attributes.PutStr(k2, "v2") sanitizer := NewUTF8Sanitizer() sanitized := sanitizer(traces) got := sanitized. ResourceSpans(). At(0). Resource(). Attributes() require.Equal(t, 2, got.Len()) expectedValues := []pcommon.Value{ getBytesValueFromString(k1 + ":v1"), getBytesValueFromString(k2 + ":v2"), } value, ok := got. Get("invalid-tag-key-1") require.True(t, ok) require.Contains(t, expectedValues, value) checked := value value, ok = got. Get("invalid-tag-key-2") require.True(t, ok) require.NotEqual(t, checked, value) require.Contains(t, expectedValues, value) } ================================================ FILE: internal/jptrace/spaniter.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jptrace import ( "iter" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" ) type SpanIterPos struct { Resource ptrace.ResourceSpans ResourceIndex int Scope ptrace.ScopeSpans ScopeIndex int } // SpanIter iterates over all spans in the provided ptrace.Traces and yields each span. func SpanIter(traces ptrace.Traces) iter.Seq2[SpanIterPos, ptrace.Span] { return func(yield func(SpanIterPos, ptrace.Span) bool) { var pos SpanIterPos for i := 0; i < traces.ResourceSpans().Len(); i++ { resource := traces.ResourceSpans().At(i) pos.Resource = resource pos.ResourceIndex = i for j := 0; j < resource.ScopeSpans().Len(); j++ { scope := resource.ScopeSpans().At(j) pos.Scope = scope pos.ScopeIndex = j for k := 0; k < scope.Spans().Len(); k++ { span := scope.Spans().At(k) if !yield(pos, span) { return } } } } } } func GetTraceID(traces ptrace.Traces) pcommon.TraceID { for _, span := range SpanIter(traces) { return span.TraceID() } return pcommon.NewTraceIDEmpty() } ================================================ FILE: internal/jptrace/spaniter_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jptrace import ( "testing" "github.com/stretchr/testify/assert" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" ) func TestSpanIter(t *testing.T) { traces := ptrace.NewTraces() resource1 := traces.ResourceSpans().AppendEmpty() scope1 := resource1.ScopeSpans().AppendEmpty() span1 := scope1.Spans().AppendEmpty() span1.SetName("span-1") span2 := scope1.Spans().AppendEmpty() span2.SetName("span-2") resource2 := traces.ResourceSpans().AppendEmpty() scope2 := resource2.ScopeSpans().AppendEmpty() span3 := scope2.Spans().AppendEmpty() span3.SetName("span-3") scope3 := resource2.ScopeSpans().AppendEmpty() span4 := scope3.Spans().AppendEmpty() span4.SetName("span-4") spanIter := SpanIter(traces) var spans []ptrace.Span var positions []SpanIterPos spanIter(func(pos SpanIterPos, span ptrace.Span) bool { spans = append(spans, span) positions = append(positions, pos) return true }) assert.Len(t, spans, 4) assert.Equal(t, "span-1", spans[0].Name()) assert.Equal(t, "span-2", spans[1].Name()) assert.Equal(t, "span-3", spans[2].Name()) assert.Equal(t, "span-4", spans[3].Name()) assert.Len(t, positions, 4) assert.Equal(t, 0, positions[0].ResourceIndex) assert.Equal(t, resource1, positions[0].Resource) assert.Equal(t, 0, positions[0].ScopeIndex) assert.Equal(t, scope1, positions[0].Scope) assert.Equal(t, 0, positions[1].ResourceIndex) assert.Equal(t, resource1, positions[1].Resource) assert.Equal(t, 0, positions[1].ScopeIndex) assert.Equal(t, scope1, positions[1].Scope) assert.Equal(t, 1, positions[2].ResourceIndex) assert.Equal(t, resource2, positions[2].Resource) assert.Equal(t, 0, positions[2].ScopeIndex) assert.Equal(t, scope2, positions[2].Scope) assert.Equal(t, 1, positions[3].ResourceIndex) assert.Equal(t, resource2, positions[3].Resource) assert.Equal(t, 1, positions[3].ScopeIndex) assert.Equal(t, scope3, positions[3].Scope) } func TestSpanIterStopIteration(t *testing.T) { traces := ptrace.NewTraces() resource1 := traces.ResourceSpans().AppendEmpty() scope1 := resource1.ScopeSpans().AppendEmpty() span1 := scope1.Spans().AppendEmpty() span1.SetName("span-1") span2 := scope1.Spans().AppendEmpty() span2.SetName("span-2") spanIter := SpanIter(traces) var spans []ptrace.Span spanIter(func(_ SpanIterPos, span ptrace.Span) bool { spans = append(spans, span) return false }) assert.Len(t, spans, 1) assert.Equal(t, "span-1", spans[0].Name()) } func TestGetTraceID(t *testing.T) { t.Run("empty traces returns empty TraceID", func(t *testing.T) { traces := ptrace.NewTraces() assert.Equal(t, pcommon.NewTraceIDEmpty(), GetTraceID(traces)) }) t.Run("returns TraceID of first span", func(t *testing.T) { traces := ptrace.NewTraces() span := traces.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty() traceID := pcommon.TraceID([16]byte{1, 2, 3}) span.SetTraceID(traceID) assert.Equal(t, traceID, GetTraceID(traces)) }) } ================================================ FILE: internal/jptrace/spankind.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jptrace import ( "strings" "go.opentelemetry.io/collector/pdata/ptrace" ) func StringToSpanKind(sk string) ptrace.SpanKind { switch strings.ToLower(sk) { case "internal": return ptrace.SpanKindInternal case "server": return ptrace.SpanKindServer case "client": return ptrace.SpanKindClient case "producer": return ptrace.SpanKindProducer case "consumer": return ptrace.SpanKindConsumer default: return ptrace.SpanKindUnspecified } } func SpanKindToString(sk ptrace.SpanKind) string { if sk == ptrace.SpanKindUnspecified { return "" } return strings.ToLower(sk.String()) } ================================================ FILE: internal/jptrace/spankind_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jptrace import ( "testing" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/ptrace" ) func TestStringToSpanKind(t *testing.T) { tests := []struct { str string want ptrace.SpanKind }{ { str: "unspecified", want: ptrace.SpanKindUnspecified, }, { str: "internal", want: ptrace.SpanKindInternal, }, { str: "server", want: ptrace.SpanKindServer, }, { str: "client", want: ptrace.SpanKindClient, }, { str: "producer", want: ptrace.SpanKindProducer, }, { str: "consumer", want: ptrace.SpanKindConsumer, }, { str: "unknown", want: ptrace.SpanKindUnspecified, }, { str: "", want: ptrace.SpanKindUnspecified, }, { str: "invalid", want: ptrace.SpanKindUnspecified, }, } for _, tt := range tests { t.Run(tt.str, func(t *testing.T) { require.Equal(t, tt.want, StringToSpanKind(tt.str)) }) } } func TestSpanKindToString(t *testing.T) { tests := []struct { kind ptrace.SpanKind want string }{ { kind: ptrace.SpanKindUnspecified, want: "", }, { kind: ptrace.SpanKindInternal, want: "internal", }, { kind: ptrace.SpanKindServer, want: "server", }, { kind: ptrace.SpanKindClient, want: "client", }, { kind: ptrace.SpanKindProducer, want: "producer", }, { kind: ptrace.SpanKindConsumer, want: "consumer", }, } for _, tt := range tests { t.Run(tt.want, func(t *testing.T) { require.Equal(t, tt.want, SpanKindToString(tt.kind)) }) } } ================================================ FILE: internal/jptrace/spanmap.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jptrace import "go.opentelemetry.io/collector/pdata/ptrace" // SpanMap iterates over all spans in the provided ptrace.Traces and maps each span // to a key generated by the provided keyFn function. The resulting map has keys of type K // and values of type ptrace.Span. func SpanMap[K comparable](traces ptrace.Traces, keyFn func(ptrace.Span) K) map[K]ptrace.Span { spanMap := make(map[K]ptrace.Span) for i := 0; i < traces.ResourceSpans().Len(); i++ { resource := traces.ResourceSpans().At(i) for j := 0; j < resource.ScopeSpans().Len(); j++ { scope := resource.ScopeSpans().At(j) for k := 0; k < scope.Spans().Len(); k++ { span := scope.Spans().At(k) spanMap[keyFn(span)] = span } } } return spanMap } ================================================ FILE: internal/jptrace/spanmap_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jptrace import ( "testing" "github.com/stretchr/testify/assert" "go.opentelemetry.io/collector/pdata/ptrace" ) func TestSpanMap(t *testing.T) { traces := ptrace.NewTraces() rs := traces.ResourceSpans().AppendEmpty() ss := rs.ScopeSpans().AppendEmpty() span1 := ss.Spans().AppendEmpty() span1.SetName("span1") span2 := ss.Spans().AppendEmpty() span2.SetName("span2") keyFn := func(span ptrace.Span) string { return span.Name() } spanMap := SpanMap(traces, keyFn) expectedMap := map[string]ptrace.Span{ "span1": span1, "span2": span2, } assert.Equal(t, expectedMap, spanMap) } ================================================ FILE: internal/jptrace/statuscode.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jptrace import "go.opentelemetry.io/collector/pdata/ptrace" func StringToStatusCode(sc string) ptrace.StatusCode { switch sc { case "Ok": return ptrace.StatusCodeOk case "Error": return ptrace.StatusCodeError default: return ptrace.StatusCodeUnset } } ================================================ FILE: internal/jptrace/statuscode_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jptrace import ( "testing" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/ptrace" ) func TestConvertStatusCode(t *testing.T) { tests := []struct { str string want ptrace.StatusCode }{ { str: "Ok", want: ptrace.StatusCodeOk, }, { str: "Unset", want: ptrace.StatusCodeUnset, }, { str: "Error", want: ptrace.StatusCodeError, }, { str: "Unknown", want: ptrace.StatusCodeUnset, }, { str: "", want: ptrace.StatusCodeUnset, }, { str: "invalid", want: ptrace.StatusCodeUnset, }, } for _, tt := range tests { t.Run(tt.str, func(t *testing.T) { require.Equal(t, tt.want, StringToStatusCode(tt.str)) }) } } ================================================ FILE: internal/jptrace/traces.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jptrace import ( "github.com/gogo/protobuf/jsonpb" "github.com/gogo/protobuf/proto" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger/internal/gogocodec" ) // TracesData is an alias to ptrace.Traces that supports Gogo marshaling. // Our .proto APIs may refer to otlp.TraceData type, but its corresponding // protoc-generated struct is internal in OTel Collector, so we substitute // it for this TracesData type that implements marshaling methods by // delegating to public functions in the OTel Collector's ptrace module. type TracesData ptrace.Traces var ( _ gogocodec.CustomType = (*TracesData)(nil) _ proto.Message = (*TracesData)(nil) ) func (td TracesData) ToTraces() ptrace.Traces { return ptrace.Traces(td) } // Marshal implements gogocodec.CustomType. func (td *TracesData) Marshal() ([]byte, error) { return new(ptrace.ProtoMarshaler).MarshalTraces(td.ToTraces()) } // MarshalTo implements gogocodec.CustomType. func (td *TracesData) MarshalTo(buf []byte) (n int, err error) { return td.MarshalToSizedBuffer(buf) } // MarshalToSizedBuffer is used by Gogo. func (td *TracesData) MarshalToSizedBuffer(buf []byte) (int, error) { data, err := td.Marshal() if err != nil { return 0, err } n := copy(buf, data) return n, nil } // MarshalJSONPB implements gogocodec.CustomType. func (td *TracesData) MarshalJSONPB(*jsonpb.Marshaler) ([]byte, error) { return new(ptrace.JSONMarshaler).MarshalTraces(td.ToTraces()) } // UnmarshalJSONPB implements gogocodec.CustomType. func (td *TracesData) UnmarshalJSONPB(_ *jsonpb.Unmarshaler, data []byte) error { t, err := new(ptrace.JSONUnmarshaler).UnmarshalTraces(data) if err != nil { return err } *td = TracesData(t) return nil } // Size implements gogocodec.CustomType. func (td *TracesData) Size() int { return new(ptrace.ProtoMarshaler).TracesSize(td.ToTraces()) } // Unmarshal implements gogocodec.CustomType. func (td *TracesData) Unmarshal(data []byte) error { t, err := new(ptrace.ProtoUnmarshaler).UnmarshalTraces(data) if err != nil { return err } *td = TracesData(t) return nil } // ProtoMessage implements proto.Message. func (*TracesData) ProtoMessage() { // nothing to do here } // Reset implements proto.Message. func (td *TracesData) Reset() { *td = TracesData(ptrace.NewTraces()) } // String implements proto.Message. func (*TracesData) String() string { return "*TracesData" } ================================================ FILE: internal/jptrace/traces_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jptrace import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/ptrace" ) func TestTracesData(t *testing.T) { td := TracesData(ptrace.NewTraces()) // Test ToTraces assert.Equal(t, ptrace.Traces(td), td.ToTraces()) // Test Marshal _, err := td.Marshal() require.NoError(t, err) // Test MarshalTo _, err = td.MarshalTo(make([]byte, td.Size())) require.NoError(t, err) // Test MarshalJSONPB _, err = td.MarshalJSONPB(nil) require.NoError(t, err) // Test UnmarshalJSONPB err = td.UnmarshalJSONPB(nil, []byte(`{"resourceSpans":[]}`)) require.NoError(t, err) err = td.UnmarshalJSONPB(nil, []byte(`{"resourceSpans":123}`)) require.Error(t, err) // Test Size assert.Equal(t, 0, td.Size()) // Test Unmarshal err = td.Unmarshal([]byte{}) require.NoError(t, err) err = td.Unmarshal([]byte{1}) require.Error(t, err) // Test ProtoMessage td.ProtoMessage() // Test Reset td.Reset() assert.Equal(t, TracesData(ptrace.NewTraces()), td) // Test String assert.Equal(t, "*TracesData", td.String()) } ================================================ FILE: internal/jptrace/valuetype.go ================================================ // Copyright (c) 2026 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jptrace import ( "strings" "go.opentelemetry.io/collector/pdata/pcommon" ) func StringToValueType(vt string) pcommon.ValueType { switch strings.ToLower(vt) { case "bool": return pcommon.ValueTypeBool case "double": return pcommon.ValueTypeDouble case "int": return pcommon.ValueTypeInt case "str": return pcommon.ValueTypeStr case "bytes": return pcommon.ValueTypeBytes case "map": return pcommon.ValueTypeMap case "slice": return pcommon.ValueTypeSlice default: return pcommon.ValueTypeEmpty } } func ValueTypeToString(vt pcommon.ValueType) string { if vt == pcommon.ValueTypeEmpty { return "" } return strings.ToLower(vt.String()) } ================================================ FILE: internal/jptrace/valuetype_test.go ================================================ // Copyright (c) 2026 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jptrace import ( "testing" "github.com/stretchr/testify/assert" "go.opentelemetry.io/collector/pdata/pcommon" ) func TestStringToValueType(t *testing.T) { tests := []struct { name string input string expected pcommon.ValueType }{ { name: "bool", input: "bool", expected: pcommon.ValueTypeBool, }, { name: "BOOL uppercase", input: "BOOL", expected: pcommon.ValueTypeBool, }, { name: "double", input: "double", expected: pcommon.ValueTypeDouble, }, { name: "int", input: "int", expected: pcommon.ValueTypeInt, }, { name: "str", input: "str", expected: pcommon.ValueTypeStr, }, { name: "bytes", input: "bytes", expected: pcommon.ValueTypeBytes, }, { name: "map", input: "map", expected: pcommon.ValueTypeMap, }, { name: "slice", input: "slice", expected: pcommon.ValueTypeSlice, }, { name: "unknown string", input: "unknown", expected: pcommon.ValueTypeEmpty, }, { name: "empty string", input: "", expected: pcommon.ValueTypeEmpty, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := StringToValueType(tt.input) assert.Equal(t, tt.expected, result) }) } } func TestValueTypeToString(t *testing.T) { tests := []struct { name string input pcommon.ValueType expected string }{ { name: "ValueTypeBool", input: pcommon.ValueTypeBool, expected: "bool", }, { name: "ValueTypeDouble", input: pcommon.ValueTypeDouble, expected: "double", }, { name: "ValueTypeInt", input: pcommon.ValueTypeInt, expected: "int", }, { name: "ValueTypeStr", input: pcommon.ValueTypeStr, expected: "str", }, { name: "ValueTypeBytes", input: pcommon.ValueTypeBytes, expected: "bytes", }, { name: "ValueTypeMap", input: pcommon.ValueTypeMap, expected: "map", }, { name: "ValueTypeSlice", input: pcommon.ValueTypeSlice, expected: "slice", }, { name: "ValueTypeEmpty", input: pcommon.ValueTypeEmpty, expected: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ValueTypeToString(tt.input) assert.Equal(t, tt.expected, result) }) } } func TestStringToValueTypeToString(t *testing.T) { validTypes := []string{"bool", "double", "int", "str", "bytes", "map", "slice"} for _, vt := range validTypes { t.Run(vt, func(t *testing.T) { valueType := StringToValueType(vt) result := ValueTypeToString(valueType) assert.Equal(t, vt, result) }) } } ================================================ FILE: internal/jptrace/warning.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jptrace import ( "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" ) func AddWarnings(span ptrace.Span, warnings ...string) { var w pcommon.Slice if currWarnings, ok := span.Attributes().Get(WarningsAttribute); ok { w = currWarnings.Slice() } else { w = span.Attributes().PutEmptySlice(WarningsAttribute) } for _, warning := range warnings { w.AppendEmpty().SetStr(warning) } } func GetWarnings(span ptrace.Span) []string { if wa, ok := span.Attributes().Get(WarningsAttribute); ok { switch wa.Type() { case pcommon.ValueTypeSlice: warnings := []string{} ws := wa.Slice() for i := 0; i < ws.Len(); i++ { warnings = append(warnings, ws.At(i).Str()) } return warnings default: // fallback for malformed data return []string{wa.AsString()} } } return nil } ================================================ FILE: internal/jptrace/warning_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jptrace import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/ptrace" ) func TestAddWarning(t *testing.T) { tests := []struct { name string existing []string newWarn string expected []string }{ { name: "add to nil warnings", existing: nil, newWarn: "new warning", expected: []string{"new warning"}, }, { name: "add to empty warnings", existing: []string{}, newWarn: "new warning", expected: []string{"new warning"}, }, { name: "add to existing warnings", existing: []string{"existing warning 1", "existing warning 2"}, newWarn: "new warning", expected: []string{"existing warning 1", "existing warning 2", "new warning"}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { span := ptrace.NewSpan() attrs := span.Attributes() if test.existing != nil { warnings := attrs.PutEmptySlice(WarningsAttribute) for _, warn := range test.existing { warnings.AppendEmpty().SetStr(warn) } } AddWarnings(span, test.newWarn) warnings, ok := attrs.Get(WarningsAttribute) assert.True(t, ok) assert.Equal(t, len(test.expected), warnings.Slice().Len()) for i, expectedWarn := range test.expected { assert.Equal(t, expectedWarn, warnings.Slice().At(i).Str()) } }) } } func TestAddWarning_MultipleWarnings(t *testing.T) { span := ptrace.NewSpan() AddWarnings(span, "warning-1", "warning-2") warnings, ok := span.Attributes().Get(WarningsAttribute) require.True(t, ok) require.Equal(t, "warning-1", warnings.Slice().At(0).Str()) require.Equal(t, "warning-2", warnings.Slice().At(1).Str()) } func TestGetWarnings(t *testing.T) { tests := []struct { name string existing []string expected []string }{ { name: "get from nil warnings", existing: nil, expected: nil, }, { name: "get from empty warnings", existing: []string{}, expected: []string{}, }, { name: "get from existing warnings", existing: []string{"existing warning 1", "existing warning 2"}, expected: []string{"existing warning 1", "existing warning 2"}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { span := ptrace.NewSpan() attrs := span.Attributes() if test.existing != nil { warnings := attrs.PutEmptySlice(WarningsAttribute) for _, warn := range test.existing { warnings.AppendEmpty().SetStr(warn) } } actual := GetWarnings(span) assert.Equal(t, test.expected, actual) }) } } func TestGetWarnings_EmptySpan(t *testing.T) { span := ptrace.NewSpan() span.Attributes().PutStr(WarningsAttribute, "warning-1") actual := GetWarnings(span) assert.Equal(t, []string{"warning-1"}, actual) } ================================================ FILE: internal/jtracer/jtracer.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jtracer import ( "context" "os" "strings" "sync" "go.opentelemetry.io/contrib/samplers/jaegerremote" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace" "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) var once sync.Once func NewProvider(ctx context.Context, serviceName string) (trace.TracerProvider, func(ctx context.Context) error, error) { return newProviderHelper(ctx, serviceName, initOTEL) } func newProviderHelper( ctx context.Context, serviceName string, tracerProvider func(ctx context.Context, svc string) (*sdktrace.TracerProvider, func(), error), ) (trace.TracerProvider, func(ctx context.Context) error, error) { provider, closeSampler, err := tracerProvider(ctx, serviceName) if err != nil { return nil, nil, err } closer := func(ctx context.Context) error { if closeSampler != nil { closeSampler() } return provider.Shutdown(ctx) } return provider, closer, nil } // initOTEL initializes OTEL Tracer func initOTEL(ctx context.Context, svc string) (*sdktrace.TracerProvider, func(), error) { return initHelper(ctx, svc, otelExporter, otelResource) } func initHelper( ctx context.Context, svc string, otelExporter func(_ context.Context) (sdktrace.SpanExporter, error), otelResource func(_ context.Context, _ /* svc */ string) (*resource.Resource, error), ) (*sdktrace.TracerProvider, func(), error) { res, err := otelResource(ctx, svc) if err != nil { return nil, nil, err } traceExporter, err := otelExporter(ctx) if err != nil { return nil, nil, err } // Register the trace exporter with a TracerProvider, using a batch // span processor to aggregate spans before export. bsp := sdktrace.NewBatchSpanProcessor(traceExporter) opts := []sdktrace.TracerProviderOption{ sdktrace.WithSpanProcessor(bsp), sdktrace.WithResource(res), } var closeSampler func() if strings.ToLower(os.Getenv("OTEL_TRACES_SAMPLER")) == "jaeger_remote" { s := jaegerremote.New(svc) opts = append(opts, sdktrace.WithSampler(s)) closeSampler = s.Close } tracerProvider := sdktrace.NewTracerProvider(opts...) once.Do(func() { otel.SetTextMapPropagator( propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, propagation.Baggage{}, )) }) otel.SetTracerProvider(tracerProvider) return tracerProvider, closeSampler, nil } func otelResource(ctx context.Context, svc string) (*resource.Resource, error) { return resource.New( ctx, resource.WithSchemaURL(otelsemconv.SchemaURL), resource.WithAttributes(otelsemconv.ServiceNameAttribute(svc)), resource.WithTelemetrySDK(), resource.WithHost(), resource.WithOSType(), resource.WithFromEnv(), ) } func defaultGRPCOptions() []otlptracegrpc.Option { var options []otlptracegrpc.Option if !strings.HasPrefix(os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT"), "https://") && strings.ToLower(os.Getenv("OTEL_EXPORTER_OTLP_INSECURE")) != "false" { options = append(options, otlptracegrpc.WithInsecure()) } return options } func otelExporter(ctx context.Context) (sdktrace.SpanExporter, error) { client := otlptracegrpc.NewClient( defaultGRPCOptions()..., ) return otlptrace.New(ctx, client) } ================================================ FILE: internal/jtracer/jtracer_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package jtracer import ( "context" "errors" "testing" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestNewProvider(t *testing.T) { p, c, err := NewProvider(t.Context(), "serviceName") require.NoError(t, err) require.NotNil(t, p, "Expected OTEL not to be nil") require.NotNil(t, c, "Expected closer not to be nil") c(t.Context()) } func TestNewHelperProviderError(t *testing.T) { fakeErr := errors.New("fakeProviderError") _, _, err := newProviderHelper( t.Context(), "svc", func(_ context.Context, _ /* svc */ string) (*sdktrace.TracerProvider, func(), error) { return nil, nil, fakeErr }) require.Error(t, err) require.EqualError(t, err, fakeErr.Error()) } func TestInitHelperExporterError(t *testing.T) { fakeErr := errors.New("fakeExporterError") _, _, err := initHelper( context.Background(), "svc", func(_ context.Context) (sdktrace.SpanExporter, error) { return nil, fakeErr }, func(_ context.Context, _ /* svc */ string) (*resource.Resource, error) { return nil, nil }, ) require.Error(t, err) require.EqualError(t, err, fakeErr.Error()) } func TestInitHelperResourceError(t *testing.T) { fakeErr := errors.New("fakeResourceError") tp, _, err := initHelper( context.Background(), "svc", otelExporter, func(_ context.Context, _ /* svc */ string) (*resource.Resource, error) { return nil, fakeErr }, ) require.Error(t, err) require.Nil(t, tp) require.EqualError(t, err, fakeErr.Error()) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/leaderelection/leader_election.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package leaderelection import ( "io" "sync" "sync/atomic" "time" "go.uber.org/zap" dl "github.com/jaegertracing/jaeger/internal/distributedlock" ) const ( acquireLockErrMsg = "Failed to acquire lock" ) // ElectionParticipant partakes in leader election to become leader. type ElectionParticipant interface { io.Closer IsLeader() bool Start() error } // DistributedElectionParticipant implements ElectionParticipant on top of a distributed lock. type DistributedElectionParticipant struct { ElectionParticipantOptions lock dl.Lock isLeader atomic.Bool resourceName string closeChan chan struct{} wg sync.WaitGroup } // ElectionParticipantOptions control behavior of the election participant. TODO func applyDefaults(), parameter error checking, etc. type ElectionParticipantOptions struct { LeaderLeaseRefreshInterval time.Duration FollowerLeaseRefreshInterval time.Duration Logger *zap.Logger } // NewElectionParticipant returns a ElectionParticipant which attempts to become leader. func NewElectionParticipant(lock dl.Lock, resourceName string, options ElectionParticipantOptions) *DistributedElectionParticipant { return &DistributedElectionParticipant{ ElectionParticipantOptions: options, lock: lock, resourceName: resourceName, closeChan: make(chan struct{}), } } // Start runs a background thread which attempts to acquire the leader lock. func (p *DistributedElectionParticipant) Start() error { p.wg.Add(1) go p.runAcquireLockLoop() return nil } // Close implements io.Closer. func (p *DistributedElectionParticipant) Close() error { close(p.closeChan) p.wg.Wait() return nil } // IsLeader returns true if this process is the leader. func (p *DistributedElectionParticipant) IsLeader() bool { return p.isLeader.Load() } // runAcquireLockLoop attempts to acquire the leader lock. If it succeeds, it will attempt to retain it, // otherwise it sleeps and attempts to gain the lock again. func (p *DistributedElectionParticipant) runAcquireLockLoop() { defer p.wg.Done() ticker := time.NewTicker(p.acquireLock()) for { select { case <-ticker.C: ticker.Stop() ticker = time.NewTicker(p.acquireLock()) case <-p.closeChan: ticker.Stop() return } } } // acquireLock attempts to acquire the lock and returns the interval to sleep before the next retry. func (p *DistributedElectionParticipant) acquireLock() time.Duration { if acquiredLeaderLock, err := p.lock.Acquire(p.resourceName, p.FollowerLeaseRefreshInterval); err == nil { p.setLeader(acquiredLeaderLock) } else { p.Logger.Error(acquireLockErrMsg, zap.Error(err)) } if p.IsLeader() { // If this process holds the leader lock, retry with a shorter cadence // to retain the leader lease. return p.LeaderLeaseRefreshInterval } // If this process failed to acquire the leader lock, retry with a longer cadence return p.FollowerLeaseRefreshInterval } func (p *DistributedElectionParticipant) setLeader(isLeader bool) { p.isLeader.Store(isLeader) } ================================================ FILE: internal/leaderelection/leader_election_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package leaderelection import ( "errors" "io" "strconv" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" lmocks "github.com/jaegertracing/jaeger/internal/distributedlock/mocks" "github.com/jaegertracing/jaeger/internal/testutils" ) var errTestLock = errors.New("lock error") var _ io.Closer = new(DistributedElectionParticipant) func TestAcquireLock(t *testing.T) { const ( leaderInterval = time.Millisecond followerInterval = 5 * time.Millisecond ) tests := []struct { isLeader bool acquiredLock bool err error expectedInterval time.Duration expectedError bool }{ {isLeader: true, acquiredLock: true, err: nil, expectedInterval: leaderInterval, expectedError: false}, {isLeader: true, acquiredLock: false, err: errTestLock, expectedInterval: leaderInterval, expectedError: true}, {isLeader: true, acquiredLock: false, err: nil, expectedInterval: followerInterval, expectedError: false}, {isLeader: false, acquiredLock: false, err: nil, expectedInterval: followerInterval, expectedError: false}, {isLeader: false, acquiredLock: false, err: errTestLock, expectedInterval: followerInterval, expectedError: true}, {isLeader: false, acquiredLock: true, err: nil, expectedInterval: leaderInterval, expectedError: false}, } for i, test := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { logger, logBuffer := testutils.NewLogger() mockLock := &lmocks.Lock{} mockLock.On("Acquire", "sampling_lock", followerInterval).Return(test.acquiredLock, test.err) p := &DistributedElectionParticipant{ ElectionParticipantOptions: ElectionParticipantOptions{ LeaderLeaseRefreshInterval: leaderInterval, FollowerLeaseRefreshInterval: followerInterval, Logger: logger, }, lock: mockLock, resourceName: "sampling_lock", } p.setLeader(test.isLeader) assert.Equal(t, test.expectedInterval, p.acquireLock()) match, errMsg := testutils.LogMatcher(1, acquireLockErrMsg, logBuffer.Lines()) assert.Equal(t, test.expectedError, match, errMsg) }) } } func TestRunAcquireLockLoopFollowerOnly(t *testing.T) { logger, logBuffer := testutils.NewLogger() mockLock := &lmocks.Lock{} mockLock.On("Acquire", "sampling_lock", time.Duration(5*time.Millisecond)).Return(false, errTestLock) p := NewElectionParticipant(mockLock, "sampling_lock", ElectionParticipantOptions{ LeaderLeaseRefreshInterval: time.Millisecond, FollowerLeaseRefreshInterval: 5 * time.Millisecond, Logger: logger, }, ) defer func() { require.NoError(t, p.Close()) }() go p.Start() expectedErrorMsg := "Failed to acquire lock" for range 1000 { // match logs specific to acquireLockErrMsg. if match, _ := testutils.LogMatcher(2, expectedErrorMsg, logBuffer.Lines()); match { break } time.Sleep(time.Millisecond) } match, errMsg := testutils.LogMatcher(2, expectedErrorMsg, logBuffer.Lines()) assert.True(t, match, errMsg) assert.False(t, p.IsLeader()) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/leaderelection/mocks/mocks.go ================================================ // Copyright (c) The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // // Run 'make generate-mocks' to regenerate. // Code generated by mockery; DO NOT EDIT. // github.com/vektra/mockery // template: testify package mocks import ( mock "github.com/stretchr/testify/mock" ) // NewElectionParticipant creates a new instance of ElectionParticipant. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewElectionParticipant(t interface { mock.TestingT Cleanup(func()) }) *ElectionParticipant { mock := &ElectionParticipant{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // ElectionParticipant is an autogenerated mock type for the ElectionParticipant type type ElectionParticipant struct { mock.Mock } type ElectionParticipant_Expecter struct { mock *mock.Mock } func (_m *ElectionParticipant) EXPECT() *ElectionParticipant_Expecter { return &ElectionParticipant_Expecter{mock: &_m.Mock} } // Close provides a mock function for the type ElectionParticipant func (_mock *ElectionParticipant) Close() error { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Close") } var r0 error if returnFunc, ok := ret.Get(0).(func() error); ok { r0 = returnFunc() } else { r0 = ret.Error(0) } return r0 } // ElectionParticipant_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' type ElectionParticipant_Close_Call struct { *mock.Call } // Close is a helper method to define mock.On call func (_e *ElectionParticipant_Expecter) Close() *ElectionParticipant_Close_Call { return &ElectionParticipant_Close_Call{Call: _e.mock.On("Close")} } func (_c *ElectionParticipant_Close_Call) Run(run func()) *ElectionParticipant_Close_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *ElectionParticipant_Close_Call) Return(err error) *ElectionParticipant_Close_Call { _c.Call.Return(err) return _c } func (_c *ElectionParticipant_Close_Call) RunAndReturn(run func() error) *ElectionParticipant_Close_Call { _c.Call.Return(run) return _c } // IsLeader provides a mock function for the type ElectionParticipant func (_mock *ElectionParticipant) IsLeader() bool { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for IsLeader") } var r0 bool if returnFunc, ok := ret.Get(0).(func() bool); ok { r0 = returnFunc() } else { r0 = ret.Get(0).(bool) } return r0 } // ElectionParticipant_IsLeader_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsLeader' type ElectionParticipant_IsLeader_Call struct { *mock.Call } // IsLeader is a helper method to define mock.On call func (_e *ElectionParticipant_Expecter) IsLeader() *ElectionParticipant_IsLeader_Call { return &ElectionParticipant_IsLeader_Call{Call: _e.mock.On("IsLeader")} } func (_c *ElectionParticipant_IsLeader_Call) Run(run func()) *ElectionParticipant_IsLeader_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *ElectionParticipant_IsLeader_Call) Return(b bool) *ElectionParticipant_IsLeader_Call { _c.Call.Return(b) return _c } func (_c *ElectionParticipant_IsLeader_Call) RunAndReturn(run func() bool) *ElectionParticipant_IsLeader_Call { _c.Call.Return(run) return _c } // Start provides a mock function for the type ElectionParticipant func (_mock *ElectionParticipant) Start() error { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Start") } var r0 error if returnFunc, ok := ret.Get(0).(func() error); ok { r0 = returnFunc() } else { r0 = ret.Error(0) } return r0 } // ElectionParticipant_Start_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Start' type ElectionParticipant_Start_Call struct { *mock.Call } // Start is a helper method to define mock.On call func (_e *ElectionParticipant_Expecter) Start() *ElectionParticipant_Start_Call { return &ElectionParticipant_Start_Call{Call: _e.mock.On("Start")} } func (_c *ElectionParticipant_Start_Call) Run(run func()) *ElectionParticipant_Start_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *ElectionParticipant_Start_Call) Return(err error) *ElectionParticipant_Start_Call { _c.Call.Return(err) return _c } func (_c *ElectionParticipant_Start_Call) RunAndReturn(run func() error) *ElectionParticipant_Start_Call { _c.Call.Return(run) return _c } ================================================ FILE: internal/metrics/benchmark/benchmark_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package benchmark_test import ( "testing" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" promexporter "go.opentelemetry.io/otel/exporters/prometheus" "go.opentelemetry.io/otel/sdk/metric" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/metrics/otelmetrics" prom "github.com/jaegertracing/jaeger/internal/metrics/prometheus" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } func setupPrometheusFactory() metrics.Factory { reg := prometheus.NewRegistry() return prom.New(prom.WithRegisterer(reg)) } func setupOTELFactory(b *testing.B) metrics.Factory { registry := prometheus.NewRegistry() exporter, err := promexporter.New(promexporter.WithRegisterer(registry)) require.NoError(b, err) meterProvider := metric.NewMeterProvider( metric.WithReader(exporter), ) return otelmetrics.NewFactory(meterProvider) } func benchmarkCounter(b *testing.B, factory metrics.Factory) { counter := factory.Counter(metrics.Options{ Name: "test_counter", Tags: map[string]string{"tag1": "value1"}, }) for i := 0; i < b.N; i++ { counter.Inc(1) } } func benchmarkGauge(b *testing.B, factory metrics.Factory) { gauge := factory.Gauge(metrics.Options{ Name: "test_gauge", Tags: map[string]string{"tag1": "value1"}, }) for i := 0; i < b.N; i++ { gauge.Update(1) } } func benchmarkTimer(b *testing.B, factory metrics.Factory) { timer := factory.Timer(metrics.TimerOptions{ Name: "test_timer", Tags: map[string]string{"tag1": "value1"}, }) for i := 0; i < b.N; i++ { timer.Record(100) } } func benchmarkHistogram(b *testing.B, factory metrics.Factory) { histogram := factory.Histogram(metrics.HistogramOptions{ Name: "test_histogram", Tags: map[string]string{"tag1": "value1"}, }) for i := 0; i < b.N; i++ { histogram.Record(1.0) } } func BenchmarkPrometheusCounter(b *testing.B) { benchmarkCounter(b, setupPrometheusFactory()) } func BenchmarkOTELCounter(b *testing.B) { benchmarkCounter(b, setupOTELFactory(b)) } func BenchmarkPrometheusGauge(b *testing.B) { benchmarkGauge(b, setupPrometheusFactory()) } func BenchmarkOTELGauge(b *testing.B) { benchmarkGauge(b, setupOTELFactory(b)) } func BenchmarkPrometheusTimer(b *testing.B) { benchmarkTimer(b, setupPrometheusFactory()) } func BenchmarkOTELTimer(b *testing.B) { benchmarkTimer(b, setupOTELFactory(b)) } func BenchmarkPrometheusHistogram(b *testing.B) { benchmarkHistogram(b, setupPrometheusFactory()) } func BenchmarkOTELHistogram(b *testing.B) { benchmarkHistogram(b, setupOTELFactory(b)) } ================================================ FILE: internal/metrics/counter.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package metrics // Counter tracks the number of times an event has occurred type Counter interface { // Inc adds the given value to the counter. Inc(int64) } // NullCounter counter that does nothing var NullCounter Counter = nullCounter{} type nullCounter struct{} func (nullCounter) Inc(int64) {} ================================================ FILE: internal/metrics/factory.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package metrics import ( "time" ) // NSOptions defines the name and tags map associated with a factory namespace type NSOptions struct { Name string Tags map[string]string } // Options defines the information associated with a metric type Options struct { Name string Tags map[string]string Help string } // TimerOptions defines the information associated with a metric type TimerOptions struct { Name string Tags map[string]string Help string Buckets []time.Duration } // HistogramOptions defines the information associated with a metric type HistogramOptions struct { Name string Tags map[string]string Help string Buckets []float64 } // Factory creates new metrics type Factory interface { Counter(metric Options) Counter Timer(metric TimerOptions) Timer Gauge(metric Options) Gauge Histogram(metric HistogramOptions) Histogram // Namespace returns a nested metrics factory. Namespace(scope NSOptions) Factory } // NullFactory is a metrics factory that returns NullCounter, NullTimer, and NullGauge. var NullFactory Factory = nullFactory{} type nullFactory struct{} func (nullFactory) Counter(Options) Counter { return NullCounter } func (nullFactory) Timer(TimerOptions) Timer { return NullTimer } func (nullFactory) Gauge(Options) Gauge { return NullGauge } func (nullFactory) Histogram(HistogramOptions) Histogram { return NullHistogram } func (nullFactory) Namespace(NSOptions /* scope */) Factory { return NullFactory } ================================================ FILE: internal/metrics/gauge.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package metrics // Gauge returns instantaneous measurements of something as an int64 value type Gauge interface { // Update the gauge to the value passed in. Update(int64) } // NullGauge gauge that does nothing var NullGauge Gauge = nullGauge{} type nullGauge struct{} func (nullGauge) Update(int64) {} ================================================ FILE: internal/metrics/histogram.go ================================================ // Copyright (c) 2018 The Jaeger Authors // SPDX-License-Identifier: Apache-2.0 package metrics // Histogram that keeps track of a distribution of values. type Histogram interface { // Records the value passed in. Record(float64) } // NullHistogram that does nothing var NullHistogram Histogram = nullHistogram{} type nullHistogram struct{} func (nullHistogram) Record(float64) {} ================================================ FILE: internal/metrics/metrics.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package metrics import ( "fmt" "maps" "reflect" "strconv" "strings" ) // MustInit initializes the passed in metrics and initializes its fields using the passed in factory. // // It uses reflection to initialize a struct containing metrics fields // by assigning new Counter/Gauge/Timer values with the metric name retrieved // from the `metric` tag and stats tags retrieved from the `tags` tag. // // Note: all fields of the struct must be exported, have a `metric` tag, and be // of type Counter or Gauge or Timer. // // Errors during Init lead to a panic. func MustInit(metrics any, factory Factory, globalTags map[string]string) { if err := Init(metrics, factory, globalTags); err != nil { panic(err.Error()) } } // Init does the same as MustInit, but returns an error instead of // panicking. func Init(m any, factory Factory, globalTags map[string]string) error { // Allow user to opt out of reporting metrics by passing in nil. if factory == nil { factory = NullFactory } counterPtrType := reflect.TypeFor[Counter]() gaugePtrType := reflect.TypeFor[Gauge]() timerPtrType := reflect.TypeFor[Timer]() histogramPtrType := reflect.TypeFor[Histogram]() v := reflect.ValueOf(m).Elem() t := v.Type() for i := 0; i < t.NumField(); i++ { tags := make(map[string]string) maps.Copy(tags, globalTags) var buckets []float64 field := t.Field(i) metric := field.Tag.Get("metric") if metric == "" { return fmt.Errorf("Field %s is missing a tag 'metric'", field.Name) } if tagString := field.Tag.Get("tags"); tagString != "" { for tagPair := range strings.SplitSeq(tagString, ",") { tag := strings.Split(tagPair, "=") if len(tag) != 2 { return fmt.Errorf( "Field [%s]: Tag [%s] is not of the form key=value in 'tags' string [%s]", field.Name, tagPair, tagString) } tags[tag[0]] = tag[1] } } if bucketString := field.Tag.Get("buckets"); bucketString != "" { switch { case field.Type.AssignableTo(timerPtrType): // TODO: Parse timer duration buckets return fmt.Errorf( "Field [%s]: Buckets are not currently initialized for timer metrics", field.Name) case field.Type.AssignableTo(histogramPtrType): bucketValues := strings.Split(bucketString, ",") for _, bucket := range bucketValues { b, err := strconv.ParseFloat(bucket, 64) if err != nil { return fmt.Errorf( "Field [%s]: Bucket [%s] could not be converted to float64 in 'buckets' string [%s]", field.Name, bucket, bucketString) } buckets = append(buckets, b) } default: return fmt.Errorf( "Field [%s]: Buckets should only be defined for Timer and Histogram metric types", field.Name) } } help := field.Tag.Get("help") var obj any switch { case field.Type.AssignableTo(counterPtrType): obj = factory.Counter(Options{ Name: metric, Tags: tags, Help: help, }) case field.Type.AssignableTo(gaugePtrType): obj = factory.Gauge(Options{ Name: metric, Tags: tags, Help: help, }) case field.Type.AssignableTo(timerPtrType): // TODO: Add buckets once parsed (see TODO above) obj = factory.Timer(TimerOptions{ Name: metric, Tags: tags, Help: help, }) case field.Type.AssignableTo(histogramPtrType): obj = factory.Histogram(HistogramOptions{ Name: metric, Tags: tags, Help: help, Buckets: buckets, }) default: return fmt.Errorf( "Field %s is not a pointer to timer, gauge, or counter", field.Name) } v.Field(i).Set(reflect.ValueOf(obj)) } return nil } ================================================ FILE: internal/metrics/metrics_test.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // Must use separate test package to break import cycle. package metrics_test import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/metricstest" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestInitMetrics(t *testing.T) { testMetrics := struct { Gauge metrics.Gauge `metric:"gauge" tags:"1=one,2=two"` Counter metrics.Counter `metric:"counter"` Timer metrics.Timer `metric:"timer"` Histogram metrics.Histogram `metric:"histogram" buckets:"20,40,60,80"` }{} f := metricstest.NewFactory(0) defer f.Stop() globalTags := map[string]string{"key": "value"} err := metrics.Init(&testMetrics, f, globalTags) require.NoError(t, err) testMetrics.Gauge.Update(10) testMetrics.Counter.Inc(5) testMetrics.Timer.Record(time.Duration(time.Second * 35)) testMetrics.Histogram.Record(42) // wait for metrics for range 1000 { c, _ := f.Snapshot() if _, ok := c["counter"]; ok { break } time.Sleep(1 * time.Millisecond) } c, g := f.Snapshot() assert.EqualValues(t, 5, c["counter|key=value"]) assert.EqualValues(t, 10, g["gauge|1=one|2=two|key=value"]) assert.EqualValues(t, 35000, g["timer|key=value.P50"]) assert.EqualValues(t, 42, g["histogram|key=value.P50"]) stopwatch := metrics.StartStopwatch(testMetrics.Timer) stopwatch.Stop() assert.Positive(t, stopwatch.ElapsedTime()) } var ( noMetricTag = struct { NoMetricTag metrics.Counter }{} badTags = struct { BadTags metrics.Counter `metric:"counter" tags:"1=one,noValue"` }{} invalidMetricType = struct { InvalidMetricType int64 `metric:"counter"` }{} badHistogramBucket = struct { BadHistogramBucket metrics.Histogram `metric:"histogram" buckets:"1,2,a,4"` }{} badTimerBucket = struct { BadTimerBucket metrics.Timer `metric:"timer" buckets:"1"` }{} invalidBuckets = struct { InvalidBuckets metrics.Counter `metric:"counter" buckets:"1"` }{} ) func TestInitMetricsFailures(t *testing.T) { require.EqualError(t, metrics.Init(&noMetricTag, nil, nil), "Field NoMetricTag is missing a tag 'metric'") require.EqualError(t, metrics.Init(&badTags, nil, nil), "Field [BadTags]: Tag [noValue] is not of the form key=value in 'tags' string [1=one,noValue]") require.EqualError(t, metrics.Init(&invalidMetricType, nil, nil), "Field InvalidMetricType is not a pointer to timer, gauge, or counter") require.EqualError(t, metrics.Init(&badHistogramBucket, nil, nil), "Field [BadHistogramBucket]: Bucket [a] could not be converted to float64 in 'buckets' string [1,2,a,4]") require.EqualError(t, metrics.Init(&badTimerBucket, nil, nil), "Field [BadTimerBucket]: Buckets are not currently initialized for timer metrics") require.EqualError(t, metrics.Init(&invalidBuckets, nil, nil), "Field [InvalidBuckets]: Buckets should only be defined for Timer and Histogram metric types") } func TestInitPanic(t *testing.T) { defer func() { if r := recover(); r == nil { t.Error("The code did not panic") } }() metrics.MustInit(&noMetricTag, metrics.NullFactory, nil) } func TestNullMetrics(*testing.T) { // This test is just for cover metrics.NullFactory.Timer(metrics.TimerOptions{ Name: "name", }).Record(0) metrics.NullFactory.Counter(metrics.Options{ Name: "name", }).Inc(0) metrics.NullFactory.Gauge(metrics.Options{ Name: "name", }).Update(0) metrics.NullFactory.Histogram(metrics.HistogramOptions{ Name: "name", }).Record(0) metrics.NullFactory.Namespace(metrics.NSOptions{ Name: "name", }).Gauge(metrics.Options{ Name: "name2", }).Update(0) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/metrics/metricsbuilder/builder.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package metricsbuilder import ( "errors" "flag" "net/http" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/spf13/viper" "github.com/jaegertracing/jaeger/internal/metrics" jprom "github.com/jaegertracing/jaeger/internal/metrics/prometheus" ) const ( metricsBackend = "metrics-backend" metricsHTTPRoute = "metrics-http-route" defaultMetricsBackend = "prometheus" defaultMetricsRoute = "/metrics" ) var errUnknownBackend = errors.New("unknown metrics backend specified") // Builder provides command line options to configure metrics backend used by Jaeger executables. type Builder struct { Backend string HTTPRoute string // endpoint name to expose metrics, e.g. for scraping handler http.Handler } // AddFlags adds flags for Builder. func AddFlags(flags *flag.FlagSet) { flags.String( metricsBackend, defaultMetricsBackend, "Defines which metrics backend to use for metrics reporting: prometheus or none") flags.String( metricsHTTPRoute, defaultMetricsRoute, "Defines the route of HTTP endpoint for metrics backends that support scraping") } // InitFromViper initializes Builder with properties retrieved from Viper. func (b *Builder) InitFromViper(v *viper.Viper) *Builder { b.Backend = v.GetString(metricsBackend) b.HTTPRoute = v.GetString(metricsHTTPRoute) return b } // CreateMetricsFactory creates a metrics factory based on the configured type of the backend. // If the metrics backend supports HTTP endpoint for scraping, it is stored in the builder and // can be later added by RegisterHandler function. func (b *Builder) CreateMetricsFactory(namespace string) (metrics.Factory, error) { if b.Backend == "prometheus" { metricsFactory := jprom.New().Namespace(metrics.NSOptions{Name: namespace, Tags: nil}) b.handler = promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{DisableCompression: true}) return metricsFactory, nil } if b.Backend == "none" || b.Backend == "" { return metrics.NullFactory, nil } return nil, errUnknownBackend } // Handler returns an http.Handler for the metrics endpoint. func (b *Builder) Handler() http.Handler { return b.handler } ================================================ FILE: internal/metrics/metricsbuilder/builder_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package metricsbuilder import ( "flag" "testing" "github.com/prometheus/client_golang/prometheus" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestAddFlags(t *testing.T) { v := viper.New() command := cobra.Command{} flags := &flag.FlagSet{} AddFlags(flags) command.PersistentFlags().AddGoFlagSet(flags) v.BindPFlags(command.PersistentFlags()) command.ParseFlags([]string{ "--metrics-backend=foo", "--metrics-http-route=bar", }) b := &Builder{} b.InitFromViper(v) assert.Equal(t, "foo", b.Backend) assert.Equal(t, "bar", b.HTTPRoute) } func TestBuilder(t *testing.T) { assertPromCounter := func() { families, err := prometheus.DefaultGatherer.Gather() require.NoError(t, err) for _, mf := range families { if mf.GetName() == "foo_counter_total" { return } } t.FailNow() } testCases := []struct { backend string route string err error handler bool assert func() }{ { backend: "prometheus", route: "/", handler: true, assert: assertPromCounter, }, { backend: "none", handler: false, }, { backend: "", handler: false, }, { backend: "invalid", err: errUnknownBackend, }, } for i := range testCases { testCase := testCases[i] b := &Builder{ Backend: testCase.backend, HTTPRoute: testCase.route, } mf, err := b.CreateMetricsFactory("foo") if testCase.err != nil { assert.Equal(t, err, testCase.err) continue } require.NotNil(t, mf) mf.Counter(metrics.Options{Name: "counter", Tags: nil}).Inc(1) if testCase.assert != nil { testCase.assert() } if testCase.handler { require.NotNil(t, b.Handler()) } } } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/metrics/otelmetrics/counter.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package otelmetrics import ( "context" "go.opentelemetry.io/otel/metric" ) type otelCounter struct { counter metric.Int64Counter fixedCtx context.Context option metric.AddOption } func (c *otelCounter) Inc(value int64) { c.counter.Add(c.fixedCtx, value, c.option) } ================================================ FILE: internal/metrics/otelmetrics/factory.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package otelmetrics import ( "context" "log" "maps" "strings" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" "github.com/jaegertracing/jaeger/internal/metrics" ) type otelFactory struct { meter metric.Meter scope string separator string normalizer *strings.Replacer tags map[string]string } func NewFactory(meterProvider metric.MeterProvider) metrics.Factory { return &otelFactory{ meter: meterProvider.Meter("jaeger-v2"), separator: ".", normalizer: strings.NewReplacer(" ", "_", ".", "_", "-", "_"), tags: make(map[string]string), } } func (f *otelFactory) Counter(opts metrics.Options) metrics.Counter { counter, err := f.meter.Int64Counter(f.subScope(opts.Name)) if err != nil { log.Printf("Error creating OTEL counter: %v", err) return metrics.NullCounter } return &otelCounter{ counter: counter, fixedCtx: context.Background(), option: attributeSetOption(f.mergeTags(opts.Tags)), } } func (f *otelFactory) Gauge(opts metrics.Options) metrics.Gauge { name := f.subScope(opts.Name) gauge, err := f.meter.Int64Gauge(name) if err != nil { log.Printf("Error creating OTEL gauge: %v", err) return metrics.NullGauge } return &otelGauge{ gauge: gauge, fixedCtx: context.Background(), option: attributeSetOption(f.mergeTags(opts.Tags)), } } func (f *otelFactory) Histogram(opts metrics.HistogramOptions) metrics.Histogram { name := f.subScope(opts.Name) histogram, err := f.meter.Float64Histogram(name) if err != nil { log.Printf("Error creating OTEL histogram: %v", err) return metrics.NullHistogram } return &otelHistogram{ histogram: histogram, fixedCtx: context.Background(), option: attributeSetOption(f.mergeTags(opts.Tags)), } } func (f *otelFactory) Timer(opts metrics.TimerOptions) metrics.Timer { name := f.subScope(opts.Name) timer, err := f.meter.Float64Histogram(name, metric.WithUnit("s")) if err != nil { log.Printf("Error creating OTEL timer: %v", err) return metrics.NullTimer } return &otelTimer{ histogram: timer, fixedCtx: context.Background(), option: attributeSetOption(f.mergeTags(opts.Tags)), } } func (f *otelFactory) Namespace(opts metrics.NSOptions) metrics.Factory { return &otelFactory{ meter: f.meter, scope: f.subScope(opts.Name), separator: f.separator, normalizer: f.normalizer, tags: f.mergeTags(opts.Tags), } } func (f *otelFactory) subScope(name string) string { if f.scope == "" { return f.normalize(name) } if name == "" { return f.normalize(f.scope) } return f.normalize(f.scope + f.separator + name) } func (f *otelFactory) normalize(v string) string { return f.normalizer.Replace(v) } func (f *otelFactory) mergeTags(tags map[string]string) map[string]string { merged := make(map[string]string) maps.Copy(merged, f.tags) maps.Copy(merged, tags) return merged } func attributeSetOption(tags map[string]string) metric.MeasurementOption { attributes := make([]attribute.KeyValue, 0, len(tags)) for k, v := range tags { attributes = append(attributes, attribute.String(k, v)) } return metric.WithAttributes(attributes...) } ================================================ FILE: internal/metrics/otelmetrics/factory_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package otelmetrics_test import ( "testing" "time" promreg "github.com/prometheus/client_golang/prometheus" prommodel "github.com/prometheus/client_model/go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/exporters/prometheus" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/metrics/otelmetrics" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } func newTestFactory(t *testing.T, registry *promreg.Registry) metrics.Factory { exporter, err := prometheus.New( prometheus.WithRegisterer(registry), prometheus.WithoutScopeInfo(), ) require.NoError(t, err) meterProvider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(exporter)) return otelmetrics.NewFactory(meterProvider) } func findMetric(t *testing.T, registry *promreg.Registry, name string) *prommodel.MetricFamily { metricFamilies, err := registry.Gather() require.NoError(t, err) for _, mf := range metricFamilies { t.Log(mf.GetName()) t.Log(name) if mf.GetName() == name { return mf } } require.Fail(t, "Expected to find Metric Family") return nil } func promLabelsToMap(labels []*prommodel.LabelPair) map[string]string { labelMap := make(map[string]string) for _, label := range labels { labelMap[label.GetName()] = label.GetValue() } return labelMap } func TestInvalidCounter(t *testing.T) { factory := newTestFactory(t, promreg.NewPedanticRegistry()) counter := factory.Counter(metrics.Options{ Name: "invalid*counter%", }) assert.Equal(t, counter, metrics.NullCounter, "Expected NullCounter, got %v", counter) } func TestInvalidGauge(t *testing.T) { factory := newTestFactory(t, promreg.NewPedanticRegistry()) gauge := factory.Gauge(metrics.Options{ Name: "#invalid>gauge%", }) assert.Equal(t, gauge, metrics.NullGauge, "Expected NullCounter, got %v", gauge) } func TestInvalidHistogram(t *testing.T) { factory := newTestFactory(t, promreg.NewPedanticRegistry()) histogram := factory.Histogram(metrics.HistogramOptions{ Name: "invalid>histogram?%", }) assert.Equal(t, histogram, metrics.NullHistogram, "Expected NullCounter, got %v", histogram) } func TestInvalidTimer(t *testing.T) { factory := newTestFactory(t, promreg.NewPedanticRegistry()) timer := factory.Timer(metrics.TimerOptions{ Name: "invalid*<=timer%", }) assert.Equal(t, timer, metrics.NullTimer, "Expected NullCounter, got %v", timer) } func TestCounter(t *testing.T) { registry := promreg.NewPedanticRegistry() factory := newTestFactory(t, registry) counter := factory.Counter(metrics.Options{ Name: "test_counter", Tags: map[string]string{"tag1": "value1"}, }) require.NotNil(t, counter) counter.Inc(1) counter.Inc(1) testCounter := findMetric(t, registry, "test_counter_total") metricData := testCounter.GetMetric() assert.InDelta(t, float64(2), metricData[0].GetCounter().GetValue(), 0.01) expectedLabels := map[string]string{ "tag1": "value1", } assert.Equal(t, expectedLabels, promLabelsToMap(metricData[0].GetLabel())) } func TestGauge(t *testing.T) { registry := promreg.NewPedanticRegistry() factory := newTestFactory(t, registry) gauge := factory.Gauge(metrics.Options{ Name: "test_gauge", Tags: map[string]string{"tag1": "value1"}, }) require.NotNil(t, gauge) gauge.Update(2) testGauge := findMetric(t, registry, "test_gauge") metricData := testGauge.GetMetric() assert.InDelta(t, float64(2), metricData[0].GetGauge().GetValue(), 0.01) expectedLabels := map[string]string{ "tag1": "value1", } assert.Equal(t, expectedLabels, promLabelsToMap(metricData[0].GetLabel())) } func TestHistogram(t *testing.T) { registry := promreg.NewPedanticRegistry() factory := newTestFactory(t, registry) histogram := factory.Histogram(metrics.HistogramOptions{ Name: "test_histogram", Tags: map[string]string{"tag1": "value1"}, }) require.NotNil(t, histogram) histogram.Record(1.0) testHistogram := findMetric(t, registry, "test_histogram") metricData := testHistogram.GetMetric() assert.InDelta(t, float64(1), metricData[0].GetHistogram().GetSampleSum(), 0.01) expectedLabels := map[string]string{ "tag1": "value1", } assert.Equal(t, expectedLabels, promLabelsToMap(metricData[0].GetLabel())) } func TestTimer(t *testing.T) { registry := promreg.NewPedanticRegistry() factory := newTestFactory(t, registry) timer := factory.Timer(metrics.TimerOptions{ Name: "test_timer", Tags: map[string]string{"tag1": "value1"}, }) require.NotNil(t, timer) timer.Record(100 * time.Millisecond) testTimer := findMetric(t, registry, "test_timer_seconds") metricData := testTimer.GetMetric() assert.InDelta(t, float64(0.1), metricData[0].GetHistogram().GetSampleSum(), 0.01) expectedLabels := map[string]string{ "tag1": "value1", } assert.Equal(t, expectedLabels, promLabelsToMap(metricData[0].GetLabel())) } func TestNamespace(t *testing.T) { testCases := []struct { name string nsOptions1 metrics.NSOptions nsOptions2 metrics.NSOptions expectedName string expectedLabels map[string]string }{ { name: "Nested Namespace", nsOptions1: metrics.NSOptions{ Name: "first_namespace", Tags: map[string]string{"ns_tag1": "ns_value1"}, }, nsOptions2: metrics.NSOptions{ Name: "second_namespace", Tags: map[string]string{"ns_tag3": "ns_value3"}, }, expectedName: "first_namespace_second_namespace_test_counter_total", expectedLabels: map[string]string{ "ns_tag1": "ns_value1", "ns_tag3": "ns_value3", "tag1": "value1", }, }, { name: "Single Namespace", nsOptions1: metrics.NSOptions{ Name: "single_namespace", Tags: map[string]string{"ns_tag2": "ns_value2"}, }, nsOptions2: metrics.NSOptions{}, expectedName: "single_namespace_test_counter_total", expectedLabels: map[string]string{ "ns_tag2": "ns_value2", "tag1": "value1", }, }, { name: "Empty Namespace Name", nsOptions1: metrics.NSOptions{}, nsOptions2: metrics.NSOptions{}, expectedName: "test_counter_total", expectedLabels: map[string]string{ "tag1": "value1", }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { registry := promreg.NewPedanticRegistry() factory := newTestFactory(t, registry) nsFactory1 := factory.Namespace(tc.nsOptions1) nsFactory2 := nsFactory1.Namespace(tc.nsOptions2) counter := nsFactory2.Counter(metrics.Options{ Name: "test_counter", Tags: map[string]string{"tag1": "value1"}, }) require.NotNil(t, counter) counter.Inc(1) testCounter := findMetric(t, registry, tc.expectedName) metrics := testCounter.GetMetric() assert.InDelta(t, float64(1), metrics[0].GetCounter().GetValue(), 0.01) assert.Equal(t, tc.expectedLabels, promLabelsToMap(metrics[0].GetLabel())) }) } } func TestNormalization(t *testing.T) { registry := promreg.NewPedanticRegistry() factory := newTestFactory(t, registry) normalizedFactory := factory.Namespace(metrics.NSOptions{ Name: "My Namespace", }) gauge := normalizedFactory.Gauge(metrics.Options{ Name: "My Gauge", }) require.NotNil(t, gauge) gauge.Update(1) testGauge := findMetric(t, registry, "My_Namespace_My_Gauge") metricData := testGauge.GetMetric() assert.InDelta(t, float64(1), metricData[0].GetGauge().GetValue(), 0.01) } ================================================ FILE: internal/metrics/otelmetrics/gauge.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package otelmetrics import ( "context" "go.opentelemetry.io/otel/metric" ) type otelGauge struct { gauge metric.Int64Gauge fixedCtx context.Context option metric.RecordOption } func (g *otelGauge) Update(value int64) { g.gauge.Record(g.fixedCtx, value, g.option) } ================================================ FILE: internal/metrics/otelmetrics/histogram.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package otelmetrics import ( "context" "go.opentelemetry.io/otel/metric" ) type otelHistogram struct { histogram metric.Float64Histogram fixedCtx context.Context option metric.RecordOption } func (h *otelHistogram) Record(value float64) { h.histogram.Record(h.fixedCtx, value, h.option) } ================================================ FILE: internal/metrics/otelmetrics/timer.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package otelmetrics import ( "context" "time" "go.opentelemetry.io/otel/metric" ) type otelTimer struct { histogram metric.Float64Histogram fixedCtx context.Context option metric.RecordOption } func (t *otelTimer) Record(d time.Duration) { t.histogram.Record(t.fixedCtx, d.Seconds(), t.option) } ================================================ FILE: internal/metrics/package.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 // Package metrics provides an internal abstraction for metrics API, // and command line flags for configuring the metrics backend. package metrics ================================================ FILE: internal/metrics/prometheus/cache.go ================================================ // Copyright (c) 2017 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package prometheus import ( "strings" "sync" "github.com/prometheus/client_golang/prometheus" ) // vectorCache is used to avoid creating Prometheus vectors with the same set of labels more than once. type vectorCache struct { registerer prometheus.Registerer lock sync.Mutex cVecs map[string]*prometheus.CounterVec gVecs map[string]*prometheus.GaugeVec hVecs map[string]*prometheus.HistogramVec } func newVectorCache(registerer prometheus.Registerer) *vectorCache { return &vectorCache{ registerer: registerer, cVecs: make(map[string]*prometheus.CounterVec), gVecs: make(map[string]*prometheus.GaugeVec), hVecs: make(map[string]*prometheus.HistogramVec), } } func (c *vectorCache) getOrMakeCounterVec(opts prometheus.CounterOpts, labelNames []string) *prometheus.CounterVec { c.lock.Lock() defer c.lock.Unlock() cacheKey := c.getCacheKey(opts.Name, labelNames) cv, cvExists := c.cVecs[cacheKey] if !cvExists { cv = prometheus.NewCounterVec(opts, labelNames) c.registerer.MustRegister(cv) c.cVecs[cacheKey] = cv } return cv } func (c *vectorCache) getOrMakeGaugeVec(opts prometheus.GaugeOpts, labelNames []string) *prometheus.GaugeVec { c.lock.Lock() defer c.lock.Unlock() cacheKey := c.getCacheKey(opts.Name, labelNames) gv, gvExists := c.gVecs[cacheKey] if !gvExists { gv = prometheus.NewGaugeVec(opts, labelNames) c.registerer.MustRegister(gv) c.gVecs[cacheKey] = gv } return gv } func (c *vectorCache) getOrMakeHistogramVec(opts prometheus.HistogramOpts, labelNames []string) *prometheus.HistogramVec { c.lock.Lock() defer c.lock.Unlock() cacheKey := c.getCacheKey(opts.Name, labelNames) hv, hvExists := c.hVecs[cacheKey] if !hvExists { hv = prometheus.NewHistogramVec(opts, labelNames) c.registerer.MustRegister(hv) c.hVecs[cacheKey] = hv } return hv } func (*vectorCache) getCacheKey(name string, labels []string) string { return strings.Join(append([]string{name}, labels...), "||") } ================================================ FILE: internal/metrics/prometheus/factory.go ================================================ // Copyright (c) 2017 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package prometheus import ( "maps" "sort" "strings" "time" "github.com/prometheus/client_golang/prometheus" "github.com/jaegertracing/jaeger/internal/metrics" ) // Factory implements metrics.Factory backed by Prometheus registry. type Factory struct { scope string tags map[string]string cache *vectorCache buckets []float64 normalizer *strings.Replacer separator Separator } var _ metrics.Factory = (*Factory)(nil) type options struct { registerer prometheus.Registerer buckets []float64 separator Separator } // Separator represents the namespace separator to use type Separator rune const ( // SeparatorUnderscore uses an underscore as separator SeparatorUnderscore Separator = '_' // SeparatorColon uses a colon as separator SeparatorColon = ':' ) // Option is a function that sets some option for the Factory constructor. type Option func(*options) // WithRegisterer returns an option that sets the registerer. // If not used we fallback to prometheus.DefaultRegisterer. func WithRegisterer(registerer prometheus.Registerer) Option { return func(opts *options) { opts.registerer = registerer } } // WithBuckets returns an option that sets the default buckets for histogram. // If not used, we fallback to default Prometheus buckets. func WithBuckets(buckets []float64) Option { return func(opts *options) { opts.buckets = buckets } } // WithSeparator returns an option that sets the default separator for the namespace // If not used, we fallback to underscore. func WithSeparator(separator Separator) Option { return func(opts *options) { opts.separator = separator } } func applyOptions(opts []Option) *options { options := new(options) for _, o := range opts { o(options) } if options.registerer == nil { options.registerer = prometheus.DefaultRegisterer } if options.separator == '\x00' { options.separator = SeparatorUnderscore } return options } // New creates a Factory backed by Prometheus registry. // Typically the first argument should be prometheus.DefaultRegisterer. // // Parameter buckets defines the buckets into which Timer observations are counted. // Each element in the slice is the upper inclusive bound of a bucket. The // values must be sorted in strictly increasing order. There is no need // to add a highest bucket with +Inf bound, it will be added // implicitly. The default value is prometheus.DefBuckets. func New(opts ...Option) *Factory { options := applyOptions(opts) return newFactory( &Factory{ // dummy struct to be discarded cache: newVectorCache(options.registerer), buckets: options.buckets, normalizer: strings.NewReplacer(".", "_", "-", "_"), separator: options.separator, }, "", // scope nil) // tags } func newFactory(parent *Factory, scope string, tags map[string]string) *Factory { return &Factory{ cache: parent.cache, buckets: parent.buckets, normalizer: parent.normalizer, separator: parent.separator, scope: scope, tags: tags, } } // Counter implements Counter of metrics.Factory. func (f *Factory) Counter(options metrics.Options) metrics.Counter { help := strings.TrimSpace(options.Help) if help == "" { help = options.Name } name := counterNamingConvention(f.subScope(options.Name)) tags := f.mergeTags(options.Tags) labelNames := f.tagNames(tags) opts := prometheus.CounterOpts{ Name: name, Help: help, } cv := f.cache.getOrMakeCounterVec(opts, labelNames) labelValues := f.tagsAsLabelValues(labelNames, tags) metric, err := cv.GetMetricWithLabelValues(labelValues...) if err != nil { return metrics.NullCounter } return &counter{ counter: metric, } } // Gauge implements Gauge of metrics.Factory. func (f *Factory) Gauge(options metrics.Options) metrics.Gauge { help := strings.TrimSpace(options.Help) if help == "" { help = options.Name } name := f.subScope(options.Name) tags := f.mergeTags(options.Tags) labelNames := f.tagNames(tags) opts := prometheus.GaugeOpts{ Name: name, Help: help, } gv := f.cache.getOrMakeGaugeVec(opts, labelNames) labelValues := f.tagsAsLabelValues(labelNames, tags) metric, err := gv.GetMetricWithLabelValues(labelValues...) if err != nil { return metrics.NullGauge } return &gauge{ gauge: metric, } } // Timer implements Timer of metrics.Factory. func (f *Factory) Timer(options metrics.TimerOptions) metrics.Timer { help := strings.TrimSpace(options.Help) if help == "" { help = options.Name } name := f.subScope(options.Name) buckets := f.selectBuckets(asFloatBuckets(options.Buckets)) tags := f.mergeTags(options.Tags) labelNames := f.tagNames(tags) opts := prometheus.HistogramOpts{ Name: name, Help: help, Buckets: buckets, } hv := f.cache.getOrMakeHistogramVec(opts, labelNames) labelValues := f.tagsAsLabelValues(labelNames, tags) metric, err := hv.GetMetricWithLabelValues(labelValues...) if err != nil { return metrics.NullTimer } return &timer{ histogram: metric, } } func asFloatBuckets(buckets []time.Duration) []float64 { data := make([]float64, len(buckets)) for i := range data { data[i] = float64(buckets[i]) / float64(time.Second) } return data } // Histogram implements Histogram of metrics.Factory. func (f *Factory) Histogram(options metrics.HistogramOptions) metrics.Histogram { help := strings.TrimSpace(options.Help) if help == "" { help = options.Name } name := f.subScope(options.Name) buckets := f.selectBuckets(options.Buckets) tags := f.mergeTags(options.Tags) labelNames := f.tagNames(tags) opts := prometheus.HistogramOpts{ Name: name, Help: help, Buckets: buckets, } hv := f.cache.getOrMakeHistogramVec(opts, labelNames) labelValues := f.tagsAsLabelValues(labelNames, tags) metric, err := hv.GetMetricWithLabelValues(labelValues...) if err != nil { return metrics.NullHistogram } return &histogram{ histogram: metric, } } // Namespace implements Namespace of metrics.Factory. func (f *Factory) Namespace(scope metrics.NSOptions) metrics.Factory { return newFactory(f, f.subScope(scope.Name), f.mergeTags(scope.Tags)) } type counter struct { counter prometheus.Counter } func (c *counter) Inc(v int64) { c.counter.Add(float64(v)) } type gauge struct { gauge prometheus.Gauge } func (g *gauge) Update(v int64) { g.gauge.Set(float64(v)) } type observer interface { Observe(v float64) } type timer struct { histogram observer } func (t *timer) Record(v time.Duration) { t.histogram.Observe(float64(v.Nanoseconds()) / float64(time.Second/time.Nanosecond)) } type histogram struct { histogram observer } func (h *histogram) Record(v float64) { h.histogram.Observe(v) } func (f *Factory) subScope(name string) string { if f.scope == "" { return f.normalize(name) } if name == "" { return f.normalize(f.scope) } return f.normalize(f.scope + string(f.separator) + name) } func (f *Factory) normalize(v string) string { return f.normalizer.Replace(v) } func (f *Factory) mergeTags(tags map[string]string) map[string]string { ret := make(map[string]string, len(f.tags)+len(tags)) maps.Copy(ret, f.tags) maps.Copy(ret, tags) return ret } func (*Factory) tagNames(tags map[string]string) []string { ret := make([]string, 0, len(tags)) for k := range tags { ret = append(ret, k) } sort.Strings(ret) return ret } func (*Factory) tagsAsLabelValues(labels []string, tags map[string]string) []string { ret := make([]string, 0, len(tags)) for _, l := range labels { ret = append(ret, tags[l]) } return ret } func (f *Factory) selectBuckets(buckets []float64) []float64 { if len(buckets) > 0 { return buckets } return f.buckets } func counterNamingConvention(name string) string { if !strings.HasSuffix(name, "_total") { name += "_total" } return name } ================================================ FILE: internal/metrics/prometheus/factory_test.go ================================================ // Copyright (c) 2017 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package prometheus_test import ( "testing" "time" "github.com/prometheus/client_golang/prometheus" prommodel "github.com/prometheus/client_model/go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/metrics" prommetrics "github.com/jaegertracing/jaeger/internal/metrics/prometheus" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestOptions(t *testing.T) { f1 := prommetrics.New() assert.NotNil(t, f1) } func TestSeparator(t *testing.T) { registry := prometheus.NewPedanticRegistry() f1 := prommetrics.New(prommetrics.WithRegisterer(registry), prommetrics.WithSeparator(prommetrics.SeparatorColon)) c1 := f1.Namespace(metrics.NSOptions{ Name: "bender", }).Counter(metrics.Options{ Name: "rodriguez", Tags: map[string]string{"a": "b"}, Help: "Help message", }) c1.Inc(1) snapshot, err := registry.Gather() require.NoError(t, err) m1 := findMetric(t, snapshot, "bender:rodriguez_total", map[string]string{"a": "b"}) assert.InDelta(t, 1.0, m1.GetCounter().GetValue(), 0.01, "%+v", m1) } func TestCounter(t *testing.T) { registry := prometheus.NewPedanticRegistry() f1 := prommetrics.New(prommetrics.WithRegisterer(registry)) fDummy := f1.Namespace(metrics.NSOptions{}) f2 := fDummy.Namespace(metrics.NSOptions{ Name: "bender", Tags: map[string]string{"a": "b"}, }) f3 := f2.Namespace(metrics.NSOptions{}) c1 := f2.Counter(metrics.Options{ Name: "rodriguez", Tags: map[string]string{"x": "y"}, Help: "Help message", }) c2 := f2.Counter(metrics.Options{ Name: "rodriguez", Tags: map[string]string{"x": "z"}, Help: "Help message", }) c3 := f3.Counter(metrics.Options{ Name: "rodriguez", Tags: map[string]string{"x": "z"}, Help: "Help message", }) // same tags as c2, but from f3 c1.Inc(1) c1.Inc(2) c2.Inc(3) c3.Inc(4) snapshot, err := registry.Gather() require.NoError(t, err) assert.Equal(t, "Help message", snapshot[0].GetHelp()) m1 := findMetric(t, snapshot, "bender_rodriguez_total", map[string]string{"a": "b", "x": "y"}) assert.InDelta(t, 3.0, m1.GetCounter().GetValue(), 0.01, "%+v", m1) m2 := findMetric(t, snapshot, "bender_rodriguez_total", map[string]string{"a": "b", "x": "z"}) assert.InDelta(t, 7.0, m2.GetCounter().GetValue(), 0.01, "%+v", m2) } func TestCounterDefaultHelp(t *testing.T) { registry := prometheus.NewPedanticRegistry() f1 := prommetrics.New(prommetrics.WithRegisterer(registry)) c1 := f1.Counter(metrics.Options{ Name: "rodriguez", Tags: map[string]string{"x": "y"}, }) c1.Inc(1) snapshot, err := registry.Gather() require.NoError(t, err) assert.Equal(t, "rodriguez", snapshot[0].GetHelp()) } func TestCounterNotValidLabel(t *testing.T) { registry := prometheus.NewPedanticRegistry() f1 := prommetrics.New( prommetrics.WithRegisterer(registry), ) c1 := f1.Counter(metrics.Options{ Name: "ilia", Tags: map[string]string{"x": "label__3d\x85_this_will_panic-repro"}, }) assert.Equal(t, c1, metrics.NullCounter, "Expected NullCounter, got %v", c1) } func TestGauge(t *testing.T) { registry := prometheus.NewPedanticRegistry() f1 := prommetrics.New(prommetrics.WithRegisterer(registry)) f2 := f1.Namespace(metrics.NSOptions{ Name: "bender", Tags: map[string]string{"a": "b"}, }) f3 := f2.Namespace(metrics.NSOptions{ Tags: map[string]string{"a": "b"}, }) // essentially same as f2 g1 := f2.Gauge(metrics.Options{ Name: "rodriguez", Tags: map[string]string{"x": "y"}, Help: "Help message", }) g2 := f2.Gauge(metrics.Options{ Name: "rodriguez", Tags: map[string]string{"x": "z"}, Help: "Help message", }) g3 := f3.Gauge(metrics.Options{ Name: "rodriguez", Tags: map[string]string{"x": "z"}, Help: "Help message", }) // same as g2, but from f3 g1.Update(1) g1.Update(2) g2.Update(3) g3.Update(4) snapshot, err := registry.Gather() require.NoError(t, err) assert.Equal(t, "Help message", snapshot[0].GetHelp()) m1 := findMetric(t, snapshot, "bender_rodriguez", map[string]string{"a": "b", "x": "y"}) assert.InDelta(t, 2.0, m1.GetGauge().GetValue(), 0.01, "%+v", m1) m2 := findMetric(t, snapshot, "bender_rodriguez", map[string]string{"a": "b", "x": "z"}) assert.InDelta(t, 4.0, m2.GetGauge().GetValue(), 0.01, "%+v", m2) } func TestGaugeDefaultHelp(t *testing.T) { registry := prometheus.NewPedanticRegistry() f1 := prommetrics.New(prommetrics.WithRegisterer(registry)) g1 := f1.Gauge(metrics.Options{ Name: "rodriguez", Tags: map[string]string{"x": "y"}, }) g1.Update(1) snapshot, err := registry.Gather() require.NoError(t, err) assert.Equal(t, "rodriguez", snapshot[0].GetHelp()) } func TestGaugeNotValidLabel(t *testing.T) { registry := prometheus.NewPedanticRegistry() f1 := prommetrics.New( prommetrics.WithRegisterer(registry), ) c1 := f1.Gauge(metrics.Options{ Name: "ilia", Tags: map[string]string{"x": "label__3d\x85_this_will_panic-repro"}, }) assert.Equal(t, c1, metrics.NullGauge, "Expected NullGauge, got %v", c1) } func TestTimer(t *testing.T) { registry := prometheus.NewPedanticRegistry() f1 := prommetrics.New(prommetrics.WithRegisterer(registry)) f2 := f1.Namespace(metrics.NSOptions{ Name: "bender", Tags: map[string]string{"a": "b"}, }) f3 := f2.Namespace(metrics.NSOptions{ Tags: map[string]string{"a": "b"}, }) // essentially same as f2 t1 := f2.Timer(metrics.TimerOptions{ Name: "rodriguez", Tags: map[string]string{"x": "y"}, Help: "Help message", }) t2 := f2.Timer(metrics.TimerOptions{ Name: "rodriguez", Tags: map[string]string{"x": "z"}, Help: "Help message", }) t3 := f3.Timer(metrics.TimerOptions{ Name: "rodriguez", Tags: map[string]string{"x": "z"}, Help: "Help message", }) // same as t2, but from f3 t1.Record(1 * time.Second) t1.Record(2 * time.Second) t2.Record(3 * time.Second) t3.Record(4 * time.Second) snapshot, err := registry.Gather() require.NoError(t, err) assert.Equal(t, "Help message", snapshot[0].GetHelp()) m1 := findMetric(t, snapshot, "bender_rodriguez", map[string]string{"a": "b", "x": "y"}) assert.EqualValues(t, 2, m1.GetHistogram().GetSampleCount(), "%+v", m1) assert.InDelta(t, 3.0, m1.GetHistogram().GetSampleSum(), 0.01, "%+v", m1) for _, bucket := range m1.GetHistogram().GetBucket() { switch { case bucket.GetUpperBound() < 1: assert.EqualValues(t, 0, bucket.GetCumulativeCount()) case bucket.GetUpperBound() < 2: assert.EqualValues(t, 1, bucket.GetCumulativeCount()) default: assert.EqualValues(t, 2, bucket.GetCumulativeCount()) } } m2 := findMetric(t, snapshot, "bender_rodriguez", map[string]string{"a": "b", "x": "z"}) assert.EqualValues(t, 2, m2.GetHistogram().GetSampleCount(), "%+v", m2) assert.InDelta(t, 7.0, m2.GetHistogram().GetSampleSum(), 0.01, "%+v", m2) for _, bucket := range m2.GetHistogram().GetBucket() { switch { case bucket.GetUpperBound() < 3: assert.EqualValues(t, 0, bucket.GetCumulativeCount()) case bucket.GetUpperBound() < 4: assert.EqualValues(t, 1, bucket.GetCumulativeCount()) default: assert.EqualValues(t, 2, bucket.GetCumulativeCount()) } } } func TestTimerDefaultHelp(t *testing.T) { registry := prometheus.NewPedanticRegistry() f1 := prommetrics.New(prommetrics.WithRegisterer(registry)) t1 := f1.Timer(metrics.TimerOptions{ Name: "rodriguez", Tags: map[string]string{"x": "y"}, }) t1.Record(1 * time.Second) snapshot, err := registry.Gather() require.NoError(t, err) assert.Equal(t, "rodriguez", snapshot[0].GetHelp()) } func TestTimerNotValidLabel(t *testing.T) { registry := prometheus.NewPedanticRegistry() f1 := prommetrics.New( prommetrics.WithRegisterer(registry), ) c1 := f1.Timer(metrics.TimerOptions{ Name: "ilia", Tags: map[string]string{"x": "label__3d\x85_this_will_panic-repro"}, }) assert.Equal(t, c1, metrics.NullTimer, "Expected NullTimer, got %v", c1) } func TestTimerCustomBuckets(t *testing.T) { registry := prometheus.NewPedanticRegistry() f1 := prommetrics.New(prommetrics.WithRegisterer(registry), prommetrics.WithBuckets([]float64{1.5})) // dot and dash in the metric name will be replaced with underscore t1 := f1.Timer(metrics.TimerOptions{ Name: "bender.bending-rodriguez", Tags: map[string]string{"x": "y"}, Buckets: []time.Duration{time.Nanosecond, 5 * time.Nanosecond}, }) t1.Record(1 * time.Second) t1.Record(2 * time.Second) snapshot, err := registry.Gather() require.NoError(t, err) m1 := findMetric(t, snapshot, "bender_bending_rodriguez", map[string]string{"x": "y"}) assert.EqualValues(t, 2, m1.GetHistogram().GetSampleCount(), "%+v", m1) assert.InDelta(t, 3.0, m1.GetHistogram().GetSampleSum(), 0.01, "%+v", m1) assert.Len(t, m1.GetHistogram().GetBucket(), 2) } func TestTimerDefaultBuckets(t *testing.T) { registry := prometheus.NewPedanticRegistry() f1 := prommetrics.New(prommetrics.WithRegisterer(registry), prommetrics.WithBuckets([]float64{1.5, 2})) // dot and dash in the metric name will be replaced with underscore t1 := f1.Timer(metrics.TimerOptions{ Name: "bender.bending-rodriguez", Tags: map[string]string{"x": "y"}, Buckets: nil, }) t1.Record(1 * time.Second) t1.Record(2 * time.Second) snapshot, err := registry.Gather() require.NoError(t, err) m1 := findMetric(t, snapshot, "bender_bending_rodriguez", map[string]string{"x": "y"}) assert.EqualValues(t, 2, m1.GetHistogram().GetSampleCount(), "%+v", m1) assert.InDelta(t, 3.0, m1.GetHistogram().GetSampleSum(), 0.01, "%+v", m1) assert.Len(t, m1.GetHistogram().GetBucket(), 2) } func TestHistogram(t *testing.T) { registry := prometheus.NewPedanticRegistry() f1 := prommetrics.New(prommetrics.WithRegisterer(registry)) f2 := f1.Namespace(metrics.NSOptions{ Name: "bender", Tags: map[string]string{"a": "b"}, }) f3 := f2.Namespace(metrics.NSOptions{ Tags: map[string]string{"a": "b"}, }) // essentially same as f2 t1 := f2.Histogram(metrics.HistogramOptions{ Name: "rodriguez", Tags: map[string]string{"x": "y"}, Help: "Help message", }) t2 := f2.Histogram(metrics.HistogramOptions{ Name: "rodriguez", Tags: map[string]string{"x": "z"}, Help: "Help message", }) t3 := f3.Histogram(metrics.HistogramOptions{ Name: "rodriguez", Tags: map[string]string{"x": "z"}, Help: "Help message", }) // same as t2, but from f3 t1.Record(1) t1.Record(2) t2.Record(3) t3.Record(4) snapshot, err := registry.Gather() require.NoError(t, err) assert.Equal(t, "Help message", snapshot[0].GetHelp()) m1 := findMetric(t, snapshot, "bender_rodriguez", map[string]string{"a": "b", "x": "y"}) assert.EqualValues(t, 2, m1.GetHistogram().GetSampleCount(), "%+v", m1) assert.InDelta(t, 3.0, m1.GetHistogram().GetSampleSum(), 0.01, "%+v", m1) for _, bucket := range m1.GetHistogram().GetBucket() { switch { case bucket.GetUpperBound() < 1: assert.EqualValues(t, 0, bucket.GetCumulativeCount()) case bucket.GetUpperBound() < 2: assert.EqualValues(t, 1, bucket.GetCumulativeCount()) default: assert.EqualValues(t, 2, bucket.GetCumulativeCount()) } } m2 := findMetric(t, snapshot, "bender_rodriguez", map[string]string{"a": "b", "x": "z"}) assert.EqualValues(t, 2, m2.GetHistogram().GetSampleCount(), "%+v", m2) assert.InDelta(t, 7.0, m2.GetHistogram().GetSampleSum(), 0.01, "%+v", m2) for _, bucket := range m2.GetHistogram().GetBucket() { switch { case bucket.GetUpperBound() < 3: assert.EqualValues(t, 0, bucket.GetCumulativeCount()) case bucket.GetUpperBound() < 4: assert.EqualValues(t, 1, bucket.GetCumulativeCount()) default: assert.EqualValues(t, 2, bucket.GetCumulativeCount()) } } } func TestHistogramDefaultHelp(t *testing.T) { registry := prometheus.NewPedanticRegistry() f1 := prommetrics.New(prommetrics.WithRegisterer(registry)) t1 := f1.Histogram(metrics.HistogramOptions{ Name: "rodriguez", Tags: map[string]string{"x": "y"}, }) t1.Record(1) snapshot, err := registry.Gather() require.NoError(t, err) assert.Equal(t, "rodriguez", snapshot[0].GetHelp()) } func TestHistogramCustomBuckets(t *testing.T) { registry := prometheus.NewPedanticRegistry() f1 := prommetrics.New(prommetrics.WithRegisterer(registry)) // dot and dash in the metric name will be replaced with underscore t1 := f1.Histogram(metrics.HistogramOptions{ Name: "bender.bending-rodriguez", Tags: map[string]string{"x": "y"}, Buckets: []float64{1.5}, }) t1.Record(1) t1.Record(2) snapshot, err := registry.Gather() require.NoError(t, err) m1 := findMetric(t, snapshot, "bender_bending_rodriguez", map[string]string{"x": "y"}) assert.EqualValues(t, 2, m1.GetHistogram().GetSampleCount(), "%+v", m1) assert.InDelta(t, 3.0, m1.GetHistogram().GetSampleSum(), 0.01, "%+v", m1) assert.Len(t, m1.GetHistogram().GetBucket(), 1) } func TestHistogramNotValidLabel(t *testing.T) { registry := prometheus.NewPedanticRegistry() f1 := prommetrics.New( prommetrics.WithRegisterer(registry), ) c1 := f1.Histogram(metrics.HistogramOptions{ Name: "ilia", Tags: map[string]string{"x": "label__3d\x85_this_will_panic-repro"}, }) assert.Equal(t, c1, metrics.NullHistogram, "Expected NullHistogram, got %v", c1) } func TestHistogramDefaultBuckets(t *testing.T) { registry := prometheus.NewPedanticRegistry() f1 := prommetrics.New(prommetrics.WithRegisterer(registry), prommetrics.WithBuckets([]float64{1.5})) // dot and dash in the metric name will be replaced with underscore t1 := f1.Histogram(metrics.HistogramOptions{ Name: "bender.bending-rodriguez", Tags: map[string]string{"x": "y"}, Buckets: nil, }) t1.Record(1) t1.Record(2) snapshot, err := registry.Gather() require.NoError(t, err) m1 := findMetric(t, snapshot, "bender_bending_rodriguez", map[string]string{"x": "y"}) assert.EqualValues(t, 2, m1.GetHistogram().GetSampleCount(), "%+v", m1) assert.InDelta(t, 3.0, m1.GetHistogram().GetSampleSum(), 0.01, "%+v", m1) assert.Len(t, m1.GetHistogram().GetBucket(), 1) } func findMetric(t *testing.T, snapshot []*prommodel.MetricFamily, name string, tags map[string]string) *prommodel.Metric { for _, mf := range snapshot { if mf.GetName() != name { continue } for _, m := range mf.GetMetric() { require.Lenf(t, m.GetLabel(), len(tags), "Mismatching labels for metric %v: want %v, have %v", name, tags, m.GetLabel()) match := true for _, l := range m.GetLabel() { if v, ok := tags[l.GetName()]; !ok || v != l.GetValue() { match = false } } if match { return m } } } t.Logf("Cannot find metric %v %v", name, tags) for _, nf := range snapshot { t.Logf("Family: %v", nf.GetName()) for _, m := range nf.GetMetric() { t.Logf("==> %v", m) } } t.FailNow() return nil } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/metrics/stopwatch.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package metrics import ( "time" ) // StartStopwatch begins recording the executing time of an event, returning // a Stopwatch that should be used to stop the recording the time for // that event. Multiple events can be occurring simultaneously each // represented by different active Stopwatches func StartStopwatch(timer Timer) Stopwatch { return Stopwatch{t: timer, start: time.Now()} } // A Stopwatch tracks the execution time of a specific event type Stopwatch struct { t Timer start time.Time } // Stop stops executing of the stopwatch and records the amount of elapsed time func (s Stopwatch) Stop() { s.t.Record(s.ElapsedTime()) } // ElapsedTime returns the amount of elapsed time (in time.Duration) func (s Stopwatch) ElapsedTime() time.Duration { return time.Since(s.start) } ================================================ FILE: internal/metrics/timer.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package metrics import ( "time" ) // Timer accumulates observations about how long some operation took, // and also maintains a historgam of percentiles. type Timer interface { // Records the time passed in. Record(time.Duration) } // NullTimer timer that does nothing var NullTimer Timer = nullTimer{} type nullTimer struct{} func (nullTimer) Record(time.Duration) {} ================================================ FILE: internal/metricstest/keys.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package metricstest import ( "sort" ) // GetKey converts name+tags into a single string of the form // "name|tag1=value1|...|tagN=valueN", where tag names are // sorted alphabetically. func GetKey(name string, tags map[string]string, tagsSep string, tagKVSep string) string { keys := make([]string, 0, len(tags)) for k := range tags { keys = append(keys, k) } sort.Strings(keys) key := name for _, k := range keys { key = key + tagsSep + k + tagKVSep + tags[k] } return key } ================================================ FILE: internal/metricstest/local.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package metricstest import ( "maps" "slices" "sync" "sync/atomic" "time" "github.com/jaegertracing/jaeger/internal/metrics" ) // This is intentionally very similar to github.com/codahale/metrics, the // main difference being that counters/gauges are scoped to the provider // rather than being global (to facilitate testing). // numeric is a constraint that permits int64 and float64. type numeric interface { ~int64 | ~float64 } // simpleHistogram is a simple histogram that stores all observations // and computes percentiles from a sorted list. It uses generics to // support both int64 (for timers) and float64 (for histograms). type simpleHistogram[T numeric] struct { sync.Mutex observations []T } func (h *simpleHistogram[T]) record(v T) { h.Lock() defer h.Unlock() h.observations = append(h.observations, v) } func (h *simpleHistogram[T]) valueAtPercentile(q float64) int64 { h.Lock() defer h.Unlock() if len(h.observations) == 0 { return 0 } sorted := slices.Clone(h.observations) slices.Sort(sorted) idx := int(float64(len(sorted)-1) * q / 100.0) return int64(sorted[idx]) } // A Backend is a metrics provider which aggregates data in-vm, and // allows exporting snapshots to shove the data into a remote collector type Backend struct { cm sync.Mutex gm sync.Mutex tm sync.Mutex hm sync.Mutex counters map[string]*int64 gauges map[string]*int64 timers map[string]*simpleHistogram[int64] histograms map[string]*simpleHistogram[float64] TagsSep string TagKVSep string } // NewBackend returns a new Backend. The collectionInterval is the histogram // time window for each timer. func NewBackend(_ time.Duration) *Backend { return &Backend{ counters: make(map[string]*int64), gauges: make(map[string]*int64), timers: make(map[string]*simpleHistogram[int64]), histograms: make(map[string]*simpleHistogram[float64]), TagsSep: "|", TagKVSep: "=", } } // Clear discards accumulated stats func (b *Backend) Clear() { b.cm.Lock() defer b.cm.Unlock() b.gm.Lock() defer b.gm.Unlock() b.tm.Lock() defer b.tm.Unlock() b.hm.Lock() defer b.hm.Unlock() b.counters = make(map[string]*int64) b.gauges = make(map[string]*int64) b.timers = make(map[string]*simpleHistogram[int64]) b.histograms = make(map[string]*simpleHistogram[float64]) } // IncCounter increments a counter value func (b *Backend) IncCounter(name string, tags map[string]string, delta int64) { name = GetKey(name, tags, b.TagsSep, b.TagKVSep) b.cm.Lock() defer b.cm.Unlock() counter := b.counters[name] if counter == nil { b.counters[name] = new(int64) *b.counters[name] = delta return } atomic.AddInt64(counter, delta) } // UpdateGauge updates the value of a gauge func (b *Backend) UpdateGauge(name string, tags map[string]string, value int64) { name = GetKey(name, tags, b.TagsSep, b.TagKVSep) b.gm.Lock() defer b.gm.Unlock() gauge := b.gauges[name] if gauge == nil { b.gauges[name] = new(int64) *b.gauges[name] = value return } atomic.StoreInt64(gauge, value) } // RecordHistogram records a histogram value func (b *Backend) RecordHistogram(name string, tags map[string]string, v float64) { name = GetKey(name, tags, b.TagsSep, b.TagKVSep) histogram := b.findOrCreateHistogram(name) histogram.record(v) } func (b *Backend) findOrCreateHistogram(name string) *simpleHistogram[float64] { b.hm.Lock() defer b.hm.Unlock() if h, ok := b.histograms[name]; ok { return h } h := &simpleHistogram[float64]{} b.histograms[name] = h return h } // RecordTimer records a timing duration func (b *Backend) RecordTimer(name string, tags map[string]string, d time.Duration) { name = GetKey(name, tags, b.TagsSep, b.TagKVSep) timer := b.findOrCreateTimer(name) timer.record(int64(d / time.Millisecond)) } func (b *Backend) findOrCreateTimer(name string) *simpleHistogram[int64] { b.tm.Lock() defer b.tm.Unlock() if t, ok := b.timers[name]; ok { return t } t := &simpleHistogram[int64]{} b.timers[name] = t return t } var percentiles = map[string]float64{ "P50": 50, "P75": 75, "P90": 90, "P95": 95, "P99": 99, "P999": 99.9, } // Snapshot captures a snapshot of the current counter and gauge values func (b *Backend) Snapshot() (counters, gauges map[string]int64) { b.cm.Lock() defer b.cm.Unlock() counters = make(map[string]int64, len(b.counters)) for name, value := range b.counters { counters[name] = atomic.LoadInt64(value) } b.gm.Lock() defer b.gm.Unlock() gauges = make(map[string]int64, len(b.gauges)) for name, value := range b.gauges { gauges[name] = atomic.LoadInt64(value) } b.tm.Lock() timers := make(map[string]*simpleHistogram[int64]) maps.Copy(timers, b.timers) b.tm.Unlock() for timerName, timer := range timers { for name, q := range percentiles { gauges[timerName+"."+name] = timer.valueAtPercentile(q) } } b.hm.Lock() histograms := make(map[string]*simpleHistogram[float64]) maps.Copy(histograms, b.histograms) b.hm.Unlock() for histogramName, histogram := range histograms { for name, q := range percentiles { gauges[histogramName+"."+name] = histogram.valueAtPercentile(q) } } return counters, gauges } // Stop is a no-op for this simple backend (no background goroutines). func (*Backend) Stop() {} type stats struct { name string tags map[string]string buckets []float64 durationBuckets []time.Duration localBackend *Backend } type localTimer struct { stats } func (l *localTimer) Record(d time.Duration) { l.localBackend.RecordTimer(l.name, l.tags, d) } type localHistogram struct { stats } func (l *localHistogram) Record(v float64) { l.localBackend.RecordHistogram(l.name, l.tags, v) } type localCounter struct { stats } func (l *localCounter) Inc(delta int64) { l.localBackend.IncCounter(l.name, l.tags, delta) } type localGauge struct { stats } func (l *localGauge) Update(value int64) { l.localBackend.UpdateGauge(l.name, l.tags, value) } // Factory stats factory that creates metrics that are stored locally type Factory struct { *Backend namespace string tags map[string]string } // NewFactory returns a new LocalMetricsFactory func NewFactory(collectionInterval time.Duration) *Factory { return &Factory{ Backend: NewBackend(collectionInterval), } } // appendTags adds the tags to the namespace tags and returns a combined map. func (f *Factory) appendTags(tags map[string]string) map[string]string { newTags := make(map[string]string) maps.Copy(newTags, f.tags) maps.Copy(newTags, tags) return newTags } func (f *Factory) newNamespace(name string) string { if f.namespace == "" { return name } if name == "" { return f.namespace } return f.namespace + "." + name } // Counter returns a local stats counter func (f *Factory) Counter(options metrics.Options) metrics.Counter { return &localCounter{ stats{ name: f.newNamespace(options.Name), tags: f.appendTags(options.Tags), localBackend: f.Backend, }, } } // Timer returns a local stats timer. func (f *Factory) Timer(options metrics.TimerOptions) metrics.Timer { return &localTimer{ stats{ name: f.newNamespace(options.Name), tags: f.appendTags(options.Tags), durationBuckets: options.Buckets, localBackend: f.Backend, }, } } // Gauge returns a local stats gauge. func (f *Factory) Gauge(options metrics.Options) metrics.Gauge { return &localGauge{ stats{ name: f.newNamespace(options.Name), tags: f.appendTags(options.Tags), localBackend: f.Backend, }, } } // Histogram returns a local stats histogram. func (f *Factory) Histogram(options metrics.HistogramOptions) metrics.Histogram { return &localHistogram{ stats{ name: f.newNamespace(options.Name), tags: f.appendTags(options.Tags), buckets: options.Buckets, localBackend: f.Backend, }, } } // Namespace returns a new namespace. func (f *Factory) Namespace(scope metrics.NSOptions) metrics.Factory { return &Factory{ namespace: f.newNamespace(scope.Name), tags: f.appendTags(scope.Tags), Backend: f.Backend, } } func (f *Factory) Stop() { f.Backend.Stop() } ================================================ FILE: internal/metricstest/local_test.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package metricstest import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/metrics" ) func TestLocalMetrics(t *testing.T) { tags := map[string]string{ "x": "y", } f := NewFactory(0) defer f.Stop() f.Counter(metrics.Options{ Name: "my-counter", Tags: tags, }).Inc(4) f.Counter(metrics.Options{ Name: "my-counter", Tags: tags, }).Inc(6) f.Counter(metrics.Options{ Name: "my-counter", }).Inc(6) f.Counter(metrics.Options{ Name: "other-counter", }).Inc(8) f.Gauge(metrics.Options{ Name: "my-gauge", }).Update(25) f.Gauge(metrics.Options{ Name: "my-gauge", }).Update(43) f.Gauge(metrics.Options{ Name: "other-gauge", }).Update(74) f.Namespace(metrics.NSOptions{ Name: "namespace", Tags: tags, }).Counter(metrics.Options{ Name: "my-counter", }).Inc(7) f.Namespace(metrics.NSOptions{ Name: "ns.subns", }).Counter(metrics.Options{ Tags: map[string]string{"service": "a-service"}, }).Inc(9) timings := map[string][]time.Duration{ "foo-latency": { time.Second * 35, time.Second * 6, time.Millisecond * 576, time.Second * 12, }, "bar-latency": { time.Minute*4 + time.Second*34, time.Minute*7 + time.Second*12, time.Second * 625, time.Second * 12, }, } for metric, timing := range timings { for _, d := range timing { f.Timer(metrics.TimerOptions{ Name: metric, }).Record(d) } } histogram := f.Histogram(metrics.HistogramOptions{ Name: "my-histo", }) histogram.Record(321) histogram.Record(42) c, g := f.Snapshot() require.NotNil(t, c) require.NotNil(t, g) assert.Equal(t, map[string]int64{ "my-counter|x=y": 10, "my-counter": 6, "other-counter": 8, "namespace.my-counter|x=y": 7, "ns.subns|service=a-service": 9, }, c) assert.Equal(t, map[string]int64{ "bar-latency.P50": 274000, "bar-latency.P75": 432000, "bar-latency.P90": 432000, "bar-latency.P95": 432000, "bar-latency.P99": 432000, "bar-latency.P999": 432000, "foo-latency.P50": 6000, "foo-latency.P75": 12000, "foo-latency.P90": 12000, "foo-latency.P95": 12000, "foo-latency.P99": 12000, "foo-latency.P999": 12000, "my-gauge": 43, "my-histo.P50": 42, "my-histo.P75": 42, "my-histo.P90": 42, "my-histo.P95": 42, "my-histo.P99": 42, "my-histo.P999": 42, "other-gauge": 74, }, g) f.Clear() c, g = f.Snapshot() require.Empty(t, c) require.Empty(t, g) } func TestLocalMetricsInterval(t *testing.T) { f := NewFactory(time.Millisecond) defer f.Stop() f.Timer(metrics.TimerOptions{ Name: "timer", }).Record(time.Millisecond * 100) f.tm.Lock() timer := f.timers["timer"] f.tm.Unlock() require.NotNil(t, timer) timer.Lock() assert.Len(t, timer.observations, 1) assert.Equal(t, int64(100), timer.observations[0]) timer.Unlock() } ================================================ FILE: internal/metricstest/metricstest.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package metricstest import ( "testing" "github.com/stretchr/testify/assert" ) // ExpectedMetric contains metrics under test. type ExpectedMetric struct { Name string Tags map[string]string Value int } // ExpectedTimerMetric contains timer metrics under test. type ExpectedTimerMetric struct { Name string Tags map[string]string Percentile string // e.g., "P50", "P75", "P90", "P95", "P99", "P999" Value int // expected value in milliseconds } // AssertTimerMetrics checks if timer metrics exist with expected percentile values. func (f *Factory) AssertTimerMetrics(t *testing.T, expectedMetrics ...ExpectedTimerMetric) { _, gauges := f.Snapshot() for _, expected := range expectedMetrics { key := GetKey(expected.Name, expected.Tags, "|", "=") fullKey := key + "." + expected.Percentile assert.EqualValues(t, expected.Value, gauges[fullKey], "expected timer metric name=%s percentile=%s tags: %+v; got: %+v", expected.Name, expected.Percentile, expected.Tags, gauges, ) } } // AssertCounterMetrics checks if counter metrics exist. func (f *Factory) AssertCounterMetrics(t *testing.T, expectedMetrics ...ExpectedMetric) { counters, _ := f.Snapshot() assertMetrics(t, counters, expectedMetrics...) } // AssertGaugeMetrics checks if gauge metrics exist. func (f *Factory) AssertGaugeMetrics(t *testing.T, expectedMetrics ...ExpectedMetric) { _, gauges := f.Snapshot() assertMetrics(t, gauges, expectedMetrics...) } func assertMetrics(t *testing.T, actualMetrics map[string]int64, expectedMetrics ...ExpectedMetric) { for _, expected := range expectedMetrics { key := GetKey(expected.Name, expected.Tags, "|", "=") assert.EqualValues(t, expected.Value, actualMetrics[key], "expected metric name=%s tags: %+v; got: %+v", expected.Name, expected.Tags, actualMetrics, ) } } ================================================ FILE: internal/metricstest/metricstest_test.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package metricstest import ( "testing" "time" ) func TestAssertMetrics(t *testing.T) { f := NewFactory(0) tags := map[string]string{"key": "value"} f.IncCounter("counter", tags, 1) f.UpdateGauge("gauge", tags, 11) f.AssertCounterMetrics(t, ExpectedMetric{Name: "counter", Tags: tags, Value: 1}) f.AssertGaugeMetrics(t, ExpectedMetric{Name: "gauge", Tags: tags, Value: 11}) } func TestAssertTimerMetrics(t *testing.T) { f := NewFactory(0) tags := map[string]string{"service": "test"} // Record some timer values (in milliseconds: 10, 20, 30, 40, 50) f.RecordTimer("request_duration", tags, 10*time.Millisecond) f.RecordTimer("request_duration", tags, 20*time.Millisecond) f.RecordTimer("request_duration", tags, 30*time.Millisecond) f.RecordTimer("request_duration", tags, 40*time.Millisecond) f.RecordTimer("request_duration", tags, 50*time.Millisecond) // With 5 values [10, 20, 30, 40, 50]: // P50 = sorted[int(4 * 0.50)] = sorted[2] = 30 // P99 = sorted[int(4 * 0.99)] = sorted[3] = 40 f.AssertTimerMetrics(t, ExpectedTimerMetric{Name: "request_duration", Tags: tags, Percentile: "P50", Value: 30}, ExpectedTimerMetric{Name: "request_duration", Tags: tags, Percentile: "P99", Value: 40}, ) } ================================================ FILE: internal/metricstest/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package metricstest import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/proto/api_v3/query_service.pb.go ================================================ // Code generated by protoc-gen-gogo. DO NOT EDIT. // source: query_service.proto package api_v3 import ( context "context" fmt "fmt" _ "github.com/gogo/protobuf/gogoproto" proto "github.com/gogo/protobuf/proto" _ "github.com/gogo/protobuf/types" github_com_gogo_protobuf_types "github.com/gogo/protobuf/types" v1 "github.com/jaegertracing/jaeger/internal/jptrace" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" io "io" math "math" math_bits "math/bits" time "time" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf var _ = time.Kitchen // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package // Request object to get a trace. type GetTraceRequest struct { // Hex encoded 64 or 128 bit trace ID. TraceId string `protobuf:"bytes,1,opt,name=trace_id,json=traceId,proto3" json:"trace_id,omitempty"` // Optional. The start time to search trace ID. StartTime time.Time `protobuf:"bytes,2,opt,name=start_time,json=startTime,proto3,stdtime" json:"start_time"` // Optional. The end time to search trace ID. EndTime time.Time `protobuf:"bytes,3,opt,name=end_time,json=endTime,proto3,stdtime" json:"end_time"` // Optional. If set to true, the response will not include any // enrichments to the trace, such as clock skew adjustment. // Instead, the trace will be returned exactly as stored. RawTraces bool `protobuf:"varint,4,opt,name=raw_traces,json=rawTraces,proto3" json:"raw_traces,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GetTraceRequest) Reset() { *m = GetTraceRequest{} } func (m *GetTraceRequest) String() string { return proto.CompactTextString(m) } func (*GetTraceRequest) ProtoMessage() {} func (*GetTraceRequest) Descriptor() ([]byte, []int) { return fileDescriptor_5fcb6756dc1afb8d, []int{0} } func (m *GetTraceRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GetTraceRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GetTraceRequest.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GetTraceRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_GetTraceRequest.Merge(m, src) } func (m *GetTraceRequest) XXX_Size() int { return m.Size() } func (m *GetTraceRequest) XXX_DiscardUnknown() { xxx_messageInfo_GetTraceRequest.DiscardUnknown(m) } var xxx_messageInfo_GetTraceRequest proto.InternalMessageInfo func (m *GetTraceRequest) GetTraceId() string { if m != nil { return m.TraceId } return "" } func (m *GetTraceRequest) GetStartTime() time.Time { if m != nil { return m.StartTime } return time.Time{} } func (m *GetTraceRequest) GetEndTime() time.Time { if m != nil { return m.EndTime } return time.Time{} } func (m *GetTraceRequest) GetRawTraces() bool { if m != nil { return m.RawTraces } return false } // Query parameters to find traces. // // All fields form a conjunction (e.g., "service_name='X' AND operation_name='Y' AND ..."), // except for `search_depth` and `raw_traces`. // // Fields are matched against individual spans, not the trace level. The results include // traces with at least one matching span. // // The results have no guaranteed ordering. type TraceQueryParameters struct { // service_name filters spans generated by a specific service. ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` // operation_name filters spans by a specific operation / span name. OperationName string `protobuf:"bytes,2,opt,name=operation_name,json=operationName,proto3" json:"operation_name,omitempty"` // attributes contains key-value pairs where the key is the attribute name // and the value is its string representation. Attributes are matched against // span and resource attributes. At least one span must match all specified attributes. Attributes map[string]string `protobuf:"bytes,3,rep,name=attributes,proto3" json:"attributes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // start_time_min is the start of the time interval (inclusive) for the query. // Only traces with spans that started on or after this time will be returned. // // The HTTP API uses RFC-3339ns format. // // This field is required. StartTimeMin time.Time `protobuf:"bytes,4,opt,name=start_time_min,json=startTimeMin,proto3,stdtime" json:"start_time_min"` // start_time_max is the end of the time interval (exclusive) for the query. // Only traces with spans that started before this time will be returned. // // The HTTP API uses RFC-3339ns format. // // This field is required. StartTimeMax time.Time `protobuf:"bytes,5,opt,name=start_time_max,json=startTimeMax,proto3,stdtime" json:"start_time_max"` // duration_min is the minimum duration of a span in the trace. // Only traces with spans that lasted at least this long will be returned. // // The HTTP API uses Golang's time format (e.g., "10s"). DurationMin time.Duration `protobuf:"bytes,6,opt,name=duration_min,json=durationMin,proto3,stdduration" json:"duration_min"` // duration_max is the maximum duration of a span in the trace. // Only traces with spans that lasted at most this long will be returned. // // The HTTP API uses Golang's time format (e.g., "10s"). DurationMax time.Duration `protobuf:"bytes,7,opt,name=duration_max,json=durationMax,proto3,stdduration" json:"duration_max"` // search_depth defines the maximum search depth. Depending on the backend storage implementation, // this may behave like an SQL `LIMIT` clause. However, some implementations might not support // precise limits, and a larger value generally results in more traces being returned. SearchDepth int32 `protobuf:"varint,8,opt,name=search_depth,json=searchDepth,proto3" json:"search_depth,omitempty"` // If set to true, the response will exclude any enrichments to the trace, such as clock skew adjustments. // The trace will be returned exactly as stored. // // This field is optional. RawTraces bool `protobuf:"varint,9,opt,name=raw_traces,json=rawTraces,proto3" json:"raw_traces,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *TraceQueryParameters) Reset() { *m = TraceQueryParameters{} } func (m *TraceQueryParameters) String() string { return proto.CompactTextString(m) } func (*TraceQueryParameters) ProtoMessage() {} func (*TraceQueryParameters) Descriptor() ([]byte, []int) { return fileDescriptor_5fcb6756dc1afb8d, []int{1} } func (m *TraceQueryParameters) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *TraceQueryParameters) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_TraceQueryParameters.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *TraceQueryParameters) XXX_Merge(src proto.Message) { xxx_messageInfo_TraceQueryParameters.Merge(m, src) } func (m *TraceQueryParameters) XXX_Size() int { return m.Size() } func (m *TraceQueryParameters) XXX_DiscardUnknown() { xxx_messageInfo_TraceQueryParameters.DiscardUnknown(m) } var xxx_messageInfo_TraceQueryParameters proto.InternalMessageInfo func (m *TraceQueryParameters) GetServiceName() string { if m != nil { return m.ServiceName } return "" } func (m *TraceQueryParameters) GetOperationName() string { if m != nil { return m.OperationName } return "" } func (m *TraceQueryParameters) GetAttributes() map[string]string { if m != nil { return m.Attributes } return nil } func (m *TraceQueryParameters) GetStartTimeMin() time.Time { if m != nil { return m.StartTimeMin } return time.Time{} } func (m *TraceQueryParameters) GetStartTimeMax() time.Time { if m != nil { return m.StartTimeMax } return time.Time{} } func (m *TraceQueryParameters) GetDurationMin() time.Duration { if m != nil { return m.DurationMin } return 0 } func (m *TraceQueryParameters) GetDurationMax() time.Duration { if m != nil { return m.DurationMax } return 0 } func (m *TraceQueryParameters) GetSearchDepth() int32 { if m != nil { return m.SearchDepth } return 0 } func (m *TraceQueryParameters) GetRawTraces() bool { if m != nil { return m.RawTraces } return false } // Request object to search traces. type FindTracesRequest struct { Query *TraceQueryParameters `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *FindTracesRequest) Reset() { *m = FindTracesRequest{} } func (m *FindTracesRequest) String() string { return proto.CompactTextString(m) } func (*FindTracesRequest) ProtoMessage() {} func (*FindTracesRequest) Descriptor() ([]byte, []int) { return fileDescriptor_5fcb6756dc1afb8d, []int{2} } func (m *FindTracesRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *FindTracesRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_FindTracesRequest.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *FindTracesRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_FindTracesRequest.Merge(m, src) } func (m *FindTracesRequest) XXX_Size() int { return m.Size() } func (m *FindTracesRequest) XXX_DiscardUnknown() { xxx_messageInfo_FindTracesRequest.DiscardUnknown(m) } var xxx_messageInfo_FindTracesRequest proto.InternalMessageInfo func (m *FindTracesRequest) GetQuery() *TraceQueryParameters { if m != nil { return m.Query } return nil } // Request object to get service names. type GetServicesRequest struct { XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GetServicesRequest) Reset() { *m = GetServicesRequest{} } func (m *GetServicesRequest) String() string { return proto.CompactTextString(m) } func (*GetServicesRequest) ProtoMessage() {} func (*GetServicesRequest) Descriptor() ([]byte, []int) { return fileDescriptor_5fcb6756dc1afb8d, []int{3} } func (m *GetServicesRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GetServicesRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GetServicesRequest.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GetServicesRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_GetServicesRequest.Merge(m, src) } func (m *GetServicesRequest) XXX_Size() int { return m.Size() } func (m *GetServicesRequest) XXX_DiscardUnknown() { xxx_messageInfo_GetServicesRequest.DiscardUnknown(m) } var xxx_messageInfo_GetServicesRequest proto.InternalMessageInfo // Response object to get service names. type GetServicesResponse struct { Services []string `protobuf:"bytes,1,rep,name=services,proto3" json:"services,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GetServicesResponse) Reset() { *m = GetServicesResponse{} } func (m *GetServicesResponse) String() string { return proto.CompactTextString(m) } func (*GetServicesResponse) ProtoMessage() {} func (*GetServicesResponse) Descriptor() ([]byte, []int) { return fileDescriptor_5fcb6756dc1afb8d, []int{4} } func (m *GetServicesResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GetServicesResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GetServicesResponse.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GetServicesResponse) XXX_Merge(src proto.Message) { xxx_messageInfo_GetServicesResponse.Merge(m, src) } func (m *GetServicesResponse) XXX_Size() int { return m.Size() } func (m *GetServicesResponse) XXX_DiscardUnknown() { xxx_messageInfo_GetServicesResponse.DiscardUnknown(m) } var xxx_messageInfo_GetServicesResponse proto.InternalMessageInfo func (m *GetServicesResponse) GetServices() []string { if m != nil { return m.Services } return nil } // Request object to get operation names. type GetOperationsRequest struct { // Required service name. Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` // Optional span kind. SpanKind string `protobuf:"bytes,2,opt,name=span_kind,json=spanKind,proto3" json:"span_kind,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GetOperationsRequest) Reset() { *m = GetOperationsRequest{} } func (m *GetOperationsRequest) String() string { return proto.CompactTextString(m) } func (*GetOperationsRequest) ProtoMessage() {} func (*GetOperationsRequest) Descriptor() ([]byte, []int) { return fileDescriptor_5fcb6756dc1afb8d, []int{5} } func (m *GetOperationsRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GetOperationsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GetOperationsRequest.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GetOperationsRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_GetOperationsRequest.Merge(m, src) } func (m *GetOperationsRequest) XXX_Size() int { return m.Size() } func (m *GetOperationsRequest) XXX_DiscardUnknown() { xxx_messageInfo_GetOperationsRequest.DiscardUnknown(m) } var xxx_messageInfo_GetOperationsRequest proto.InternalMessageInfo func (m *GetOperationsRequest) GetService() string { if m != nil { return m.Service } return "" } func (m *GetOperationsRequest) GetSpanKind() string { if m != nil { return m.SpanKind } return "" } // Operation encapsulates information about operation. type Operation struct { Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` SpanKind string `protobuf:"bytes,2,opt,name=span_kind,json=spanKind,proto3" json:"span_kind,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *Operation) Reset() { *m = Operation{} } func (m *Operation) String() string { return proto.CompactTextString(m) } func (*Operation) ProtoMessage() {} func (*Operation) Descriptor() ([]byte, []int) { return fileDescriptor_5fcb6756dc1afb8d, []int{6} } func (m *Operation) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *Operation) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_Operation.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *Operation) XXX_Merge(src proto.Message) { xxx_messageInfo_Operation.Merge(m, src) } func (m *Operation) XXX_Size() int { return m.Size() } func (m *Operation) XXX_DiscardUnknown() { xxx_messageInfo_Operation.DiscardUnknown(m) } var xxx_messageInfo_Operation proto.InternalMessageInfo func (m *Operation) GetName() string { if m != nil { return m.Name } return "" } func (m *Operation) GetSpanKind() string { if m != nil { return m.SpanKind } return "" } // Response object to get operation names. type GetOperationsResponse struct { Operations []*Operation `protobuf:"bytes,1,rep,name=operations,proto3" json:"operations,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GetOperationsResponse) Reset() { *m = GetOperationsResponse{} } func (m *GetOperationsResponse) String() string { return proto.CompactTextString(m) } func (*GetOperationsResponse) ProtoMessage() {} func (*GetOperationsResponse) Descriptor() ([]byte, []int) { return fileDescriptor_5fcb6756dc1afb8d, []int{7} } func (m *GetOperationsResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GetOperationsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GetOperationsResponse.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GetOperationsResponse) XXX_Merge(src proto.Message) { xxx_messageInfo_GetOperationsResponse.Merge(m, src) } func (m *GetOperationsResponse) XXX_Size() int { return m.Size() } func (m *GetOperationsResponse) XXX_DiscardUnknown() { xxx_messageInfo_GetOperationsResponse.DiscardUnknown(m) } var xxx_messageInfo_GetOperationsResponse proto.InternalMessageInfo func (m *GetOperationsResponse) GetOperations() []*Operation { if m != nil { return m.Operations } return nil } // GRPCGatewayError is the type returned when GRPC server returns an error. // Example: {"error":{"grpcCode":2,"httpCode":500,"message":"...","httpStatus":"text..."}}. type GRPCGatewayError struct { Error *GRPCGatewayError_GRPCGatewayErrorDetails `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GRPCGatewayError) Reset() { *m = GRPCGatewayError{} } func (m *GRPCGatewayError) String() string { return proto.CompactTextString(m) } func (*GRPCGatewayError) ProtoMessage() {} func (*GRPCGatewayError) Descriptor() ([]byte, []int) { return fileDescriptor_5fcb6756dc1afb8d, []int{8} } func (m *GRPCGatewayError) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GRPCGatewayError) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GRPCGatewayError.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GRPCGatewayError) XXX_Merge(src proto.Message) { xxx_messageInfo_GRPCGatewayError.Merge(m, src) } func (m *GRPCGatewayError) XXX_Size() int { return m.Size() } func (m *GRPCGatewayError) XXX_DiscardUnknown() { xxx_messageInfo_GRPCGatewayError.DiscardUnknown(m) } var xxx_messageInfo_GRPCGatewayError proto.InternalMessageInfo func (m *GRPCGatewayError) GetError() *GRPCGatewayError_GRPCGatewayErrorDetails { if m != nil { return m.Error } return nil } type GRPCGatewayError_GRPCGatewayErrorDetails struct { GrpcCode int32 `protobuf:"varint,1,opt,name=grpcCode,proto3" json:"grpcCode,omitempty"` HttpCode int32 `protobuf:"varint,2,opt,name=httpCode,proto3" json:"httpCode,omitempty"` Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` HttpStatus string `protobuf:"bytes,4,opt,name=httpStatus,proto3" json:"httpStatus,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GRPCGatewayError_GRPCGatewayErrorDetails) Reset() { *m = GRPCGatewayError_GRPCGatewayErrorDetails{} } func (m *GRPCGatewayError_GRPCGatewayErrorDetails) String() string { return proto.CompactTextString(m) } func (*GRPCGatewayError_GRPCGatewayErrorDetails) ProtoMessage() {} func (*GRPCGatewayError_GRPCGatewayErrorDetails) Descriptor() ([]byte, []int) { return fileDescriptor_5fcb6756dc1afb8d, []int{8, 0} } func (m *GRPCGatewayError_GRPCGatewayErrorDetails) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GRPCGatewayError_GRPCGatewayErrorDetails) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GRPCGatewayError_GRPCGatewayErrorDetails.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GRPCGatewayError_GRPCGatewayErrorDetails) XXX_Merge(src proto.Message) { xxx_messageInfo_GRPCGatewayError_GRPCGatewayErrorDetails.Merge(m, src) } func (m *GRPCGatewayError_GRPCGatewayErrorDetails) XXX_Size() int { return m.Size() } func (m *GRPCGatewayError_GRPCGatewayErrorDetails) XXX_DiscardUnknown() { xxx_messageInfo_GRPCGatewayError_GRPCGatewayErrorDetails.DiscardUnknown(m) } var xxx_messageInfo_GRPCGatewayError_GRPCGatewayErrorDetails proto.InternalMessageInfo func (m *GRPCGatewayError_GRPCGatewayErrorDetails) GetGrpcCode() int32 { if m != nil { return m.GrpcCode } return 0 } func (m *GRPCGatewayError_GRPCGatewayErrorDetails) GetHttpCode() int32 { if m != nil { return m.HttpCode } return 0 } func (m *GRPCGatewayError_GRPCGatewayErrorDetails) GetMessage() string { if m != nil { return m.Message } return "" } func (m *GRPCGatewayError_GRPCGatewayErrorDetails) GetHttpStatus() string { if m != nil { return m.HttpStatus } return "" } // GRPCGatewayWrapper wraps streaming responses from GetTrace/FindTraces for HTTP. // Today there is always only one response because internally the HTTP server gets // data from QueryService that does not support multiple responses. But in the // future the server may return multiple responeses using Transfer-Encoding: chunked. // In case of errors, GRPCGatewayError above is used. // // Example: // {"result": {"resourceSpans": ...}} // // See https://github.com/grpc-ecosystem/grpc-gateway/issues/2189 // type GRPCGatewayWrapper struct { Result *v1.TracesData `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GRPCGatewayWrapper) Reset() { *m = GRPCGatewayWrapper{} } func (m *GRPCGatewayWrapper) String() string { return proto.CompactTextString(m) } func (*GRPCGatewayWrapper) ProtoMessage() {} func (*GRPCGatewayWrapper) Descriptor() ([]byte, []int) { return fileDescriptor_5fcb6756dc1afb8d, []int{9} } func (m *GRPCGatewayWrapper) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GRPCGatewayWrapper) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GRPCGatewayWrapper.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GRPCGatewayWrapper) XXX_Merge(src proto.Message) { xxx_messageInfo_GRPCGatewayWrapper.Merge(m, src) } func (m *GRPCGatewayWrapper) XXX_Size() int { return m.Size() } func (m *GRPCGatewayWrapper) XXX_DiscardUnknown() { xxx_messageInfo_GRPCGatewayWrapper.DiscardUnknown(m) } var xxx_messageInfo_GRPCGatewayWrapper proto.InternalMessageInfo func (m *GRPCGatewayWrapper) GetResult() *v1.TracesData { if m != nil { return m.Result } return nil } func init() { proto.RegisterType((*GetTraceRequest)(nil), "jaeger.api_v3.GetTraceRequest") proto.RegisterType((*TraceQueryParameters)(nil), "jaeger.api_v3.TraceQueryParameters") proto.RegisterMapType((map[string]string)(nil), "jaeger.api_v3.TraceQueryParameters.AttributesEntry") proto.RegisterType((*FindTracesRequest)(nil), "jaeger.api_v3.FindTracesRequest") proto.RegisterType((*GetServicesRequest)(nil), "jaeger.api_v3.GetServicesRequest") proto.RegisterType((*GetServicesResponse)(nil), "jaeger.api_v3.GetServicesResponse") proto.RegisterType((*GetOperationsRequest)(nil), "jaeger.api_v3.GetOperationsRequest") proto.RegisterType((*Operation)(nil), "jaeger.api_v3.Operation") proto.RegisterType((*GetOperationsResponse)(nil), "jaeger.api_v3.GetOperationsResponse") proto.RegisterType((*GRPCGatewayError)(nil), "jaeger.api_v3.GRPCGatewayError") proto.RegisterType((*GRPCGatewayError_GRPCGatewayErrorDetails)(nil), "jaeger.api_v3.GRPCGatewayError.GRPCGatewayErrorDetails") proto.RegisterType((*GRPCGatewayWrapper)(nil), "jaeger.api_v3.GRPCGatewayWrapper") } func init() { proto.RegisterFile("query_service.proto", fileDescriptor_5fcb6756dc1afb8d) } var fileDescriptor_5fcb6756dc1afb8d = []byte{ // 871 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x55, 0xdd, 0x72, 0xdb, 0x44, 0x14, 0x8e, 0xec, 0x38, 0xb1, 0x8e, 0x93, 0xb6, 0x6c, 0xcd, 0x54, 0x15, 0x83, 0xe3, 0xaa, 0x30, 0xe3, 0x1b, 0x64, 0xe2, 0x5c, 0x50, 0x18, 0x18, 0xa0, 0x49, 0xeb, 0x01, 0x26, 0xa5, 0x55, 0x3a, 0x85, 0x61, 0x3a, 0xa3, 0xd9, 0x44, 0x07, 0x47, 0xd4, 0x96, 0xd4, 0xdd, 0x95, 0x63, 0x3f, 0x03, 0x37, 0x5c, 0xf2, 0x48, 0xbd, 0x84, 0x17, 0x28, 0x4c, 0x5e, 0x80, 0x17, 0xe0, 0x82, 0xd9, 0x1f, 0xa9, 0xb6, 0x4c, 0x33, 0x69, 0xaf, 0xbc, 0xe7, 0xec, 0x77, 0x3e, 0x9f, 0x9f, 0xef, 0xac, 0xe0, 0xfa, 0xf3, 0x1c, 0xd9, 0x3c, 0xe4, 0xc8, 0xa6, 0xf1, 0x09, 0xfa, 0x19, 0x4b, 0x45, 0x4a, 0xb6, 0x7f, 0xa1, 0x38, 0x42, 0xe6, 0xd3, 0x2c, 0x0e, 0xa7, 0x7b, 0x6e, 0x67, 0x94, 0xa6, 0xa3, 0x31, 0xf6, 0xd5, 0xe5, 0x71, 0xfe, 0x73, 0x3f, 0xca, 0x19, 0x15, 0x71, 0x9a, 0x68, 0xb8, 0xdb, 0x1e, 0xa5, 0xa3, 0x54, 0x1d, 0xfb, 0xf2, 0x64, 0xbc, 0x3b, 0xd5, 0x28, 0x11, 0x4f, 0x90, 0x0b, 0x3a, 0xc9, 0x0c, 0xa0, 0x97, 0x66, 0x98, 0x08, 0x1c, 0xe3, 0x04, 0x05, 0x9b, 0x6b, 0x5c, 0x5f, 0x30, 0x7a, 0x82, 0xfd, 0xe9, 0xae, 0x3e, 0x68, 0xa4, 0xf7, 0xa7, 0x05, 0x57, 0x87, 0x28, 0x1e, 0x4b, 0x57, 0x80, 0xcf, 0x73, 0xe4, 0x82, 0xdc, 0x84, 0xa6, 0x82, 0x84, 0x71, 0xe4, 0x58, 0x5d, 0xab, 0x67, 0x07, 0x9b, 0xca, 0xfe, 0x26, 0x22, 0xfb, 0x00, 0x5c, 0x50, 0x26, 0x42, 0xf9, 0x8f, 0x4e, 0xad, 0x6b, 0xf5, 0x5a, 0x03, 0xd7, 0xd7, 0xe9, 0xf8, 0x45, 0x3a, 0xfe, 0xe3, 0x22, 0x9d, 0xbb, 0xcd, 0x17, 0x2f, 0x77, 0xd6, 0x7e, 0xfb, 0x6b, 0xc7, 0x0a, 0x6c, 0x15, 0x27, 0x6f, 0xc8, 0x97, 0xd0, 0xc4, 0x24, 0xd2, 0x14, 0xf5, 0x37, 0xa0, 0xd8, 0xc4, 0x24, 0x52, 0x04, 0xef, 0x03, 0x30, 0x7a, 0x16, 0xaa, 0xa4, 0xb8, 0xb3, 0xde, 0xb5, 0x7a, 0xcd, 0xc0, 0x66, 0xf4, 0x4c, 0x55, 0xc1, 0xbd, 0x97, 0xeb, 0xd0, 0x56, 0xc7, 0x47, 0x72, 0x00, 0x0f, 0x29, 0xa3, 0x13, 0x14, 0xc8, 0x38, 0xb9, 0x05, 0x5b, 0x66, 0x1a, 0x61, 0x42, 0x27, 0x68, 0x8a, 0x6b, 0x19, 0xdf, 0x03, 0x3a, 0x41, 0xf2, 0x21, 0x5c, 0x49, 0x33, 0xd4, 0x33, 0xd0, 0xa0, 0x9a, 0x02, 0x6d, 0x97, 0x5e, 0x05, 0x3b, 0x02, 0xa0, 0x42, 0xb0, 0xf8, 0x38, 0x17, 0xc8, 0x9d, 0x7a, 0xb7, 0xde, 0x6b, 0x0d, 0xf6, 0xfc, 0xa5, 0xd9, 0xfa, 0xff, 0x97, 0x82, 0xff, 0x75, 0x19, 0x75, 0x2f, 0x11, 0x6c, 0x1e, 0x2c, 0xd0, 0x90, 0x6f, 0xe1, 0xca, 0xab, 0xe6, 0x86, 0x93, 0x38, 0x51, 0xa5, 0x5d, 0xb6, 0x3b, 0x5b, 0x65, 0x83, 0x0f, 0xe3, 0xa4, 0xca, 0x45, 0x67, 0x4e, 0xe3, 0xed, 0xb8, 0xe8, 0x8c, 0xdc, 0x87, 0xad, 0x42, 0x96, 0x2a, 0xab, 0x0d, 0xc5, 0x74, 0x73, 0x85, 0xe9, 0xc0, 0x80, 0x34, 0xd1, 0xef, 0x92, 0xa8, 0x55, 0x04, 0xca, 0x9c, 0x96, 0x78, 0xe8, 0xcc, 0xd9, 0x7c, 0x1b, 0x1e, 0x3a, 0xd3, 0x63, 0xa4, 0xec, 0xe4, 0x34, 0x8c, 0x30, 0x13, 0xa7, 0x4e, 0xb3, 0x6b, 0xf5, 0x1a, 0x72, 0x8c, 0xd2, 0x77, 0x20, 0x5d, 0x15, 0x85, 0xd8, 0x15, 0x85, 0xb8, 0x5f, 0xc0, 0xd5, 0xca, 0x20, 0xc8, 0x35, 0xa8, 0x3f, 0xc3, 0xb9, 0x91, 0x84, 0x3c, 0x92, 0x36, 0x34, 0xa6, 0x74, 0x9c, 0x17, 0x0a, 0xd0, 0xc6, 0x67, 0xb5, 0x3b, 0x96, 0xf7, 0x00, 0xde, 0xb9, 0x1f, 0x27, 0x91, 0x26, 0x2b, 0xb6, 0xe6, 0x53, 0x68, 0xa8, 0x85, 0x57, 0x14, 0xad, 0xc1, 0xed, 0x4b, 0xa8, 0x21, 0xd0, 0x11, 0x5e, 0x1b, 0xc8, 0x10, 0xc5, 0x91, 0x96, 0x61, 0x41, 0xe8, 0xed, 0xc2, 0xf5, 0x25, 0x2f, 0xcf, 0xd2, 0x84, 0x23, 0x71, 0xa1, 0x69, 0x04, 0xcb, 0x1d, 0xab, 0x5b, 0xef, 0xd9, 0x41, 0x69, 0x7b, 0x87, 0xd0, 0x1e, 0xa2, 0xf8, 0xbe, 0x90, 0x6a, 0x99, 0x9b, 0x03, 0x9b, 0x06, 0x53, 0x2c, 0xb4, 0x31, 0xc9, 0x7b, 0x60, 0xf3, 0x8c, 0x26, 0xe1, 0xb3, 0x38, 0x89, 0x4c, 0xa1, 0x4d, 0xe9, 0xf8, 0x2e, 0x4e, 0x22, 0xef, 0x73, 0xb0, 0x4b, 0x2e, 0x42, 0x60, 0x7d, 0x61, 0x69, 0xd4, 0xf9, 0xe2, 0xe8, 0x47, 0xf0, 0x6e, 0x25, 0x19, 0x53, 0xc1, 0x1d, 0x80, 0x72, 0x9b, 0x74, 0x0d, 0xad, 0x81, 0x53, 0x69, 0x57, 0x19, 0x16, 0x2c, 0x60, 0xbd, 0x7f, 0x2c, 0xb8, 0x36, 0x0c, 0x1e, 0xee, 0x0f, 0xa9, 0xc0, 0x33, 0x3a, 0xbf, 0xc7, 0x58, 0xca, 0xc8, 0x21, 0x34, 0x50, 0x1e, 0x4c, 0xe3, 0x3f, 0xa9, 0x30, 0x55, 0xf1, 0x2b, 0x8e, 0x03, 0x14, 0x34, 0x1e, 0xf3, 0x40, 0xb3, 0xb8, 0xbf, 0x5a, 0x70, 0xe3, 0x35, 0x10, 0xd9, 0xfb, 0x11, 0xcb, 0x4e, 0xf6, 0xd3, 0x48, 0xf7, 0xa1, 0x11, 0x94, 0xb6, 0xbc, 0x3b, 0x15, 0x22, 0x53, 0x77, 0x35, 0x7d, 0x57, 0xd8, 0xb2, 0xff, 0x13, 0xe4, 0x9c, 0x8e, 0xf4, 0x83, 0x67, 0x07, 0x85, 0x49, 0x3a, 0x00, 0x12, 0x75, 0x24, 0xa8, 0xc8, 0xf5, 0x53, 0x66, 0x07, 0x0b, 0x1e, 0xef, 0x09, 0x90, 0x85, 0x64, 0x7e, 0x60, 0x34, 0xcb, 0x90, 0x91, 0xaf, 0x60, 0x83, 0x21, 0xcf, 0xc7, 0xc2, 0xd4, 0xdc, 0xf3, 0x97, 0x1e, 0x7c, 0xbd, 0x4a, 0xbe, 0x7e, 0xe7, 0xa7, 0xbb, 0x5a, 0x7b, 0xfc, 0x80, 0x0a, 0x1a, 0x98, 0xb8, 0xc1, 0xbf, 0x35, 0xd8, 0x52, 0x6a, 0x34, 0xfa, 0x22, 0x3f, 0x42, 0xb3, 0xf8, 0x0e, 0x90, 0x4e, 0xb5, 0x85, 0xcb, 0x1f, 0x08, 0xf7, 0xd2, 0x7f, 0xe7, 0xad, 0x7d, 0x6c, 0x91, 0xa7, 0x00, 0xaf, 0xb6, 0x85, 0x74, 0x2b, 0xdc, 0x2b, 0x8b, 0xf4, 0x86, 0xec, 0x4f, 0xa0, 0xb5, 0xb0, 0x25, 0xe4, 0xd6, 0x6a, 0xea, 0x95, 0xbd, 0x72, 0xbd, 0x8b, 0x20, 0x5a, 0xa2, 0xde, 0x1a, 0x79, 0x0a, 0xdb, 0x4b, 0xea, 0x25, 0xb7, 0x57, 0xc3, 0x56, 0x16, 0xcd, 0xfd, 0xe0, 0x62, 0x50, 0xc1, 0x7e, 0xf7, 0xa3, 0x17, 0xe7, 0x1d, 0xeb, 0x8f, 0xf3, 0x8e, 0xf5, 0xf7, 0x79, 0xc7, 0x82, 0x1b, 0x71, 0x6a, 0xe2, 0x64, 0x95, 0x71, 0x32, 0x32, 0xe1, 0x3f, 0x6d, 0xe8, 0xdf, 0xe3, 0x0d, 0xd5, 0x83, 0xbd, 0xff, 0x02, 0x00, 0x00, 0xff, 0xff, 0x1f, 0x1c, 0x84, 0x3f, 0x53, 0x08, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. var _ context.Context var _ grpc.ClientConn // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. const _ = grpc.SupportPackageIsVersion4 // QueryServiceClient is the client API for QueryService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type QueryServiceClient interface { // GetTrace returns a single trace. // Note that the JSON response over HTTP is wrapped into result envelope "{"result": ...}" // It means that the JSON response cannot be directly unmarshalled using JSONPb. // This can be fixed by first parsing into user-defined envelope with standard JSON library // or string manipulation to remove the envelope. Alternatively generate objects using OpenAPI. GetTrace(ctx context.Context, in *GetTraceRequest, opts ...grpc.CallOption) (QueryService_GetTraceClient, error) // FindTraces searches for traces. // See GetTrace for JSON unmarshalling. FindTraces(ctx context.Context, in *FindTracesRequest, opts ...grpc.CallOption) (QueryService_FindTracesClient, error) // GetServices returns service names. GetServices(ctx context.Context, in *GetServicesRequest, opts ...grpc.CallOption) (*GetServicesResponse, error) // GetOperations returns operation names. GetOperations(ctx context.Context, in *GetOperationsRequest, opts ...grpc.CallOption) (*GetOperationsResponse, error) } type queryServiceClient struct { cc *grpc.ClientConn } func NewQueryServiceClient(cc *grpc.ClientConn) QueryServiceClient { return &queryServiceClient{cc} } func (c *queryServiceClient) GetTrace(ctx context.Context, in *GetTraceRequest, opts ...grpc.CallOption) (QueryService_GetTraceClient, error) { stream, err := c.cc.NewStream(ctx, &_QueryService_serviceDesc.Streams[0], "/jaeger.api_v3.QueryService/GetTrace", opts...) if err != nil { return nil, err } x := &queryServiceGetTraceClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type QueryService_GetTraceClient interface { Recv() (*v1.TracesData, error) grpc.ClientStream } type queryServiceGetTraceClient struct { grpc.ClientStream } func (x *queryServiceGetTraceClient) Recv() (*v1.TracesData, error) { m := new(v1.TracesData) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *queryServiceClient) FindTraces(ctx context.Context, in *FindTracesRequest, opts ...grpc.CallOption) (QueryService_FindTracesClient, error) { stream, err := c.cc.NewStream(ctx, &_QueryService_serviceDesc.Streams[1], "/jaeger.api_v3.QueryService/FindTraces", opts...) if err != nil { return nil, err } x := &queryServiceFindTracesClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type QueryService_FindTracesClient interface { Recv() (*v1.TracesData, error) grpc.ClientStream } type queryServiceFindTracesClient struct { grpc.ClientStream } func (x *queryServiceFindTracesClient) Recv() (*v1.TracesData, error) { m := new(v1.TracesData) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *queryServiceClient) GetServices(ctx context.Context, in *GetServicesRequest, opts ...grpc.CallOption) (*GetServicesResponse, error) { out := new(GetServicesResponse) err := c.cc.Invoke(ctx, "/jaeger.api_v3.QueryService/GetServices", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *queryServiceClient) GetOperations(ctx context.Context, in *GetOperationsRequest, opts ...grpc.CallOption) (*GetOperationsResponse, error) { out := new(GetOperationsResponse) err := c.cc.Invoke(ctx, "/jaeger.api_v3.QueryService/GetOperations", in, out, opts...) if err != nil { return nil, err } return out, nil } // QueryServiceServer is the server API for QueryService service. type QueryServiceServer interface { // GetTrace returns a single trace. // Note that the JSON response over HTTP is wrapped into result envelope "{"result": ...}" // It means that the JSON response cannot be directly unmarshalled using JSONPb. // This can be fixed by first parsing into user-defined envelope with standard JSON library // or string manipulation to remove the envelope. Alternatively generate objects using OpenAPI. GetTrace(*GetTraceRequest, QueryService_GetTraceServer) error // FindTraces searches for traces. // See GetTrace for JSON unmarshalling. FindTraces(*FindTracesRequest, QueryService_FindTracesServer) error // GetServices returns service names. GetServices(context.Context, *GetServicesRequest) (*GetServicesResponse, error) // GetOperations returns operation names. GetOperations(context.Context, *GetOperationsRequest) (*GetOperationsResponse, error) } // UnimplementedQueryServiceServer can be embedded to have forward compatible implementations. type UnimplementedQueryServiceServer struct { } func (*UnimplementedQueryServiceServer) GetTrace(req *GetTraceRequest, srv QueryService_GetTraceServer) error { return status.Errorf(codes.Unimplemented, "method GetTrace not implemented") } func (*UnimplementedQueryServiceServer) FindTraces(req *FindTracesRequest, srv QueryService_FindTracesServer) error { return status.Errorf(codes.Unimplemented, "method FindTraces not implemented") } func (*UnimplementedQueryServiceServer) GetServices(ctx context.Context, req *GetServicesRequest) (*GetServicesResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetServices not implemented") } func (*UnimplementedQueryServiceServer) GetOperations(ctx context.Context, req *GetOperationsRequest) (*GetOperationsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetOperations not implemented") } func RegisterQueryServiceServer(s *grpc.Server, srv QueryServiceServer) { s.RegisterService(&_QueryService_serviceDesc, srv) } func _QueryService_GetTrace_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(GetTraceRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(QueryServiceServer).GetTrace(m, &queryServiceGetTraceServer{stream}) } type QueryService_GetTraceServer interface { Send(*v1.TracesData) error grpc.ServerStream } type queryServiceGetTraceServer struct { grpc.ServerStream } func (x *queryServiceGetTraceServer) Send(m *v1.TracesData) error { return x.ServerStream.SendMsg(m) } func _QueryService_FindTraces_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(FindTracesRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(QueryServiceServer).FindTraces(m, &queryServiceFindTracesServer{stream}) } type QueryService_FindTracesServer interface { Send(*v1.TracesData) error grpc.ServerStream } type queryServiceFindTracesServer struct { grpc.ServerStream } func (x *queryServiceFindTracesServer) Send(m *v1.TracesData) error { return x.ServerStream.SendMsg(m) } func _QueryService_GetServices_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetServicesRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(QueryServiceServer).GetServices(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/jaeger.api_v3.QueryService/GetServices", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(QueryServiceServer).GetServices(ctx, req.(*GetServicesRequest)) } return interceptor(ctx, in, info, handler) } func _QueryService_GetOperations_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetOperationsRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(QueryServiceServer).GetOperations(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/jaeger.api_v3.QueryService/GetOperations", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(QueryServiceServer).GetOperations(ctx, req.(*GetOperationsRequest)) } return interceptor(ctx, in, info, handler) } var _QueryService_serviceDesc = grpc.ServiceDesc{ ServiceName: "jaeger.api_v3.QueryService", HandlerType: (*QueryServiceServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "GetServices", Handler: _QueryService_GetServices_Handler, }, { MethodName: "GetOperations", Handler: _QueryService_GetOperations_Handler, }, }, Streams: []grpc.StreamDesc{ { StreamName: "GetTrace", Handler: _QueryService_GetTrace_Handler, ServerStreams: true, }, { StreamName: "FindTraces", Handler: _QueryService_FindTraces_Handler, ServerStreams: true, }, }, Metadata: "query_service.proto", } func (m *GetTraceRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GetTraceRequest) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GetTraceRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if m.RawTraces { i-- if m.RawTraces { dAtA[i] = 1 } else { dAtA[i] = 0 } i-- dAtA[i] = 0x20 } n1, err1 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.EndTime, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.EndTime):]) if err1 != nil { return 0, err1 } i -= n1 i = encodeVarintQueryService(dAtA, i, uint64(n1)) i-- dAtA[i] = 0x1a n2, err2 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.StartTime, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTime):]) if err2 != nil { return 0, err2 } i -= n2 i = encodeVarintQueryService(dAtA, i, uint64(n2)) i-- dAtA[i] = 0x12 if len(m.TraceId) > 0 { i -= len(m.TraceId) copy(dAtA[i:], m.TraceId) i = encodeVarintQueryService(dAtA, i, uint64(len(m.TraceId))) i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *TraceQueryParameters) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *TraceQueryParameters) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *TraceQueryParameters) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if m.RawTraces { i-- if m.RawTraces { dAtA[i] = 1 } else { dAtA[i] = 0 } i-- dAtA[i] = 0x48 } if m.SearchDepth != 0 { i = encodeVarintQueryService(dAtA, i, uint64(m.SearchDepth)) i-- dAtA[i] = 0x40 } n3, err3 := github_com_gogo_protobuf_types.StdDurationMarshalTo(m.DurationMax, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdDuration(m.DurationMax):]) if err3 != nil { return 0, err3 } i -= n3 i = encodeVarintQueryService(dAtA, i, uint64(n3)) i-- dAtA[i] = 0x3a n4, err4 := github_com_gogo_protobuf_types.StdDurationMarshalTo(m.DurationMin, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdDuration(m.DurationMin):]) if err4 != nil { return 0, err4 } i -= n4 i = encodeVarintQueryService(dAtA, i, uint64(n4)) i-- dAtA[i] = 0x32 n5, err5 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.StartTimeMax, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTimeMax):]) if err5 != nil { return 0, err5 } i -= n5 i = encodeVarintQueryService(dAtA, i, uint64(n5)) i-- dAtA[i] = 0x2a n6, err6 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.StartTimeMin, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTimeMin):]) if err6 != nil { return 0, err6 } i -= n6 i = encodeVarintQueryService(dAtA, i, uint64(n6)) i-- dAtA[i] = 0x22 if len(m.Attributes) > 0 { for k := range m.Attributes { v := m.Attributes[k] baseI := i i -= len(v) copy(dAtA[i:], v) i = encodeVarintQueryService(dAtA, i, uint64(len(v))) i-- dAtA[i] = 0x12 i -= len(k) copy(dAtA[i:], k) i = encodeVarintQueryService(dAtA, i, uint64(len(k))) i-- dAtA[i] = 0xa i = encodeVarintQueryService(dAtA, i, uint64(baseI-i)) i-- dAtA[i] = 0x1a } } if len(m.OperationName) > 0 { i -= len(m.OperationName) copy(dAtA[i:], m.OperationName) i = encodeVarintQueryService(dAtA, i, uint64(len(m.OperationName))) i-- dAtA[i] = 0x12 } if len(m.ServiceName) > 0 { i -= len(m.ServiceName) copy(dAtA[i:], m.ServiceName) i = encodeVarintQueryService(dAtA, i, uint64(len(m.ServiceName))) i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *FindTracesRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *FindTracesRequest) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *FindTracesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if m.Query != nil { { size, err := m.Query.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintQueryService(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *GetServicesRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GetServicesRequest) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GetServicesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } return len(dAtA) - i, nil } func (m *GetServicesResponse) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GetServicesResponse) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GetServicesResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.Services) > 0 { for iNdEx := len(m.Services) - 1; iNdEx >= 0; iNdEx-- { i -= len(m.Services[iNdEx]) copy(dAtA[i:], m.Services[iNdEx]) i = encodeVarintQueryService(dAtA, i, uint64(len(m.Services[iNdEx]))) i-- dAtA[i] = 0xa } } return len(dAtA) - i, nil } func (m *GetOperationsRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GetOperationsRequest) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GetOperationsRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.SpanKind) > 0 { i -= len(m.SpanKind) copy(dAtA[i:], m.SpanKind) i = encodeVarintQueryService(dAtA, i, uint64(len(m.SpanKind))) i-- dAtA[i] = 0x12 } if len(m.Service) > 0 { i -= len(m.Service) copy(dAtA[i:], m.Service) i = encodeVarintQueryService(dAtA, i, uint64(len(m.Service))) i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *Operation) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *Operation) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *Operation) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.SpanKind) > 0 { i -= len(m.SpanKind) copy(dAtA[i:], m.SpanKind) i = encodeVarintQueryService(dAtA, i, uint64(len(m.SpanKind))) i-- dAtA[i] = 0x12 } if len(m.Name) > 0 { i -= len(m.Name) copy(dAtA[i:], m.Name) i = encodeVarintQueryService(dAtA, i, uint64(len(m.Name))) i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *GetOperationsResponse) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GetOperationsResponse) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GetOperationsResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.Operations) > 0 { for iNdEx := len(m.Operations) - 1; iNdEx >= 0; iNdEx-- { { size, err := m.Operations[iNdEx].MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintQueryService(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } } return len(dAtA) - i, nil } func (m *GRPCGatewayError) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GRPCGatewayError) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GRPCGatewayError) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if m.Error != nil { { size, err := m.Error.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintQueryService(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *GRPCGatewayError_GRPCGatewayErrorDetails) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GRPCGatewayError_GRPCGatewayErrorDetails) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GRPCGatewayError_GRPCGatewayErrorDetails) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.HttpStatus) > 0 { i -= len(m.HttpStatus) copy(dAtA[i:], m.HttpStatus) i = encodeVarintQueryService(dAtA, i, uint64(len(m.HttpStatus))) i-- dAtA[i] = 0x22 } if len(m.Message) > 0 { i -= len(m.Message) copy(dAtA[i:], m.Message) i = encodeVarintQueryService(dAtA, i, uint64(len(m.Message))) i-- dAtA[i] = 0x1a } if m.HttpCode != 0 { i = encodeVarintQueryService(dAtA, i, uint64(m.HttpCode)) i-- dAtA[i] = 0x10 } if m.GrpcCode != 0 { i = encodeVarintQueryService(dAtA, i, uint64(m.GrpcCode)) i-- dAtA[i] = 0x8 } return len(dAtA) - i, nil } func (m *GRPCGatewayWrapper) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GRPCGatewayWrapper) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GRPCGatewayWrapper) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if m.Result != nil { { size, err := m.Result.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintQueryService(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func encodeVarintQueryService(dAtA []byte, offset int, v uint64) int { offset -= sovQueryService(v) base := offset for v >= 1<<7 { dAtA[offset] = uint8(v&0x7f | 0x80) v >>= 7 offset++ } dAtA[offset] = uint8(v) return base } func (m *GetTraceRequest) Size() (n int) { if m == nil { return 0 } var l int _ = l l = len(m.TraceId) if l > 0 { n += 1 + l + sovQueryService(uint64(l)) } l = github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTime) n += 1 + l + sovQueryService(uint64(l)) l = github_com_gogo_protobuf_types.SizeOfStdTime(m.EndTime) n += 1 + l + sovQueryService(uint64(l)) if m.RawTraces { n += 2 } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *TraceQueryParameters) Size() (n int) { if m == nil { return 0 } var l int _ = l l = len(m.ServiceName) if l > 0 { n += 1 + l + sovQueryService(uint64(l)) } l = len(m.OperationName) if l > 0 { n += 1 + l + sovQueryService(uint64(l)) } if len(m.Attributes) > 0 { for k, v := range m.Attributes { _ = k _ = v mapEntrySize := 1 + len(k) + sovQueryService(uint64(len(k))) + 1 + len(v) + sovQueryService(uint64(len(v))) n += mapEntrySize + 1 + sovQueryService(uint64(mapEntrySize)) } } l = github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTimeMin) n += 1 + l + sovQueryService(uint64(l)) l = github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTimeMax) n += 1 + l + sovQueryService(uint64(l)) l = github_com_gogo_protobuf_types.SizeOfStdDuration(m.DurationMin) n += 1 + l + sovQueryService(uint64(l)) l = github_com_gogo_protobuf_types.SizeOfStdDuration(m.DurationMax) n += 1 + l + sovQueryService(uint64(l)) if m.SearchDepth != 0 { n += 1 + sovQueryService(uint64(m.SearchDepth)) } if m.RawTraces { n += 2 } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *FindTracesRequest) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.Query != nil { l = m.Query.Size() n += 1 + l + sovQueryService(uint64(l)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *GetServicesRequest) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *GetServicesResponse) Size() (n int) { if m == nil { return 0 } var l int _ = l if len(m.Services) > 0 { for _, s := range m.Services { l = len(s) n += 1 + l + sovQueryService(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *GetOperationsRequest) Size() (n int) { if m == nil { return 0 } var l int _ = l l = len(m.Service) if l > 0 { n += 1 + l + sovQueryService(uint64(l)) } l = len(m.SpanKind) if l > 0 { n += 1 + l + sovQueryService(uint64(l)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *Operation) Size() (n int) { if m == nil { return 0 } var l int _ = l l = len(m.Name) if l > 0 { n += 1 + l + sovQueryService(uint64(l)) } l = len(m.SpanKind) if l > 0 { n += 1 + l + sovQueryService(uint64(l)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *GetOperationsResponse) Size() (n int) { if m == nil { return 0 } var l int _ = l if len(m.Operations) > 0 { for _, e := range m.Operations { l = e.Size() n += 1 + l + sovQueryService(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *GRPCGatewayError) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.Error != nil { l = m.Error.Size() n += 1 + l + sovQueryService(uint64(l)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *GRPCGatewayError_GRPCGatewayErrorDetails) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.GrpcCode != 0 { n += 1 + sovQueryService(uint64(m.GrpcCode)) } if m.HttpCode != 0 { n += 1 + sovQueryService(uint64(m.HttpCode)) } l = len(m.Message) if l > 0 { n += 1 + l + sovQueryService(uint64(l)) } l = len(m.HttpStatus) if l > 0 { n += 1 + l + sovQueryService(uint64(l)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *GRPCGatewayWrapper) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.Result != nil { l = m.Result.Size() n += 1 + l + sovQueryService(uint64(l)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func sovQueryService(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } func sozQueryService(x uint64) (n int) { return sovQueryService(uint64((x << 1) ^ uint64((int64(x) >> 63)))) } func (m *GetTraceRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GetTraceRequest: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GetTraceRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field TraceId", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthQueryService } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthQueryService } if postIndex > l { return io.ErrUnexpectedEOF } m.TraceId = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field StartTime", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthQueryService } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthQueryService } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.StartTime, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field EndTime", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthQueryService } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthQueryService } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.EndTime, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 4: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field RawTraces", wireType) } var v int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= int(b&0x7F) << shift if b < 0x80 { break } } m.RawTraces = bool(v != 0) default: iNdEx = preIndex skippy, err := skipQueryService(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthQueryService } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *TraceQueryParameters) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: TraceQueryParameters: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: TraceQueryParameters: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field ServiceName", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthQueryService } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthQueryService } if postIndex > l { return io.ErrUnexpectedEOF } m.ServiceName = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field OperationName", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthQueryService } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthQueryService } if postIndex > l { return io.ErrUnexpectedEOF } m.OperationName = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Attributes", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthQueryService } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthQueryService } if postIndex > l { return io.ErrUnexpectedEOF } if m.Attributes == nil { m.Attributes = make(map[string]string) } var mapkey string var mapvalue string for iNdEx < postIndex { entryPreIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) if fieldNum == 1 { var stringLenmapkey uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLenmapkey |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLenmapkey := int(stringLenmapkey) if intStringLenmapkey < 0 { return ErrInvalidLengthQueryService } postStringIndexmapkey := iNdEx + intStringLenmapkey if postStringIndexmapkey < 0 { return ErrInvalidLengthQueryService } if postStringIndexmapkey > l { return io.ErrUnexpectedEOF } mapkey = string(dAtA[iNdEx:postStringIndexmapkey]) iNdEx = postStringIndexmapkey } else if fieldNum == 2 { var stringLenmapvalue uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLenmapvalue |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLenmapvalue := int(stringLenmapvalue) if intStringLenmapvalue < 0 { return ErrInvalidLengthQueryService } postStringIndexmapvalue := iNdEx + intStringLenmapvalue if postStringIndexmapvalue < 0 { return ErrInvalidLengthQueryService } if postStringIndexmapvalue > l { return io.ErrUnexpectedEOF } mapvalue = string(dAtA[iNdEx:postStringIndexmapvalue]) iNdEx = postStringIndexmapvalue } else { iNdEx = entryPreIndex skippy, err := skipQueryService(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthQueryService } if (iNdEx + skippy) > postIndex { return io.ErrUnexpectedEOF } iNdEx += skippy } } m.Attributes[mapkey] = mapvalue iNdEx = postIndex case 4: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field StartTimeMin", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthQueryService } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthQueryService } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.StartTimeMin, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 5: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field StartTimeMax", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthQueryService } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthQueryService } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.StartTimeMax, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 6: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field DurationMin", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthQueryService } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthQueryService } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdDurationUnmarshal(&m.DurationMin, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 7: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field DurationMax", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthQueryService } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthQueryService } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdDurationUnmarshal(&m.DurationMax, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 8: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field SearchDepth", wireType) } m.SearchDepth = 0 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ m.SearchDepth |= int32(b&0x7F) << shift if b < 0x80 { break } } case 9: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field RawTraces", wireType) } var v int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= int(b&0x7F) << shift if b < 0x80 { break } } m.RawTraces = bool(v != 0) default: iNdEx = preIndex skippy, err := skipQueryService(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthQueryService } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *FindTracesRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: FindTracesRequest: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: FindTracesRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Query", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthQueryService } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthQueryService } if postIndex > l { return io.ErrUnexpectedEOF } if m.Query == nil { m.Query = &TraceQueryParameters{} } if err := m.Query.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipQueryService(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthQueryService } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *GetServicesRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GetServicesRequest: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GetServicesRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { default: iNdEx = preIndex skippy, err := skipQueryService(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthQueryService } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *GetServicesResponse) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GetServicesResponse: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GetServicesResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Services", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthQueryService } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthQueryService } if postIndex > l { return io.ErrUnexpectedEOF } m.Services = append(m.Services, string(dAtA[iNdEx:postIndex])) iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipQueryService(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthQueryService } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *GetOperationsRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GetOperationsRequest: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GetOperationsRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Service", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthQueryService } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthQueryService } if postIndex > l { return io.ErrUnexpectedEOF } m.Service = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field SpanKind", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthQueryService } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthQueryService } if postIndex > l { return io.ErrUnexpectedEOF } m.SpanKind = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipQueryService(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthQueryService } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *Operation) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: Operation: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: Operation: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthQueryService } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthQueryService } if postIndex > l { return io.ErrUnexpectedEOF } m.Name = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field SpanKind", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthQueryService } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthQueryService } if postIndex > l { return io.ErrUnexpectedEOF } m.SpanKind = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipQueryService(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthQueryService } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *GetOperationsResponse) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GetOperationsResponse: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GetOperationsResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Operations", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthQueryService } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthQueryService } if postIndex > l { return io.ErrUnexpectedEOF } m.Operations = append(m.Operations, &Operation{}) if err := m.Operations[len(m.Operations)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipQueryService(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthQueryService } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *GRPCGatewayError) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GRPCGatewayError: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GRPCGatewayError: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Error", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthQueryService } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthQueryService } if postIndex > l { return io.ErrUnexpectedEOF } if m.Error == nil { m.Error = &GRPCGatewayError_GRPCGatewayErrorDetails{} } if err := m.Error.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipQueryService(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthQueryService } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *GRPCGatewayError_GRPCGatewayErrorDetails) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GRPCGatewayErrorDetails: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GRPCGatewayErrorDetails: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field GrpcCode", wireType) } m.GrpcCode = 0 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ m.GrpcCode |= int32(b&0x7F) << shift if b < 0x80 { break } } case 2: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field HttpCode", wireType) } m.HttpCode = 0 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ m.HttpCode |= int32(b&0x7F) << shift if b < 0x80 { break } } case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Message", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthQueryService } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthQueryService } if postIndex > l { return io.ErrUnexpectedEOF } m.Message = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 4: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field HttpStatus", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthQueryService } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthQueryService } if postIndex > l { return io.ErrUnexpectedEOF } m.HttpStatus = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipQueryService(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthQueryService } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *GRPCGatewayWrapper) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GRPCGatewayWrapper: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GRPCGatewayWrapper: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Result", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowQueryService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthQueryService } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthQueryService } if postIndex > l { return io.ErrUnexpectedEOF } if m.Result == nil { m.Result = &v1.TracesData{} } if err := m.Result.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipQueryService(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthQueryService } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func skipQueryService(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 depth := 0 for iNdEx < l { var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowQueryService } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } wireType := int(wire & 0x7) switch wireType { case 0: for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowQueryService } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } iNdEx++ if dAtA[iNdEx-1] < 0x80 { break } } case 1: iNdEx += 8 case 2: var length int for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowQueryService } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ length |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if length < 0 { return 0, ErrInvalidLengthQueryService } iNdEx += length case 3: depth++ case 4: if depth == 0 { return 0, ErrUnexpectedEndOfGroupQueryService } depth-- case 5: iNdEx += 4 default: return 0, fmt.Errorf("proto: illegal wireType %d", wireType) } if iNdEx < 0 { return 0, ErrInvalidLengthQueryService } if depth == 0 { return iNdEx, nil } } return 0, io.ErrUnexpectedEOF } var ( ErrInvalidLengthQueryService = fmt.Errorf("proto: negative length found during unmarshaling") ErrIntOverflowQueryService = fmt.Errorf("proto: integer overflow") ErrUnexpectedEndOfGroupQueryService = fmt.Errorf("proto: unexpected end of group") ) ================================================ FILE: internal/proto/metrics/README.md ================================================ # Metrics Query Service Defines the MetricsQueryService's set of APIs along with required data models. ## Overview Contained in this directory are a set of shared Protobuf data model definitions from https://github.com/OpenObservability/OpenMetrics, namely: - openmetrics.proto: OpenMetrics' data model. The reasons for adopting OpenMetrics' data model over OpenTelemetry's are: - OpenTelemetry is still changing, demonstrated by a recent copy of the data model being taken which became outdated after a few weeks. - OpenTelemetry is supposedly fully compatible with OpenMetrics by the end of the year. - OpenMetrics is the de-factor Prometheus format used by many backends already. - OpenMetrics is stable. - OpenTelemetry will support OpenTelemetry <-> OpenMetrics conversion in the future, so we would still be able to implement support for OpenTelemetry-native backends. Importing data models directly from the OpenObservability/OpenMetrics github repo (via a submodule) was considered and explored; however, without custom marshaling enabled, which is required for sending imported message types over the wire, errors such as the following result: `panic: invalid Go type v1.Metric for field jaeger.api_v2.GetMetricsResponse.Metrics` Enabling gogoproto's custom Marshal and Unmarshal methods to address the above issue result in compilation errors from the generated code as the referenced protobuf definition does not have gogoproto.marshaler_all, gogoproto.unmarshaler_all, etc. enabled. Moreover, if direct imports of other repositories were possible, it would mean importing and generating code for transitive dependencies not required by Jaeger leading to longer build times, and potentially larger container image sizes. Given the aforementioned limitations, selectively copying necessary messages and enums allow for: - Marshaling and unmarshaling of externally defined custom data models such as those from OpenMetrics. - Using Gogoproto's custom un/marshalers takes advantage of [reportedly faster marshaling and unmarshaling](https://github.com/cockroachdb/gogoproto/blob/master/extensions.md). - Avoiding unwanted dependencies leading to simpler proto definitions, faster build times and smaller image sizes. The key trade-offs are: - Synchronizing with the original source proto definition. - It is anticipated that the maintenance effort to synchronize data models will be minimal considering there is no direct dependency between Jaeger and OpenTelemetry in the context of querying metrics, with exception to `SpanKind`, and the existing data model more than satisfies existing metrics querying requirements. The OpenTelemetry metrics data model primarily serves as a carrier of metrics data, rather than a protocol of communication between Jaeger and OpenTelemetry components. ================================================ FILE: internal/proto/metrics/openmetrics.proto ================================================ // Copyright (c) 2021 The Jaeger Authors. // // 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. // This file is a copy of https://github.com/OpenObservability/OpenMetrics/blob/v1.0.0/proto/openmetrics_data_model.proto, // with the following additions (see README.md for more details): // // * Add import to gogoproto/gogo.proto. // * Add options defining the per-language generated code's packages. // * Add options enabling gogoproto un/marshal, required for sending imported message types over the wire. syntax="proto3"; // The OpenMetrics protobuf schema which defines the protobuf wire format. // Ensure to interpret "required" as semantically required for a valid message. // All string fields MUST be UTF-8 encoded strings. package jaeger.api_v2.metrics; import "google/protobuf/timestamp.proto"; // -- BEGIN ADDITIONS import "gogoproto/gogo.proto"; option go_package = "metrics"; option java_package = "io.jaegertracing.api_v2.metrics"; // Enable gogoprotobuf extensions (https://github.com/gogo/protobuf/blob/master/extensions.md). // Enable custom Marshal method. option (gogoproto.marshaler_all) = true; // Enable custom Unmarshal method. option (gogoproto.unmarshaler_all) = true; // Enable custom Size method (Required by Marshal and Unmarshal). option (gogoproto.sizer_all) = true; // -- END ADDITIONS // The top-level container type that is encoded and sent over the wire. message MetricSet { // Each MetricFamily has one or more MetricPoints for a single Metric. repeated MetricFamily metric_families = 1; } // One or more Metrics for a single MetricFamily, where each Metric // has one or more MetricPoints. message MetricFamily { // Required. string name = 1; // Optional. MetricType type = 2; // Optional. string unit = 3; // Optional. string help = 4; // Optional. repeated Metric metrics = 5; } // The type of a Metric. enum MetricType { // Unknown must use unknown MetricPoint values. UNKNOWN = 0; // Gauge must use gauge MetricPoint values. GAUGE = 1; // Counter must use counter MetricPoint values. COUNTER = 2; // State set must use state set MetricPoint values. STATE_SET = 3; // Info must use info MetricPoint values. INFO = 4; // Histogram must use histogram value MetricPoint values. HISTOGRAM = 5; // Gauge histogram must use histogram value MetricPoint values. GAUGE_HISTOGRAM = 6; // Summary quantiles must use summary value MetricPoint values. SUMMARY = 7; } // A single metric with a unique set of labels within a metric family. message Metric { // Optional. repeated Label labels = 1; // Optional. repeated MetricPoint metric_points = 2; } // A name-value pair. These are used in multiple places: identifying // timeseries, value of INFO metrics, and exemplars in Histograms. message Label { // Required. string name = 1; // Required. string value = 2; } // A MetricPoint in a Metric. message MetricPoint { // Required. oneof value { UnknownValue unknown_value = 1; GaugeValue gauge_value = 2; CounterValue counter_value = 3; HistogramValue histogram_value = 4; StateSetValue state_set_value = 5; InfoValue info_value = 6; SummaryValue summary_value = 7; } // Optional. google.protobuf.Timestamp timestamp = 8; } // Value for UNKNOWN MetricPoint. message UnknownValue { // Required. oneof value { double double_value = 1; int64 int_value = 2; } } // Value for GAUGE MetricPoint. message GaugeValue { // Required. oneof value { double double_value = 1; int64 int_value = 2; } } // Value for COUNTER MetricPoint. message CounterValue { // Required. oneof total { double double_value = 1; uint64 int_value = 2; } // The time values began being collected for this counter. // Optional. google.protobuf.Timestamp created = 3; // Optional. Exemplar exemplar = 4; } // Value for HISTOGRAM or GAUGE_HISTOGRAM MetricPoint. message HistogramValue { // Optional. oneof sum { double double_value = 1; int64 int_value = 2; } // Optional. uint64 count = 3; // The time values began being collected for this histogram. // Optional. google.protobuf.Timestamp created = 4; // Optional. repeated Bucket buckets = 5; // Bucket is the number of values for a bucket in the histogram // with an optional exemplar. message Bucket { // Required. uint64 count = 1; // Optional. double upper_bound = 2; // Optional. Exemplar exemplar = 3; } } message Exemplar { // Required. double value = 1; // Optional. google.protobuf.Timestamp timestamp = 2; // Labels are additional information about the exemplar value (e.g. trace id). // Optional. repeated Label label = 3; } // Value for STATE_SET MetricPoint. message StateSetValue { // Optional. repeated State states = 1; message State { // Required. bool enabled = 1; // Required. string name = 2; } } // Value for INFO MetricPoint. message InfoValue { // Optional. repeated Label info = 1; } // Value for SUMMARY MetricPoint. message SummaryValue { // Optional. oneof sum { double double_value = 1; int64 int_value = 2; } // Optional. uint64 count = 3; // The time sum and count values began being collected for this summary. // Optional. google.protobuf.Timestamp created = 4; // Optional. repeated Quantile quantile = 5; message Quantile { // Required. double quantile = 1; // Required. double value = 2; } } ================================================ FILE: internal/proto/metrics/otelspankind.proto ================================================ // Copyright (c) 2021 The Jaeger Authors. // // 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. // Based on: https://github.com/open-telemetry/opentelemetry-proto/blob/v0.8.0/opentelemetry/proto/trace/v1/trace.proto syntax="proto3"; package jaeger.api_v2.metrics; import "gogoproto/gogo.proto"; option go_package = "metrics"; option java_package = "io.jaegertracing.api_v2.metrics"; // Enable gogoprotobuf extensions (https://github.com/gogo/protobuf/blob/master/extensions.md). // Enable custom Marshal method. option (gogoproto.marshaler_all) = true; // Enable custom Unmarshal method. option (gogoproto.unmarshaler_all) = true; // Enable custom Size method (Required by Marshal and Unmarshal). option (gogoproto.sizer_all) = true; // SpanKind is the type of span. Can be used to specify additional relationships between spans // in addition to a parent/child relationship. enum SpanKind { // Unspecified. Do NOT use as default. // Implementations MAY assume SpanKind to be INTERNAL when receiving UNSPECIFIED. SPAN_KIND_UNSPECIFIED = 0; // Indicates that the span represents an internal operation within an application, // as opposed to an operation happening at the boundaries. Default value. SPAN_KIND_INTERNAL = 1; // Indicates that the span covers server-side handling of an RPC or other // remote network request. SPAN_KIND_SERVER = 2; // Indicates that the span describes a request to some remote service. SPAN_KIND_CLIENT = 3; // Indicates that the span describes a producer sending a message to a broker. // Unlike CLIENT and SERVER, there is often no direct critical path latency relationship // between producer and consumer spans. A PRODUCER span ends when the message was accepted // by the broker while the logical processing of the message might span a much longer time. SPAN_KIND_PRODUCER = 4; // Indicates that the span describes consumer receiving a message from a broker. // Like the PRODUCER kind, there is often no direct critical path latency relationship // between producer and consumer spans. SPAN_KIND_CONSUMER = 5; } ================================================ FILE: internal/proto-gen/.gitignore ================================================ .patched/ ================================================ FILE: internal/proto-gen/api_v2/metrics/openmetrics.pb.go ================================================ // Code generated by protoc-gen-gogo. DO NOT EDIT. // source: openmetrics.proto // The OpenMetrics protobuf schema which defines the protobuf wire format. // Ensure to interpret "required" as semantically required for a valid message. // All string fields MUST be UTF-8 encoded strings. package metrics import ( encoding_binary "encoding/binary" fmt "fmt" _ "github.com/gogo/protobuf/gogoproto" proto "github.com/gogo/protobuf/proto" types "github.com/gogo/protobuf/types" io "io" math "math" math_bits "math/bits" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package // The type of a Metric. type MetricType int32 const ( // Unknown must use unknown MetricPoint values. MetricType_UNKNOWN MetricType = 0 // Gauge must use gauge MetricPoint values. MetricType_GAUGE MetricType = 1 // Counter must use counter MetricPoint values. MetricType_COUNTER MetricType = 2 // State set must use state set MetricPoint values. MetricType_STATE_SET MetricType = 3 // Info must use info MetricPoint values. MetricType_INFO MetricType = 4 // Histogram must use histogram value MetricPoint values. MetricType_HISTOGRAM MetricType = 5 // Gauge histogram must use histogram value MetricPoint values. MetricType_GAUGE_HISTOGRAM MetricType = 6 // Summary quantiles must use summary value MetricPoint values. MetricType_SUMMARY MetricType = 7 ) var MetricType_name = map[int32]string{ 0: "UNKNOWN", 1: "GAUGE", 2: "COUNTER", 3: "STATE_SET", 4: "INFO", 5: "HISTOGRAM", 6: "GAUGE_HISTOGRAM", 7: "SUMMARY", } var MetricType_value = map[string]int32{ "UNKNOWN": 0, "GAUGE": 1, "COUNTER": 2, "STATE_SET": 3, "INFO": 4, "HISTOGRAM": 5, "GAUGE_HISTOGRAM": 6, "SUMMARY": 7, } func (x MetricType) String() string { return proto.EnumName(MetricType_name, int32(x)) } func (MetricType) EnumDescriptor() ([]byte, []int) { return fileDescriptor_0b803df83757ec01, []int{0} } // The top-level container type that is encoded and sent over the wire. type MetricSet struct { // Each MetricFamily has one or more MetricPoints for a single Metric. MetricFamilies []*MetricFamily `protobuf:"bytes,1,rep,name=metric_families,json=metricFamilies,proto3" json:"metric_families,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *MetricSet) Reset() { *m = MetricSet{} } func (m *MetricSet) String() string { return proto.CompactTextString(m) } func (*MetricSet) ProtoMessage() {} func (*MetricSet) Descriptor() ([]byte, []int) { return fileDescriptor_0b803df83757ec01, []int{0} } func (m *MetricSet) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *MetricSet) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_MetricSet.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *MetricSet) XXX_Merge(src proto.Message) { xxx_messageInfo_MetricSet.Merge(m, src) } func (m *MetricSet) XXX_Size() int { return m.Size() } func (m *MetricSet) XXX_DiscardUnknown() { xxx_messageInfo_MetricSet.DiscardUnknown(m) } var xxx_messageInfo_MetricSet proto.InternalMessageInfo func (m *MetricSet) GetMetricFamilies() []*MetricFamily { if m != nil { return m.MetricFamilies } return nil } // One or more Metrics for a single MetricFamily, where each Metric // has one or more MetricPoints. type MetricFamily struct { // Required. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Optional. Type MetricType `protobuf:"varint,2,opt,name=type,proto3,enum=jaeger.api_v2.metrics.MetricType" json:"type,omitempty"` // Optional. Unit string `protobuf:"bytes,3,opt,name=unit,proto3" json:"unit,omitempty"` // Optional. Help string `protobuf:"bytes,4,opt,name=help,proto3" json:"help,omitempty"` // Optional. Metrics []*Metric `protobuf:"bytes,5,rep,name=metrics,proto3" json:"metrics,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *MetricFamily) Reset() { *m = MetricFamily{} } func (m *MetricFamily) String() string { return proto.CompactTextString(m) } func (*MetricFamily) ProtoMessage() {} func (*MetricFamily) Descriptor() ([]byte, []int) { return fileDescriptor_0b803df83757ec01, []int{1} } func (m *MetricFamily) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *MetricFamily) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_MetricFamily.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *MetricFamily) XXX_Merge(src proto.Message) { xxx_messageInfo_MetricFamily.Merge(m, src) } func (m *MetricFamily) XXX_Size() int { return m.Size() } func (m *MetricFamily) XXX_DiscardUnknown() { xxx_messageInfo_MetricFamily.DiscardUnknown(m) } var xxx_messageInfo_MetricFamily proto.InternalMessageInfo func (m *MetricFamily) GetName() string { if m != nil { return m.Name } return "" } func (m *MetricFamily) GetType() MetricType { if m != nil { return m.Type } return MetricType_UNKNOWN } func (m *MetricFamily) GetUnit() string { if m != nil { return m.Unit } return "" } func (m *MetricFamily) GetHelp() string { if m != nil { return m.Help } return "" } func (m *MetricFamily) GetMetrics() []*Metric { if m != nil { return m.Metrics } return nil } // A single metric with a unique set of labels within a metric family. type Metric struct { // Optional. Labels []*Label `protobuf:"bytes,1,rep,name=labels,proto3" json:"labels,omitempty"` // Optional. MetricPoints []*MetricPoint `protobuf:"bytes,2,rep,name=metric_points,json=metricPoints,proto3" json:"metric_points,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *Metric) Reset() { *m = Metric{} } func (m *Metric) String() string { return proto.CompactTextString(m) } func (*Metric) ProtoMessage() {} func (*Metric) Descriptor() ([]byte, []int) { return fileDescriptor_0b803df83757ec01, []int{2} } func (m *Metric) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *Metric) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_Metric.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *Metric) XXX_Merge(src proto.Message) { xxx_messageInfo_Metric.Merge(m, src) } func (m *Metric) XXX_Size() int { return m.Size() } func (m *Metric) XXX_DiscardUnknown() { xxx_messageInfo_Metric.DiscardUnknown(m) } var xxx_messageInfo_Metric proto.InternalMessageInfo func (m *Metric) GetLabels() []*Label { if m != nil { return m.Labels } return nil } func (m *Metric) GetMetricPoints() []*MetricPoint { if m != nil { return m.MetricPoints } return nil } // A name-value pair. These are used in multiple places: identifying // timeseries, value of INFO metrics, and exemplars in Histograms. type Label struct { // Required. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Required. Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *Label) Reset() { *m = Label{} } func (m *Label) String() string { return proto.CompactTextString(m) } func (*Label) ProtoMessage() {} func (*Label) Descriptor() ([]byte, []int) { return fileDescriptor_0b803df83757ec01, []int{3} } func (m *Label) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *Label) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_Label.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *Label) XXX_Merge(src proto.Message) { xxx_messageInfo_Label.Merge(m, src) } func (m *Label) XXX_Size() int { return m.Size() } func (m *Label) XXX_DiscardUnknown() { xxx_messageInfo_Label.DiscardUnknown(m) } var xxx_messageInfo_Label proto.InternalMessageInfo func (m *Label) GetName() string { if m != nil { return m.Name } return "" } func (m *Label) GetValue() string { if m != nil { return m.Value } return "" } // A MetricPoint in a Metric. type MetricPoint struct { // Required. // // Types that are valid to be assigned to Value: // *MetricPoint_UnknownValue // *MetricPoint_GaugeValue // *MetricPoint_CounterValue // *MetricPoint_HistogramValue // *MetricPoint_StateSetValue // *MetricPoint_InfoValue // *MetricPoint_SummaryValue Value isMetricPoint_Value `protobuf_oneof:"value"` // Optional. Timestamp *types.Timestamp `protobuf:"bytes,8,opt,name=timestamp,proto3" json:"timestamp,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *MetricPoint) Reset() { *m = MetricPoint{} } func (m *MetricPoint) String() string { return proto.CompactTextString(m) } func (*MetricPoint) ProtoMessage() {} func (*MetricPoint) Descriptor() ([]byte, []int) { return fileDescriptor_0b803df83757ec01, []int{4} } func (m *MetricPoint) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *MetricPoint) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_MetricPoint.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *MetricPoint) XXX_Merge(src proto.Message) { xxx_messageInfo_MetricPoint.Merge(m, src) } func (m *MetricPoint) XXX_Size() int { return m.Size() } func (m *MetricPoint) XXX_DiscardUnknown() { xxx_messageInfo_MetricPoint.DiscardUnknown(m) } var xxx_messageInfo_MetricPoint proto.InternalMessageInfo type isMetricPoint_Value interface { isMetricPoint_Value() MarshalTo([]byte) (int, error) Size() int } type MetricPoint_UnknownValue struct { UnknownValue *UnknownValue `protobuf:"bytes,1,opt,name=unknown_value,json=unknownValue,proto3,oneof" json:"unknown_value,omitempty"` } type MetricPoint_GaugeValue struct { GaugeValue *GaugeValue `protobuf:"bytes,2,opt,name=gauge_value,json=gaugeValue,proto3,oneof" json:"gauge_value,omitempty"` } type MetricPoint_CounterValue struct { CounterValue *CounterValue `protobuf:"bytes,3,opt,name=counter_value,json=counterValue,proto3,oneof" json:"counter_value,omitempty"` } type MetricPoint_HistogramValue struct { HistogramValue *HistogramValue `protobuf:"bytes,4,opt,name=histogram_value,json=histogramValue,proto3,oneof" json:"histogram_value,omitempty"` } type MetricPoint_StateSetValue struct { StateSetValue *StateSetValue `protobuf:"bytes,5,opt,name=state_set_value,json=stateSetValue,proto3,oneof" json:"state_set_value,omitempty"` } type MetricPoint_InfoValue struct { InfoValue *InfoValue `protobuf:"bytes,6,opt,name=info_value,json=infoValue,proto3,oneof" json:"info_value,omitempty"` } type MetricPoint_SummaryValue struct { SummaryValue *SummaryValue `protobuf:"bytes,7,opt,name=summary_value,json=summaryValue,proto3,oneof" json:"summary_value,omitempty"` } func (*MetricPoint_UnknownValue) isMetricPoint_Value() {} func (*MetricPoint_GaugeValue) isMetricPoint_Value() {} func (*MetricPoint_CounterValue) isMetricPoint_Value() {} func (*MetricPoint_HistogramValue) isMetricPoint_Value() {} func (*MetricPoint_StateSetValue) isMetricPoint_Value() {} func (*MetricPoint_InfoValue) isMetricPoint_Value() {} func (*MetricPoint_SummaryValue) isMetricPoint_Value() {} func (m *MetricPoint) GetValue() isMetricPoint_Value { if m != nil { return m.Value } return nil } func (m *MetricPoint) GetUnknownValue() *UnknownValue { if x, ok := m.GetValue().(*MetricPoint_UnknownValue); ok { return x.UnknownValue } return nil } func (m *MetricPoint) GetGaugeValue() *GaugeValue { if x, ok := m.GetValue().(*MetricPoint_GaugeValue); ok { return x.GaugeValue } return nil } func (m *MetricPoint) GetCounterValue() *CounterValue { if x, ok := m.GetValue().(*MetricPoint_CounterValue); ok { return x.CounterValue } return nil } func (m *MetricPoint) GetHistogramValue() *HistogramValue { if x, ok := m.GetValue().(*MetricPoint_HistogramValue); ok { return x.HistogramValue } return nil } func (m *MetricPoint) GetStateSetValue() *StateSetValue { if x, ok := m.GetValue().(*MetricPoint_StateSetValue); ok { return x.StateSetValue } return nil } func (m *MetricPoint) GetInfoValue() *InfoValue { if x, ok := m.GetValue().(*MetricPoint_InfoValue); ok { return x.InfoValue } return nil } func (m *MetricPoint) GetSummaryValue() *SummaryValue { if x, ok := m.GetValue().(*MetricPoint_SummaryValue); ok { return x.SummaryValue } return nil } func (m *MetricPoint) GetTimestamp() *types.Timestamp { if m != nil { return m.Timestamp } return nil } // XXX_OneofWrappers is for the internal use of the proto package. func (*MetricPoint) XXX_OneofWrappers() []interface{} { return []interface{}{ (*MetricPoint_UnknownValue)(nil), (*MetricPoint_GaugeValue)(nil), (*MetricPoint_CounterValue)(nil), (*MetricPoint_HistogramValue)(nil), (*MetricPoint_StateSetValue)(nil), (*MetricPoint_InfoValue)(nil), (*MetricPoint_SummaryValue)(nil), } } // Value for UNKNOWN MetricPoint. type UnknownValue struct { // Required. // // Types that are valid to be assigned to Value: // *UnknownValue_DoubleValue // *UnknownValue_IntValue Value isUnknownValue_Value `protobuf_oneof:"value"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *UnknownValue) Reset() { *m = UnknownValue{} } func (m *UnknownValue) String() string { return proto.CompactTextString(m) } func (*UnknownValue) ProtoMessage() {} func (*UnknownValue) Descriptor() ([]byte, []int) { return fileDescriptor_0b803df83757ec01, []int{5} } func (m *UnknownValue) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *UnknownValue) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_UnknownValue.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *UnknownValue) XXX_Merge(src proto.Message) { xxx_messageInfo_UnknownValue.Merge(m, src) } func (m *UnknownValue) XXX_Size() int { return m.Size() } func (m *UnknownValue) XXX_DiscardUnknown() { xxx_messageInfo_UnknownValue.DiscardUnknown(m) } var xxx_messageInfo_UnknownValue proto.InternalMessageInfo type isUnknownValue_Value interface { isUnknownValue_Value() MarshalTo([]byte) (int, error) Size() int } type UnknownValue_DoubleValue struct { DoubleValue float64 `protobuf:"fixed64,1,opt,name=double_value,json=doubleValue,proto3,oneof" json:"double_value,omitempty"` } type UnknownValue_IntValue struct { IntValue int64 `protobuf:"varint,2,opt,name=int_value,json=intValue,proto3,oneof" json:"int_value,omitempty"` } func (*UnknownValue_DoubleValue) isUnknownValue_Value() {} func (*UnknownValue_IntValue) isUnknownValue_Value() {} func (m *UnknownValue) GetValue() isUnknownValue_Value { if m != nil { return m.Value } return nil } func (m *UnknownValue) GetDoubleValue() float64 { if x, ok := m.GetValue().(*UnknownValue_DoubleValue); ok { return x.DoubleValue } return 0 } func (m *UnknownValue) GetIntValue() int64 { if x, ok := m.GetValue().(*UnknownValue_IntValue); ok { return x.IntValue } return 0 } // XXX_OneofWrappers is for the internal use of the proto package. func (*UnknownValue) XXX_OneofWrappers() []interface{} { return []interface{}{ (*UnknownValue_DoubleValue)(nil), (*UnknownValue_IntValue)(nil), } } // Value for GAUGE MetricPoint. type GaugeValue struct { // Required. // // Types that are valid to be assigned to Value: // *GaugeValue_DoubleValue // *GaugeValue_IntValue Value isGaugeValue_Value `protobuf_oneof:"value"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GaugeValue) Reset() { *m = GaugeValue{} } func (m *GaugeValue) String() string { return proto.CompactTextString(m) } func (*GaugeValue) ProtoMessage() {} func (*GaugeValue) Descriptor() ([]byte, []int) { return fileDescriptor_0b803df83757ec01, []int{6} } func (m *GaugeValue) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GaugeValue) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GaugeValue.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GaugeValue) XXX_Merge(src proto.Message) { xxx_messageInfo_GaugeValue.Merge(m, src) } func (m *GaugeValue) XXX_Size() int { return m.Size() } func (m *GaugeValue) XXX_DiscardUnknown() { xxx_messageInfo_GaugeValue.DiscardUnknown(m) } var xxx_messageInfo_GaugeValue proto.InternalMessageInfo type isGaugeValue_Value interface { isGaugeValue_Value() MarshalTo([]byte) (int, error) Size() int } type GaugeValue_DoubleValue struct { DoubleValue float64 `protobuf:"fixed64,1,opt,name=double_value,json=doubleValue,proto3,oneof" json:"double_value,omitempty"` } type GaugeValue_IntValue struct { IntValue int64 `protobuf:"varint,2,opt,name=int_value,json=intValue,proto3,oneof" json:"int_value,omitempty"` } func (*GaugeValue_DoubleValue) isGaugeValue_Value() {} func (*GaugeValue_IntValue) isGaugeValue_Value() {} func (m *GaugeValue) GetValue() isGaugeValue_Value { if m != nil { return m.Value } return nil } func (m *GaugeValue) GetDoubleValue() float64 { if x, ok := m.GetValue().(*GaugeValue_DoubleValue); ok { return x.DoubleValue } return 0 } func (m *GaugeValue) GetIntValue() int64 { if x, ok := m.GetValue().(*GaugeValue_IntValue); ok { return x.IntValue } return 0 } // XXX_OneofWrappers is for the internal use of the proto package. func (*GaugeValue) XXX_OneofWrappers() []interface{} { return []interface{}{ (*GaugeValue_DoubleValue)(nil), (*GaugeValue_IntValue)(nil), } } // Value for COUNTER MetricPoint. type CounterValue struct { // Required. // // Types that are valid to be assigned to Total: // *CounterValue_DoubleValue // *CounterValue_IntValue Total isCounterValue_Total `protobuf_oneof:"total"` // The time values began being collected for this counter. // Optional. Created *types.Timestamp `protobuf:"bytes,3,opt,name=created,proto3" json:"created,omitempty"` // Optional. Exemplar *Exemplar `protobuf:"bytes,4,opt,name=exemplar,proto3" json:"exemplar,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *CounterValue) Reset() { *m = CounterValue{} } func (m *CounterValue) String() string { return proto.CompactTextString(m) } func (*CounterValue) ProtoMessage() {} func (*CounterValue) Descriptor() ([]byte, []int) { return fileDescriptor_0b803df83757ec01, []int{7} } func (m *CounterValue) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *CounterValue) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_CounterValue.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *CounterValue) XXX_Merge(src proto.Message) { xxx_messageInfo_CounterValue.Merge(m, src) } func (m *CounterValue) XXX_Size() int { return m.Size() } func (m *CounterValue) XXX_DiscardUnknown() { xxx_messageInfo_CounterValue.DiscardUnknown(m) } var xxx_messageInfo_CounterValue proto.InternalMessageInfo type isCounterValue_Total interface { isCounterValue_Total() MarshalTo([]byte) (int, error) Size() int } type CounterValue_DoubleValue struct { DoubleValue float64 `protobuf:"fixed64,1,opt,name=double_value,json=doubleValue,proto3,oneof" json:"double_value,omitempty"` } type CounterValue_IntValue struct { IntValue uint64 `protobuf:"varint,2,opt,name=int_value,json=intValue,proto3,oneof" json:"int_value,omitempty"` } func (*CounterValue_DoubleValue) isCounterValue_Total() {} func (*CounterValue_IntValue) isCounterValue_Total() {} func (m *CounterValue) GetTotal() isCounterValue_Total { if m != nil { return m.Total } return nil } func (m *CounterValue) GetDoubleValue() float64 { if x, ok := m.GetTotal().(*CounterValue_DoubleValue); ok { return x.DoubleValue } return 0 } func (m *CounterValue) GetIntValue() uint64 { if x, ok := m.GetTotal().(*CounterValue_IntValue); ok { return x.IntValue } return 0 } func (m *CounterValue) GetCreated() *types.Timestamp { if m != nil { return m.Created } return nil } func (m *CounterValue) GetExemplar() *Exemplar { if m != nil { return m.Exemplar } return nil } // XXX_OneofWrappers is for the internal use of the proto package. func (*CounterValue) XXX_OneofWrappers() []interface{} { return []interface{}{ (*CounterValue_DoubleValue)(nil), (*CounterValue_IntValue)(nil), } } // Value for HISTOGRAM or GAUGE_HISTOGRAM MetricPoint. type HistogramValue struct { // Optional. // // Types that are valid to be assigned to Sum: // *HistogramValue_DoubleValue // *HistogramValue_IntValue Sum isHistogramValue_Sum `protobuf_oneof:"sum"` // Optional. Count uint64 `protobuf:"varint,3,opt,name=count,proto3" json:"count,omitempty"` // The time values began being collected for this histogram. // Optional. Created *types.Timestamp `protobuf:"bytes,4,opt,name=created,proto3" json:"created,omitempty"` // Optional. Buckets []*HistogramValue_Bucket `protobuf:"bytes,5,rep,name=buckets,proto3" json:"buckets,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *HistogramValue) Reset() { *m = HistogramValue{} } func (m *HistogramValue) String() string { return proto.CompactTextString(m) } func (*HistogramValue) ProtoMessage() {} func (*HistogramValue) Descriptor() ([]byte, []int) { return fileDescriptor_0b803df83757ec01, []int{8} } func (m *HistogramValue) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *HistogramValue) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_HistogramValue.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *HistogramValue) XXX_Merge(src proto.Message) { xxx_messageInfo_HistogramValue.Merge(m, src) } func (m *HistogramValue) XXX_Size() int { return m.Size() } func (m *HistogramValue) XXX_DiscardUnknown() { xxx_messageInfo_HistogramValue.DiscardUnknown(m) } var xxx_messageInfo_HistogramValue proto.InternalMessageInfo type isHistogramValue_Sum interface { isHistogramValue_Sum() MarshalTo([]byte) (int, error) Size() int } type HistogramValue_DoubleValue struct { DoubleValue float64 `protobuf:"fixed64,1,opt,name=double_value,json=doubleValue,proto3,oneof" json:"double_value,omitempty"` } type HistogramValue_IntValue struct { IntValue int64 `protobuf:"varint,2,opt,name=int_value,json=intValue,proto3,oneof" json:"int_value,omitempty"` } func (*HistogramValue_DoubleValue) isHistogramValue_Sum() {} func (*HistogramValue_IntValue) isHistogramValue_Sum() {} func (m *HistogramValue) GetSum() isHistogramValue_Sum { if m != nil { return m.Sum } return nil } func (m *HistogramValue) GetDoubleValue() float64 { if x, ok := m.GetSum().(*HistogramValue_DoubleValue); ok { return x.DoubleValue } return 0 } func (m *HistogramValue) GetIntValue() int64 { if x, ok := m.GetSum().(*HistogramValue_IntValue); ok { return x.IntValue } return 0 } func (m *HistogramValue) GetCount() uint64 { if m != nil { return m.Count } return 0 } func (m *HistogramValue) GetCreated() *types.Timestamp { if m != nil { return m.Created } return nil } func (m *HistogramValue) GetBuckets() []*HistogramValue_Bucket { if m != nil { return m.Buckets } return nil } // XXX_OneofWrappers is for the internal use of the proto package. func (*HistogramValue) XXX_OneofWrappers() []interface{} { return []interface{}{ (*HistogramValue_DoubleValue)(nil), (*HistogramValue_IntValue)(nil), } } // Bucket is the number of values for a bucket in the histogram // with an optional exemplar. type HistogramValue_Bucket struct { // Required. Count uint64 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"` // Optional. UpperBound float64 `protobuf:"fixed64,2,opt,name=upper_bound,json=upperBound,proto3" json:"upper_bound,omitempty"` // Optional. Exemplar *Exemplar `protobuf:"bytes,3,opt,name=exemplar,proto3" json:"exemplar,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *HistogramValue_Bucket) Reset() { *m = HistogramValue_Bucket{} } func (m *HistogramValue_Bucket) String() string { return proto.CompactTextString(m) } func (*HistogramValue_Bucket) ProtoMessage() {} func (*HistogramValue_Bucket) Descriptor() ([]byte, []int) { return fileDescriptor_0b803df83757ec01, []int{8, 0} } func (m *HistogramValue_Bucket) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *HistogramValue_Bucket) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_HistogramValue_Bucket.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *HistogramValue_Bucket) XXX_Merge(src proto.Message) { xxx_messageInfo_HistogramValue_Bucket.Merge(m, src) } func (m *HistogramValue_Bucket) XXX_Size() int { return m.Size() } func (m *HistogramValue_Bucket) XXX_DiscardUnknown() { xxx_messageInfo_HistogramValue_Bucket.DiscardUnknown(m) } var xxx_messageInfo_HistogramValue_Bucket proto.InternalMessageInfo func (m *HistogramValue_Bucket) GetCount() uint64 { if m != nil { return m.Count } return 0 } func (m *HistogramValue_Bucket) GetUpperBound() float64 { if m != nil { return m.UpperBound } return 0 } func (m *HistogramValue_Bucket) GetExemplar() *Exemplar { if m != nil { return m.Exemplar } return nil } type Exemplar struct { // Required. Value float64 `protobuf:"fixed64,1,opt,name=value,proto3" json:"value,omitempty"` // Optional. Timestamp *types.Timestamp `protobuf:"bytes,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` // Labels are additional information about the exemplar value (e.g. trace id). // Optional. Label []*Label `protobuf:"bytes,3,rep,name=label,proto3" json:"label,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *Exemplar) Reset() { *m = Exemplar{} } func (m *Exemplar) String() string { return proto.CompactTextString(m) } func (*Exemplar) ProtoMessage() {} func (*Exemplar) Descriptor() ([]byte, []int) { return fileDescriptor_0b803df83757ec01, []int{9} } func (m *Exemplar) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *Exemplar) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_Exemplar.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *Exemplar) XXX_Merge(src proto.Message) { xxx_messageInfo_Exemplar.Merge(m, src) } func (m *Exemplar) XXX_Size() int { return m.Size() } func (m *Exemplar) XXX_DiscardUnknown() { xxx_messageInfo_Exemplar.DiscardUnknown(m) } var xxx_messageInfo_Exemplar proto.InternalMessageInfo func (m *Exemplar) GetValue() float64 { if m != nil { return m.Value } return 0 } func (m *Exemplar) GetTimestamp() *types.Timestamp { if m != nil { return m.Timestamp } return nil } func (m *Exemplar) GetLabel() []*Label { if m != nil { return m.Label } return nil } // Value for STATE_SET MetricPoint. type StateSetValue struct { // Optional. States []*StateSetValue_State `protobuf:"bytes,1,rep,name=states,proto3" json:"states,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *StateSetValue) Reset() { *m = StateSetValue{} } func (m *StateSetValue) String() string { return proto.CompactTextString(m) } func (*StateSetValue) ProtoMessage() {} func (*StateSetValue) Descriptor() ([]byte, []int) { return fileDescriptor_0b803df83757ec01, []int{10} } func (m *StateSetValue) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *StateSetValue) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_StateSetValue.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *StateSetValue) XXX_Merge(src proto.Message) { xxx_messageInfo_StateSetValue.Merge(m, src) } func (m *StateSetValue) XXX_Size() int { return m.Size() } func (m *StateSetValue) XXX_DiscardUnknown() { xxx_messageInfo_StateSetValue.DiscardUnknown(m) } var xxx_messageInfo_StateSetValue proto.InternalMessageInfo func (m *StateSetValue) GetStates() []*StateSetValue_State { if m != nil { return m.States } return nil } type StateSetValue_State struct { // Required. Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` // Required. Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *StateSetValue_State) Reset() { *m = StateSetValue_State{} } func (m *StateSetValue_State) String() string { return proto.CompactTextString(m) } func (*StateSetValue_State) ProtoMessage() {} func (*StateSetValue_State) Descriptor() ([]byte, []int) { return fileDescriptor_0b803df83757ec01, []int{10, 0} } func (m *StateSetValue_State) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *StateSetValue_State) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_StateSetValue_State.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *StateSetValue_State) XXX_Merge(src proto.Message) { xxx_messageInfo_StateSetValue_State.Merge(m, src) } func (m *StateSetValue_State) XXX_Size() int { return m.Size() } func (m *StateSetValue_State) XXX_DiscardUnknown() { xxx_messageInfo_StateSetValue_State.DiscardUnknown(m) } var xxx_messageInfo_StateSetValue_State proto.InternalMessageInfo func (m *StateSetValue_State) GetEnabled() bool { if m != nil { return m.Enabled } return false } func (m *StateSetValue_State) GetName() string { if m != nil { return m.Name } return "" } // Value for INFO MetricPoint. type InfoValue struct { // Optional. Info []*Label `protobuf:"bytes,1,rep,name=info,proto3" json:"info,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *InfoValue) Reset() { *m = InfoValue{} } func (m *InfoValue) String() string { return proto.CompactTextString(m) } func (*InfoValue) ProtoMessage() {} func (*InfoValue) Descriptor() ([]byte, []int) { return fileDescriptor_0b803df83757ec01, []int{11} } func (m *InfoValue) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *InfoValue) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_InfoValue.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *InfoValue) XXX_Merge(src proto.Message) { xxx_messageInfo_InfoValue.Merge(m, src) } func (m *InfoValue) XXX_Size() int { return m.Size() } func (m *InfoValue) XXX_DiscardUnknown() { xxx_messageInfo_InfoValue.DiscardUnknown(m) } var xxx_messageInfo_InfoValue proto.InternalMessageInfo func (m *InfoValue) GetInfo() []*Label { if m != nil { return m.Info } return nil } // Value for SUMMARY MetricPoint. type SummaryValue struct { // Optional. // // Types that are valid to be assigned to Sum: // *SummaryValue_DoubleValue // *SummaryValue_IntValue Sum isSummaryValue_Sum `protobuf_oneof:"sum"` // Optional. Count uint64 `protobuf:"varint,3,opt,name=count,proto3" json:"count,omitempty"` // The time sum and count values began being collected for this summary. // Optional. Created *types.Timestamp `protobuf:"bytes,4,opt,name=created,proto3" json:"created,omitempty"` // Optional. Quantile []*SummaryValue_Quantile `protobuf:"bytes,5,rep,name=quantile,proto3" json:"quantile,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *SummaryValue) Reset() { *m = SummaryValue{} } func (m *SummaryValue) String() string { return proto.CompactTextString(m) } func (*SummaryValue) ProtoMessage() {} func (*SummaryValue) Descriptor() ([]byte, []int) { return fileDescriptor_0b803df83757ec01, []int{12} } func (m *SummaryValue) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *SummaryValue) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_SummaryValue.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *SummaryValue) XXX_Merge(src proto.Message) { xxx_messageInfo_SummaryValue.Merge(m, src) } func (m *SummaryValue) XXX_Size() int { return m.Size() } func (m *SummaryValue) XXX_DiscardUnknown() { xxx_messageInfo_SummaryValue.DiscardUnknown(m) } var xxx_messageInfo_SummaryValue proto.InternalMessageInfo type isSummaryValue_Sum interface { isSummaryValue_Sum() MarshalTo([]byte) (int, error) Size() int } type SummaryValue_DoubleValue struct { DoubleValue float64 `protobuf:"fixed64,1,opt,name=double_value,json=doubleValue,proto3,oneof" json:"double_value,omitempty"` } type SummaryValue_IntValue struct { IntValue int64 `protobuf:"varint,2,opt,name=int_value,json=intValue,proto3,oneof" json:"int_value,omitempty"` } func (*SummaryValue_DoubleValue) isSummaryValue_Sum() {} func (*SummaryValue_IntValue) isSummaryValue_Sum() {} func (m *SummaryValue) GetSum() isSummaryValue_Sum { if m != nil { return m.Sum } return nil } func (m *SummaryValue) GetDoubleValue() float64 { if x, ok := m.GetSum().(*SummaryValue_DoubleValue); ok { return x.DoubleValue } return 0 } func (m *SummaryValue) GetIntValue() int64 { if x, ok := m.GetSum().(*SummaryValue_IntValue); ok { return x.IntValue } return 0 } func (m *SummaryValue) GetCount() uint64 { if m != nil { return m.Count } return 0 } func (m *SummaryValue) GetCreated() *types.Timestamp { if m != nil { return m.Created } return nil } func (m *SummaryValue) GetQuantile() []*SummaryValue_Quantile { if m != nil { return m.Quantile } return nil } // XXX_OneofWrappers is for the internal use of the proto package. func (*SummaryValue) XXX_OneofWrappers() []interface{} { return []interface{}{ (*SummaryValue_DoubleValue)(nil), (*SummaryValue_IntValue)(nil), } } type SummaryValue_Quantile struct { // Required. Quantile float64 `protobuf:"fixed64,1,opt,name=quantile,proto3" json:"quantile,omitempty"` // Required. Value float64 `protobuf:"fixed64,2,opt,name=value,proto3" json:"value,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *SummaryValue_Quantile) Reset() { *m = SummaryValue_Quantile{} } func (m *SummaryValue_Quantile) String() string { return proto.CompactTextString(m) } func (*SummaryValue_Quantile) ProtoMessage() {} func (*SummaryValue_Quantile) Descriptor() ([]byte, []int) { return fileDescriptor_0b803df83757ec01, []int{12, 0} } func (m *SummaryValue_Quantile) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *SummaryValue_Quantile) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_SummaryValue_Quantile.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *SummaryValue_Quantile) XXX_Merge(src proto.Message) { xxx_messageInfo_SummaryValue_Quantile.Merge(m, src) } func (m *SummaryValue_Quantile) XXX_Size() int { return m.Size() } func (m *SummaryValue_Quantile) XXX_DiscardUnknown() { xxx_messageInfo_SummaryValue_Quantile.DiscardUnknown(m) } var xxx_messageInfo_SummaryValue_Quantile proto.InternalMessageInfo func (m *SummaryValue_Quantile) GetQuantile() float64 { if m != nil { return m.Quantile } return 0 } func (m *SummaryValue_Quantile) GetValue() float64 { if m != nil { return m.Value } return 0 } func init() { proto.RegisterEnum("jaeger.api_v2.metrics.MetricType", MetricType_name, MetricType_value) proto.RegisterType((*MetricSet)(nil), "jaeger.api_v2.metrics.MetricSet") proto.RegisterType((*MetricFamily)(nil), "jaeger.api_v2.metrics.MetricFamily") proto.RegisterType((*Metric)(nil), "jaeger.api_v2.metrics.Metric") proto.RegisterType((*Label)(nil), "jaeger.api_v2.metrics.Label") proto.RegisterType((*MetricPoint)(nil), "jaeger.api_v2.metrics.MetricPoint") proto.RegisterType((*UnknownValue)(nil), "jaeger.api_v2.metrics.UnknownValue") proto.RegisterType((*GaugeValue)(nil), "jaeger.api_v2.metrics.GaugeValue") proto.RegisterType((*CounterValue)(nil), "jaeger.api_v2.metrics.CounterValue") proto.RegisterType((*HistogramValue)(nil), "jaeger.api_v2.metrics.HistogramValue") proto.RegisterType((*HistogramValue_Bucket)(nil), "jaeger.api_v2.metrics.HistogramValue.Bucket") proto.RegisterType((*Exemplar)(nil), "jaeger.api_v2.metrics.Exemplar") proto.RegisterType((*StateSetValue)(nil), "jaeger.api_v2.metrics.StateSetValue") proto.RegisterType((*StateSetValue_State)(nil), "jaeger.api_v2.metrics.StateSetValue.State") proto.RegisterType((*InfoValue)(nil), "jaeger.api_v2.metrics.InfoValue") proto.RegisterType((*SummaryValue)(nil), "jaeger.api_v2.metrics.SummaryValue") proto.RegisterType((*SummaryValue_Quantile)(nil), "jaeger.api_v2.metrics.SummaryValue.Quantile") } func init() { proto.RegisterFile("openmetrics.proto", fileDescriptor_0b803df83757ec01) } var fileDescriptor_0b803df83757ec01 = []byte{ // 981 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x96, 0xdd, 0x6e, 0xe3, 0x44, 0x14, 0xc7, 0xeb, 0xc4, 0xf9, 0x3a, 0x49, 0xda, 0x30, 0x2c, 0x92, 0x15, 0xb1, 0x6d, 0xf1, 0x82, 0x54, 0xad, 0x90, 0x0b, 0x65, 0x17, 0x90, 0x80, 0x8b, 0x64, 0x49, 0x93, 0xc2, 0x36, 0x5d, 0x26, 0x09, 0xa8, 0x70, 0x11, 0x39, 0xe9, 0xd4, 0x35, 0xeb, 0x2f, 0xec, 0xf1, 0x42, 0x05, 0xf7, 0x48, 0x5c, 0xf0, 0x26, 0xbc, 0x00, 0x4f, 0xc0, 0x15, 0xe2, 0x0d, 0x40, 0xbd, 0xe7, 0x1d, 0xd0, 0x7c, 0xc5, 0x0e, 0xda, 0x84, 0x2c, 0xda, 0x8b, 0xbd, 0x9b, 0x73, 0xfc, 0x3f, 0xbf, 0x99, 0x73, 0x7c, 0x7c, 0xc6, 0xf0, 0x52, 0x18, 0x91, 0xc0, 0x27, 0x34, 0x76, 0xe7, 0x89, 0x15, 0xc5, 0x21, 0x0d, 0xd1, 0x2b, 0x5f, 0xdb, 0xc4, 0x21, 0xb1, 0x65, 0x47, 0xee, 0xf4, 0xc9, 0x91, 0x25, 0x1f, 0xb6, 0xf7, 0x9c, 0x30, 0x74, 0x3c, 0x72, 0xc8, 0x45, 0xb3, 0xf4, 0xf2, 0x90, 0xba, 0x3e, 0x49, 0xa8, 0xed, 0x47, 0x22, 0xae, 0x7d, 0xcb, 0x09, 0x9d, 0x90, 0x2f, 0x0f, 0xd9, 0x4a, 0x78, 0xcd, 0x73, 0xa8, 0x9d, 0x72, 0xc2, 0x88, 0x50, 0xf4, 0x10, 0x76, 0x04, 0x6e, 0x7a, 0x69, 0xfb, 0xae, 0xe7, 0x92, 0xc4, 0xd0, 0xf6, 0x8b, 0x07, 0xf5, 0xa3, 0x3b, 0xd6, 0x53, 0x37, 0xb5, 0x44, 0xe8, 0x31, 0x13, 0x5f, 0xe3, 0x6d, 0x3f, 0xb3, 0x5c, 0x92, 0x98, 0xbf, 0x6a, 0xd0, 0xc8, 0x0b, 0x10, 0x02, 0x3d, 0xb0, 0x7d, 0x62, 0x68, 0xfb, 0xda, 0x41, 0x0d, 0xf3, 0x35, 0xba, 0x0f, 0x3a, 0xbd, 0x8e, 0x88, 0x51, 0xd8, 0xd7, 0x0e, 0xb6, 0x8f, 0x5e, 0x5b, 0xbb, 0xcf, 0xf8, 0x3a, 0x22, 0x98, 0xcb, 0x19, 0x2a, 0x0d, 0x5c, 0x6a, 0x14, 0x05, 0x8a, 0xad, 0x99, 0xef, 0x8a, 0x78, 0x91, 0xa1, 0x0b, 0x1f, 0x5b, 0xa3, 0xf7, 0xa0, 0x22, 0x19, 0x46, 0x89, 0x67, 0x72, 0x7b, 0xed, 0x0e, 0x58, 0xa9, 0xcd, 0x1f, 0x35, 0x28, 0x0b, 0x1f, 0xba, 0x07, 0x65, 0xcf, 0x9e, 0x11, 0x4f, 0x15, 0xe3, 0xd5, 0x15, 0x88, 0x87, 0x4c, 0x84, 0xa5, 0x16, 0xf5, 0xa1, 0x29, 0x6b, 0x19, 0x85, 0x6e, 0x40, 0x13, 0xa3, 0xc0, 0x83, 0xcd, 0xb5, 0xfb, 0x3f, 0x62, 0x52, 0xdc, 0xf0, 0x33, 0x23, 0x31, 0xdf, 0x86, 0x12, 0x27, 0x3f, 0xb5, 0x7c, 0xb7, 0xa0, 0xf4, 0xc4, 0xf6, 0x52, 0x51, 0xbf, 0x1a, 0x16, 0x86, 0xf9, 0xa7, 0x0e, 0xf5, 0x1c, 0x10, 0x7d, 0x02, 0xcd, 0x34, 0x78, 0x1c, 0x84, 0xdf, 0x06, 0x53, 0xa1, 0x66, 0x88, 0xd5, 0x6f, 0x75, 0x22, 0xb4, 0x9f, 0x33, 0xe9, 0x60, 0x0b, 0x37, 0xd2, 0x9c, 0x8d, 0x3e, 0x86, 0xba, 0x63, 0xa7, 0x0e, 0x99, 0x66, 0xfb, 0xd6, 0x57, 0xbe, 0xb7, 0x3e, 0x53, 0x2a, 0x0e, 0x38, 0x0b, 0x8b, 0x9d, 0x68, 0x1e, 0xa6, 0x01, 0x25, 0xb1, 0xe4, 0x14, 0xd7, 0x9e, 0xe8, 0x81, 0xd0, 0x2e, 0x4e, 0x34, 0xcf, 0xd9, 0xe8, 0x11, 0xec, 0x5c, 0xb9, 0x09, 0x0d, 0x9d, 0xd8, 0xf6, 0x25, 0x4d, 0xe7, 0xb4, 0x37, 0x56, 0xd0, 0x06, 0x4a, 0xad, 0x78, 0xdb, 0x57, 0x4b, 0x1e, 0x34, 0x84, 0x9d, 0x84, 0xda, 0x94, 0x4c, 0x13, 0x42, 0x25, 0xb1, 0xc4, 0x89, 0xaf, 0xaf, 0x20, 0x8e, 0x98, 0x7a, 0x44, 0xa8, 0x02, 0x36, 0x93, 0xbc, 0x03, 0x75, 0x00, 0xdc, 0xe0, 0x32, 0x94, 0xa8, 0x32, 0x47, 0xed, 0xaf, 0x40, 0x9d, 0x04, 0x97, 0xa1, 0xc2, 0xd4, 0x5c, 0x65, 0xb0, 0x82, 0x25, 0xa9, 0xef, 0xdb, 0xf1, 0xb5, 0xa4, 0x54, 0xd6, 0x16, 0x6c, 0x24, 0xb4, 0x8b, 0x82, 0x25, 0x39, 0x1b, 0xbd, 0x0f, 0xb5, 0xc5, 0x70, 0x30, 0xaa, 0x9c, 0xd3, 0xb6, 0xc4, 0xf8, 0xb0, 0xd4, 0xf8, 0xb0, 0xc6, 0x4a, 0x81, 0x33, 0x71, 0xb7, 0x22, 0xdb, 0xcd, 0xfc, 0x0a, 0x1a, 0xf9, 0x2e, 0x41, 0x77, 0xa0, 0x71, 0x11, 0xa6, 0x33, 0x8f, 0xe4, 0x1a, 0x4c, 0x1b, 0x6c, 0xe1, 0xba, 0xf0, 0x0a, 0xd1, 0x6d, 0xa8, 0xb9, 0x01, 0xcd, 0x35, 0x4e, 0x71, 0xb0, 0x85, 0xab, 0x6e, 0x20, 0xaa, 0x94, 0xc1, 0xcf, 0x01, 0xb2, 0xc6, 0x79, 0xbe, 0xe8, 0xdf, 0x35, 0x68, 0xe4, 0x9b, 0xe9, 0x7f, 0xd2, 0xf5, 0x3c, 0x1d, 0xdd, 0x83, 0xca, 0x3c, 0x26, 0x36, 0x25, 0x17, 0xb2, 0x8d, 0xd7, 0x55, 0x53, 0x49, 0xd1, 0x07, 0x50, 0x25, 0xdf, 0x11, 0x3f, 0xf2, 0xec, 0x58, 0xf6, 0xeb, 0xde, 0x8a, 0x97, 0xd9, 0x93, 0x32, 0xbc, 0x08, 0x60, 0x09, 0xd1, 0x90, 0xda, 0x9e, 0xf9, 0x77, 0x01, 0xb6, 0x97, 0xfb, 0xf9, 0x79, 0x14, 0x8c, 0xcd, 0x15, 0xfe, 0x8d, 0xf1, 0x84, 0x74, 0x2c, 0x8c, 0x7c, 0xa2, 0xfa, 0xe6, 0x89, 0x1e, 0x43, 0x65, 0x96, 0xce, 0x1f, 0x13, 0xaa, 0x66, 0xf0, 0x9b, 0x1b, 0x7d, 0x97, 0x56, 0x97, 0x07, 0x61, 0x15, 0xdc, 0xfe, 0x01, 0xca, 0xc2, 0x95, 0x9d, 0x4e, 0xcb, 0x9f, 0x6e, 0x0f, 0xea, 0x69, 0x14, 0x91, 0x78, 0x3a, 0x0b, 0xd3, 0xe0, 0x82, 0x27, 0xa5, 0x61, 0xe0, 0xae, 0x2e, 0xf3, 0x2c, 0x55, 0xbc, 0xf8, 0xac, 0x15, 0x2f, 0x41, 0x31, 0x49, 0x7d, 0xf3, 0x67, 0x0d, 0xaa, 0xea, 0x69, 0x36, 0x7d, 0x79, 0x89, 0xe5, 0xf4, 0x5d, 0xfe, 0xbc, 0x0a, 0xcf, 0xf0, 0x79, 0xa1, 0x23, 0x28, 0xf1, 0xdb, 0xc3, 0x28, 0x6e, 0x70, 0xd1, 0x08, 0xa9, 0xf9, 0x93, 0x06, 0xcd, 0xa5, 0xf1, 0x83, 0xba, 0x50, 0xe6, 0xe3, 0x47, 0xdd, 0x57, 0x77, 0x37, 0x19, 0x5a, 0xc2, 0xc2, 0x32, 0xb2, 0x7d, 0x1f, 0x4a, 0xdc, 0x81, 0x0c, 0xa8, 0x90, 0xc0, 0x9e, 0x79, 0xe4, 0x82, 0x27, 0x59, 0xc5, 0xca, 0x5c, 0x5c, 0x47, 0x85, 0xec, 0x3a, 0x32, 0x3f, 0x82, 0xda, 0x62, 0x7e, 0xa1, 0xb7, 0x40, 0x67, 0xf3, 0x6b, 0xa3, 0x5b, 0x93, 0x2b, 0xcd, 0x5f, 0x0a, 0xd0, 0xc8, 0x4f, 0xae, 0x17, 0xae, 0x95, 0x07, 0x50, 0xfd, 0x26, 0xb5, 0x03, 0xea, 0x7a, 0xe4, 0x3f, 0x7a, 0x39, 0x9f, 0x86, 0xf5, 0x99, 0x8c, 0xc1, 0x8b, 0xe8, 0xf6, 0x87, 0x50, 0x55, 0x5e, 0xd4, 0xce, 0x51, 0x45, 0x27, 0x2d, 0xec, 0xe5, 0x0b, 0x5e, 0xb5, 0x98, 0x6c, 0xc6, 0xbb, 0xdf, 0x03, 0x64, 0x7f, 0x46, 0xa8, 0x0e, 0x95, 0xc9, 0xf0, 0xd3, 0xe1, 0xd9, 0x17, 0xc3, 0xd6, 0x16, 0xaa, 0x41, 0xa9, 0xdf, 0x99, 0xf4, 0x7b, 0x2d, 0x8d, 0xf9, 0x1f, 0x9c, 0x4d, 0x86, 0xe3, 0x1e, 0x6e, 0x15, 0x50, 0x13, 0x6a, 0xa3, 0x71, 0x67, 0xdc, 0x9b, 0x8e, 0x7a, 0xe3, 0x56, 0x11, 0x55, 0x41, 0x3f, 0x19, 0x1e, 0x9f, 0xb5, 0x74, 0xf6, 0x60, 0x70, 0x32, 0x1a, 0x9f, 0xf5, 0x71, 0xe7, 0xb4, 0x55, 0x42, 0x2f, 0xc3, 0x0e, 0x8f, 0x9f, 0x66, 0xce, 0x32, 0x23, 0x8d, 0x26, 0xa7, 0xa7, 0x1d, 0x7c, 0xde, 0xaa, 0x74, 0xdf, 0xfd, 0xed, 0x66, 0x57, 0xfb, 0xe3, 0x66, 0x57, 0xfb, 0xeb, 0x66, 0x57, 0x83, 0x3d, 0x37, 0x94, 0x95, 0xa0, 0xb1, 0x3d, 0x77, 0x03, 0xe7, 0x5f, 0x05, 0xf9, 0x52, 0xfd, 0x59, 0xcd, 0xca, 0xbc, 0xc0, 0xef, 0xfc, 0x13, 0x00, 0x00, 0xff, 0xff, 0x0a, 0x75, 0xe1, 0xe3, 0xdb, 0x0a, 0x00, 0x00, } func (m *MetricSet) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *MetricSet) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *MetricSet) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.MetricFamilies) > 0 { for iNdEx := len(m.MetricFamilies) - 1; iNdEx >= 0; iNdEx-- { { size, err := m.MetricFamilies[iNdEx].MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } } return len(dAtA) - i, nil } func (m *MetricFamily) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *MetricFamily) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *MetricFamily) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.Metrics) > 0 { for iNdEx := len(m.Metrics) - 1; iNdEx >= 0; iNdEx-- { { size, err := m.Metrics[iNdEx].MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x2a } } if len(m.Help) > 0 { i -= len(m.Help) copy(dAtA[i:], m.Help) i = encodeVarintOpenmetrics(dAtA, i, uint64(len(m.Help))) i-- dAtA[i] = 0x22 } if len(m.Unit) > 0 { i -= len(m.Unit) copy(dAtA[i:], m.Unit) i = encodeVarintOpenmetrics(dAtA, i, uint64(len(m.Unit))) i-- dAtA[i] = 0x1a } if m.Type != 0 { i = encodeVarintOpenmetrics(dAtA, i, uint64(m.Type)) i-- dAtA[i] = 0x10 } if len(m.Name) > 0 { i -= len(m.Name) copy(dAtA[i:], m.Name) i = encodeVarintOpenmetrics(dAtA, i, uint64(len(m.Name))) i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *Metric) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *Metric) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *Metric) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.MetricPoints) > 0 { for iNdEx := len(m.MetricPoints) - 1; iNdEx >= 0; iNdEx-- { { size, err := m.MetricPoints[iNdEx].MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x12 } } if len(m.Labels) > 0 { for iNdEx := len(m.Labels) - 1; iNdEx >= 0; iNdEx-- { { size, err := m.Labels[iNdEx].MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } } return len(dAtA) - i, nil } func (m *Label) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *Label) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *Label) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.Value) > 0 { i -= len(m.Value) copy(dAtA[i:], m.Value) i = encodeVarintOpenmetrics(dAtA, i, uint64(len(m.Value))) i-- dAtA[i] = 0x12 } if len(m.Name) > 0 { i -= len(m.Name) copy(dAtA[i:], m.Name) i = encodeVarintOpenmetrics(dAtA, i, uint64(len(m.Name))) i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *MetricPoint) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *MetricPoint) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *MetricPoint) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if m.Timestamp != nil { { size, err := m.Timestamp.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x42 } if m.Value != nil { { size := m.Value.Size() i -= size if _, err := m.Value.MarshalTo(dAtA[i:]); err != nil { return 0, err } } } return len(dAtA) - i, nil } func (m *MetricPoint_UnknownValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *MetricPoint_UnknownValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) if m.UnknownValue != nil { { size, err := m.UnknownValue.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *MetricPoint_GaugeValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *MetricPoint_GaugeValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) if m.GaugeValue != nil { { size, err := m.GaugeValue.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x12 } return len(dAtA) - i, nil } func (m *MetricPoint_CounterValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *MetricPoint_CounterValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) if m.CounterValue != nil { { size, err := m.CounterValue.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x1a } return len(dAtA) - i, nil } func (m *MetricPoint_HistogramValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *MetricPoint_HistogramValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) if m.HistogramValue != nil { { size, err := m.HistogramValue.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x22 } return len(dAtA) - i, nil } func (m *MetricPoint_StateSetValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *MetricPoint_StateSetValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) if m.StateSetValue != nil { { size, err := m.StateSetValue.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x2a } return len(dAtA) - i, nil } func (m *MetricPoint_InfoValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *MetricPoint_InfoValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) if m.InfoValue != nil { { size, err := m.InfoValue.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x32 } return len(dAtA) - i, nil } func (m *MetricPoint_SummaryValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *MetricPoint_SummaryValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) if m.SummaryValue != nil { { size, err := m.SummaryValue.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x3a } return len(dAtA) - i, nil } func (m *UnknownValue) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *UnknownValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *UnknownValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if m.Value != nil { { size := m.Value.Size() i -= size if _, err := m.Value.MarshalTo(dAtA[i:]); err != nil { return 0, err } } } return len(dAtA) - i, nil } func (m *UnknownValue_DoubleValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *UnknownValue_DoubleValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) i -= 8 encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(math.Float64bits(float64(m.DoubleValue)))) i-- dAtA[i] = 0x9 return len(dAtA) - i, nil } func (m *UnknownValue_IntValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *UnknownValue_IntValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) i = encodeVarintOpenmetrics(dAtA, i, uint64(m.IntValue)) i-- dAtA[i] = 0x10 return len(dAtA) - i, nil } func (m *GaugeValue) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GaugeValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GaugeValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if m.Value != nil { { size := m.Value.Size() i -= size if _, err := m.Value.MarshalTo(dAtA[i:]); err != nil { return 0, err } } } return len(dAtA) - i, nil } func (m *GaugeValue_DoubleValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GaugeValue_DoubleValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) i -= 8 encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(math.Float64bits(float64(m.DoubleValue)))) i-- dAtA[i] = 0x9 return len(dAtA) - i, nil } func (m *GaugeValue_IntValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GaugeValue_IntValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) i = encodeVarintOpenmetrics(dAtA, i, uint64(m.IntValue)) i-- dAtA[i] = 0x10 return len(dAtA) - i, nil } func (m *CounterValue) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *CounterValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *CounterValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if m.Exemplar != nil { { size, err := m.Exemplar.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x22 } if m.Created != nil { { size, err := m.Created.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x1a } if m.Total != nil { { size := m.Total.Size() i -= size if _, err := m.Total.MarshalTo(dAtA[i:]); err != nil { return 0, err } } } return len(dAtA) - i, nil } func (m *CounterValue_DoubleValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *CounterValue_DoubleValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) i -= 8 encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(math.Float64bits(float64(m.DoubleValue)))) i-- dAtA[i] = 0x9 return len(dAtA) - i, nil } func (m *CounterValue_IntValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *CounterValue_IntValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) i = encodeVarintOpenmetrics(dAtA, i, uint64(m.IntValue)) i-- dAtA[i] = 0x10 return len(dAtA) - i, nil } func (m *HistogramValue) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *HistogramValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *HistogramValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.Buckets) > 0 { for iNdEx := len(m.Buckets) - 1; iNdEx >= 0; iNdEx-- { { size, err := m.Buckets[iNdEx].MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x2a } } if m.Created != nil { { size, err := m.Created.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x22 } if m.Count != 0 { i = encodeVarintOpenmetrics(dAtA, i, uint64(m.Count)) i-- dAtA[i] = 0x18 } if m.Sum != nil { { size := m.Sum.Size() i -= size if _, err := m.Sum.MarshalTo(dAtA[i:]); err != nil { return 0, err } } } return len(dAtA) - i, nil } func (m *HistogramValue_DoubleValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *HistogramValue_DoubleValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) i -= 8 encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(math.Float64bits(float64(m.DoubleValue)))) i-- dAtA[i] = 0x9 return len(dAtA) - i, nil } func (m *HistogramValue_IntValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *HistogramValue_IntValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) i = encodeVarintOpenmetrics(dAtA, i, uint64(m.IntValue)) i-- dAtA[i] = 0x10 return len(dAtA) - i, nil } func (m *HistogramValue_Bucket) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *HistogramValue_Bucket) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *HistogramValue_Bucket) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if m.Exemplar != nil { { size, err := m.Exemplar.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x1a } if m.UpperBound != 0 { i -= 8 encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(math.Float64bits(float64(m.UpperBound)))) i-- dAtA[i] = 0x11 } if m.Count != 0 { i = encodeVarintOpenmetrics(dAtA, i, uint64(m.Count)) i-- dAtA[i] = 0x8 } return len(dAtA) - i, nil } func (m *Exemplar) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *Exemplar) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *Exemplar) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.Label) > 0 { for iNdEx := len(m.Label) - 1; iNdEx >= 0; iNdEx-- { { size, err := m.Label[iNdEx].MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x1a } } if m.Timestamp != nil { { size, err := m.Timestamp.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x12 } if m.Value != 0 { i -= 8 encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(math.Float64bits(float64(m.Value)))) i-- dAtA[i] = 0x9 } return len(dAtA) - i, nil } func (m *StateSetValue) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *StateSetValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *StateSetValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.States) > 0 { for iNdEx := len(m.States) - 1; iNdEx >= 0; iNdEx-- { { size, err := m.States[iNdEx].MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } } return len(dAtA) - i, nil } func (m *StateSetValue_State) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *StateSetValue_State) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *StateSetValue_State) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.Name) > 0 { i -= len(m.Name) copy(dAtA[i:], m.Name) i = encodeVarintOpenmetrics(dAtA, i, uint64(len(m.Name))) i-- dAtA[i] = 0x12 } if m.Enabled { i-- if m.Enabled { dAtA[i] = 1 } else { dAtA[i] = 0 } i-- dAtA[i] = 0x8 } return len(dAtA) - i, nil } func (m *InfoValue) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *InfoValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *InfoValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.Info) > 0 { for iNdEx := len(m.Info) - 1; iNdEx >= 0; iNdEx-- { { size, err := m.Info[iNdEx].MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } } return len(dAtA) - i, nil } func (m *SummaryValue) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *SummaryValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *SummaryValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.Quantile) > 0 { for iNdEx := len(m.Quantile) - 1; iNdEx >= 0; iNdEx-- { { size, err := m.Quantile[iNdEx].MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x2a } } if m.Created != nil { { size, err := m.Created.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintOpenmetrics(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x22 } if m.Count != 0 { i = encodeVarintOpenmetrics(dAtA, i, uint64(m.Count)) i-- dAtA[i] = 0x18 } if m.Sum != nil { { size := m.Sum.Size() i -= size if _, err := m.Sum.MarshalTo(dAtA[i:]); err != nil { return 0, err } } } return len(dAtA) - i, nil } func (m *SummaryValue_DoubleValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *SummaryValue_DoubleValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) i -= 8 encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(math.Float64bits(float64(m.DoubleValue)))) i-- dAtA[i] = 0x9 return len(dAtA) - i, nil } func (m *SummaryValue_IntValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *SummaryValue_IntValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) i = encodeVarintOpenmetrics(dAtA, i, uint64(m.IntValue)) i-- dAtA[i] = 0x10 return len(dAtA) - i, nil } func (m *SummaryValue_Quantile) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *SummaryValue_Quantile) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *SummaryValue_Quantile) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if m.Value != 0 { i -= 8 encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(math.Float64bits(float64(m.Value)))) i-- dAtA[i] = 0x11 } if m.Quantile != 0 { i -= 8 encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(math.Float64bits(float64(m.Quantile)))) i-- dAtA[i] = 0x9 } return len(dAtA) - i, nil } func encodeVarintOpenmetrics(dAtA []byte, offset int, v uint64) int { offset -= sovOpenmetrics(v) base := offset for v >= 1<<7 { dAtA[offset] = uint8(v&0x7f | 0x80) v >>= 7 offset++ } dAtA[offset] = uint8(v) return base } func (m *MetricSet) Size() (n int) { if m == nil { return 0 } var l int _ = l if len(m.MetricFamilies) > 0 { for _, e := range m.MetricFamilies { l = e.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *MetricFamily) Size() (n int) { if m == nil { return 0 } var l int _ = l l = len(m.Name) if l > 0 { n += 1 + l + sovOpenmetrics(uint64(l)) } if m.Type != 0 { n += 1 + sovOpenmetrics(uint64(m.Type)) } l = len(m.Unit) if l > 0 { n += 1 + l + sovOpenmetrics(uint64(l)) } l = len(m.Help) if l > 0 { n += 1 + l + sovOpenmetrics(uint64(l)) } if len(m.Metrics) > 0 { for _, e := range m.Metrics { l = e.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *Metric) Size() (n int) { if m == nil { return 0 } var l int _ = l if len(m.Labels) > 0 { for _, e := range m.Labels { l = e.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } } if len(m.MetricPoints) > 0 { for _, e := range m.MetricPoints { l = e.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *Label) Size() (n int) { if m == nil { return 0 } var l int _ = l l = len(m.Name) if l > 0 { n += 1 + l + sovOpenmetrics(uint64(l)) } l = len(m.Value) if l > 0 { n += 1 + l + sovOpenmetrics(uint64(l)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *MetricPoint) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.Value != nil { n += m.Value.Size() } if m.Timestamp != nil { l = m.Timestamp.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *MetricPoint_UnknownValue) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.UnknownValue != nil { l = m.UnknownValue.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } return n } func (m *MetricPoint_GaugeValue) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.GaugeValue != nil { l = m.GaugeValue.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } return n } func (m *MetricPoint_CounterValue) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.CounterValue != nil { l = m.CounterValue.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } return n } func (m *MetricPoint_HistogramValue) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.HistogramValue != nil { l = m.HistogramValue.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } return n } func (m *MetricPoint_StateSetValue) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.StateSetValue != nil { l = m.StateSetValue.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } return n } func (m *MetricPoint_InfoValue) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.InfoValue != nil { l = m.InfoValue.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } return n } func (m *MetricPoint_SummaryValue) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.SummaryValue != nil { l = m.SummaryValue.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } return n } func (m *UnknownValue) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.Value != nil { n += m.Value.Size() } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *UnknownValue_DoubleValue) Size() (n int) { if m == nil { return 0 } var l int _ = l n += 9 return n } func (m *UnknownValue_IntValue) Size() (n int) { if m == nil { return 0 } var l int _ = l n += 1 + sovOpenmetrics(uint64(m.IntValue)) return n } func (m *GaugeValue) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.Value != nil { n += m.Value.Size() } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *GaugeValue_DoubleValue) Size() (n int) { if m == nil { return 0 } var l int _ = l n += 9 return n } func (m *GaugeValue_IntValue) Size() (n int) { if m == nil { return 0 } var l int _ = l n += 1 + sovOpenmetrics(uint64(m.IntValue)) return n } func (m *CounterValue) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.Total != nil { n += m.Total.Size() } if m.Created != nil { l = m.Created.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } if m.Exemplar != nil { l = m.Exemplar.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *CounterValue_DoubleValue) Size() (n int) { if m == nil { return 0 } var l int _ = l n += 9 return n } func (m *CounterValue_IntValue) Size() (n int) { if m == nil { return 0 } var l int _ = l n += 1 + sovOpenmetrics(uint64(m.IntValue)) return n } func (m *HistogramValue) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.Sum != nil { n += m.Sum.Size() } if m.Count != 0 { n += 1 + sovOpenmetrics(uint64(m.Count)) } if m.Created != nil { l = m.Created.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } if len(m.Buckets) > 0 { for _, e := range m.Buckets { l = e.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *HistogramValue_DoubleValue) Size() (n int) { if m == nil { return 0 } var l int _ = l n += 9 return n } func (m *HistogramValue_IntValue) Size() (n int) { if m == nil { return 0 } var l int _ = l n += 1 + sovOpenmetrics(uint64(m.IntValue)) return n } func (m *HistogramValue_Bucket) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.Count != 0 { n += 1 + sovOpenmetrics(uint64(m.Count)) } if m.UpperBound != 0 { n += 9 } if m.Exemplar != nil { l = m.Exemplar.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *Exemplar) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.Value != 0 { n += 9 } if m.Timestamp != nil { l = m.Timestamp.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } if len(m.Label) > 0 { for _, e := range m.Label { l = e.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *StateSetValue) Size() (n int) { if m == nil { return 0 } var l int _ = l if len(m.States) > 0 { for _, e := range m.States { l = e.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *StateSetValue_State) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.Enabled { n += 2 } l = len(m.Name) if l > 0 { n += 1 + l + sovOpenmetrics(uint64(l)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *InfoValue) Size() (n int) { if m == nil { return 0 } var l int _ = l if len(m.Info) > 0 { for _, e := range m.Info { l = e.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *SummaryValue) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.Sum != nil { n += m.Sum.Size() } if m.Count != 0 { n += 1 + sovOpenmetrics(uint64(m.Count)) } if m.Created != nil { l = m.Created.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } if len(m.Quantile) > 0 { for _, e := range m.Quantile { l = e.Size() n += 1 + l + sovOpenmetrics(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *SummaryValue_DoubleValue) Size() (n int) { if m == nil { return 0 } var l int _ = l n += 9 return n } func (m *SummaryValue_IntValue) Size() (n int) { if m == nil { return 0 } var l int _ = l n += 1 + sovOpenmetrics(uint64(m.IntValue)) return n } func (m *SummaryValue_Quantile) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.Quantile != 0 { n += 9 } if m.Value != 0 { n += 9 } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func sovOpenmetrics(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } func sozOpenmetrics(x uint64) (n int) { return sovOpenmetrics(uint64((x << 1) ^ uint64((int64(x) >> 63)))) } func (m *MetricSet) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: MetricSet: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: MetricSet: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field MetricFamilies", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } m.MetricFamilies = append(m.MetricFamilies, &MetricFamily{}) if err := m.MetricFamilies[len(m.MetricFamilies)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipOpenmetrics(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthOpenmetrics } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *MetricFamily) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: MetricFamily: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: MetricFamily: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } m.Name = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field Type", wireType) } m.Type = 0 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ m.Type |= MetricType(b&0x7F) << shift if b < 0x80 { break } } case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Unit", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } m.Unit = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 4: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Help", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } m.Help = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 5: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Metrics", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } m.Metrics = append(m.Metrics, &Metric{}) if err := m.Metrics[len(m.Metrics)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipOpenmetrics(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthOpenmetrics } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *Metric) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: Metric: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: Metric: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Labels", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } m.Labels = append(m.Labels, &Label{}) if err := m.Labels[len(m.Labels)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field MetricPoints", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } m.MetricPoints = append(m.MetricPoints, &MetricPoint{}) if err := m.MetricPoints[len(m.MetricPoints)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipOpenmetrics(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthOpenmetrics } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *Label) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: Label: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: Label: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } m.Name = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } m.Value = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipOpenmetrics(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthOpenmetrics } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *MetricPoint) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: MetricPoint: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: MetricPoint: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field UnknownValue", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } v := &UnknownValue{} if err := v.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } m.Value = &MetricPoint_UnknownValue{v} iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field GaugeValue", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } v := &GaugeValue{} if err := v.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } m.Value = &MetricPoint_GaugeValue{v} iNdEx = postIndex case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field CounterValue", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } v := &CounterValue{} if err := v.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } m.Value = &MetricPoint_CounterValue{v} iNdEx = postIndex case 4: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field HistogramValue", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } v := &HistogramValue{} if err := v.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } m.Value = &MetricPoint_HistogramValue{v} iNdEx = postIndex case 5: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field StateSetValue", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } v := &StateSetValue{} if err := v.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } m.Value = &MetricPoint_StateSetValue{v} iNdEx = postIndex case 6: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field InfoValue", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } v := &InfoValue{} if err := v.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } m.Value = &MetricPoint_InfoValue{v} iNdEx = postIndex case 7: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field SummaryValue", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } v := &SummaryValue{} if err := v.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } m.Value = &MetricPoint_SummaryValue{v} iNdEx = postIndex case 8: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Timestamp", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } if m.Timestamp == nil { m.Timestamp = &types.Timestamp{} } if err := m.Timestamp.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipOpenmetrics(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthOpenmetrics } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *UnknownValue) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: UnknownValue: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: UnknownValue: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 1 { return fmt.Errorf("proto: wrong wireType = %d for field DoubleValue", wireType) } var v uint64 if (iNdEx + 8) > l { return io.ErrUnexpectedEOF } v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) iNdEx += 8 m.Value = &UnknownValue_DoubleValue{float64(math.Float64frombits(v))} case 2: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field IntValue", wireType) } var v int64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= int64(b&0x7F) << shift if b < 0x80 { break } } m.Value = &UnknownValue_IntValue{v} default: iNdEx = preIndex skippy, err := skipOpenmetrics(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthOpenmetrics } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *GaugeValue) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GaugeValue: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GaugeValue: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 1 { return fmt.Errorf("proto: wrong wireType = %d for field DoubleValue", wireType) } var v uint64 if (iNdEx + 8) > l { return io.ErrUnexpectedEOF } v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) iNdEx += 8 m.Value = &GaugeValue_DoubleValue{float64(math.Float64frombits(v))} case 2: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field IntValue", wireType) } var v int64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= int64(b&0x7F) << shift if b < 0x80 { break } } m.Value = &GaugeValue_IntValue{v} default: iNdEx = preIndex skippy, err := skipOpenmetrics(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthOpenmetrics } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *CounterValue) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: CounterValue: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: CounterValue: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 1 { return fmt.Errorf("proto: wrong wireType = %d for field DoubleValue", wireType) } var v uint64 if (iNdEx + 8) > l { return io.ErrUnexpectedEOF } v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) iNdEx += 8 m.Total = &CounterValue_DoubleValue{float64(math.Float64frombits(v))} case 2: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field IntValue", wireType) } var v uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= uint64(b&0x7F) << shift if b < 0x80 { break } } m.Total = &CounterValue_IntValue{v} case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Created", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } if m.Created == nil { m.Created = &types.Timestamp{} } if err := m.Created.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 4: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Exemplar", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } if m.Exemplar == nil { m.Exemplar = &Exemplar{} } if err := m.Exemplar.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipOpenmetrics(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthOpenmetrics } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *HistogramValue) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: HistogramValue: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: HistogramValue: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 1 { return fmt.Errorf("proto: wrong wireType = %d for field DoubleValue", wireType) } var v uint64 if (iNdEx + 8) > l { return io.ErrUnexpectedEOF } v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) iNdEx += 8 m.Sum = &HistogramValue_DoubleValue{float64(math.Float64frombits(v))} case 2: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field IntValue", wireType) } var v int64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= int64(b&0x7F) << shift if b < 0x80 { break } } m.Sum = &HistogramValue_IntValue{v} case 3: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field Count", wireType) } m.Count = 0 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ m.Count |= uint64(b&0x7F) << shift if b < 0x80 { break } } case 4: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Created", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } if m.Created == nil { m.Created = &types.Timestamp{} } if err := m.Created.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 5: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Buckets", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } m.Buckets = append(m.Buckets, &HistogramValue_Bucket{}) if err := m.Buckets[len(m.Buckets)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipOpenmetrics(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthOpenmetrics } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *HistogramValue_Bucket) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: Bucket: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: Bucket: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field Count", wireType) } m.Count = 0 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ m.Count |= uint64(b&0x7F) << shift if b < 0x80 { break } } case 2: if wireType != 1 { return fmt.Errorf("proto: wrong wireType = %d for field UpperBound", wireType) } var v uint64 if (iNdEx + 8) > l { return io.ErrUnexpectedEOF } v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) iNdEx += 8 m.UpperBound = float64(math.Float64frombits(v)) case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Exemplar", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } if m.Exemplar == nil { m.Exemplar = &Exemplar{} } if err := m.Exemplar.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipOpenmetrics(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthOpenmetrics } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *Exemplar) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: Exemplar: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: Exemplar: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 1 { return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) } var v uint64 if (iNdEx + 8) > l { return io.ErrUnexpectedEOF } v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) iNdEx += 8 m.Value = float64(math.Float64frombits(v)) case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Timestamp", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } if m.Timestamp == nil { m.Timestamp = &types.Timestamp{} } if err := m.Timestamp.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Label", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } m.Label = append(m.Label, &Label{}) if err := m.Label[len(m.Label)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipOpenmetrics(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthOpenmetrics } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *StateSetValue) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: StateSetValue: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: StateSetValue: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field States", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } m.States = append(m.States, &StateSetValue_State{}) if err := m.States[len(m.States)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipOpenmetrics(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthOpenmetrics } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *StateSetValue_State) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: State: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: State: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field Enabled", wireType) } var v int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= int(b&0x7F) << shift if b < 0x80 { break } } m.Enabled = bool(v != 0) case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } m.Name = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipOpenmetrics(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthOpenmetrics } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *InfoValue) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: InfoValue: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: InfoValue: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Info", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } m.Info = append(m.Info, &Label{}) if err := m.Info[len(m.Info)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipOpenmetrics(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthOpenmetrics } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *SummaryValue) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: SummaryValue: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: SummaryValue: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 1 { return fmt.Errorf("proto: wrong wireType = %d for field DoubleValue", wireType) } var v uint64 if (iNdEx + 8) > l { return io.ErrUnexpectedEOF } v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) iNdEx += 8 m.Sum = &SummaryValue_DoubleValue{float64(math.Float64frombits(v))} case 2: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field IntValue", wireType) } var v int64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= int64(b&0x7F) << shift if b < 0x80 { break } } m.Sum = &SummaryValue_IntValue{v} case 3: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field Count", wireType) } m.Count = 0 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ m.Count |= uint64(b&0x7F) << shift if b < 0x80 { break } } case 4: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Created", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } if m.Created == nil { m.Created = &types.Timestamp{} } if err := m.Created.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 5: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Quantile", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthOpenmetrics } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthOpenmetrics } if postIndex > l { return io.ErrUnexpectedEOF } m.Quantile = append(m.Quantile, &SummaryValue_Quantile{}) if err := m.Quantile[len(m.Quantile)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipOpenmetrics(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthOpenmetrics } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *SummaryValue_Quantile) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowOpenmetrics } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: Quantile: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: Quantile: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 1 { return fmt.Errorf("proto: wrong wireType = %d for field Quantile", wireType) } var v uint64 if (iNdEx + 8) > l { return io.ErrUnexpectedEOF } v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) iNdEx += 8 m.Quantile = float64(math.Float64frombits(v)) case 2: if wireType != 1 { return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) } var v uint64 if (iNdEx + 8) > l { return io.ErrUnexpectedEOF } v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) iNdEx += 8 m.Value = float64(math.Float64frombits(v)) default: iNdEx = preIndex skippy, err := skipOpenmetrics(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthOpenmetrics } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func skipOpenmetrics(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 depth := 0 for iNdEx < l { var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowOpenmetrics } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } wireType := int(wire & 0x7) switch wireType { case 0: for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowOpenmetrics } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } iNdEx++ if dAtA[iNdEx-1] < 0x80 { break } } case 1: iNdEx += 8 case 2: var length int for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowOpenmetrics } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ length |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if length < 0 { return 0, ErrInvalidLengthOpenmetrics } iNdEx += length case 3: depth++ case 4: if depth == 0 { return 0, ErrUnexpectedEndOfGroupOpenmetrics } depth-- case 5: iNdEx += 4 default: return 0, fmt.Errorf("proto: illegal wireType %d", wireType) } if iNdEx < 0 { return 0, ErrInvalidLengthOpenmetrics } if depth == 0 { return iNdEx, nil } } return 0, io.ErrUnexpectedEOF } var ( ErrInvalidLengthOpenmetrics = fmt.Errorf("proto: negative length found during unmarshaling") ErrIntOverflowOpenmetrics = fmt.Errorf("proto: integer overflow") ErrUnexpectedEndOfGroupOpenmetrics = fmt.Errorf("proto: unexpected end of group") ) ================================================ FILE: internal/proto-gen/api_v2/metrics/otelspankind.pb.go ================================================ // Code generated by protoc-gen-gogo. DO NOT EDIT. // source: otelspankind.proto package metrics import ( fmt "fmt" _ "github.com/gogo/protobuf/gogoproto" proto "github.com/gogo/protobuf/proto" math "math" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package // SpanKind is the type of span. Can be used to specify additional relationships between spans // in addition to a parent/child relationship. type SpanKind int32 const ( // Unspecified. Do NOT use as default. // Implementations MAY assume SpanKind to be INTERNAL when receiving UNSPECIFIED. SpanKind_SPAN_KIND_UNSPECIFIED SpanKind = 0 // Indicates that the span represents an internal operation within an application, // as opposed to an operation happening at the boundaries. Default value. SpanKind_SPAN_KIND_INTERNAL SpanKind = 1 // Indicates that the span covers server-side handling of an RPC or other // remote network request. SpanKind_SPAN_KIND_SERVER SpanKind = 2 // Indicates that the span describes a request to some remote service. SpanKind_SPAN_KIND_CLIENT SpanKind = 3 // Indicates that the span describes a producer sending a message to a broker. // Unlike CLIENT and SERVER, there is often no direct critical path latency relationship // between producer and consumer spans. A PRODUCER span ends when the message was accepted // by the broker while the logical processing of the message might span a much longer time. SpanKind_SPAN_KIND_PRODUCER SpanKind = 4 // Indicates that the span describes consumer receiving a message from a broker. // Like the PRODUCER kind, there is often no direct critical path latency relationship // between producer and consumer spans. SpanKind_SPAN_KIND_CONSUMER SpanKind = 5 ) var SpanKind_name = map[int32]string{ 0: "SPAN_KIND_UNSPECIFIED", 1: "SPAN_KIND_INTERNAL", 2: "SPAN_KIND_SERVER", 3: "SPAN_KIND_CLIENT", 4: "SPAN_KIND_PRODUCER", 5: "SPAN_KIND_CONSUMER", } var SpanKind_value = map[string]int32{ "SPAN_KIND_UNSPECIFIED": 0, "SPAN_KIND_INTERNAL": 1, "SPAN_KIND_SERVER": 2, "SPAN_KIND_CLIENT": 3, "SPAN_KIND_PRODUCER": 4, "SPAN_KIND_CONSUMER": 5, } func (x SpanKind) String() string { return proto.EnumName(SpanKind_name, int32(x)) } func (SpanKind) EnumDescriptor() ([]byte, []int) { return fileDescriptor_77f837d0289d1179, []int{0} } func init() { proto.RegisterEnum("jaeger.api_v2.metrics.SpanKind", SpanKind_name, SpanKind_value) } func init() { proto.RegisterFile("otelspankind.proto", fileDescriptor_77f837d0289d1179) } var fileDescriptor_77f837d0289d1179 = []byte{ // 224 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0xca, 0x2f, 0x49, 0xcd, 0x29, 0x2e, 0x48, 0xcc, 0xcb, 0xce, 0xcc, 0x4b, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x12, 0xcd, 0x4a, 0x4c, 0x4d, 0x4f, 0x2d, 0xd2, 0x4b, 0x2c, 0xc8, 0x8c, 0x2f, 0x33, 0xd2, 0xcb, 0x4d, 0x2d, 0x29, 0xca, 0x4c, 0x2e, 0x96, 0x12, 0x49, 0xcf, 0x4f, 0xcf, 0x07, 0xab, 0xd0, 0x07, 0xb1, 0x20, 0x8a, 0xb5, 0x66, 0x32, 0x72, 0x71, 0x04, 0x17, 0x24, 0xe6, 0x79, 0x67, 0xe6, 0xa5, 0x08, 0x49, 0x72, 0x89, 0x06, 0x07, 0x38, 0xfa, 0xc5, 0x7b, 0x7b, 0xfa, 0xb9, 0xc4, 0x87, 0xfa, 0x05, 0x07, 0xb8, 0x3a, 0x7b, 0xba, 0x79, 0xba, 0xba, 0x08, 0x30, 0x08, 0x89, 0x71, 0x09, 0x21, 0xa4, 0x3c, 0xfd, 0x42, 0x5c, 0x83, 0xfc, 0x1c, 0x7d, 0x04, 0x18, 0x85, 0x44, 0xb8, 0x04, 0x10, 0xe2, 0xc1, 0xae, 0x41, 0x61, 0xae, 0x41, 0x02, 0x4c, 0xa8, 0xa2, 0xce, 0x3e, 0x9e, 0xae, 0x7e, 0x21, 0x02, 0xcc, 0xa8, 0x66, 0x04, 0x04, 0xf9, 0xbb, 0x84, 0x3a, 0xbb, 0x06, 0x09, 0xb0, 0xa0, 0x8a, 0x3b, 0xfb, 0xfb, 0x05, 0x87, 0xfa, 0xba, 0x06, 0x09, 0xb0, 0x3a, 0x99, 0x9d, 0x78, 0x24, 0xc7, 0x78, 0xe1, 0x91, 0x1c, 0xe3, 0x83, 0x47, 0x72, 0x8c, 0x5c, 0xf2, 0x99, 0xf9, 0x7a, 0x10, 0x9f, 0x95, 0x14, 0x25, 0x26, 0x67, 0xe6, 0xa5, 0xa3, 0x79, 0x30, 0x8a, 0x1d, 0xca, 0x48, 0x62, 0x03, 0x7b, 0xcd, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x87, 0x34, 0x50, 0x06, 0x1d, 0x01, 0x00, 0x00, } ================================================ FILE: internal/proto-gen/patch.sed ================================================ 0,/import "google\/protobuf\/.*.proto";/{ s|import "google/protobuf/.*.proto";|&\ \ import "gogoproto/gogo.proto";\ \ option (gogoproto.marshaler_all) = true;\ option (gogoproto.unmarshaler_all) = true;\ option (gogoproto.sizer_all) = true;\ | } s|google.protobuf.Timestamp \(.*\);|google.protobuf.Timestamp \1 \ [\ (gogoproto.nullable) = false,\ (gogoproto.stdtime) = true\ ];|g s|google.protobuf.Duration \(.*\);|google.protobuf.Duration \1 \ [\ (gogoproto.nullable) = false,\ (gogoproto.stdduration) = true\ ];|g ================================================ FILE: internal/proto-gen/storage/v2/dependency_storage.pb.go ================================================ // Code generated by protoc-gen-gogo. DO NOT EDIT. // source: dependency_storage.proto package storage import ( context "context" fmt "fmt" _ "github.com/gogo/protobuf/gogoproto" proto "github.com/gogo/protobuf/proto" _ "github.com/gogo/protobuf/types" github_com_gogo_protobuf_types "github.com/gogo/protobuf/types" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" io "io" math "math" math_bits "math/bits" time "time" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf var _ = time.Kitchen // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package type GetDependenciesRequest struct { // start_time is the start of the time interval to search for the dependencies. StartTime time.Time `protobuf:"bytes,1,opt,name=start_time,json=startTime,proto3,stdtime" json:"start_time"` // end_time is the end of the time interval to search for the dependencies. EndTime time.Time `protobuf:"bytes,2,opt,name=end_time,json=endTime,proto3,stdtime" json:"end_time"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GetDependenciesRequest) Reset() { *m = GetDependenciesRequest{} } func (m *GetDependenciesRequest) String() string { return proto.CompactTextString(m) } func (*GetDependenciesRequest) ProtoMessage() {} func (*GetDependenciesRequest) Descriptor() ([]byte, []int) { return fileDescriptor_17393f3b58692e2b, []int{0} } func (m *GetDependenciesRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GetDependenciesRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GetDependenciesRequest.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GetDependenciesRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_GetDependenciesRequest.Merge(m, src) } func (m *GetDependenciesRequest) XXX_Size() int { return m.Size() } func (m *GetDependenciesRequest) XXX_DiscardUnknown() { xxx_messageInfo_GetDependenciesRequest.DiscardUnknown(m) } var xxx_messageInfo_GetDependenciesRequest proto.InternalMessageInfo func (m *GetDependenciesRequest) GetStartTime() time.Time { if m != nil { return m.StartTime } return time.Time{} } func (m *GetDependenciesRequest) GetEndTime() time.Time { if m != nil { return m.EndTime } return time.Time{} } // Dependency represents a relationship between two services. type Dependency struct { // parent is the name of the caller service. Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` // child is the name of the service being called. Child string `protobuf:"bytes,2,opt,name=child,proto3" json:"child,omitempty"` // call_count is the number of times the parent service called the child service. CallCount uint64 `protobuf:"varint,3,opt,name=call_count,json=callCount,proto3" json:"call_count,omitempty"` // source contains the origin from where the dependency was extracted. Source string `protobuf:"bytes,4,opt,name=source,proto3" json:"source,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *Dependency) Reset() { *m = Dependency{} } func (m *Dependency) String() string { return proto.CompactTextString(m) } func (*Dependency) ProtoMessage() {} func (*Dependency) Descriptor() ([]byte, []int) { return fileDescriptor_17393f3b58692e2b, []int{1} } func (m *Dependency) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *Dependency) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_Dependency.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *Dependency) XXX_Merge(src proto.Message) { xxx_messageInfo_Dependency.Merge(m, src) } func (m *Dependency) XXX_Size() int { return m.Size() } func (m *Dependency) XXX_DiscardUnknown() { xxx_messageInfo_Dependency.DiscardUnknown(m) } var xxx_messageInfo_Dependency proto.InternalMessageInfo func (m *Dependency) GetParent() string { if m != nil { return m.Parent } return "" } func (m *Dependency) GetChild() string { if m != nil { return m.Child } return "" } func (m *Dependency) GetCallCount() uint64 { if m != nil { return m.CallCount } return 0 } func (m *Dependency) GetSource() string { if m != nil { return m.Source } return "" } type GetDependenciesResponse struct { Dependencies []*Dependency `protobuf:"bytes,1,rep,name=dependencies,proto3" json:"dependencies,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GetDependenciesResponse) Reset() { *m = GetDependenciesResponse{} } func (m *GetDependenciesResponse) String() string { return proto.CompactTextString(m) } func (*GetDependenciesResponse) ProtoMessage() {} func (*GetDependenciesResponse) Descriptor() ([]byte, []int) { return fileDescriptor_17393f3b58692e2b, []int{2} } func (m *GetDependenciesResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GetDependenciesResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GetDependenciesResponse.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GetDependenciesResponse) XXX_Merge(src proto.Message) { xxx_messageInfo_GetDependenciesResponse.Merge(m, src) } func (m *GetDependenciesResponse) XXX_Size() int { return m.Size() } func (m *GetDependenciesResponse) XXX_DiscardUnknown() { xxx_messageInfo_GetDependenciesResponse.DiscardUnknown(m) } var xxx_messageInfo_GetDependenciesResponse proto.InternalMessageInfo func (m *GetDependenciesResponse) GetDependencies() []*Dependency { if m != nil { return m.Dependencies } return nil } func init() { proto.RegisterType((*GetDependenciesRequest)(nil), "jaeger.storage.v2.GetDependenciesRequest") proto.RegisterType((*Dependency)(nil), "jaeger.storage.v2.Dependency") proto.RegisterType((*GetDependenciesResponse)(nil), "jaeger.storage.v2.GetDependenciesResponse") } func init() { proto.RegisterFile("dependency_storage.proto", fileDescriptor_17393f3b58692e2b) } var fileDescriptor_17393f3b58692e2b = []byte{ // 349 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x51, 0xcd, 0x4e, 0xc2, 0x40, 0x10, 0x76, 0x05, 0x81, 0x0e, 0x26, 0xea, 0x06, 0xb1, 0x69, 0xc2, 0x4f, 0x38, 0xa1, 0x87, 0x92, 0xd4, 0x07, 0x30, 0x82, 0x89, 0xf7, 0xc6, 0x93, 0x31, 0x21, 0xa5, 0x1d, 0x0b, 0xa6, 0x74, 0xcb, 0xee, 0xd6, 0x84, 0xc4, 0x87, 0xf0, 0x09, 0x7c, 0x1e, 0x8e, 0x3e, 0x81, 0x1a, 0x9e, 0xc4, 0x6c, 0x97, 0x2a, 0x0a, 0x07, 0xbd, 0xcd, 0x7c, 0x33, 0xdf, 0xf7, 0xcd, 0xee, 0x07, 0x66, 0x80, 0x09, 0xc6, 0x01, 0xc6, 0xfe, 0x7c, 0x28, 0x24, 0xe3, 0x5e, 0x88, 0x76, 0xc2, 0x99, 0x64, 0xf4, 0xe8, 0xc1, 0xc3, 0x10, 0xb9, 0x9d, 0xa3, 0x8f, 0x8e, 0xd5, 0x0a, 0x19, 0x0b, 0x23, 0xec, 0x65, 0x0b, 0xa3, 0xf4, 0xbe, 0x27, 0x27, 0x53, 0x14, 0xd2, 0x9b, 0x26, 0x9a, 0x63, 0xd5, 0x42, 0x16, 0xb2, 0xac, 0xec, 0xa9, 0x4a, 0xa3, 0x9d, 0x17, 0x02, 0xf5, 0x6b, 0x94, 0x57, 0xb9, 0xd3, 0x04, 0x85, 0x8b, 0xb3, 0x14, 0x85, 0xa4, 0x03, 0x00, 0x21, 0x3d, 0x2e, 0x87, 0x4a, 0xc9, 0x24, 0x6d, 0xd2, 0xad, 0x3a, 0x96, 0xad, 0x6d, 0xec, 0xdc, 0xc6, 0xbe, 0xc9, 0x6d, 0xfa, 0x95, 0xc5, 0x5b, 0x6b, 0xe7, 0xf9, 0xbd, 0x45, 0x5c, 0x23, 0xe3, 0xa9, 0x09, 0xbd, 0x80, 0x0a, 0xc6, 0x81, 0x96, 0xd8, 0xfd, 0x87, 0x44, 0x19, 0xe3, 0x40, 0xe1, 0x9d, 0x19, 0xc0, 0xd7, 0x71, 0x73, 0x5a, 0x87, 0x52, 0xe2, 0x71, 0x8c, 0x65, 0x76, 0x8f, 0xe1, 0xae, 0x3a, 0x5a, 0x83, 0x3d, 0x7f, 0x3c, 0x89, 0x82, 0xcc, 0xc3, 0x70, 0x75, 0x43, 0x1b, 0x00, 0xbe, 0x17, 0x45, 0x43, 0x9f, 0xa5, 0xb1, 0x34, 0x0b, 0x6d, 0xd2, 0x2d, 0xba, 0x86, 0x42, 0x06, 0x0a, 0x50, 0x62, 0x82, 0xa5, 0xdc, 0x47, 0xb3, 0xa8, 0xc5, 0x74, 0xd7, 0xb9, 0x83, 0x93, 0x8d, 0x2f, 0x11, 0x09, 0x8b, 0x05, 0xd2, 0x4b, 0xd8, 0x0f, 0xd6, 0x70, 0x93, 0xb4, 0x0b, 0xdd, 0xaa, 0xd3, 0xb0, 0x37, 0xf2, 0xb0, 0xbf, 0x8f, 0x76, 0x7f, 0x50, 0x9c, 0x27, 0x38, 0x5c, 0x9b, 0xa1, 0x17, 0x20, 0xa7, 0x63, 0x38, 0xf8, 0xe5, 0x48, 0x4f, 0xb7, 0x68, 0x6e, 0x0f, 0xca, 0x3a, 0xfb, 0xcb, 0xaa, 0x7e, 0x40, 0xff, 0x78, 0xb1, 0x6c, 0x92, 0xd7, 0x65, 0x93, 0x7c, 0x2c, 0x9b, 0xe4, 0xb6, 0xbc, 0x62, 0x8c, 0x4a, 0x59, 0x18, 0xe7, 0x9f, 0x01, 0x00, 0x00, 0xff, 0xff, 0xa9, 0xde, 0x4d, 0xdd, 0x73, 0x02, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. var _ context.Context var _ grpc.ClientConn // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. const _ = grpc.SupportPackageIsVersion4 // DependencyReaderClient is the client API for DependencyReader service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type DependencyReaderClient interface { // GetDependencies loads service dependencies from storage. GetDependencies(ctx context.Context, in *GetDependenciesRequest, opts ...grpc.CallOption) (*GetDependenciesResponse, error) } type dependencyReaderClient struct { cc *grpc.ClientConn } func NewDependencyReaderClient(cc *grpc.ClientConn) DependencyReaderClient { return &dependencyReaderClient{cc} } func (c *dependencyReaderClient) GetDependencies(ctx context.Context, in *GetDependenciesRequest, opts ...grpc.CallOption) (*GetDependenciesResponse, error) { out := new(GetDependenciesResponse) err := c.cc.Invoke(ctx, "/jaeger.storage.v2.DependencyReader/GetDependencies", in, out, opts...) if err != nil { return nil, err } return out, nil } // DependencyReaderServer is the server API for DependencyReader service. type DependencyReaderServer interface { // GetDependencies loads service dependencies from storage. GetDependencies(context.Context, *GetDependenciesRequest) (*GetDependenciesResponse, error) } // UnimplementedDependencyReaderServer can be embedded to have forward compatible implementations. type UnimplementedDependencyReaderServer struct { } func (*UnimplementedDependencyReaderServer) GetDependencies(ctx context.Context, req *GetDependenciesRequest) (*GetDependenciesResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetDependencies not implemented") } func RegisterDependencyReaderServer(s *grpc.Server, srv DependencyReaderServer) { s.RegisterService(&_DependencyReader_serviceDesc, srv) } func _DependencyReader_GetDependencies_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetDependenciesRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DependencyReaderServer).GetDependencies(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/jaeger.storage.v2.DependencyReader/GetDependencies", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DependencyReaderServer).GetDependencies(ctx, req.(*GetDependenciesRequest)) } return interceptor(ctx, in, info, handler) } var _DependencyReader_serviceDesc = grpc.ServiceDesc{ ServiceName: "jaeger.storage.v2.DependencyReader", HandlerType: (*DependencyReaderServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "GetDependencies", Handler: _DependencyReader_GetDependencies_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "dependency_storage.proto", } func (m *GetDependenciesRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GetDependenciesRequest) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GetDependenciesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } n1, err1 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.EndTime, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.EndTime):]) if err1 != nil { return 0, err1 } i -= n1 i = encodeVarintDependencyStorage(dAtA, i, uint64(n1)) i-- dAtA[i] = 0x12 n2, err2 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.StartTime, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTime):]) if err2 != nil { return 0, err2 } i -= n2 i = encodeVarintDependencyStorage(dAtA, i, uint64(n2)) i-- dAtA[i] = 0xa return len(dAtA) - i, nil } func (m *Dependency) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *Dependency) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *Dependency) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.Source) > 0 { i -= len(m.Source) copy(dAtA[i:], m.Source) i = encodeVarintDependencyStorage(dAtA, i, uint64(len(m.Source))) i-- dAtA[i] = 0x22 } if m.CallCount != 0 { i = encodeVarintDependencyStorage(dAtA, i, uint64(m.CallCount)) i-- dAtA[i] = 0x18 } if len(m.Child) > 0 { i -= len(m.Child) copy(dAtA[i:], m.Child) i = encodeVarintDependencyStorage(dAtA, i, uint64(len(m.Child))) i-- dAtA[i] = 0x12 } if len(m.Parent) > 0 { i -= len(m.Parent) copy(dAtA[i:], m.Parent) i = encodeVarintDependencyStorage(dAtA, i, uint64(len(m.Parent))) i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *GetDependenciesResponse) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GetDependenciesResponse) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GetDependenciesResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.Dependencies) > 0 { for iNdEx := len(m.Dependencies) - 1; iNdEx >= 0; iNdEx-- { { size, err := m.Dependencies[iNdEx].MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintDependencyStorage(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } } return len(dAtA) - i, nil } func encodeVarintDependencyStorage(dAtA []byte, offset int, v uint64) int { offset -= sovDependencyStorage(v) base := offset for v >= 1<<7 { dAtA[offset] = uint8(v&0x7f | 0x80) v >>= 7 offset++ } dAtA[offset] = uint8(v) return base } func (m *GetDependenciesRequest) Size() (n int) { if m == nil { return 0 } var l int _ = l l = github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTime) n += 1 + l + sovDependencyStorage(uint64(l)) l = github_com_gogo_protobuf_types.SizeOfStdTime(m.EndTime) n += 1 + l + sovDependencyStorage(uint64(l)) if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *Dependency) Size() (n int) { if m == nil { return 0 } var l int _ = l l = len(m.Parent) if l > 0 { n += 1 + l + sovDependencyStorage(uint64(l)) } l = len(m.Child) if l > 0 { n += 1 + l + sovDependencyStorage(uint64(l)) } if m.CallCount != 0 { n += 1 + sovDependencyStorage(uint64(m.CallCount)) } l = len(m.Source) if l > 0 { n += 1 + l + sovDependencyStorage(uint64(l)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *GetDependenciesResponse) Size() (n int) { if m == nil { return 0 } var l int _ = l if len(m.Dependencies) > 0 { for _, e := range m.Dependencies { l = e.Size() n += 1 + l + sovDependencyStorage(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func sovDependencyStorage(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } func sozDependencyStorage(x uint64) (n int) { return sovDependencyStorage(uint64((x << 1) ^ uint64((int64(x) >> 63)))) } func (m *GetDependenciesRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowDependencyStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GetDependenciesRequest: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GetDependenciesRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field StartTime", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowDependencyStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthDependencyStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthDependencyStorage } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.StartTime, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field EndTime", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowDependencyStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthDependencyStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthDependencyStorage } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.EndTime, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipDependencyStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthDependencyStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *Dependency) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowDependencyStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: Dependency: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: Dependency: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Parent", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowDependencyStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthDependencyStorage } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthDependencyStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.Parent = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Child", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowDependencyStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthDependencyStorage } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthDependencyStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.Child = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 3: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field CallCount", wireType) } m.CallCount = 0 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowDependencyStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ m.CallCount |= uint64(b&0x7F) << shift if b < 0x80 { break } } case 4: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Source", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowDependencyStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthDependencyStorage } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthDependencyStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.Source = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipDependencyStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthDependencyStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *GetDependenciesResponse) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowDependencyStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GetDependenciesResponse: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GetDependenciesResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Dependencies", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowDependencyStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthDependencyStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthDependencyStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.Dependencies = append(m.Dependencies, &Dependency{}) if err := m.Dependencies[len(m.Dependencies)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipDependencyStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthDependencyStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func skipDependencyStorage(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 depth := 0 for iNdEx < l { var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowDependencyStorage } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } wireType := int(wire & 0x7) switch wireType { case 0: for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowDependencyStorage } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } iNdEx++ if dAtA[iNdEx-1] < 0x80 { break } } case 1: iNdEx += 8 case 2: var length int for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowDependencyStorage } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ length |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if length < 0 { return 0, ErrInvalidLengthDependencyStorage } iNdEx += length case 3: depth++ case 4: if depth == 0 { return 0, ErrUnexpectedEndOfGroupDependencyStorage } depth-- case 5: iNdEx += 4 default: return 0, fmt.Errorf("proto: illegal wireType %d", wireType) } if iNdEx < 0 { return 0, ErrInvalidLengthDependencyStorage } if depth == 0 { return iNdEx, nil } } return 0, io.ErrUnexpectedEOF } var ( ErrInvalidLengthDependencyStorage = fmt.Errorf("proto: negative length found during unmarshaling") ErrIntOverflowDependencyStorage = fmt.Errorf("proto: integer overflow") ErrUnexpectedEndOfGroupDependencyStorage = fmt.Errorf("proto: unexpected end of group") ) ================================================ FILE: internal/proto-gen/storage/v2/trace_storage.pb.go ================================================ // Code generated by protoc-gen-gogo. DO NOT EDIT. // source: trace_storage.proto package storage import ( context "context" encoding_binary "encoding/binary" fmt "fmt" _ "github.com/gogo/protobuf/gogoproto" proto "github.com/gogo/protobuf/proto" _ "github.com/gogo/protobuf/types" github_com_gogo_protobuf_types "github.com/gogo/protobuf/types" v1 "github.com/jaegertracing/jaeger/internal/jptrace" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" io "io" math "math" math_bits "math/bits" time "time" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf var _ = time.Kitchen // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package // GetTraceParams represents the query for a single trace from the storage backend. type GetTraceParams struct { // trace_id is a 16 byte array containing the unique identifier for the trace to query. TraceId []byte `protobuf:"bytes,1,opt,name=trace_id,json=traceId,proto3" json:"trace_id,omitempty"` // start_time is the start of the time interval to search for the trace_id. // // This field is optional. StartTime time.Time `protobuf:"bytes,2,opt,name=start_time,json=startTime,proto3,stdtime" json:"start_time"` // end_time is the end of the time interval to search for the trace_id. // // This field is optional. EndTime time.Time `protobuf:"bytes,3,opt,name=end_time,json=endTime,proto3,stdtime" json:"end_time"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GetTraceParams) Reset() { *m = GetTraceParams{} } func (m *GetTraceParams) String() string { return proto.CompactTextString(m) } func (*GetTraceParams) ProtoMessage() {} func (*GetTraceParams) Descriptor() ([]byte, []int) { return fileDescriptor_3441c0fd9397413c, []int{0} } func (m *GetTraceParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GetTraceParams) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GetTraceParams.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GetTraceParams) XXX_Merge(src proto.Message) { xxx_messageInfo_GetTraceParams.Merge(m, src) } func (m *GetTraceParams) XXX_Size() int { return m.Size() } func (m *GetTraceParams) XXX_DiscardUnknown() { xxx_messageInfo_GetTraceParams.DiscardUnknown(m) } var xxx_messageInfo_GetTraceParams proto.InternalMessageInfo func (m *GetTraceParams) GetTraceId() []byte { if m != nil { return m.TraceId } return nil } func (m *GetTraceParams) GetStartTime() time.Time { if m != nil { return m.StartTime } return time.Time{} } func (m *GetTraceParams) GetEndTime() time.Time { if m != nil { return m.EndTime } return time.Time{} } // GetTracesRequest represents a request to retrieve multiple traces. type GetTracesRequest struct { Query []*GetTraceParams `protobuf:"bytes,1,rep,name=query,proto3" json:"query,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GetTracesRequest) Reset() { *m = GetTracesRequest{} } func (m *GetTracesRequest) String() string { return proto.CompactTextString(m) } func (*GetTracesRequest) ProtoMessage() {} func (*GetTracesRequest) Descriptor() ([]byte, []int) { return fileDescriptor_3441c0fd9397413c, []int{1} } func (m *GetTracesRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GetTracesRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GetTracesRequest.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GetTracesRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_GetTracesRequest.Merge(m, src) } func (m *GetTracesRequest) XXX_Size() int { return m.Size() } func (m *GetTracesRequest) XXX_DiscardUnknown() { xxx_messageInfo_GetTracesRequest.DiscardUnknown(m) } var xxx_messageInfo_GetTracesRequest proto.InternalMessageInfo func (m *GetTracesRequest) GetQuery() []*GetTraceParams { if m != nil { return m.Query } return nil } // GetServicesRequest represents a request to get service names. type GetServicesRequest struct { XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GetServicesRequest) Reset() { *m = GetServicesRequest{} } func (m *GetServicesRequest) String() string { return proto.CompactTextString(m) } func (*GetServicesRequest) ProtoMessage() {} func (*GetServicesRequest) Descriptor() ([]byte, []int) { return fileDescriptor_3441c0fd9397413c, []int{2} } func (m *GetServicesRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GetServicesRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GetServicesRequest.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GetServicesRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_GetServicesRequest.Merge(m, src) } func (m *GetServicesRequest) XXX_Size() int { return m.Size() } func (m *GetServicesRequest) XXX_DiscardUnknown() { xxx_messageInfo_GetServicesRequest.DiscardUnknown(m) } var xxx_messageInfo_GetServicesRequest proto.InternalMessageInfo // GetServicesResponse represents the response for GetServicesRequest. type GetServicesResponse struct { Services []string `protobuf:"bytes,1,rep,name=services,proto3" json:"services,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GetServicesResponse) Reset() { *m = GetServicesResponse{} } func (m *GetServicesResponse) String() string { return proto.CompactTextString(m) } func (*GetServicesResponse) ProtoMessage() {} func (*GetServicesResponse) Descriptor() ([]byte, []int) { return fileDescriptor_3441c0fd9397413c, []int{3} } func (m *GetServicesResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GetServicesResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GetServicesResponse.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GetServicesResponse) XXX_Merge(src proto.Message) { xxx_messageInfo_GetServicesResponse.Merge(m, src) } func (m *GetServicesResponse) XXX_Size() int { return m.Size() } func (m *GetServicesResponse) XXX_DiscardUnknown() { xxx_messageInfo_GetServicesResponse.DiscardUnknown(m) } var xxx_messageInfo_GetServicesResponse proto.InternalMessageInfo func (m *GetServicesResponse) GetServices() []string { if m != nil { return m.Services } return nil } // GetOperationsRequest represents a request to get operation names. type GetOperationsRequest struct { // service is the name of the service for which to get operation names. // // This field is required. Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` // span_kind is the type of span which is used to distinguish between // spans generated in a particular context. // // This field is optional. SpanKind string `protobuf:"bytes,2,opt,name=span_kind,json=spanKind,proto3" json:"span_kind,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GetOperationsRequest) Reset() { *m = GetOperationsRequest{} } func (m *GetOperationsRequest) String() string { return proto.CompactTextString(m) } func (*GetOperationsRequest) ProtoMessage() {} func (*GetOperationsRequest) Descriptor() ([]byte, []int) { return fileDescriptor_3441c0fd9397413c, []int{4} } func (m *GetOperationsRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GetOperationsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GetOperationsRequest.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GetOperationsRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_GetOperationsRequest.Merge(m, src) } func (m *GetOperationsRequest) XXX_Size() int { return m.Size() } func (m *GetOperationsRequest) XXX_DiscardUnknown() { xxx_messageInfo_GetOperationsRequest.DiscardUnknown(m) } var xxx_messageInfo_GetOperationsRequest proto.InternalMessageInfo func (m *GetOperationsRequest) GetService() string { if m != nil { return m.Service } return "" } func (m *GetOperationsRequest) GetSpanKind() string { if m != nil { return m.SpanKind } return "" } // Operation contains information about an operation for a given service. type Operation struct { Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` SpanKind string `protobuf:"bytes,2,opt,name=span_kind,json=spanKind,proto3" json:"span_kind,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *Operation) Reset() { *m = Operation{} } func (m *Operation) String() string { return proto.CompactTextString(m) } func (*Operation) ProtoMessage() {} func (*Operation) Descriptor() ([]byte, []int) { return fileDescriptor_3441c0fd9397413c, []int{5} } func (m *Operation) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *Operation) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_Operation.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *Operation) XXX_Merge(src proto.Message) { xxx_messageInfo_Operation.Merge(m, src) } func (m *Operation) XXX_Size() int { return m.Size() } func (m *Operation) XXX_DiscardUnknown() { xxx_messageInfo_Operation.DiscardUnknown(m) } var xxx_messageInfo_Operation proto.InternalMessageInfo func (m *Operation) GetName() string { if m != nil { return m.Name } return "" } func (m *Operation) GetSpanKind() string { if m != nil { return m.SpanKind } return "" } // GetOperationsResponse represents the response for GetOperationsRequest. type GetOperationsResponse struct { Operations []*Operation `protobuf:"bytes,1,rep,name=operations,proto3" json:"operations,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GetOperationsResponse) Reset() { *m = GetOperationsResponse{} } func (m *GetOperationsResponse) String() string { return proto.CompactTextString(m) } func (*GetOperationsResponse) ProtoMessage() {} func (*GetOperationsResponse) Descriptor() ([]byte, []int) { return fileDescriptor_3441c0fd9397413c, []int{6} } func (m *GetOperationsResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GetOperationsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GetOperationsResponse.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GetOperationsResponse) XXX_Merge(src proto.Message) { xxx_messageInfo_GetOperationsResponse.Merge(m, src) } func (m *GetOperationsResponse) XXX_Size() int { return m.Size() } func (m *GetOperationsResponse) XXX_DiscardUnknown() { xxx_messageInfo_GetOperationsResponse.DiscardUnknown(m) } var xxx_messageInfo_GetOperationsResponse proto.InternalMessageInfo func (m *GetOperationsResponse) GetOperations() []*Operation { if m != nil { return m.Operations } return nil } // KeyValue and all its associated types are copied from opentelemetry-proto/common/v1/common.proto // (https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/common/v1/common.proto). // This type is used to store attributes in traces. type KeyValue struct { Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` Value *AnyValue `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *KeyValue) Reset() { *m = KeyValue{} } func (m *KeyValue) String() string { return proto.CompactTextString(m) } func (*KeyValue) ProtoMessage() {} func (*KeyValue) Descriptor() ([]byte, []int) { return fileDescriptor_3441c0fd9397413c, []int{7} } func (m *KeyValue) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *KeyValue) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_KeyValue.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *KeyValue) XXX_Merge(src proto.Message) { xxx_messageInfo_KeyValue.Merge(m, src) } func (m *KeyValue) XXX_Size() int { return m.Size() } func (m *KeyValue) XXX_DiscardUnknown() { xxx_messageInfo_KeyValue.DiscardUnknown(m) } var xxx_messageInfo_KeyValue proto.InternalMessageInfo func (m *KeyValue) GetKey() string { if m != nil { return m.Key } return "" } func (m *KeyValue) GetValue() *AnyValue { if m != nil { return m.Value } return nil } type AnyValue struct { // Types that are valid to be assigned to Value: // *AnyValue_StringValue // *AnyValue_BoolValue // *AnyValue_IntValue // *AnyValue_DoubleValue // *AnyValue_ArrayValue // *AnyValue_KvlistValue // *AnyValue_BytesValue Value isAnyValue_Value `protobuf_oneof:"value"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *AnyValue) Reset() { *m = AnyValue{} } func (m *AnyValue) String() string { return proto.CompactTextString(m) } func (*AnyValue) ProtoMessage() {} func (*AnyValue) Descriptor() ([]byte, []int) { return fileDescriptor_3441c0fd9397413c, []int{8} } func (m *AnyValue) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *AnyValue) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_AnyValue.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *AnyValue) XXX_Merge(src proto.Message) { xxx_messageInfo_AnyValue.Merge(m, src) } func (m *AnyValue) XXX_Size() int { return m.Size() } func (m *AnyValue) XXX_DiscardUnknown() { xxx_messageInfo_AnyValue.DiscardUnknown(m) } var xxx_messageInfo_AnyValue proto.InternalMessageInfo type isAnyValue_Value interface { isAnyValue_Value() MarshalTo([]byte) (int, error) Size() int } type AnyValue_StringValue struct { StringValue string `protobuf:"bytes,1,opt,name=string_value,json=stringValue,proto3,oneof" json:"string_value,omitempty"` } type AnyValue_BoolValue struct { BoolValue bool `protobuf:"varint,2,opt,name=bool_value,json=boolValue,proto3,oneof" json:"bool_value,omitempty"` } type AnyValue_IntValue struct { IntValue int64 `protobuf:"varint,3,opt,name=int_value,json=intValue,proto3,oneof" json:"int_value,omitempty"` } type AnyValue_DoubleValue struct { DoubleValue float64 `protobuf:"fixed64,4,opt,name=double_value,json=doubleValue,proto3,oneof" json:"double_value,omitempty"` } type AnyValue_ArrayValue struct { ArrayValue *ArrayValue `protobuf:"bytes,5,opt,name=array_value,json=arrayValue,proto3,oneof" json:"array_value,omitempty"` } type AnyValue_KvlistValue struct { KvlistValue *KeyValueList `protobuf:"bytes,6,opt,name=kvlist_value,json=kvlistValue,proto3,oneof" json:"kvlist_value,omitempty"` } type AnyValue_BytesValue struct { BytesValue []byte `protobuf:"bytes,7,opt,name=bytes_value,json=bytesValue,proto3,oneof" json:"bytes_value,omitempty"` } func (*AnyValue_StringValue) isAnyValue_Value() {} func (*AnyValue_BoolValue) isAnyValue_Value() {} func (*AnyValue_IntValue) isAnyValue_Value() {} func (*AnyValue_DoubleValue) isAnyValue_Value() {} func (*AnyValue_ArrayValue) isAnyValue_Value() {} func (*AnyValue_KvlistValue) isAnyValue_Value() {} func (*AnyValue_BytesValue) isAnyValue_Value() {} func (m *AnyValue) GetValue() isAnyValue_Value { if m != nil { return m.Value } return nil } func (m *AnyValue) GetStringValue() string { if x, ok := m.GetValue().(*AnyValue_StringValue); ok { return x.StringValue } return "" } func (m *AnyValue) GetBoolValue() bool { if x, ok := m.GetValue().(*AnyValue_BoolValue); ok { return x.BoolValue } return false } func (m *AnyValue) GetIntValue() int64 { if x, ok := m.GetValue().(*AnyValue_IntValue); ok { return x.IntValue } return 0 } func (m *AnyValue) GetDoubleValue() float64 { if x, ok := m.GetValue().(*AnyValue_DoubleValue); ok { return x.DoubleValue } return 0 } func (m *AnyValue) GetArrayValue() *ArrayValue { if x, ok := m.GetValue().(*AnyValue_ArrayValue); ok { return x.ArrayValue } return nil } func (m *AnyValue) GetKvlistValue() *KeyValueList { if x, ok := m.GetValue().(*AnyValue_KvlistValue); ok { return x.KvlistValue } return nil } func (m *AnyValue) GetBytesValue() []byte { if x, ok := m.GetValue().(*AnyValue_BytesValue); ok { return x.BytesValue } return nil } // XXX_OneofWrappers is for the internal use of the proto package. func (*AnyValue) XXX_OneofWrappers() []interface{} { return []interface{}{ (*AnyValue_StringValue)(nil), (*AnyValue_BoolValue)(nil), (*AnyValue_IntValue)(nil), (*AnyValue_DoubleValue)(nil), (*AnyValue_ArrayValue)(nil), (*AnyValue_KvlistValue)(nil), (*AnyValue_BytesValue)(nil), } } type KeyValueList struct { Values []*KeyValue `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *KeyValueList) Reset() { *m = KeyValueList{} } func (m *KeyValueList) String() string { return proto.CompactTextString(m) } func (*KeyValueList) ProtoMessage() {} func (*KeyValueList) Descriptor() ([]byte, []int) { return fileDescriptor_3441c0fd9397413c, []int{9} } func (m *KeyValueList) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *KeyValueList) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_KeyValueList.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *KeyValueList) XXX_Merge(src proto.Message) { xxx_messageInfo_KeyValueList.Merge(m, src) } func (m *KeyValueList) XXX_Size() int { return m.Size() } func (m *KeyValueList) XXX_DiscardUnknown() { xxx_messageInfo_KeyValueList.DiscardUnknown(m) } var xxx_messageInfo_KeyValueList proto.InternalMessageInfo func (m *KeyValueList) GetValues() []*KeyValue { if m != nil { return m.Values } return nil } type ArrayValue struct { Values []*AnyValue `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *ArrayValue) Reset() { *m = ArrayValue{} } func (m *ArrayValue) String() string { return proto.CompactTextString(m) } func (*ArrayValue) ProtoMessage() {} func (*ArrayValue) Descriptor() ([]byte, []int) { return fileDescriptor_3441c0fd9397413c, []int{10} } func (m *ArrayValue) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *ArrayValue) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_ArrayValue.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *ArrayValue) XXX_Merge(src proto.Message) { xxx_messageInfo_ArrayValue.Merge(m, src) } func (m *ArrayValue) XXX_Size() int { return m.Size() } func (m *ArrayValue) XXX_DiscardUnknown() { xxx_messageInfo_ArrayValue.DiscardUnknown(m) } var xxx_messageInfo_ArrayValue proto.InternalMessageInfo func (m *ArrayValue) GetValues() []*AnyValue { if m != nil { return m.Values } return nil } // TraceQueryParameters contains query parameters to find traces. For a detailed // definition of each field in this message, refer to `TraceQueryParameters` in `jaeger.api_v3` // (https://github.com/jaegertracing/jaeger-idl/blob/main/proto/api_v3/query_service.proto). type TraceQueryParameters struct { ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` OperationName string `protobuf:"bytes,2,opt,name=operation_name,json=operationName,proto3" json:"operation_name,omitempty"` Attributes []*KeyValue `protobuf:"bytes,3,rep,name=attributes,proto3" json:"attributes,omitempty"` StartTimeMin time.Time `protobuf:"bytes,4,opt,name=start_time_min,json=startTimeMin,proto3,stdtime" json:"start_time_min"` StartTimeMax time.Time `protobuf:"bytes,5,opt,name=start_time_max,json=startTimeMax,proto3,stdtime" json:"start_time_max"` DurationMin time.Duration `protobuf:"bytes,6,opt,name=duration_min,json=durationMin,proto3,stdduration" json:"duration_min"` DurationMax time.Duration `protobuf:"bytes,7,opt,name=duration_max,json=durationMax,proto3,stdduration" json:"duration_max"` SearchDepth int32 `protobuf:"varint,8,opt,name=search_depth,json=searchDepth,proto3" json:"search_depth,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *TraceQueryParameters) Reset() { *m = TraceQueryParameters{} } func (m *TraceQueryParameters) String() string { return proto.CompactTextString(m) } func (*TraceQueryParameters) ProtoMessage() {} func (*TraceQueryParameters) Descriptor() ([]byte, []int) { return fileDescriptor_3441c0fd9397413c, []int{11} } func (m *TraceQueryParameters) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *TraceQueryParameters) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_TraceQueryParameters.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *TraceQueryParameters) XXX_Merge(src proto.Message) { xxx_messageInfo_TraceQueryParameters.Merge(m, src) } func (m *TraceQueryParameters) XXX_Size() int { return m.Size() } func (m *TraceQueryParameters) XXX_DiscardUnknown() { xxx_messageInfo_TraceQueryParameters.DiscardUnknown(m) } var xxx_messageInfo_TraceQueryParameters proto.InternalMessageInfo func (m *TraceQueryParameters) GetServiceName() string { if m != nil { return m.ServiceName } return "" } func (m *TraceQueryParameters) GetOperationName() string { if m != nil { return m.OperationName } return "" } func (m *TraceQueryParameters) GetAttributes() []*KeyValue { if m != nil { return m.Attributes } return nil } func (m *TraceQueryParameters) GetStartTimeMin() time.Time { if m != nil { return m.StartTimeMin } return time.Time{} } func (m *TraceQueryParameters) GetStartTimeMax() time.Time { if m != nil { return m.StartTimeMax } return time.Time{} } func (m *TraceQueryParameters) GetDurationMin() time.Duration { if m != nil { return m.DurationMin } return 0 } func (m *TraceQueryParameters) GetDurationMax() time.Duration { if m != nil { return m.DurationMax } return 0 } func (m *TraceQueryParameters) GetSearchDepth() int32 { if m != nil { return m.SearchDepth } return 0 } // FindTracesRequest represents a request to find traces. // It can be used to retrieve the traces (FindTraces) or simply // the trace IDs (FindTraceIDs). type FindTracesRequest struct { Query *TraceQueryParameters `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *FindTracesRequest) Reset() { *m = FindTracesRequest{} } func (m *FindTracesRequest) String() string { return proto.CompactTextString(m) } func (*FindTracesRequest) ProtoMessage() {} func (*FindTracesRequest) Descriptor() ([]byte, []int) { return fileDescriptor_3441c0fd9397413c, []int{12} } func (m *FindTracesRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *FindTracesRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_FindTracesRequest.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *FindTracesRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_FindTracesRequest.Merge(m, src) } func (m *FindTracesRequest) XXX_Size() int { return m.Size() } func (m *FindTracesRequest) XXX_DiscardUnknown() { xxx_messageInfo_FindTracesRequest.DiscardUnknown(m) } var xxx_messageInfo_FindTracesRequest proto.InternalMessageInfo func (m *FindTracesRequest) GetQuery() *TraceQueryParameters { if m != nil { return m.Query } return nil } // FoundTraceID is a wrapper around trace ID returned from FindTraceIDs // with an optional time range that may be used in GetTraces calls. // // The time range is provided as an optimization hint for some storage backends // that can perform more efficient queries when they know the approximate time range. // The value should not be used for precise time-based filtering or assumptions. // It is meant as a rough boundary and may not be populated in all cases. type FoundTraceID struct { TraceId []byte `protobuf:"bytes,1,opt,name=trace_id,json=traceId,proto3" json:"trace_id,omitempty"` Start time.Time `protobuf:"bytes,2,opt,name=start,proto3,stdtime" json:"start"` End time.Time `protobuf:"bytes,3,opt,name=end,proto3,stdtime" json:"end"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *FoundTraceID) Reset() { *m = FoundTraceID{} } func (m *FoundTraceID) String() string { return proto.CompactTextString(m) } func (*FoundTraceID) ProtoMessage() {} func (*FoundTraceID) Descriptor() ([]byte, []int) { return fileDescriptor_3441c0fd9397413c, []int{13} } func (m *FoundTraceID) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *FoundTraceID) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_FoundTraceID.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *FoundTraceID) XXX_Merge(src proto.Message) { xxx_messageInfo_FoundTraceID.Merge(m, src) } func (m *FoundTraceID) XXX_Size() int { return m.Size() } func (m *FoundTraceID) XXX_DiscardUnknown() { xxx_messageInfo_FoundTraceID.DiscardUnknown(m) } var xxx_messageInfo_FoundTraceID proto.InternalMessageInfo func (m *FoundTraceID) GetTraceId() []byte { if m != nil { return m.TraceId } return nil } func (m *FoundTraceID) GetStart() time.Time { if m != nil { return m.Start } return time.Time{} } func (m *FoundTraceID) GetEnd() time.Time { if m != nil { return m.End } return time.Time{} } // FindTraceIDsResponse represents the response for FindTracesRequest. type FindTraceIDsResponse struct { TraceIds []*FoundTraceID `protobuf:"bytes,1,rep,name=trace_ids,json=traceIds,proto3" json:"trace_ids,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *FindTraceIDsResponse) Reset() { *m = FindTraceIDsResponse{} } func (m *FindTraceIDsResponse) String() string { return proto.CompactTextString(m) } func (*FindTraceIDsResponse) ProtoMessage() {} func (*FindTraceIDsResponse) Descriptor() ([]byte, []int) { return fileDescriptor_3441c0fd9397413c, []int{14} } func (m *FindTraceIDsResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *FindTraceIDsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_FindTraceIDsResponse.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *FindTraceIDsResponse) XXX_Merge(src proto.Message) { xxx_messageInfo_FindTraceIDsResponse.Merge(m, src) } func (m *FindTraceIDsResponse) XXX_Size() int { return m.Size() } func (m *FindTraceIDsResponse) XXX_DiscardUnknown() { xxx_messageInfo_FindTraceIDsResponse.DiscardUnknown(m) } var xxx_messageInfo_FindTraceIDsResponse proto.InternalMessageInfo func (m *FindTraceIDsResponse) GetTraceIds() []*FoundTraceID { if m != nil { return m.TraceIds } return nil } func init() { proto.RegisterType((*GetTraceParams)(nil), "jaeger.storage.v2.GetTraceParams") proto.RegisterType((*GetTracesRequest)(nil), "jaeger.storage.v2.GetTracesRequest") proto.RegisterType((*GetServicesRequest)(nil), "jaeger.storage.v2.GetServicesRequest") proto.RegisterType((*GetServicesResponse)(nil), "jaeger.storage.v2.GetServicesResponse") proto.RegisterType((*GetOperationsRequest)(nil), "jaeger.storage.v2.GetOperationsRequest") proto.RegisterType((*Operation)(nil), "jaeger.storage.v2.Operation") proto.RegisterType((*GetOperationsResponse)(nil), "jaeger.storage.v2.GetOperationsResponse") proto.RegisterType((*KeyValue)(nil), "jaeger.storage.v2.KeyValue") proto.RegisterType((*AnyValue)(nil), "jaeger.storage.v2.AnyValue") proto.RegisterType((*KeyValueList)(nil), "jaeger.storage.v2.KeyValueList") proto.RegisterType((*ArrayValue)(nil), "jaeger.storage.v2.ArrayValue") proto.RegisterType((*TraceQueryParameters)(nil), "jaeger.storage.v2.TraceQueryParameters") proto.RegisterType((*FindTracesRequest)(nil), "jaeger.storage.v2.FindTracesRequest") proto.RegisterType((*FoundTraceID)(nil), "jaeger.storage.v2.FoundTraceID") proto.RegisterType((*FindTraceIDsResponse)(nil), "jaeger.storage.v2.FindTraceIDsResponse") } func init() { proto.RegisterFile("trace_storage.proto", fileDescriptor_3441c0fd9397413c) } var fileDescriptor_3441c0fd9397413c = []byte{ // 968 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x55, 0xdd, 0x72, 0xdb, 0x44, 0x14, 0xb6, 0xea, 0x3a, 0x96, 0x8e, 0xd4, 0x4c, 0xbb, 0x75, 0x67, 0x5c, 0x87, 0xc6, 0x8e, 0x4a, 0x89, 0xaf, 0x64, 0x92, 0xce, 0xc0, 0x0c, 0x94, 0x81, 0xa6, 0x9e, 0xfc, 0x10, 0x4a, 0x41, 0x04, 0x2e, 0xb8, 0xa8, 0x58, 0x57, 0x8b, 0x2b, 0x62, 0xaf, 0x5c, 0xed, 0xda, 0x63, 0xbf, 0x05, 0x97, 0xdc, 0xf0, 0x10, 0xbc, 0x45, 0x2e, 0x79, 0x02, 0x60, 0x72, 0xc5, 0x13, 0x70, 0xcd, 0xec, 0x8f, 0x64, 0xd9, 0xd1, 0x38, 0xf1, 0xdd, 0xea, 0xd3, 0x77, 0xbe, 0xf3, 0xb3, 0x67, 0xcf, 0x81, 0xfb, 0x3c, 0xc1, 0x6f, 0x48, 0xc0, 0x78, 0x9c, 0xe0, 0x3e, 0xf1, 0x46, 0x49, 0xcc, 0x63, 0x74, 0xef, 0x17, 0x4c, 0xfa, 0x24, 0xf1, 0x52, 0x74, 0xb2, 0xdf, 0xd8, 0xee, 0xc7, 0x71, 0x7f, 0x40, 0x3a, 0x92, 0xd0, 0x1b, 0xff, 0xdc, 0x09, 0xc7, 0x09, 0xe6, 0x51, 0x4c, 0x95, 0x49, 0xa3, 0xd6, 0x8f, 0xfb, 0xb1, 0x3c, 0x76, 0xc4, 0x49, 0xa3, 0xcd, 0x65, 0x2b, 0x1e, 0x0d, 0x09, 0xe3, 0x78, 0x38, 0xd2, 0x84, 0x76, 0x3c, 0x22, 0x94, 0x93, 0x01, 0x19, 0x12, 0x9e, 0xcc, 0x14, 0xaf, 0x23, 0x43, 0xea, 0x4c, 0xf6, 0xd4, 0x41, 0x31, 0xdd, 0x3f, 0x0c, 0xd8, 0x3c, 0x22, 0xfc, 0x4c, 0x40, 0xdf, 0xe0, 0x04, 0x0f, 0x19, 0x7a, 0x08, 0xa6, 0x8a, 0x3e, 0x0a, 0xeb, 0x46, 0xcb, 0x68, 0x3b, 0x7e, 0x55, 0x7e, 0x9f, 0x84, 0xe8, 0x05, 0x00, 0xe3, 0x38, 0xe1, 0x81, 0x70, 0x58, 0xbf, 0xd5, 0x32, 0xda, 0xf6, 0x7e, 0xc3, 0x53, 0xd1, 0x78, 0x69, 0x34, 0xde, 0x59, 0x1a, 0xcd, 0x81, 0x79, 0xf1, 0x57, 0xb3, 0xf4, 0xeb, 0xdf, 0x4d, 0xc3, 0xb7, 0xa4, 0x9d, 0xf8, 0x83, 0x3e, 0x07, 0x93, 0xd0, 0x50, 0x49, 0x94, 0xd7, 0x90, 0xa8, 0x12, 0x1a, 0x0a, 0xdc, 0x3d, 0x85, 0xbb, 0x69, 0xc8, 0xcc, 0x27, 0xef, 0xc6, 0x84, 0x71, 0xf4, 0x31, 0x54, 0xde, 0x8d, 0x49, 0x32, 0xab, 0x1b, 0xad, 0x72, 0xdb, 0xde, 0xdf, 0xf1, 0xae, 0xd4, 0xda, 0x5b, 0x4c, 0xd3, 0x57, 0x7c, 0xb7, 0x06, 0xe8, 0x88, 0xf0, 0xef, 0x48, 0x32, 0x89, 0xe6, 0x72, 0xee, 0x1e, 0xdc, 0x5f, 0x40, 0xd9, 0x28, 0xa6, 0x8c, 0xa0, 0x06, 0x98, 0x4c, 0x63, 0xd2, 0x91, 0xe5, 0x67, 0xdf, 0xee, 0x4b, 0xa8, 0x1d, 0x11, 0xfe, 0x6a, 0x44, 0xd4, 0x05, 0x66, 0x91, 0xd5, 0xa1, 0xaa, 0x39, 0xb2, 0x9a, 0x96, 0x9f, 0x7e, 0xa2, 0x2d, 0xb0, 0xd8, 0x08, 0xd3, 0xe0, 0x3c, 0xa2, 0xa1, 0x2c, 0xa6, 0x90, 0x1b, 0x61, 0x7a, 0x1a, 0xd1, 0xd0, 0x7d, 0x06, 0x56, 0xa6, 0x85, 0x10, 0xdc, 0xa6, 0x78, 0x98, 0x0a, 0xc8, 0xf3, 0x6a, 0xeb, 0xef, 0xe1, 0xc1, 0x52, 0x30, 0x3a, 0x83, 0x67, 0x00, 0x71, 0x86, 0xea, 0x62, 0xbd, 0x57, 0x50, 0xac, 0xcc, 0xd4, 0xcf, 0xf1, 0xdd, 0x57, 0x60, 0x9e, 0x92, 0xd9, 0x0f, 0x78, 0x30, 0x26, 0xe8, 0x2e, 0x94, 0xcf, 0xc9, 0x4c, 0x87, 0x24, 0x8e, 0x68, 0x0f, 0x2a, 0x13, 0xf1, 0x4b, 0x37, 0xc6, 0x56, 0x81, 0xec, 0x73, 0xaa, 0xac, 0x7d, 0xc5, 0x74, 0x2f, 0x6e, 0x81, 0x99, 0x62, 0xe8, 0x31, 0x38, 0x8c, 0x27, 0x11, 0xed, 0x07, 0x4a, 0x46, 0x4a, 0x1f, 0x97, 0x7c, 0x5b, 0xa1, 0x8a, 0xd4, 0x04, 0xe8, 0xc5, 0xf1, 0x20, 0x98, 0x7b, 0x32, 0x8f, 0x4b, 0xbe, 0x25, 0x30, 0x45, 0x78, 0x04, 0x56, 0x44, 0xb9, 0xfe, 0x2f, 0xfa, 0xab, 0x7c, 0x5c, 0xf2, 0xcd, 0x88, 0xf2, 0xcc, 0x49, 0x18, 0x8f, 0x7b, 0x03, 0xa2, 0x19, 0xb7, 0x5b, 0x46, 0xdb, 0x10, 0x4e, 0x14, 0xaa, 0x48, 0x5f, 0x80, 0x8d, 0x93, 0x04, 0xcf, 0x34, 0xa7, 0x22, 0xf3, 0x79, 0x54, 0x94, 0x8f, 0x60, 0x49, 0x9b, 0xe3, 0x92, 0x0f, 0x38, 0xfb, 0x42, 0x5d, 0x70, 0xce, 0x27, 0x83, 0x88, 0xa5, 0x81, 0x6c, 0x48, 0x89, 0x66, 0x81, 0x44, 0x5a, 0xd0, 0xaf, 0x22, 0xc6, 0x45, 0x1c, 0xca, 0x4c, 0xa9, 0xec, 0x80, 0xdd, 0x9b, 0x71, 0xc2, 0xb4, 0x48, 0x55, 0xbc, 0x46, 0xe1, 0x48, 0x82, 0x92, 0x72, 0x50, 0xd5, 0x45, 0x77, 0x5f, 0x80, 0x93, 0x97, 0x42, 0x4f, 0x61, 0x43, 0xfe, 0x48, 0x6f, 0x79, 0x6b, 0x85, 0x6f, 0x5f, 0x53, 0xdd, 0xe7, 0x00, 0xf3, 0x94, 0x6e, 0x24, 0x91, 0xdd, 0x68, 0x2a, 0xf1, 0x6f, 0x19, 0x6a, 0xf2, 0x9d, 0x7d, 0x2b, 0xde, 0x97, 0x7c, 0x6c, 0x84, 0x93, 0x84, 0xa1, 0x1d, 0x70, 0x74, 0xe7, 0x07, 0xb9, 0x66, 0xb6, 0x35, 0xf6, 0xb5, 0xe8, 0xe9, 0x27, 0xb0, 0x99, 0x75, 0x9b, 0x22, 0xa9, 0xc6, 0xbe, 0x93, 0xa1, 0x92, 0xf6, 0x29, 0x00, 0xe6, 0x3c, 0x89, 0x7a, 0x63, 0x4e, 0x58, 0xbd, 0x7c, 0x7d, 0x7a, 0x39, 0x3a, 0xfa, 0x12, 0x36, 0xe7, 0x33, 0x2c, 0x18, 0x46, 0x54, 0xb6, 0xc0, 0x4d, 0x87, 0x90, 0x93, 0xcd, 0xb1, 0x97, 0x11, 0x5d, 0xd6, 0xc2, 0x53, 0xdd, 0x2a, 0x6b, 0x6b, 0xe1, 0x29, 0x3a, 0x04, 0x27, 0x1d, 0xfe, 0x32, 0x2a, 0xd5, 0x31, 0x0f, 0xaf, 0x28, 0x75, 0x35, 0x49, 0x09, 0xfd, 0x26, 0x84, 0xec, 0xd4, 0x50, 0xc4, 0xb4, 0xa0, 0x83, 0xa7, 0xb2, 0x69, 0xd6, 0xd6, 0xc1, 0x53, 0x75, 0x5d, 0x38, 0x79, 0xf3, 0x36, 0x08, 0xc9, 0x88, 0xbf, 0xad, 0x9b, 0x2d, 0xa3, 0x5d, 0x11, 0xd7, 0x25, 0xb0, 0xae, 0x80, 0x5c, 0x1f, 0xee, 0x1d, 0x46, 0x34, 0x5c, 0x9c, 0xc4, 0x9f, 0xcd, 0x27, 0xb1, 0x70, 0xbc, 0x5b, 0x70, 0x2f, 0x45, 0xed, 0x91, 0xce, 0xe3, 0xdf, 0x0d, 0x70, 0x0e, 0xe3, 0xb1, 0x56, 0x3d, 0xe9, 0xae, 0x5a, 0x47, 0x9f, 0x40, 0x45, 0x96, 0x70, 0xad, 0x4d, 0xa4, 0x4c, 0xd0, 0x47, 0x50, 0x26, 0x34, 0x5c, 0x6b, 0x01, 0x09, 0x03, 0xf7, 0x0c, 0x6a, 0x59, 0xce, 0x27, 0xdd, 0xfc, 0x60, 0xb5, 0xd2, 0x30, 0xd3, 0xe7, 0x52, 0xf4, 0xda, 0xf3, 0xa9, 0xf9, 0xa6, 0x4e, 0x84, 0xed, 0xff, 0x57, 0x06, 0x5b, 0xa2, 0x3e, 0xc1, 0x21, 0x49, 0xd0, 0x6b, 0xb0, 0xb2, 0x15, 0x87, 0x1e, 0xaf, 0x58, 0x66, 0x69, 0xd9, 0x1b, 0x6d, 0x6f, 0x61, 0xe7, 0xab, 0x4c, 0x3c, 0xb5, 0xea, 0x27, 0x7b, 0xaa, 0xe4, 0xac, 0x8b, 0x39, 0x76, 0x4b, 0x1f, 0x1a, 0xe8, 0x35, 0xd8, 0xb9, 0xfd, 0x86, 0x9e, 0x14, 0x7b, 0x58, 0xda, 0x8a, 0x8d, 0x0f, 0xae, 0xa3, 0xa9, 0x5a, 0xb8, 0x25, 0x14, 0xc2, 0x9d, 0x85, 0xfd, 0x83, 0x76, 0x8b, 0x4d, 0xaf, 0xac, 0xcb, 0x46, 0xfb, 0x7a, 0x62, 0xe6, 0xe5, 0x27, 0x80, 0x79, 0xff, 0xa1, 0xf7, 0x8b, 0xca, 0xbd, 0xdc, 0x9e, 0x6b, 0xd6, 0x29, 0x00, 0x27, 0x7f, 0xdb, 0x37, 0xf4, 0xb1, 0xbb, 0x8a, 0x95, 0x6b, 0x1a, 0xb7, 0x74, 0xf0, 0xe0, 0xe2, 0x72, 0xdb, 0xf8, 0xf3, 0x72, 0xdb, 0xf8, 0xe7, 0x72, 0xdb, 0xf8, 0xb1, 0xaa, 0x0d, 0x7a, 0x1b, 0x32, 0xac, 0xa7, 0xff, 0x07, 0x00, 0x00, 0xff, 0xff, 0xba, 0xc1, 0x9f, 0xcf, 0x48, 0x0a, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. var _ context.Context var _ grpc.ClientConn // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. const _ = grpc.SupportPackageIsVersion4 // TraceReaderClient is the client API for TraceReader service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type TraceReaderClient interface { // GetTraces returns a stream that retrieves all traces with given IDs. // // Chunking requirements: // - A single TracesData chunk MUST NOT contain spans from multiple traces. // - Large traces MAY be split across multiple, *consecutive* TracesData chunks. // - Each returned TracesData object MUST NOT be empty. // // Edge cases: // - If no spans are found for any given trace ID, the ID is ignored. // - If none of the trace IDs are found in the storage, an empty response is returned. GetTraces(ctx context.Context, in *GetTracesRequest, opts ...grpc.CallOption) (TraceReader_GetTracesClient, error) // GetServices returns all service names known to the backend from traces // within its retention period. GetServices(ctx context.Context, in *GetServicesRequest, opts ...grpc.CallOption) (*GetServicesResponse, error) // GetOperations returns all operation names for a given service // known to the backend from traces within its retention period. GetOperations(ctx context.Context, in *GetOperationsRequest, opts ...grpc.CallOption) (*GetOperationsResponse, error) // FindTraces returns a stream that retrieves traces matching query parameters. // // The chunking rules are the same as for GetTraces. // // If no matching traces are found, an empty stream is returned. FindTraces(ctx context.Context, in *FindTracesRequest, opts ...grpc.CallOption) (TraceReader_FindTracesClient, error) // FindTraceIDs returns a stream that retrieves IDs of traces matching query parameters. // // If no matching traces are found, an empty stream is returned. // // This call behaves identically to FindTraces, except that it returns only the list // of matching trace IDs. This is useful in some contexts, such as batch jobs, where a // large list of trace IDs may be queried first and then the full traces are loaded // in batches. FindTraceIDs(ctx context.Context, in *FindTracesRequest, opts ...grpc.CallOption) (*FindTraceIDsResponse, error) } type traceReaderClient struct { cc *grpc.ClientConn } func NewTraceReaderClient(cc *grpc.ClientConn) TraceReaderClient { return &traceReaderClient{cc} } func (c *traceReaderClient) GetTraces(ctx context.Context, in *GetTracesRequest, opts ...grpc.CallOption) (TraceReader_GetTracesClient, error) { stream, err := c.cc.NewStream(ctx, &_TraceReader_serviceDesc.Streams[0], "/jaeger.storage.v2.TraceReader/GetTraces", opts...) if err != nil { return nil, err } x := &traceReaderGetTracesClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type TraceReader_GetTracesClient interface { Recv() (*v1.TracesData, error) grpc.ClientStream } type traceReaderGetTracesClient struct { grpc.ClientStream } func (x *traceReaderGetTracesClient) Recv() (*v1.TracesData, error) { m := new(v1.TracesData) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *traceReaderClient) GetServices(ctx context.Context, in *GetServicesRequest, opts ...grpc.CallOption) (*GetServicesResponse, error) { out := new(GetServicesResponse) err := c.cc.Invoke(ctx, "/jaeger.storage.v2.TraceReader/GetServices", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *traceReaderClient) GetOperations(ctx context.Context, in *GetOperationsRequest, opts ...grpc.CallOption) (*GetOperationsResponse, error) { out := new(GetOperationsResponse) err := c.cc.Invoke(ctx, "/jaeger.storage.v2.TraceReader/GetOperations", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *traceReaderClient) FindTraces(ctx context.Context, in *FindTracesRequest, opts ...grpc.CallOption) (TraceReader_FindTracesClient, error) { stream, err := c.cc.NewStream(ctx, &_TraceReader_serviceDesc.Streams[1], "/jaeger.storage.v2.TraceReader/FindTraces", opts...) if err != nil { return nil, err } x := &traceReaderFindTracesClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type TraceReader_FindTracesClient interface { Recv() (*v1.TracesData, error) grpc.ClientStream } type traceReaderFindTracesClient struct { grpc.ClientStream } func (x *traceReaderFindTracesClient) Recv() (*v1.TracesData, error) { m := new(v1.TracesData) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *traceReaderClient) FindTraceIDs(ctx context.Context, in *FindTracesRequest, opts ...grpc.CallOption) (*FindTraceIDsResponse, error) { out := new(FindTraceIDsResponse) err := c.cc.Invoke(ctx, "/jaeger.storage.v2.TraceReader/FindTraceIDs", in, out, opts...) if err != nil { return nil, err } return out, nil } // TraceReaderServer is the server API for TraceReader service. type TraceReaderServer interface { // GetTraces returns a stream that retrieves all traces with given IDs. // // Chunking requirements: // - A single TracesData chunk MUST NOT contain spans from multiple traces. // - Large traces MAY be split across multiple, *consecutive* TracesData chunks. // - Each returned TracesData object MUST NOT be empty. // // Edge cases: // - If no spans are found for any given trace ID, the ID is ignored. // - If none of the trace IDs are found in the storage, an empty response is returned. GetTraces(*GetTracesRequest, TraceReader_GetTracesServer) error // GetServices returns all service names known to the backend from traces // within its retention period. GetServices(context.Context, *GetServicesRequest) (*GetServicesResponse, error) // GetOperations returns all operation names for a given service // known to the backend from traces within its retention period. GetOperations(context.Context, *GetOperationsRequest) (*GetOperationsResponse, error) // FindTraces returns a stream that retrieves traces matching query parameters. // // The chunking rules are the same as for GetTraces. // // If no matching traces are found, an empty stream is returned. FindTraces(*FindTracesRequest, TraceReader_FindTracesServer) error // FindTraceIDs returns a stream that retrieves IDs of traces matching query parameters. // // If no matching traces are found, an empty stream is returned. // // This call behaves identically to FindTraces, except that it returns only the list // of matching trace IDs. This is useful in some contexts, such as batch jobs, where a // large list of trace IDs may be queried first and then the full traces are loaded // in batches. FindTraceIDs(context.Context, *FindTracesRequest) (*FindTraceIDsResponse, error) } // UnimplementedTraceReaderServer can be embedded to have forward compatible implementations. type UnimplementedTraceReaderServer struct { } func (*UnimplementedTraceReaderServer) GetTraces(req *GetTracesRequest, srv TraceReader_GetTracesServer) error { return status.Errorf(codes.Unimplemented, "method GetTraces not implemented") } func (*UnimplementedTraceReaderServer) GetServices(ctx context.Context, req *GetServicesRequest) (*GetServicesResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetServices not implemented") } func (*UnimplementedTraceReaderServer) GetOperations(ctx context.Context, req *GetOperationsRequest) (*GetOperationsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetOperations not implemented") } func (*UnimplementedTraceReaderServer) FindTraces(req *FindTracesRequest, srv TraceReader_FindTracesServer) error { return status.Errorf(codes.Unimplemented, "method FindTraces not implemented") } func (*UnimplementedTraceReaderServer) FindTraceIDs(ctx context.Context, req *FindTracesRequest) (*FindTraceIDsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method FindTraceIDs not implemented") } func RegisterTraceReaderServer(s *grpc.Server, srv TraceReaderServer) { s.RegisterService(&_TraceReader_serviceDesc, srv) } func _TraceReader_GetTraces_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(GetTracesRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(TraceReaderServer).GetTraces(m, &traceReaderGetTracesServer{stream}) } type TraceReader_GetTracesServer interface { Send(*v1.TracesData) error grpc.ServerStream } type traceReaderGetTracesServer struct { grpc.ServerStream } func (x *traceReaderGetTracesServer) Send(m *v1.TracesData) error { return x.ServerStream.SendMsg(m) } func _TraceReader_GetServices_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetServicesRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(TraceReaderServer).GetServices(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/jaeger.storage.v2.TraceReader/GetServices", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(TraceReaderServer).GetServices(ctx, req.(*GetServicesRequest)) } return interceptor(ctx, in, info, handler) } func _TraceReader_GetOperations_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetOperationsRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(TraceReaderServer).GetOperations(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/jaeger.storage.v2.TraceReader/GetOperations", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(TraceReaderServer).GetOperations(ctx, req.(*GetOperationsRequest)) } return interceptor(ctx, in, info, handler) } func _TraceReader_FindTraces_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(FindTracesRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(TraceReaderServer).FindTraces(m, &traceReaderFindTracesServer{stream}) } type TraceReader_FindTracesServer interface { Send(*v1.TracesData) error grpc.ServerStream } type traceReaderFindTracesServer struct { grpc.ServerStream } func (x *traceReaderFindTracesServer) Send(m *v1.TracesData) error { return x.ServerStream.SendMsg(m) } func _TraceReader_FindTraceIDs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(FindTracesRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(TraceReaderServer).FindTraceIDs(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/jaeger.storage.v2.TraceReader/FindTraceIDs", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(TraceReaderServer).FindTraceIDs(ctx, req.(*FindTracesRequest)) } return interceptor(ctx, in, info, handler) } var _TraceReader_serviceDesc = grpc.ServiceDesc{ ServiceName: "jaeger.storage.v2.TraceReader", HandlerType: (*TraceReaderServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "GetServices", Handler: _TraceReader_GetServices_Handler, }, { MethodName: "GetOperations", Handler: _TraceReader_GetOperations_Handler, }, { MethodName: "FindTraceIDs", Handler: _TraceReader_FindTraceIDs_Handler, }, }, Streams: []grpc.StreamDesc{ { StreamName: "GetTraces", Handler: _TraceReader_GetTraces_Handler, ServerStreams: true, }, { StreamName: "FindTraces", Handler: _TraceReader_FindTraces_Handler, ServerStreams: true, }, }, Metadata: "trace_storage.proto", } func (m *GetTraceParams) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GetTraceParams) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GetTraceParams) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } n1, err1 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.EndTime, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.EndTime):]) if err1 != nil { return 0, err1 } i -= n1 i = encodeVarintTraceStorage(dAtA, i, uint64(n1)) i-- dAtA[i] = 0x1a n2, err2 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.StartTime, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTime):]) if err2 != nil { return 0, err2 } i -= n2 i = encodeVarintTraceStorage(dAtA, i, uint64(n2)) i-- dAtA[i] = 0x12 if len(m.TraceId) > 0 { i -= len(m.TraceId) copy(dAtA[i:], m.TraceId) i = encodeVarintTraceStorage(dAtA, i, uint64(len(m.TraceId))) i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *GetTracesRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GetTracesRequest) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GetTracesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.Query) > 0 { for iNdEx := len(m.Query) - 1; iNdEx >= 0; iNdEx-- { { size, err := m.Query[iNdEx].MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintTraceStorage(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } } return len(dAtA) - i, nil } func (m *GetServicesRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GetServicesRequest) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GetServicesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } return len(dAtA) - i, nil } func (m *GetServicesResponse) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GetServicesResponse) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GetServicesResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.Services) > 0 { for iNdEx := len(m.Services) - 1; iNdEx >= 0; iNdEx-- { i -= len(m.Services[iNdEx]) copy(dAtA[i:], m.Services[iNdEx]) i = encodeVarintTraceStorage(dAtA, i, uint64(len(m.Services[iNdEx]))) i-- dAtA[i] = 0xa } } return len(dAtA) - i, nil } func (m *GetOperationsRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GetOperationsRequest) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GetOperationsRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.SpanKind) > 0 { i -= len(m.SpanKind) copy(dAtA[i:], m.SpanKind) i = encodeVarintTraceStorage(dAtA, i, uint64(len(m.SpanKind))) i-- dAtA[i] = 0x12 } if len(m.Service) > 0 { i -= len(m.Service) copy(dAtA[i:], m.Service) i = encodeVarintTraceStorage(dAtA, i, uint64(len(m.Service))) i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *Operation) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *Operation) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *Operation) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.SpanKind) > 0 { i -= len(m.SpanKind) copy(dAtA[i:], m.SpanKind) i = encodeVarintTraceStorage(dAtA, i, uint64(len(m.SpanKind))) i-- dAtA[i] = 0x12 } if len(m.Name) > 0 { i -= len(m.Name) copy(dAtA[i:], m.Name) i = encodeVarintTraceStorage(dAtA, i, uint64(len(m.Name))) i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *GetOperationsResponse) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GetOperationsResponse) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GetOperationsResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.Operations) > 0 { for iNdEx := len(m.Operations) - 1; iNdEx >= 0; iNdEx-- { { size, err := m.Operations[iNdEx].MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintTraceStorage(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } } return len(dAtA) - i, nil } func (m *KeyValue) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *KeyValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *KeyValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if m.Value != nil { { size, err := m.Value.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintTraceStorage(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x12 } if len(m.Key) > 0 { i -= len(m.Key) copy(dAtA[i:], m.Key) i = encodeVarintTraceStorage(dAtA, i, uint64(len(m.Key))) i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *AnyValue) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *AnyValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *AnyValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if m.Value != nil { { size := m.Value.Size() i -= size if _, err := m.Value.MarshalTo(dAtA[i:]); err != nil { return 0, err } } } return len(dAtA) - i, nil } func (m *AnyValue_StringValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *AnyValue_StringValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) i -= len(m.StringValue) copy(dAtA[i:], m.StringValue) i = encodeVarintTraceStorage(dAtA, i, uint64(len(m.StringValue))) i-- dAtA[i] = 0xa return len(dAtA) - i, nil } func (m *AnyValue_BoolValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *AnyValue_BoolValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) i-- if m.BoolValue { dAtA[i] = 1 } else { dAtA[i] = 0 } i-- dAtA[i] = 0x10 return len(dAtA) - i, nil } func (m *AnyValue_IntValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *AnyValue_IntValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) i = encodeVarintTraceStorage(dAtA, i, uint64(m.IntValue)) i-- dAtA[i] = 0x18 return len(dAtA) - i, nil } func (m *AnyValue_DoubleValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *AnyValue_DoubleValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) i -= 8 encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(math.Float64bits(float64(m.DoubleValue)))) i-- dAtA[i] = 0x21 return len(dAtA) - i, nil } func (m *AnyValue_ArrayValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *AnyValue_ArrayValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) if m.ArrayValue != nil { { size, err := m.ArrayValue.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintTraceStorage(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x2a } return len(dAtA) - i, nil } func (m *AnyValue_KvlistValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *AnyValue_KvlistValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) if m.KvlistValue != nil { { size, err := m.KvlistValue.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintTraceStorage(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x32 } return len(dAtA) - i, nil } func (m *AnyValue_BytesValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *AnyValue_BytesValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) if m.BytesValue != nil { i -= len(m.BytesValue) copy(dAtA[i:], m.BytesValue) i = encodeVarintTraceStorage(dAtA, i, uint64(len(m.BytesValue))) i-- dAtA[i] = 0x3a } return len(dAtA) - i, nil } func (m *KeyValueList) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *KeyValueList) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *KeyValueList) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.Values) > 0 { for iNdEx := len(m.Values) - 1; iNdEx >= 0; iNdEx-- { { size, err := m.Values[iNdEx].MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintTraceStorage(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } } return len(dAtA) - i, nil } func (m *ArrayValue) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *ArrayValue) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *ArrayValue) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.Values) > 0 { for iNdEx := len(m.Values) - 1; iNdEx >= 0; iNdEx-- { { size, err := m.Values[iNdEx].MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintTraceStorage(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } } return len(dAtA) - i, nil } func (m *TraceQueryParameters) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *TraceQueryParameters) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *TraceQueryParameters) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if m.SearchDepth != 0 { i = encodeVarintTraceStorage(dAtA, i, uint64(m.SearchDepth)) i-- dAtA[i] = 0x40 } n6, err6 := github_com_gogo_protobuf_types.StdDurationMarshalTo(m.DurationMax, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdDuration(m.DurationMax):]) if err6 != nil { return 0, err6 } i -= n6 i = encodeVarintTraceStorage(dAtA, i, uint64(n6)) i-- dAtA[i] = 0x3a n7, err7 := github_com_gogo_protobuf_types.StdDurationMarshalTo(m.DurationMin, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdDuration(m.DurationMin):]) if err7 != nil { return 0, err7 } i -= n7 i = encodeVarintTraceStorage(dAtA, i, uint64(n7)) i-- dAtA[i] = 0x32 n8, err8 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.StartTimeMax, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTimeMax):]) if err8 != nil { return 0, err8 } i -= n8 i = encodeVarintTraceStorage(dAtA, i, uint64(n8)) i-- dAtA[i] = 0x2a n9, err9 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.StartTimeMin, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTimeMin):]) if err9 != nil { return 0, err9 } i -= n9 i = encodeVarintTraceStorage(dAtA, i, uint64(n9)) i-- dAtA[i] = 0x22 if len(m.Attributes) > 0 { for iNdEx := len(m.Attributes) - 1; iNdEx >= 0; iNdEx-- { { size, err := m.Attributes[iNdEx].MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintTraceStorage(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x1a } } if len(m.OperationName) > 0 { i -= len(m.OperationName) copy(dAtA[i:], m.OperationName) i = encodeVarintTraceStorage(dAtA, i, uint64(len(m.OperationName))) i-- dAtA[i] = 0x12 } if len(m.ServiceName) > 0 { i -= len(m.ServiceName) copy(dAtA[i:], m.ServiceName) i = encodeVarintTraceStorage(dAtA, i, uint64(len(m.ServiceName))) i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *FindTracesRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *FindTracesRequest) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *FindTracesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if m.Query != nil { { size, err := m.Query.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintTraceStorage(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *FoundTraceID) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *FoundTraceID) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *FoundTraceID) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } n11, err11 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.End, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.End):]) if err11 != nil { return 0, err11 } i -= n11 i = encodeVarintTraceStorage(dAtA, i, uint64(n11)) i-- dAtA[i] = 0x1a n12, err12 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.Start, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.Start):]) if err12 != nil { return 0, err12 } i -= n12 i = encodeVarintTraceStorage(dAtA, i, uint64(n12)) i-- dAtA[i] = 0x12 if len(m.TraceId) > 0 { i -= len(m.TraceId) copy(dAtA[i:], m.TraceId) i = encodeVarintTraceStorage(dAtA, i, uint64(len(m.TraceId))) i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *FindTraceIDsResponse) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *FindTraceIDsResponse) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *FindTraceIDsResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.TraceIds) > 0 { for iNdEx := len(m.TraceIds) - 1; iNdEx >= 0; iNdEx-- { { size, err := m.TraceIds[iNdEx].MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintTraceStorage(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } } return len(dAtA) - i, nil } func encodeVarintTraceStorage(dAtA []byte, offset int, v uint64) int { offset -= sovTraceStorage(v) base := offset for v >= 1<<7 { dAtA[offset] = uint8(v&0x7f | 0x80) v >>= 7 offset++ } dAtA[offset] = uint8(v) return base } func (m *GetTraceParams) Size() (n int) { if m == nil { return 0 } var l int _ = l l = len(m.TraceId) if l > 0 { n += 1 + l + sovTraceStorage(uint64(l)) } l = github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTime) n += 1 + l + sovTraceStorage(uint64(l)) l = github_com_gogo_protobuf_types.SizeOfStdTime(m.EndTime) n += 1 + l + sovTraceStorage(uint64(l)) if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *GetTracesRequest) Size() (n int) { if m == nil { return 0 } var l int _ = l if len(m.Query) > 0 { for _, e := range m.Query { l = e.Size() n += 1 + l + sovTraceStorage(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *GetServicesRequest) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *GetServicesResponse) Size() (n int) { if m == nil { return 0 } var l int _ = l if len(m.Services) > 0 { for _, s := range m.Services { l = len(s) n += 1 + l + sovTraceStorage(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *GetOperationsRequest) Size() (n int) { if m == nil { return 0 } var l int _ = l l = len(m.Service) if l > 0 { n += 1 + l + sovTraceStorage(uint64(l)) } l = len(m.SpanKind) if l > 0 { n += 1 + l + sovTraceStorage(uint64(l)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *Operation) Size() (n int) { if m == nil { return 0 } var l int _ = l l = len(m.Name) if l > 0 { n += 1 + l + sovTraceStorage(uint64(l)) } l = len(m.SpanKind) if l > 0 { n += 1 + l + sovTraceStorage(uint64(l)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *GetOperationsResponse) Size() (n int) { if m == nil { return 0 } var l int _ = l if len(m.Operations) > 0 { for _, e := range m.Operations { l = e.Size() n += 1 + l + sovTraceStorage(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *KeyValue) Size() (n int) { if m == nil { return 0 } var l int _ = l l = len(m.Key) if l > 0 { n += 1 + l + sovTraceStorage(uint64(l)) } if m.Value != nil { l = m.Value.Size() n += 1 + l + sovTraceStorage(uint64(l)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *AnyValue) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.Value != nil { n += m.Value.Size() } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *AnyValue_StringValue) Size() (n int) { if m == nil { return 0 } var l int _ = l l = len(m.StringValue) n += 1 + l + sovTraceStorage(uint64(l)) return n } func (m *AnyValue_BoolValue) Size() (n int) { if m == nil { return 0 } var l int _ = l n += 2 return n } func (m *AnyValue_IntValue) Size() (n int) { if m == nil { return 0 } var l int _ = l n += 1 + sovTraceStorage(uint64(m.IntValue)) return n } func (m *AnyValue_DoubleValue) Size() (n int) { if m == nil { return 0 } var l int _ = l n += 9 return n } func (m *AnyValue_ArrayValue) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.ArrayValue != nil { l = m.ArrayValue.Size() n += 1 + l + sovTraceStorage(uint64(l)) } return n } func (m *AnyValue_KvlistValue) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.KvlistValue != nil { l = m.KvlistValue.Size() n += 1 + l + sovTraceStorage(uint64(l)) } return n } func (m *AnyValue_BytesValue) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.BytesValue != nil { l = len(m.BytesValue) n += 1 + l + sovTraceStorage(uint64(l)) } return n } func (m *KeyValueList) Size() (n int) { if m == nil { return 0 } var l int _ = l if len(m.Values) > 0 { for _, e := range m.Values { l = e.Size() n += 1 + l + sovTraceStorage(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *ArrayValue) Size() (n int) { if m == nil { return 0 } var l int _ = l if len(m.Values) > 0 { for _, e := range m.Values { l = e.Size() n += 1 + l + sovTraceStorage(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *TraceQueryParameters) Size() (n int) { if m == nil { return 0 } var l int _ = l l = len(m.ServiceName) if l > 0 { n += 1 + l + sovTraceStorage(uint64(l)) } l = len(m.OperationName) if l > 0 { n += 1 + l + sovTraceStorage(uint64(l)) } if len(m.Attributes) > 0 { for _, e := range m.Attributes { l = e.Size() n += 1 + l + sovTraceStorage(uint64(l)) } } l = github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTimeMin) n += 1 + l + sovTraceStorage(uint64(l)) l = github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTimeMax) n += 1 + l + sovTraceStorage(uint64(l)) l = github_com_gogo_protobuf_types.SizeOfStdDuration(m.DurationMin) n += 1 + l + sovTraceStorage(uint64(l)) l = github_com_gogo_protobuf_types.SizeOfStdDuration(m.DurationMax) n += 1 + l + sovTraceStorage(uint64(l)) if m.SearchDepth != 0 { n += 1 + sovTraceStorage(uint64(m.SearchDepth)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *FindTracesRequest) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.Query != nil { l = m.Query.Size() n += 1 + l + sovTraceStorage(uint64(l)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *FoundTraceID) Size() (n int) { if m == nil { return 0 } var l int _ = l l = len(m.TraceId) if l > 0 { n += 1 + l + sovTraceStorage(uint64(l)) } l = github_com_gogo_protobuf_types.SizeOfStdTime(m.Start) n += 1 + l + sovTraceStorage(uint64(l)) l = github_com_gogo_protobuf_types.SizeOfStdTime(m.End) n += 1 + l + sovTraceStorage(uint64(l)) if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *FindTraceIDsResponse) Size() (n int) { if m == nil { return 0 } var l int _ = l if len(m.TraceIds) > 0 { for _, e := range m.TraceIds { l = e.Size() n += 1 + l + sovTraceStorage(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func sovTraceStorage(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } func sozTraceStorage(x uint64) (n int) { return sovTraceStorage(uint64((x << 1) ^ uint64((int64(x) >> 63)))) } func (m *GetTraceParams) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GetTraceParams: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GetTraceParams: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field TraceId", wireType) } var byteLen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ byteLen |= int(b&0x7F) << shift if b < 0x80 { break } } if byteLen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + byteLen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.TraceId = append(m.TraceId[:0], dAtA[iNdEx:postIndex]...) if m.TraceId == nil { m.TraceId = []byte{} } iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field StartTime", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.StartTime, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field EndTime", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.EndTime, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTraceStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthTraceStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *GetTracesRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GetTracesRequest: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GetTracesRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Query", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.Query = append(m.Query, &GetTraceParams{}) if err := m.Query[len(m.Query)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTraceStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthTraceStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *GetServicesRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GetServicesRequest: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GetServicesRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { default: iNdEx = preIndex skippy, err := skipTraceStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthTraceStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *GetServicesResponse) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GetServicesResponse: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GetServicesResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Services", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.Services = append(m.Services, string(dAtA[iNdEx:postIndex])) iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTraceStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthTraceStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *GetOperationsRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GetOperationsRequest: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GetOperationsRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Service", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.Service = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field SpanKind", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.SpanKind = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTraceStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthTraceStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *Operation) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: Operation: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: Operation: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.Name = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field SpanKind", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.SpanKind = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTraceStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthTraceStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *GetOperationsResponse) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GetOperationsResponse: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GetOperationsResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Operations", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.Operations = append(m.Operations, &Operation{}) if err := m.Operations[len(m.Operations)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTraceStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthTraceStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *KeyValue) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: KeyValue: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: KeyValue: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.Key = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } if m.Value == nil { m.Value = &AnyValue{} } if err := m.Value.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTraceStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthTraceStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *AnyValue) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: AnyValue: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: AnyValue: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field StringValue", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.Value = &AnyValue_StringValue{string(dAtA[iNdEx:postIndex])} iNdEx = postIndex case 2: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field BoolValue", wireType) } var v int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= int(b&0x7F) << shift if b < 0x80 { break } } b := bool(v != 0) m.Value = &AnyValue_BoolValue{b} case 3: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field IntValue", wireType) } var v int64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= int64(b&0x7F) << shift if b < 0x80 { break } } m.Value = &AnyValue_IntValue{v} case 4: if wireType != 1 { return fmt.Errorf("proto: wrong wireType = %d for field DoubleValue", wireType) } var v uint64 if (iNdEx + 8) > l { return io.ErrUnexpectedEOF } v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) iNdEx += 8 m.Value = &AnyValue_DoubleValue{float64(math.Float64frombits(v))} case 5: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field ArrayValue", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } v := &ArrayValue{} if err := v.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } m.Value = &AnyValue_ArrayValue{v} iNdEx = postIndex case 6: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field KvlistValue", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } v := &KeyValueList{} if err := v.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } m.Value = &AnyValue_KvlistValue{v} iNdEx = postIndex case 7: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field BytesValue", wireType) } var byteLen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ byteLen |= int(b&0x7F) << shift if b < 0x80 { break } } if byteLen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + byteLen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } v := make([]byte, postIndex-iNdEx) copy(v, dAtA[iNdEx:postIndex]) m.Value = &AnyValue_BytesValue{v} iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTraceStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthTraceStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *KeyValueList) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: KeyValueList: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: KeyValueList: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Values", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.Values = append(m.Values, &KeyValue{}) if err := m.Values[len(m.Values)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTraceStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthTraceStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *ArrayValue) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: ArrayValue: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: ArrayValue: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Values", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.Values = append(m.Values, &AnyValue{}) if err := m.Values[len(m.Values)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTraceStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthTraceStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *TraceQueryParameters) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: TraceQueryParameters: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: TraceQueryParameters: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field ServiceName", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.ServiceName = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field OperationName", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.OperationName = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Attributes", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.Attributes = append(m.Attributes, &KeyValue{}) if err := m.Attributes[len(m.Attributes)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 4: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field StartTimeMin", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.StartTimeMin, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 5: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field StartTimeMax", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.StartTimeMax, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 6: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field DurationMin", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdDurationUnmarshal(&m.DurationMin, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 7: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field DurationMax", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdDurationUnmarshal(&m.DurationMax, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 8: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field SearchDepth", wireType) } m.SearchDepth = 0 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ m.SearchDepth |= int32(b&0x7F) << shift if b < 0x80 { break } } default: iNdEx = preIndex skippy, err := skipTraceStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthTraceStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *FindTracesRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: FindTracesRequest: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: FindTracesRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Query", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } if m.Query == nil { m.Query = &TraceQueryParameters{} } if err := m.Query.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTraceStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthTraceStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *FoundTraceID) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: FoundTraceID: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: FoundTraceID: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field TraceId", wireType) } var byteLen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ byteLen |= int(b&0x7F) << shift if b < 0x80 { break } } if byteLen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + byteLen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.TraceId = append(m.TraceId[:0], dAtA[iNdEx:postIndex]...) if m.TraceId == nil { m.TraceId = []byte{} } iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Start", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.Start, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field End", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.End, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTraceStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthTraceStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *FindTraceIDsResponse) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: FindTraceIDsResponse: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: FindTraceIDsResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field TraceIds", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTraceStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthTraceStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthTraceStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.TraceIds = append(m.TraceIds, &FoundTraceID{}) if err := m.TraceIds[len(m.TraceIds)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTraceStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthTraceStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func skipTraceStorage(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 depth := 0 for iNdEx < l { var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowTraceStorage } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } wireType := int(wire & 0x7) switch wireType { case 0: for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowTraceStorage } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } iNdEx++ if dAtA[iNdEx-1] < 0x80 { break } } case 1: iNdEx += 8 case 2: var length int for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowTraceStorage } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ length |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if length < 0 { return 0, ErrInvalidLengthTraceStorage } iNdEx += length case 3: depth++ case 4: if depth == 0 { return 0, ErrUnexpectedEndOfGroupTraceStorage } depth-- case 5: iNdEx += 4 default: return 0, fmt.Errorf("proto: illegal wireType %d", wireType) } if iNdEx < 0 { return 0, ErrInvalidLengthTraceStorage } if depth == 0 { return iNdEx, nil } } return 0, io.ErrUnexpectedEOF } var ( ErrInvalidLengthTraceStorage = fmt.Errorf("proto: negative length found during unmarshaling") ErrIntOverflowTraceStorage = fmt.Errorf("proto: integer overflow") ErrUnexpectedEndOfGroupTraceStorage = fmt.Errorf("proto: unexpected end of group") ) ================================================ FILE: internal/proto-gen/storage_v1/mocks/mocks.go ================================================ // Copyright (c) The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // // Run 'make generate-mocks' to regenerate. // Code generated by mockery; DO NOT EDIT. // github.com/vektra/mockery // template: testify package mocks import ( "context" "github.com/jaegertracing/jaeger/internal/proto-gen/storage_v1" mock "github.com/stretchr/testify/mock" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) // NewSpanWriterPluginClient creates a new instance of SpanWriterPluginClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewSpanWriterPluginClient(t interface { mock.TestingT Cleanup(func()) }) *SpanWriterPluginClient { mock := &SpanWriterPluginClient{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // SpanWriterPluginClient is an autogenerated mock type for the SpanWriterPluginClient type type SpanWriterPluginClient struct { mock.Mock } type SpanWriterPluginClient_Expecter struct { mock *mock.Mock } func (_m *SpanWriterPluginClient) EXPECT() *SpanWriterPluginClient_Expecter { return &SpanWriterPluginClient_Expecter{mock: &_m.Mock} } // Close provides a mock function for the type SpanWriterPluginClient func (_mock *SpanWriterPluginClient) Close(ctx context.Context, in *storage_v1.CloseWriterRequest, opts ...grpc.CallOption) (*storage_v1.CloseWriterResponse, error) { var tmpRet mock.Arguments if len(opts) > 0 { tmpRet = _mock.Called(ctx, in, opts) } else { tmpRet = _mock.Called(ctx, in) } ret := tmpRet if len(ret) == 0 { panic("no return value specified for Close") } var r0 *storage_v1.CloseWriterResponse var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.CloseWriterRequest, ...grpc.CallOption) (*storage_v1.CloseWriterResponse, error)); ok { return returnFunc(ctx, in, opts...) } if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.CloseWriterRequest, ...grpc.CallOption) *storage_v1.CloseWriterResponse); ok { r0 = returnFunc(ctx, in, opts...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*storage_v1.CloseWriterResponse) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *storage_v1.CloseWriterRequest, ...grpc.CallOption) error); ok { r1 = returnFunc(ctx, in, opts...) } else { r1 = ret.Error(1) } return r0, r1 } // SpanWriterPluginClient_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' type SpanWriterPluginClient_Close_Call struct { *mock.Call } // Close is a helper method to define mock.On call // - ctx context.Context // - in *storage_v1.CloseWriterRequest // - opts ...grpc.CallOption func (_e *SpanWriterPluginClient_Expecter) Close(ctx interface{}, in interface{}, opts ...interface{}) *SpanWriterPluginClient_Close_Call { return &SpanWriterPluginClient_Close_Call{Call: _e.mock.On("Close", append([]interface{}{ctx, in}, opts...)...)} } func (_c *SpanWriterPluginClient_Close_Call) Run(run func(ctx context.Context, in *storage_v1.CloseWriterRequest, opts ...grpc.CallOption)) *SpanWriterPluginClient_Close_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *storage_v1.CloseWriterRequest if args[1] != nil { arg1 = args[1].(*storage_v1.CloseWriterRequest) } var arg2 []grpc.CallOption var variadicArgs []grpc.CallOption if len(args) > 2 { variadicArgs = args[2].([]grpc.CallOption) } arg2 = variadicArgs run( arg0, arg1, arg2..., ) }) return _c } func (_c *SpanWriterPluginClient_Close_Call) Return(closeWriterResponse *storage_v1.CloseWriterResponse, err error) *SpanWriterPluginClient_Close_Call { _c.Call.Return(closeWriterResponse, err) return _c } func (_c *SpanWriterPluginClient_Close_Call) RunAndReturn(run func(ctx context.Context, in *storage_v1.CloseWriterRequest, opts ...grpc.CallOption) (*storage_v1.CloseWriterResponse, error)) *SpanWriterPluginClient_Close_Call { _c.Call.Return(run) return _c } // WriteSpan provides a mock function for the type SpanWriterPluginClient func (_mock *SpanWriterPluginClient) WriteSpan(ctx context.Context, in *storage_v1.WriteSpanRequest, opts ...grpc.CallOption) (*storage_v1.WriteSpanResponse, error) { var tmpRet mock.Arguments if len(opts) > 0 { tmpRet = _mock.Called(ctx, in, opts) } else { tmpRet = _mock.Called(ctx, in) } ret := tmpRet if len(ret) == 0 { panic("no return value specified for WriteSpan") } var r0 *storage_v1.WriteSpanResponse var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.WriteSpanRequest, ...grpc.CallOption) (*storage_v1.WriteSpanResponse, error)); ok { return returnFunc(ctx, in, opts...) } if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.WriteSpanRequest, ...grpc.CallOption) *storage_v1.WriteSpanResponse); ok { r0 = returnFunc(ctx, in, opts...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*storage_v1.WriteSpanResponse) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *storage_v1.WriteSpanRequest, ...grpc.CallOption) error); ok { r1 = returnFunc(ctx, in, opts...) } else { r1 = ret.Error(1) } return r0, r1 } // SpanWriterPluginClient_WriteSpan_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteSpan' type SpanWriterPluginClient_WriteSpan_Call struct { *mock.Call } // WriteSpan is a helper method to define mock.On call // - ctx context.Context // - in *storage_v1.WriteSpanRequest // - opts ...grpc.CallOption func (_e *SpanWriterPluginClient_Expecter) WriteSpan(ctx interface{}, in interface{}, opts ...interface{}) *SpanWriterPluginClient_WriteSpan_Call { return &SpanWriterPluginClient_WriteSpan_Call{Call: _e.mock.On("WriteSpan", append([]interface{}{ctx, in}, opts...)...)} } func (_c *SpanWriterPluginClient_WriteSpan_Call) Run(run func(ctx context.Context, in *storage_v1.WriteSpanRequest, opts ...grpc.CallOption)) *SpanWriterPluginClient_WriteSpan_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *storage_v1.WriteSpanRequest if args[1] != nil { arg1 = args[1].(*storage_v1.WriteSpanRequest) } var arg2 []grpc.CallOption var variadicArgs []grpc.CallOption if len(args) > 2 { variadicArgs = args[2].([]grpc.CallOption) } arg2 = variadicArgs run( arg0, arg1, arg2..., ) }) return _c } func (_c *SpanWriterPluginClient_WriteSpan_Call) Return(writeSpanResponse *storage_v1.WriteSpanResponse, err error) *SpanWriterPluginClient_WriteSpan_Call { _c.Call.Return(writeSpanResponse, err) return _c } func (_c *SpanWriterPluginClient_WriteSpan_Call) RunAndReturn(run func(ctx context.Context, in *storage_v1.WriteSpanRequest, opts ...grpc.CallOption) (*storage_v1.WriteSpanResponse, error)) *SpanWriterPluginClient_WriteSpan_Call { _c.Call.Return(run) return _c } // NewSpanWriterPluginServer creates a new instance of SpanWriterPluginServer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewSpanWriterPluginServer(t interface { mock.TestingT Cleanup(func()) }) *SpanWriterPluginServer { mock := &SpanWriterPluginServer{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // SpanWriterPluginServer is an autogenerated mock type for the SpanWriterPluginServer type type SpanWriterPluginServer struct { mock.Mock } type SpanWriterPluginServer_Expecter struct { mock *mock.Mock } func (_m *SpanWriterPluginServer) EXPECT() *SpanWriterPluginServer_Expecter { return &SpanWriterPluginServer_Expecter{mock: &_m.Mock} } // Close provides a mock function for the type SpanWriterPluginServer func (_mock *SpanWriterPluginServer) Close(context1 context.Context, closeWriterRequest *storage_v1.CloseWriterRequest) (*storage_v1.CloseWriterResponse, error) { ret := _mock.Called(context1, closeWriterRequest) if len(ret) == 0 { panic("no return value specified for Close") } var r0 *storage_v1.CloseWriterResponse var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.CloseWriterRequest) (*storage_v1.CloseWriterResponse, error)); ok { return returnFunc(context1, closeWriterRequest) } if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.CloseWriterRequest) *storage_v1.CloseWriterResponse); ok { r0 = returnFunc(context1, closeWriterRequest) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*storage_v1.CloseWriterResponse) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *storage_v1.CloseWriterRequest) error); ok { r1 = returnFunc(context1, closeWriterRequest) } else { r1 = ret.Error(1) } return r0, r1 } // SpanWriterPluginServer_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' type SpanWriterPluginServer_Close_Call struct { *mock.Call } // Close is a helper method to define mock.On call // - context1 context.Context // - closeWriterRequest *storage_v1.CloseWriterRequest func (_e *SpanWriterPluginServer_Expecter) Close(context1 interface{}, closeWriterRequest interface{}) *SpanWriterPluginServer_Close_Call { return &SpanWriterPluginServer_Close_Call{Call: _e.mock.On("Close", context1, closeWriterRequest)} } func (_c *SpanWriterPluginServer_Close_Call) Run(run func(context1 context.Context, closeWriterRequest *storage_v1.CloseWriterRequest)) *SpanWriterPluginServer_Close_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *storage_v1.CloseWriterRequest if args[1] != nil { arg1 = args[1].(*storage_v1.CloseWriterRequest) } run( arg0, arg1, ) }) return _c } func (_c *SpanWriterPluginServer_Close_Call) Return(closeWriterResponse *storage_v1.CloseWriterResponse, err error) *SpanWriterPluginServer_Close_Call { _c.Call.Return(closeWriterResponse, err) return _c } func (_c *SpanWriterPluginServer_Close_Call) RunAndReturn(run func(context1 context.Context, closeWriterRequest *storage_v1.CloseWriterRequest) (*storage_v1.CloseWriterResponse, error)) *SpanWriterPluginServer_Close_Call { _c.Call.Return(run) return _c } // WriteSpan provides a mock function for the type SpanWriterPluginServer func (_mock *SpanWriterPluginServer) WriteSpan(context1 context.Context, writeSpanRequest *storage_v1.WriteSpanRequest) (*storage_v1.WriteSpanResponse, error) { ret := _mock.Called(context1, writeSpanRequest) if len(ret) == 0 { panic("no return value specified for WriteSpan") } var r0 *storage_v1.WriteSpanResponse var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.WriteSpanRequest) (*storage_v1.WriteSpanResponse, error)); ok { return returnFunc(context1, writeSpanRequest) } if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.WriteSpanRequest) *storage_v1.WriteSpanResponse); ok { r0 = returnFunc(context1, writeSpanRequest) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*storage_v1.WriteSpanResponse) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *storage_v1.WriteSpanRequest) error); ok { r1 = returnFunc(context1, writeSpanRequest) } else { r1 = ret.Error(1) } return r0, r1 } // SpanWriterPluginServer_WriteSpan_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteSpan' type SpanWriterPluginServer_WriteSpan_Call struct { *mock.Call } // WriteSpan is a helper method to define mock.On call // - context1 context.Context // - writeSpanRequest *storage_v1.WriteSpanRequest func (_e *SpanWriterPluginServer_Expecter) WriteSpan(context1 interface{}, writeSpanRequest interface{}) *SpanWriterPluginServer_WriteSpan_Call { return &SpanWriterPluginServer_WriteSpan_Call{Call: _e.mock.On("WriteSpan", context1, writeSpanRequest)} } func (_c *SpanWriterPluginServer_WriteSpan_Call) Run(run func(context1 context.Context, writeSpanRequest *storage_v1.WriteSpanRequest)) *SpanWriterPluginServer_WriteSpan_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *storage_v1.WriteSpanRequest if args[1] != nil { arg1 = args[1].(*storage_v1.WriteSpanRequest) } run( arg0, arg1, ) }) return _c } func (_c *SpanWriterPluginServer_WriteSpan_Call) Return(writeSpanResponse *storage_v1.WriteSpanResponse, err error) *SpanWriterPluginServer_WriteSpan_Call { _c.Call.Return(writeSpanResponse, err) return _c } func (_c *SpanWriterPluginServer_WriteSpan_Call) RunAndReturn(run func(context1 context.Context, writeSpanRequest *storage_v1.WriteSpanRequest) (*storage_v1.WriteSpanResponse, error)) *SpanWriterPluginServer_WriteSpan_Call { _c.Call.Return(run) return _c } // NewStreamingSpanWriterPluginClient creates a new instance of StreamingSpanWriterPluginClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewStreamingSpanWriterPluginClient(t interface { mock.TestingT Cleanup(func()) }) *StreamingSpanWriterPluginClient { mock := &StreamingSpanWriterPluginClient{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // StreamingSpanWriterPluginClient is an autogenerated mock type for the StreamingSpanWriterPluginClient type type StreamingSpanWriterPluginClient struct { mock.Mock } type StreamingSpanWriterPluginClient_Expecter struct { mock *mock.Mock } func (_m *StreamingSpanWriterPluginClient) EXPECT() *StreamingSpanWriterPluginClient_Expecter { return &StreamingSpanWriterPluginClient_Expecter{mock: &_m.Mock} } // WriteSpanStream provides a mock function for the type StreamingSpanWriterPluginClient func (_mock *StreamingSpanWriterPluginClient) WriteSpanStream(ctx context.Context, opts ...grpc.CallOption) (storage_v1.StreamingSpanWriterPlugin_WriteSpanStreamClient, error) { var tmpRet mock.Arguments if len(opts) > 0 { tmpRet = _mock.Called(ctx, opts) } else { tmpRet = _mock.Called(ctx) } ret := tmpRet if len(ret) == 0 { panic("no return value specified for WriteSpanStream") } var r0 storage_v1.StreamingSpanWriterPlugin_WriteSpanStreamClient var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, ...grpc.CallOption) (storage_v1.StreamingSpanWriterPlugin_WriteSpanStreamClient, error)); ok { return returnFunc(ctx, opts...) } if returnFunc, ok := ret.Get(0).(func(context.Context, ...grpc.CallOption) storage_v1.StreamingSpanWriterPlugin_WriteSpanStreamClient); ok { r0 = returnFunc(ctx, opts...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(storage_v1.StreamingSpanWriterPlugin_WriteSpanStreamClient) } } if returnFunc, ok := ret.Get(1).(func(context.Context, ...grpc.CallOption) error); ok { r1 = returnFunc(ctx, opts...) } else { r1 = ret.Error(1) } return r0, r1 } // StreamingSpanWriterPluginClient_WriteSpanStream_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteSpanStream' type StreamingSpanWriterPluginClient_WriteSpanStream_Call struct { *mock.Call } // WriteSpanStream is a helper method to define mock.On call // - ctx context.Context // - opts ...grpc.CallOption func (_e *StreamingSpanWriterPluginClient_Expecter) WriteSpanStream(ctx interface{}, opts ...interface{}) *StreamingSpanWriterPluginClient_WriteSpanStream_Call { return &StreamingSpanWriterPluginClient_WriteSpanStream_Call{Call: _e.mock.On("WriteSpanStream", append([]interface{}{ctx}, opts...)...)} } func (_c *StreamingSpanWriterPluginClient_WriteSpanStream_Call) Run(run func(ctx context.Context, opts ...grpc.CallOption)) *StreamingSpanWriterPluginClient_WriteSpanStream_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 []grpc.CallOption var variadicArgs []grpc.CallOption if len(args) > 1 { variadicArgs = args[1].([]grpc.CallOption) } arg1 = variadicArgs run( arg0, arg1..., ) }) return _c } func (_c *StreamingSpanWriterPluginClient_WriteSpanStream_Call) Return(streamingSpanWriterPlugin_WriteSpanStreamClient storage_v1.StreamingSpanWriterPlugin_WriteSpanStreamClient, err error) *StreamingSpanWriterPluginClient_WriteSpanStream_Call { _c.Call.Return(streamingSpanWriterPlugin_WriteSpanStreamClient, err) return _c } func (_c *StreamingSpanWriterPluginClient_WriteSpanStream_Call) RunAndReturn(run func(ctx context.Context, opts ...grpc.CallOption) (storage_v1.StreamingSpanWriterPlugin_WriteSpanStreamClient, error)) *StreamingSpanWriterPluginClient_WriteSpanStream_Call { _c.Call.Return(run) return _c } // NewStreamingSpanWriterPlugin_WriteSpanStreamClient creates a new instance of StreamingSpanWriterPlugin_WriteSpanStreamClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewStreamingSpanWriterPlugin_WriteSpanStreamClient(t interface { mock.TestingT Cleanup(func()) }) *StreamingSpanWriterPlugin_WriteSpanStreamClient { mock := &StreamingSpanWriterPlugin_WriteSpanStreamClient{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // StreamingSpanWriterPlugin_WriteSpanStreamClient is an autogenerated mock type for the StreamingSpanWriterPlugin_WriteSpanStreamClient type type StreamingSpanWriterPlugin_WriteSpanStreamClient struct { mock.Mock } type StreamingSpanWriterPlugin_WriteSpanStreamClient_Expecter struct { mock *mock.Mock } func (_m *StreamingSpanWriterPlugin_WriteSpanStreamClient) EXPECT() *StreamingSpanWriterPlugin_WriteSpanStreamClient_Expecter { return &StreamingSpanWriterPlugin_WriteSpanStreamClient_Expecter{mock: &_m.Mock} } // CloseAndRecv provides a mock function for the type StreamingSpanWriterPlugin_WriteSpanStreamClient func (_mock *StreamingSpanWriterPlugin_WriteSpanStreamClient) CloseAndRecv() (*storage_v1.WriteSpanResponse, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for CloseAndRecv") } var r0 *storage_v1.WriteSpanResponse var r1 error if returnFunc, ok := ret.Get(0).(func() (*storage_v1.WriteSpanResponse, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() *storage_v1.WriteSpanResponse); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*storage_v1.WriteSpanResponse) } } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // StreamingSpanWriterPlugin_WriteSpanStreamClient_CloseAndRecv_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CloseAndRecv' type StreamingSpanWriterPlugin_WriteSpanStreamClient_CloseAndRecv_Call struct { *mock.Call } // CloseAndRecv is a helper method to define mock.On call func (_e *StreamingSpanWriterPlugin_WriteSpanStreamClient_Expecter) CloseAndRecv() *StreamingSpanWriterPlugin_WriteSpanStreamClient_CloseAndRecv_Call { return &StreamingSpanWriterPlugin_WriteSpanStreamClient_CloseAndRecv_Call{Call: _e.mock.On("CloseAndRecv")} } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_CloseAndRecv_Call) Run(run func()) *StreamingSpanWriterPlugin_WriteSpanStreamClient_CloseAndRecv_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_CloseAndRecv_Call) Return(writeSpanResponse *storage_v1.WriteSpanResponse, err error) *StreamingSpanWriterPlugin_WriteSpanStreamClient_CloseAndRecv_Call { _c.Call.Return(writeSpanResponse, err) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_CloseAndRecv_Call) RunAndReturn(run func() (*storage_v1.WriteSpanResponse, error)) *StreamingSpanWriterPlugin_WriteSpanStreamClient_CloseAndRecv_Call { _c.Call.Return(run) return _c } // CloseSend provides a mock function for the type StreamingSpanWriterPlugin_WriteSpanStreamClient func (_mock *StreamingSpanWriterPlugin_WriteSpanStreamClient) CloseSend() error { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for CloseSend") } var r0 error if returnFunc, ok := ret.Get(0).(func() error); ok { r0 = returnFunc() } else { r0 = ret.Error(0) } return r0 } // StreamingSpanWriterPlugin_WriteSpanStreamClient_CloseSend_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CloseSend' type StreamingSpanWriterPlugin_WriteSpanStreamClient_CloseSend_Call struct { *mock.Call } // CloseSend is a helper method to define mock.On call func (_e *StreamingSpanWriterPlugin_WriteSpanStreamClient_Expecter) CloseSend() *StreamingSpanWriterPlugin_WriteSpanStreamClient_CloseSend_Call { return &StreamingSpanWriterPlugin_WriteSpanStreamClient_CloseSend_Call{Call: _e.mock.On("CloseSend")} } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_CloseSend_Call) Run(run func()) *StreamingSpanWriterPlugin_WriteSpanStreamClient_CloseSend_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_CloseSend_Call) Return(err error) *StreamingSpanWriterPlugin_WriteSpanStreamClient_CloseSend_Call { _c.Call.Return(err) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_CloseSend_Call) RunAndReturn(run func() error) *StreamingSpanWriterPlugin_WriteSpanStreamClient_CloseSend_Call { _c.Call.Return(run) return _c } // Context provides a mock function for the type StreamingSpanWriterPlugin_WriteSpanStreamClient func (_mock *StreamingSpanWriterPlugin_WriteSpanStreamClient) Context() context.Context { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Context") } var r0 context.Context if returnFunc, ok := ret.Get(0).(func() context.Context); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(context.Context) } } return r0 } // StreamingSpanWriterPlugin_WriteSpanStreamClient_Context_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Context' type StreamingSpanWriterPlugin_WriteSpanStreamClient_Context_Call struct { *mock.Call } // Context is a helper method to define mock.On call func (_e *StreamingSpanWriterPlugin_WriteSpanStreamClient_Expecter) Context() *StreamingSpanWriterPlugin_WriteSpanStreamClient_Context_Call { return &StreamingSpanWriterPlugin_WriteSpanStreamClient_Context_Call{Call: _e.mock.On("Context")} } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_Context_Call) Run(run func()) *StreamingSpanWriterPlugin_WriteSpanStreamClient_Context_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_Context_Call) Return(context1 context.Context) *StreamingSpanWriterPlugin_WriteSpanStreamClient_Context_Call { _c.Call.Return(context1) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_Context_Call) RunAndReturn(run func() context.Context) *StreamingSpanWriterPlugin_WriteSpanStreamClient_Context_Call { _c.Call.Return(run) return _c } // Header provides a mock function for the type StreamingSpanWriterPlugin_WriteSpanStreamClient func (_mock *StreamingSpanWriterPlugin_WriteSpanStreamClient) Header() (metadata.MD, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Header") } var r0 metadata.MD var r1 error if returnFunc, ok := ret.Get(0).(func() (metadata.MD, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() metadata.MD); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(metadata.MD) } } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // StreamingSpanWriterPlugin_WriteSpanStreamClient_Header_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Header' type StreamingSpanWriterPlugin_WriteSpanStreamClient_Header_Call struct { *mock.Call } // Header is a helper method to define mock.On call func (_e *StreamingSpanWriterPlugin_WriteSpanStreamClient_Expecter) Header() *StreamingSpanWriterPlugin_WriteSpanStreamClient_Header_Call { return &StreamingSpanWriterPlugin_WriteSpanStreamClient_Header_Call{Call: _e.mock.On("Header")} } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_Header_Call) Run(run func()) *StreamingSpanWriterPlugin_WriteSpanStreamClient_Header_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_Header_Call) Return(mD metadata.MD, err error) *StreamingSpanWriterPlugin_WriteSpanStreamClient_Header_Call { _c.Call.Return(mD, err) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_Header_Call) RunAndReturn(run func() (metadata.MD, error)) *StreamingSpanWriterPlugin_WriteSpanStreamClient_Header_Call { _c.Call.Return(run) return _c } // RecvMsg provides a mock function for the type StreamingSpanWriterPlugin_WriteSpanStreamClient func (_mock *StreamingSpanWriterPlugin_WriteSpanStreamClient) RecvMsg(m any) error { ret := _mock.Called(m) if len(ret) == 0 { panic("no return value specified for RecvMsg") } var r0 error if returnFunc, ok := ret.Get(0).(func(any) error); ok { r0 = returnFunc(m) } else { r0 = ret.Error(0) } return r0 } // StreamingSpanWriterPlugin_WriteSpanStreamClient_RecvMsg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RecvMsg' type StreamingSpanWriterPlugin_WriteSpanStreamClient_RecvMsg_Call struct { *mock.Call } // RecvMsg is a helper method to define mock.On call // - m any func (_e *StreamingSpanWriterPlugin_WriteSpanStreamClient_Expecter) RecvMsg(m interface{}) *StreamingSpanWriterPlugin_WriteSpanStreamClient_RecvMsg_Call { return &StreamingSpanWriterPlugin_WriteSpanStreamClient_RecvMsg_Call{Call: _e.mock.On("RecvMsg", m)} } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_RecvMsg_Call) Run(run func(m any)) *StreamingSpanWriterPlugin_WriteSpanStreamClient_RecvMsg_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 any if args[0] != nil { arg0 = args[0].(any) } run( arg0, ) }) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_RecvMsg_Call) Return(err error) *StreamingSpanWriterPlugin_WriteSpanStreamClient_RecvMsg_Call { _c.Call.Return(err) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_RecvMsg_Call) RunAndReturn(run func(m any) error) *StreamingSpanWriterPlugin_WriteSpanStreamClient_RecvMsg_Call { _c.Call.Return(run) return _c } // Send provides a mock function for the type StreamingSpanWriterPlugin_WriteSpanStreamClient func (_mock *StreamingSpanWriterPlugin_WriteSpanStreamClient) Send(writeSpanRequest *storage_v1.WriteSpanRequest) error { ret := _mock.Called(writeSpanRequest) if len(ret) == 0 { panic("no return value specified for Send") } var r0 error if returnFunc, ok := ret.Get(0).(func(*storage_v1.WriteSpanRequest) error); ok { r0 = returnFunc(writeSpanRequest) } else { r0 = ret.Error(0) } return r0 } // StreamingSpanWriterPlugin_WriteSpanStreamClient_Send_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Send' type StreamingSpanWriterPlugin_WriteSpanStreamClient_Send_Call struct { *mock.Call } // Send is a helper method to define mock.On call // - writeSpanRequest *storage_v1.WriteSpanRequest func (_e *StreamingSpanWriterPlugin_WriteSpanStreamClient_Expecter) Send(writeSpanRequest interface{}) *StreamingSpanWriterPlugin_WriteSpanStreamClient_Send_Call { return &StreamingSpanWriterPlugin_WriteSpanStreamClient_Send_Call{Call: _e.mock.On("Send", writeSpanRequest)} } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_Send_Call) Run(run func(writeSpanRequest *storage_v1.WriteSpanRequest)) *StreamingSpanWriterPlugin_WriteSpanStreamClient_Send_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 *storage_v1.WriteSpanRequest if args[0] != nil { arg0 = args[0].(*storage_v1.WriteSpanRequest) } run( arg0, ) }) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_Send_Call) Return(err error) *StreamingSpanWriterPlugin_WriteSpanStreamClient_Send_Call { _c.Call.Return(err) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_Send_Call) RunAndReturn(run func(writeSpanRequest *storage_v1.WriteSpanRequest) error) *StreamingSpanWriterPlugin_WriteSpanStreamClient_Send_Call { _c.Call.Return(run) return _c } // SendMsg provides a mock function for the type StreamingSpanWriterPlugin_WriteSpanStreamClient func (_mock *StreamingSpanWriterPlugin_WriteSpanStreamClient) SendMsg(m any) error { ret := _mock.Called(m) if len(ret) == 0 { panic("no return value specified for SendMsg") } var r0 error if returnFunc, ok := ret.Get(0).(func(any) error); ok { r0 = returnFunc(m) } else { r0 = ret.Error(0) } return r0 } // StreamingSpanWriterPlugin_WriteSpanStreamClient_SendMsg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMsg' type StreamingSpanWriterPlugin_WriteSpanStreamClient_SendMsg_Call struct { *mock.Call } // SendMsg is a helper method to define mock.On call // - m any func (_e *StreamingSpanWriterPlugin_WriteSpanStreamClient_Expecter) SendMsg(m interface{}) *StreamingSpanWriterPlugin_WriteSpanStreamClient_SendMsg_Call { return &StreamingSpanWriterPlugin_WriteSpanStreamClient_SendMsg_Call{Call: _e.mock.On("SendMsg", m)} } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_SendMsg_Call) Run(run func(m any)) *StreamingSpanWriterPlugin_WriteSpanStreamClient_SendMsg_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 any if args[0] != nil { arg0 = args[0].(any) } run( arg0, ) }) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_SendMsg_Call) Return(err error) *StreamingSpanWriterPlugin_WriteSpanStreamClient_SendMsg_Call { _c.Call.Return(err) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_SendMsg_Call) RunAndReturn(run func(m any) error) *StreamingSpanWriterPlugin_WriteSpanStreamClient_SendMsg_Call { _c.Call.Return(run) return _c } // Trailer provides a mock function for the type StreamingSpanWriterPlugin_WriteSpanStreamClient func (_mock *StreamingSpanWriterPlugin_WriteSpanStreamClient) Trailer() metadata.MD { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Trailer") } var r0 metadata.MD if returnFunc, ok := ret.Get(0).(func() metadata.MD); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(metadata.MD) } } return r0 } // StreamingSpanWriterPlugin_WriteSpanStreamClient_Trailer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Trailer' type StreamingSpanWriterPlugin_WriteSpanStreamClient_Trailer_Call struct { *mock.Call } // Trailer is a helper method to define mock.On call func (_e *StreamingSpanWriterPlugin_WriteSpanStreamClient_Expecter) Trailer() *StreamingSpanWriterPlugin_WriteSpanStreamClient_Trailer_Call { return &StreamingSpanWriterPlugin_WriteSpanStreamClient_Trailer_Call{Call: _e.mock.On("Trailer")} } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_Trailer_Call) Run(run func()) *StreamingSpanWriterPlugin_WriteSpanStreamClient_Trailer_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_Trailer_Call) Return(mD metadata.MD) *StreamingSpanWriterPlugin_WriteSpanStreamClient_Trailer_Call { _c.Call.Return(mD) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamClient_Trailer_Call) RunAndReturn(run func() metadata.MD) *StreamingSpanWriterPlugin_WriteSpanStreamClient_Trailer_Call { _c.Call.Return(run) return _c } // NewStreamingSpanWriterPluginServer creates a new instance of StreamingSpanWriterPluginServer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewStreamingSpanWriterPluginServer(t interface { mock.TestingT Cleanup(func()) }) *StreamingSpanWriterPluginServer { mock := &StreamingSpanWriterPluginServer{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // StreamingSpanWriterPluginServer is an autogenerated mock type for the StreamingSpanWriterPluginServer type type StreamingSpanWriterPluginServer struct { mock.Mock } type StreamingSpanWriterPluginServer_Expecter struct { mock *mock.Mock } func (_m *StreamingSpanWriterPluginServer) EXPECT() *StreamingSpanWriterPluginServer_Expecter { return &StreamingSpanWriterPluginServer_Expecter{mock: &_m.Mock} } // WriteSpanStream provides a mock function for the type StreamingSpanWriterPluginServer func (_mock *StreamingSpanWriterPluginServer) WriteSpanStream(streamingSpanWriterPlugin_WriteSpanStreamServer storage_v1.StreamingSpanWriterPlugin_WriteSpanStreamServer) error { ret := _mock.Called(streamingSpanWriterPlugin_WriteSpanStreamServer) if len(ret) == 0 { panic("no return value specified for WriteSpanStream") } var r0 error if returnFunc, ok := ret.Get(0).(func(storage_v1.StreamingSpanWriterPlugin_WriteSpanStreamServer) error); ok { r0 = returnFunc(streamingSpanWriterPlugin_WriteSpanStreamServer) } else { r0 = ret.Error(0) } return r0 } // StreamingSpanWriterPluginServer_WriteSpanStream_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteSpanStream' type StreamingSpanWriterPluginServer_WriteSpanStream_Call struct { *mock.Call } // WriteSpanStream is a helper method to define mock.On call // - streamingSpanWriterPlugin_WriteSpanStreamServer storage_v1.StreamingSpanWriterPlugin_WriteSpanStreamServer func (_e *StreamingSpanWriterPluginServer_Expecter) WriteSpanStream(streamingSpanWriterPlugin_WriteSpanStreamServer interface{}) *StreamingSpanWriterPluginServer_WriteSpanStream_Call { return &StreamingSpanWriterPluginServer_WriteSpanStream_Call{Call: _e.mock.On("WriteSpanStream", streamingSpanWriterPlugin_WriteSpanStreamServer)} } func (_c *StreamingSpanWriterPluginServer_WriteSpanStream_Call) Run(run func(streamingSpanWriterPlugin_WriteSpanStreamServer storage_v1.StreamingSpanWriterPlugin_WriteSpanStreamServer)) *StreamingSpanWriterPluginServer_WriteSpanStream_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 storage_v1.StreamingSpanWriterPlugin_WriteSpanStreamServer if args[0] != nil { arg0 = args[0].(storage_v1.StreamingSpanWriterPlugin_WriteSpanStreamServer) } run( arg0, ) }) return _c } func (_c *StreamingSpanWriterPluginServer_WriteSpanStream_Call) Return(err error) *StreamingSpanWriterPluginServer_WriteSpanStream_Call { _c.Call.Return(err) return _c } func (_c *StreamingSpanWriterPluginServer_WriteSpanStream_Call) RunAndReturn(run func(streamingSpanWriterPlugin_WriteSpanStreamServer storage_v1.StreamingSpanWriterPlugin_WriteSpanStreamServer) error) *StreamingSpanWriterPluginServer_WriteSpanStream_Call { _c.Call.Return(run) return _c } // NewStreamingSpanWriterPlugin_WriteSpanStreamServer creates a new instance of StreamingSpanWriterPlugin_WriteSpanStreamServer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewStreamingSpanWriterPlugin_WriteSpanStreamServer(t interface { mock.TestingT Cleanup(func()) }) *StreamingSpanWriterPlugin_WriteSpanStreamServer { mock := &StreamingSpanWriterPlugin_WriteSpanStreamServer{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // StreamingSpanWriterPlugin_WriteSpanStreamServer is an autogenerated mock type for the StreamingSpanWriterPlugin_WriteSpanStreamServer type type StreamingSpanWriterPlugin_WriteSpanStreamServer struct { mock.Mock } type StreamingSpanWriterPlugin_WriteSpanStreamServer_Expecter struct { mock *mock.Mock } func (_m *StreamingSpanWriterPlugin_WriteSpanStreamServer) EXPECT() *StreamingSpanWriterPlugin_WriteSpanStreamServer_Expecter { return &StreamingSpanWriterPlugin_WriteSpanStreamServer_Expecter{mock: &_m.Mock} } // Context provides a mock function for the type StreamingSpanWriterPlugin_WriteSpanStreamServer func (_mock *StreamingSpanWriterPlugin_WriteSpanStreamServer) Context() context.Context { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Context") } var r0 context.Context if returnFunc, ok := ret.Get(0).(func() context.Context); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(context.Context) } } return r0 } // StreamingSpanWriterPlugin_WriteSpanStreamServer_Context_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Context' type StreamingSpanWriterPlugin_WriteSpanStreamServer_Context_Call struct { *mock.Call } // Context is a helper method to define mock.On call func (_e *StreamingSpanWriterPlugin_WriteSpanStreamServer_Expecter) Context() *StreamingSpanWriterPlugin_WriteSpanStreamServer_Context_Call { return &StreamingSpanWriterPlugin_WriteSpanStreamServer_Context_Call{Call: _e.mock.On("Context")} } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_Context_Call) Run(run func()) *StreamingSpanWriterPlugin_WriteSpanStreamServer_Context_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_Context_Call) Return(context1 context.Context) *StreamingSpanWriterPlugin_WriteSpanStreamServer_Context_Call { _c.Call.Return(context1) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_Context_Call) RunAndReturn(run func() context.Context) *StreamingSpanWriterPlugin_WriteSpanStreamServer_Context_Call { _c.Call.Return(run) return _c } // Recv provides a mock function for the type StreamingSpanWriterPlugin_WriteSpanStreamServer func (_mock *StreamingSpanWriterPlugin_WriteSpanStreamServer) Recv() (*storage_v1.WriteSpanRequest, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Recv") } var r0 *storage_v1.WriteSpanRequest var r1 error if returnFunc, ok := ret.Get(0).(func() (*storage_v1.WriteSpanRequest, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() *storage_v1.WriteSpanRequest); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*storage_v1.WriteSpanRequest) } } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // StreamingSpanWriterPlugin_WriteSpanStreamServer_Recv_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Recv' type StreamingSpanWriterPlugin_WriteSpanStreamServer_Recv_Call struct { *mock.Call } // Recv is a helper method to define mock.On call func (_e *StreamingSpanWriterPlugin_WriteSpanStreamServer_Expecter) Recv() *StreamingSpanWriterPlugin_WriteSpanStreamServer_Recv_Call { return &StreamingSpanWriterPlugin_WriteSpanStreamServer_Recv_Call{Call: _e.mock.On("Recv")} } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_Recv_Call) Run(run func()) *StreamingSpanWriterPlugin_WriteSpanStreamServer_Recv_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_Recv_Call) Return(writeSpanRequest *storage_v1.WriteSpanRequest, err error) *StreamingSpanWriterPlugin_WriteSpanStreamServer_Recv_Call { _c.Call.Return(writeSpanRequest, err) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_Recv_Call) RunAndReturn(run func() (*storage_v1.WriteSpanRequest, error)) *StreamingSpanWriterPlugin_WriteSpanStreamServer_Recv_Call { _c.Call.Return(run) return _c } // RecvMsg provides a mock function for the type StreamingSpanWriterPlugin_WriteSpanStreamServer func (_mock *StreamingSpanWriterPlugin_WriteSpanStreamServer) RecvMsg(m any) error { ret := _mock.Called(m) if len(ret) == 0 { panic("no return value specified for RecvMsg") } var r0 error if returnFunc, ok := ret.Get(0).(func(any) error); ok { r0 = returnFunc(m) } else { r0 = ret.Error(0) } return r0 } // StreamingSpanWriterPlugin_WriteSpanStreamServer_RecvMsg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RecvMsg' type StreamingSpanWriterPlugin_WriteSpanStreamServer_RecvMsg_Call struct { *mock.Call } // RecvMsg is a helper method to define mock.On call // - m any func (_e *StreamingSpanWriterPlugin_WriteSpanStreamServer_Expecter) RecvMsg(m interface{}) *StreamingSpanWriterPlugin_WriteSpanStreamServer_RecvMsg_Call { return &StreamingSpanWriterPlugin_WriteSpanStreamServer_RecvMsg_Call{Call: _e.mock.On("RecvMsg", m)} } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_RecvMsg_Call) Run(run func(m any)) *StreamingSpanWriterPlugin_WriteSpanStreamServer_RecvMsg_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 any if args[0] != nil { arg0 = args[0].(any) } run( arg0, ) }) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_RecvMsg_Call) Return(err error) *StreamingSpanWriterPlugin_WriteSpanStreamServer_RecvMsg_Call { _c.Call.Return(err) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_RecvMsg_Call) RunAndReturn(run func(m any) error) *StreamingSpanWriterPlugin_WriteSpanStreamServer_RecvMsg_Call { _c.Call.Return(run) return _c } // SendAndClose provides a mock function for the type StreamingSpanWriterPlugin_WriteSpanStreamServer func (_mock *StreamingSpanWriterPlugin_WriteSpanStreamServer) SendAndClose(writeSpanResponse *storage_v1.WriteSpanResponse) error { ret := _mock.Called(writeSpanResponse) if len(ret) == 0 { panic("no return value specified for SendAndClose") } var r0 error if returnFunc, ok := ret.Get(0).(func(*storage_v1.WriteSpanResponse) error); ok { r0 = returnFunc(writeSpanResponse) } else { r0 = ret.Error(0) } return r0 } // StreamingSpanWriterPlugin_WriteSpanStreamServer_SendAndClose_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendAndClose' type StreamingSpanWriterPlugin_WriteSpanStreamServer_SendAndClose_Call struct { *mock.Call } // SendAndClose is a helper method to define mock.On call // - writeSpanResponse *storage_v1.WriteSpanResponse func (_e *StreamingSpanWriterPlugin_WriteSpanStreamServer_Expecter) SendAndClose(writeSpanResponse interface{}) *StreamingSpanWriterPlugin_WriteSpanStreamServer_SendAndClose_Call { return &StreamingSpanWriterPlugin_WriteSpanStreamServer_SendAndClose_Call{Call: _e.mock.On("SendAndClose", writeSpanResponse)} } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_SendAndClose_Call) Run(run func(writeSpanResponse *storage_v1.WriteSpanResponse)) *StreamingSpanWriterPlugin_WriteSpanStreamServer_SendAndClose_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 *storage_v1.WriteSpanResponse if args[0] != nil { arg0 = args[0].(*storage_v1.WriteSpanResponse) } run( arg0, ) }) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_SendAndClose_Call) Return(err error) *StreamingSpanWriterPlugin_WriteSpanStreamServer_SendAndClose_Call { _c.Call.Return(err) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_SendAndClose_Call) RunAndReturn(run func(writeSpanResponse *storage_v1.WriteSpanResponse) error) *StreamingSpanWriterPlugin_WriteSpanStreamServer_SendAndClose_Call { _c.Call.Return(run) return _c } // SendHeader provides a mock function for the type StreamingSpanWriterPlugin_WriteSpanStreamServer func (_mock *StreamingSpanWriterPlugin_WriteSpanStreamServer) SendHeader(mD metadata.MD) error { ret := _mock.Called(mD) if len(ret) == 0 { panic("no return value specified for SendHeader") } var r0 error if returnFunc, ok := ret.Get(0).(func(metadata.MD) error); ok { r0 = returnFunc(mD) } else { r0 = ret.Error(0) } return r0 } // StreamingSpanWriterPlugin_WriteSpanStreamServer_SendHeader_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendHeader' type StreamingSpanWriterPlugin_WriteSpanStreamServer_SendHeader_Call struct { *mock.Call } // SendHeader is a helper method to define mock.On call // - mD metadata.MD func (_e *StreamingSpanWriterPlugin_WriteSpanStreamServer_Expecter) SendHeader(mD interface{}) *StreamingSpanWriterPlugin_WriteSpanStreamServer_SendHeader_Call { return &StreamingSpanWriterPlugin_WriteSpanStreamServer_SendHeader_Call{Call: _e.mock.On("SendHeader", mD)} } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_SendHeader_Call) Run(run func(mD metadata.MD)) *StreamingSpanWriterPlugin_WriteSpanStreamServer_SendHeader_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 metadata.MD if args[0] != nil { arg0 = args[0].(metadata.MD) } run( arg0, ) }) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_SendHeader_Call) Return(err error) *StreamingSpanWriterPlugin_WriteSpanStreamServer_SendHeader_Call { _c.Call.Return(err) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_SendHeader_Call) RunAndReturn(run func(mD metadata.MD) error) *StreamingSpanWriterPlugin_WriteSpanStreamServer_SendHeader_Call { _c.Call.Return(run) return _c } // SendMsg provides a mock function for the type StreamingSpanWriterPlugin_WriteSpanStreamServer func (_mock *StreamingSpanWriterPlugin_WriteSpanStreamServer) SendMsg(m any) error { ret := _mock.Called(m) if len(ret) == 0 { panic("no return value specified for SendMsg") } var r0 error if returnFunc, ok := ret.Get(0).(func(any) error); ok { r0 = returnFunc(m) } else { r0 = ret.Error(0) } return r0 } // StreamingSpanWriterPlugin_WriteSpanStreamServer_SendMsg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMsg' type StreamingSpanWriterPlugin_WriteSpanStreamServer_SendMsg_Call struct { *mock.Call } // SendMsg is a helper method to define mock.On call // - m any func (_e *StreamingSpanWriterPlugin_WriteSpanStreamServer_Expecter) SendMsg(m interface{}) *StreamingSpanWriterPlugin_WriteSpanStreamServer_SendMsg_Call { return &StreamingSpanWriterPlugin_WriteSpanStreamServer_SendMsg_Call{Call: _e.mock.On("SendMsg", m)} } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_SendMsg_Call) Run(run func(m any)) *StreamingSpanWriterPlugin_WriteSpanStreamServer_SendMsg_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 any if args[0] != nil { arg0 = args[0].(any) } run( arg0, ) }) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_SendMsg_Call) Return(err error) *StreamingSpanWriterPlugin_WriteSpanStreamServer_SendMsg_Call { _c.Call.Return(err) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_SendMsg_Call) RunAndReturn(run func(m any) error) *StreamingSpanWriterPlugin_WriteSpanStreamServer_SendMsg_Call { _c.Call.Return(run) return _c } // SetHeader provides a mock function for the type StreamingSpanWriterPlugin_WriteSpanStreamServer func (_mock *StreamingSpanWriterPlugin_WriteSpanStreamServer) SetHeader(mD metadata.MD) error { ret := _mock.Called(mD) if len(ret) == 0 { panic("no return value specified for SetHeader") } var r0 error if returnFunc, ok := ret.Get(0).(func(metadata.MD) error); ok { r0 = returnFunc(mD) } else { r0 = ret.Error(0) } return r0 } // StreamingSpanWriterPlugin_WriteSpanStreamServer_SetHeader_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetHeader' type StreamingSpanWriterPlugin_WriteSpanStreamServer_SetHeader_Call struct { *mock.Call } // SetHeader is a helper method to define mock.On call // - mD metadata.MD func (_e *StreamingSpanWriterPlugin_WriteSpanStreamServer_Expecter) SetHeader(mD interface{}) *StreamingSpanWriterPlugin_WriteSpanStreamServer_SetHeader_Call { return &StreamingSpanWriterPlugin_WriteSpanStreamServer_SetHeader_Call{Call: _e.mock.On("SetHeader", mD)} } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_SetHeader_Call) Run(run func(mD metadata.MD)) *StreamingSpanWriterPlugin_WriteSpanStreamServer_SetHeader_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 metadata.MD if args[0] != nil { arg0 = args[0].(metadata.MD) } run( arg0, ) }) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_SetHeader_Call) Return(err error) *StreamingSpanWriterPlugin_WriteSpanStreamServer_SetHeader_Call { _c.Call.Return(err) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_SetHeader_Call) RunAndReturn(run func(mD metadata.MD) error) *StreamingSpanWriterPlugin_WriteSpanStreamServer_SetHeader_Call { _c.Call.Return(run) return _c } // SetTrailer provides a mock function for the type StreamingSpanWriterPlugin_WriteSpanStreamServer func (_mock *StreamingSpanWriterPlugin_WriteSpanStreamServer) SetTrailer(mD metadata.MD) { _mock.Called(mD) return } // StreamingSpanWriterPlugin_WriteSpanStreamServer_SetTrailer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetTrailer' type StreamingSpanWriterPlugin_WriteSpanStreamServer_SetTrailer_Call struct { *mock.Call } // SetTrailer is a helper method to define mock.On call // - mD metadata.MD func (_e *StreamingSpanWriterPlugin_WriteSpanStreamServer_Expecter) SetTrailer(mD interface{}) *StreamingSpanWriterPlugin_WriteSpanStreamServer_SetTrailer_Call { return &StreamingSpanWriterPlugin_WriteSpanStreamServer_SetTrailer_Call{Call: _e.mock.On("SetTrailer", mD)} } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_SetTrailer_Call) Run(run func(mD metadata.MD)) *StreamingSpanWriterPlugin_WriteSpanStreamServer_SetTrailer_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 metadata.MD if args[0] != nil { arg0 = args[0].(metadata.MD) } run( arg0, ) }) return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_SetTrailer_Call) Return() *StreamingSpanWriterPlugin_WriteSpanStreamServer_SetTrailer_Call { _c.Call.Return() return _c } func (_c *StreamingSpanWriterPlugin_WriteSpanStreamServer_SetTrailer_Call) RunAndReturn(run func(mD metadata.MD)) *StreamingSpanWriterPlugin_WriteSpanStreamServer_SetTrailer_Call { _c.Run(run) return _c } // NewSpanReaderPluginClient creates a new instance of SpanReaderPluginClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewSpanReaderPluginClient(t interface { mock.TestingT Cleanup(func()) }) *SpanReaderPluginClient { mock := &SpanReaderPluginClient{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // SpanReaderPluginClient is an autogenerated mock type for the SpanReaderPluginClient type type SpanReaderPluginClient struct { mock.Mock } type SpanReaderPluginClient_Expecter struct { mock *mock.Mock } func (_m *SpanReaderPluginClient) EXPECT() *SpanReaderPluginClient_Expecter { return &SpanReaderPluginClient_Expecter{mock: &_m.Mock} } // FindTraceIDs provides a mock function for the type SpanReaderPluginClient func (_mock *SpanReaderPluginClient) FindTraceIDs(ctx context.Context, in *storage_v1.FindTraceIDsRequest, opts ...grpc.CallOption) (*storage_v1.FindTraceIDsResponse, error) { var tmpRet mock.Arguments if len(opts) > 0 { tmpRet = _mock.Called(ctx, in, opts) } else { tmpRet = _mock.Called(ctx, in) } ret := tmpRet if len(ret) == 0 { panic("no return value specified for FindTraceIDs") } var r0 *storage_v1.FindTraceIDsResponse var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.FindTraceIDsRequest, ...grpc.CallOption) (*storage_v1.FindTraceIDsResponse, error)); ok { return returnFunc(ctx, in, opts...) } if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.FindTraceIDsRequest, ...grpc.CallOption) *storage_v1.FindTraceIDsResponse); ok { r0 = returnFunc(ctx, in, opts...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*storage_v1.FindTraceIDsResponse) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *storage_v1.FindTraceIDsRequest, ...grpc.CallOption) error); ok { r1 = returnFunc(ctx, in, opts...) } else { r1 = ret.Error(1) } return r0, r1 } // SpanReaderPluginClient_FindTraceIDs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindTraceIDs' type SpanReaderPluginClient_FindTraceIDs_Call struct { *mock.Call } // FindTraceIDs is a helper method to define mock.On call // - ctx context.Context // - in *storage_v1.FindTraceIDsRequest // - opts ...grpc.CallOption func (_e *SpanReaderPluginClient_Expecter) FindTraceIDs(ctx interface{}, in interface{}, opts ...interface{}) *SpanReaderPluginClient_FindTraceIDs_Call { return &SpanReaderPluginClient_FindTraceIDs_Call{Call: _e.mock.On("FindTraceIDs", append([]interface{}{ctx, in}, opts...)...)} } func (_c *SpanReaderPluginClient_FindTraceIDs_Call) Run(run func(ctx context.Context, in *storage_v1.FindTraceIDsRequest, opts ...grpc.CallOption)) *SpanReaderPluginClient_FindTraceIDs_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *storage_v1.FindTraceIDsRequest if args[1] != nil { arg1 = args[1].(*storage_v1.FindTraceIDsRequest) } var arg2 []grpc.CallOption var variadicArgs []grpc.CallOption if len(args) > 2 { variadicArgs = args[2].([]grpc.CallOption) } arg2 = variadicArgs run( arg0, arg1, arg2..., ) }) return _c } func (_c *SpanReaderPluginClient_FindTraceIDs_Call) Return(findTraceIDsResponse *storage_v1.FindTraceIDsResponse, err error) *SpanReaderPluginClient_FindTraceIDs_Call { _c.Call.Return(findTraceIDsResponse, err) return _c } func (_c *SpanReaderPluginClient_FindTraceIDs_Call) RunAndReturn(run func(ctx context.Context, in *storage_v1.FindTraceIDsRequest, opts ...grpc.CallOption) (*storage_v1.FindTraceIDsResponse, error)) *SpanReaderPluginClient_FindTraceIDs_Call { _c.Call.Return(run) return _c } // FindTraces provides a mock function for the type SpanReaderPluginClient func (_mock *SpanReaderPluginClient) FindTraces(ctx context.Context, in *storage_v1.FindTracesRequest, opts ...grpc.CallOption) (storage_v1.SpanReaderPlugin_FindTracesClient, error) { var tmpRet mock.Arguments if len(opts) > 0 { tmpRet = _mock.Called(ctx, in, opts) } else { tmpRet = _mock.Called(ctx, in) } ret := tmpRet if len(ret) == 0 { panic("no return value specified for FindTraces") } var r0 storage_v1.SpanReaderPlugin_FindTracesClient var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.FindTracesRequest, ...grpc.CallOption) (storage_v1.SpanReaderPlugin_FindTracesClient, error)); ok { return returnFunc(ctx, in, opts...) } if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.FindTracesRequest, ...grpc.CallOption) storage_v1.SpanReaderPlugin_FindTracesClient); ok { r0 = returnFunc(ctx, in, opts...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(storage_v1.SpanReaderPlugin_FindTracesClient) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *storage_v1.FindTracesRequest, ...grpc.CallOption) error); ok { r1 = returnFunc(ctx, in, opts...) } else { r1 = ret.Error(1) } return r0, r1 } // SpanReaderPluginClient_FindTraces_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindTraces' type SpanReaderPluginClient_FindTraces_Call struct { *mock.Call } // FindTraces is a helper method to define mock.On call // - ctx context.Context // - in *storage_v1.FindTracesRequest // - opts ...grpc.CallOption func (_e *SpanReaderPluginClient_Expecter) FindTraces(ctx interface{}, in interface{}, opts ...interface{}) *SpanReaderPluginClient_FindTraces_Call { return &SpanReaderPluginClient_FindTraces_Call{Call: _e.mock.On("FindTraces", append([]interface{}{ctx, in}, opts...)...)} } func (_c *SpanReaderPluginClient_FindTraces_Call) Run(run func(ctx context.Context, in *storage_v1.FindTracesRequest, opts ...grpc.CallOption)) *SpanReaderPluginClient_FindTraces_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *storage_v1.FindTracesRequest if args[1] != nil { arg1 = args[1].(*storage_v1.FindTracesRequest) } var arg2 []grpc.CallOption var variadicArgs []grpc.CallOption if len(args) > 2 { variadicArgs = args[2].([]grpc.CallOption) } arg2 = variadicArgs run( arg0, arg1, arg2..., ) }) return _c } func (_c *SpanReaderPluginClient_FindTraces_Call) Return(spanReaderPlugin_FindTracesClient storage_v1.SpanReaderPlugin_FindTracesClient, err error) *SpanReaderPluginClient_FindTraces_Call { _c.Call.Return(spanReaderPlugin_FindTracesClient, err) return _c } func (_c *SpanReaderPluginClient_FindTraces_Call) RunAndReturn(run func(ctx context.Context, in *storage_v1.FindTracesRequest, opts ...grpc.CallOption) (storage_v1.SpanReaderPlugin_FindTracesClient, error)) *SpanReaderPluginClient_FindTraces_Call { _c.Call.Return(run) return _c } // GetOperations provides a mock function for the type SpanReaderPluginClient func (_mock *SpanReaderPluginClient) GetOperations(ctx context.Context, in *storage_v1.GetOperationsRequest, opts ...grpc.CallOption) (*storage_v1.GetOperationsResponse, error) { var tmpRet mock.Arguments if len(opts) > 0 { tmpRet = _mock.Called(ctx, in, opts) } else { tmpRet = _mock.Called(ctx, in) } ret := tmpRet if len(ret) == 0 { panic("no return value specified for GetOperations") } var r0 *storage_v1.GetOperationsResponse var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.GetOperationsRequest, ...grpc.CallOption) (*storage_v1.GetOperationsResponse, error)); ok { return returnFunc(ctx, in, opts...) } if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.GetOperationsRequest, ...grpc.CallOption) *storage_v1.GetOperationsResponse); ok { r0 = returnFunc(ctx, in, opts...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*storage_v1.GetOperationsResponse) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *storage_v1.GetOperationsRequest, ...grpc.CallOption) error); ok { r1 = returnFunc(ctx, in, opts...) } else { r1 = ret.Error(1) } return r0, r1 } // SpanReaderPluginClient_GetOperations_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetOperations' type SpanReaderPluginClient_GetOperations_Call struct { *mock.Call } // GetOperations is a helper method to define mock.On call // - ctx context.Context // - in *storage_v1.GetOperationsRequest // - opts ...grpc.CallOption func (_e *SpanReaderPluginClient_Expecter) GetOperations(ctx interface{}, in interface{}, opts ...interface{}) *SpanReaderPluginClient_GetOperations_Call { return &SpanReaderPluginClient_GetOperations_Call{Call: _e.mock.On("GetOperations", append([]interface{}{ctx, in}, opts...)...)} } func (_c *SpanReaderPluginClient_GetOperations_Call) Run(run func(ctx context.Context, in *storage_v1.GetOperationsRequest, opts ...grpc.CallOption)) *SpanReaderPluginClient_GetOperations_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *storage_v1.GetOperationsRequest if args[1] != nil { arg1 = args[1].(*storage_v1.GetOperationsRequest) } var arg2 []grpc.CallOption var variadicArgs []grpc.CallOption if len(args) > 2 { variadicArgs = args[2].([]grpc.CallOption) } arg2 = variadicArgs run( arg0, arg1, arg2..., ) }) return _c } func (_c *SpanReaderPluginClient_GetOperations_Call) Return(getOperationsResponse *storage_v1.GetOperationsResponse, err error) *SpanReaderPluginClient_GetOperations_Call { _c.Call.Return(getOperationsResponse, err) return _c } func (_c *SpanReaderPluginClient_GetOperations_Call) RunAndReturn(run func(ctx context.Context, in *storage_v1.GetOperationsRequest, opts ...grpc.CallOption) (*storage_v1.GetOperationsResponse, error)) *SpanReaderPluginClient_GetOperations_Call { _c.Call.Return(run) return _c } // GetServices provides a mock function for the type SpanReaderPluginClient func (_mock *SpanReaderPluginClient) GetServices(ctx context.Context, in *storage_v1.GetServicesRequest, opts ...grpc.CallOption) (*storage_v1.GetServicesResponse, error) { var tmpRet mock.Arguments if len(opts) > 0 { tmpRet = _mock.Called(ctx, in, opts) } else { tmpRet = _mock.Called(ctx, in) } ret := tmpRet if len(ret) == 0 { panic("no return value specified for GetServices") } var r0 *storage_v1.GetServicesResponse var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.GetServicesRequest, ...grpc.CallOption) (*storage_v1.GetServicesResponse, error)); ok { return returnFunc(ctx, in, opts...) } if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.GetServicesRequest, ...grpc.CallOption) *storage_v1.GetServicesResponse); ok { r0 = returnFunc(ctx, in, opts...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*storage_v1.GetServicesResponse) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *storage_v1.GetServicesRequest, ...grpc.CallOption) error); ok { r1 = returnFunc(ctx, in, opts...) } else { r1 = ret.Error(1) } return r0, r1 } // SpanReaderPluginClient_GetServices_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetServices' type SpanReaderPluginClient_GetServices_Call struct { *mock.Call } // GetServices is a helper method to define mock.On call // - ctx context.Context // - in *storage_v1.GetServicesRequest // - opts ...grpc.CallOption func (_e *SpanReaderPluginClient_Expecter) GetServices(ctx interface{}, in interface{}, opts ...interface{}) *SpanReaderPluginClient_GetServices_Call { return &SpanReaderPluginClient_GetServices_Call{Call: _e.mock.On("GetServices", append([]interface{}{ctx, in}, opts...)...)} } func (_c *SpanReaderPluginClient_GetServices_Call) Run(run func(ctx context.Context, in *storage_v1.GetServicesRequest, opts ...grpc.CallOption)) *SpanReaderPluginClient_GetServices_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *storage_v1.GetServicesRequest if args[1] != nil { arg1 = args[1].(*storage_v1.GetServicesRequest) } var arg2 []grpc.CallOption var variadicArgs []grpc.CallOption if len(args) > 2 { variadicArgs = args[2].([]grpc.CallOption) } arg2 = variadicArgs run( arg0, arg1, arg2..., ) }) return _c } func (_c *SpanReaderPluginClient_GetServices_Call) Return(getServicesResponse *storage_v1.GetServicesResponse, err error) *SpanReaderPluginClient_GetServices_Call { _c.Call.Return(getServicesResponse, err) return _c } func (_c *SpanReaderPluginClient_GetServices_Call) RunAndReturn(run func(ctx context.Context, in *storage_v1.GetServicesRequest, opts ...grpc.CallOption) (*storage_v1.GetServicesResponse, error)) *SpanReaderPluginClient_GetServices_Call { _c.Call.Return(run) return _c } // GetTrace provides a mock function for the type SpanReaderPluginClient func (_mock *SpanReaderPluginClient) GetTrace(ctx context.Context, in *storage_v1.GetTraceRequest, opts ...grpc.CallOption) (storage_v1.SpanReaderPlugin_GetTraceClient, error) { var tmpRet mock.Arguments if len(opts) > 0 { tmpRet = _mock.Called(ctx, in, opts) } else { tmpRet = _mock.Called(ctx, in) } ret := tmpRet if len(ret) == 0 { panic("no return value specified for GetTrace") } var r0 storage_v1.SpanReaderPlugin_GetTraceClient var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.GetTraceRequest, ...grpc.CallOption) (storage_v1.SpanReaderPlugin_GetTraceClient, error)); ok { return returnFunc(ctx, in, opts...) } if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.GetTraceRequest, ...grpc.CallOption) storage_v1.SpanReaderPlugin_GetTraceClient); ok { r0 = returnFunc(ctx, in, opts...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(storage_v1.SpanReaderPlugin_GetTraceClient) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *storage_v1.GetTraceRequest, ...grpc.CallOption) error); ok { r1 = returnFunc(ctx, in, opts...) } else { r1 = ret.Error(1) } return r0, r1 } // SpanReaderPluginClient_GetTrace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTrace' type SpanReaderPluginClient_GetTrace_Call struct { *mock.Call } // GetTrace is a helper method to define mock.On call // - ctx context.Context // - in *storage_v1.GetTraceRequest // - opts ...grpc.CallOption func (_e *SpanReaderPluginClient_Expecter) GetTrace(ctx interface{}, in interface{}, opts ...interface{}) *SpanReaderPluginClient_GetTrace_Call { return &SpanReaderPluginClient_GetTrace_Call{Call: _e.mock.On("GetTrace", append([]interface{}{ctx, in}, opts...)...)} } func (_c *SpanReaderPluginClient_GetTrace_Call) Run(run func(ctx context.Context, in *storage_v1.GetTraceRequest, opts ...grpc.CallOption)) *SpanReaderPluginClient_GetTrace_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *storage_v1.GetTraceRequest if args[1] != nil { arg1 = args[1].(*storage_v1.GetTraceRequest) } var arg2 []grpc.CallOption var variadicArgs []grpc.CallOption if len(args) > 2 { variadicArgs = args[2].([]grpc.CallOption) } arg2 = variadicArgs run( arg0, arg1, arg2..., ) }) return _c } func (_c *SpanReaderPluginClient_GetTrace_Call) Return(spanReaderPlugin_GetTraceClient storage_v1.SpanReaderPlugin_GetTraceClient, err error) *SpanReaderPluginClient_GetTrace_Call { _c.Call.Return(spanReaderPlugin_GetTraceClient, err) return _c } func (_c *SpanReaderPluginClient_GetTrace_Call) RunAndReturn(run func(ctx context.Context, in *storage_v1.GetTraceRequest, opts ...grpc.CallOption) (storage_v1.SpanReaderPlugin_GetTraceClient, error)) *SpanReaderPluginClient_GetTrace_Call { _c.Call.Return(run) return _c } // NewSpanReaderPlugin_GetTraceClient creates a new instance of SpanReaderPlugin_GetTraceClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewSpanReaderPlugin_GetTraceClient(t interface { mock.TestingT Cleanup(func()) }) *SpanReaderPlugin_GetTraceClient { mock := &SpanReaderPlugin_GetTraceClient{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // SpanReaderPlugin_GetTraceClient is an autogenerated mock type for the SpanReaderPlugin_GetTraceClient type type SpanReaderPlugin_GetTraceClient struct { mock.Mock } type SpanReaderPlugin_GetTraceClient_Expecter struct { mock *mock.Mock } func (_m *SpanReaderPlugin_GetTraceClient) EXPECT() *SpanReaderPlugin_GetTraceClient_Expecter { return &SpanReaderPlugin_GetTraceClient_Expecter{mock: &_m.Mock} } // CloseSend provides a mock function for the type SpanReaderPlugin_GetTraceClient func (_mock *SpanReaderPlugin_GetTraceClient) CloseSend() error { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for CloseSend") } var r0 error if returnFunc, ok := ret.Get(0).(func() error); ok { r0 = returnFunc() } else { r0 = ret.Error(0) } return r0 } // SpanReaderPlugin_GetTraceClient_CloseSend_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CloseSend' type SpanReaderPlugin_GetTraceClient_CloseSend_Call struct { *mock.Call } // CloseSend is a helper method to define mock.On call func (_e *SpanReaderPlugin_GetTraceClient_Expecter) CloseSend() *SpanReaderPlugin_GetTraceClient_CloseSend_Call { return &SpanReaderPlugin_GetTraceClient_CloseSend_Call{Call: _e.mock.On("CloseSend")} } func (_c *SpanReaderPlugin_GetTraceClient_CloseSend_Call) Run(run func()) *SpanReaderPlugin_GetTraceClient_CloseSend_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *SpanReaderPlugin_GetTraceClient_CloseSend_Call) Return(err error) *SpanReaderPlugin_GetTraceClient_CloseSend_Call { _c.Call.Return(err) return _c } func (_c *SpanReaderPlugin_GetTraceClient_CloseSend_Call) RunAndReturn(run func() error) *SpanReaderPlugin_GetTraceClient_CloseSend_Call { _c.Call.Return(run) return _c } // Context provides a mock function for the type SpanReaderPlugin_GetTraceClient func (_mock *SpanReaderPlugin_GetTraceClient) Context() context.Context { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Context") } var r0 context.Context if returnFunc, ok := ret.Get(0).(func() context.Context); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(context.Context) } } return r0 } // SpanReaderPlugin_GetTraceClient_Context_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Context' type SpanReaderPlugin_GetTraceClient_Context_Call struct { *mock.Call } // Context is a helper method to define mock.On call func (_e *SpanReaderPlugin_GetTraceClient_Expecter) Context() *SpanReaderPlugin_GetTraceClient_Context_Call { return &SpanReaderPlugin_GetTraceClient_Context_Call{Call: _e.mock.On("Context")} } func (_c *SpanReaderPlugin_GetTraceClient_Context_Call) Run(run func()) *SpanReaderPlugin_GetTraceClient_Context_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *SpanReaderPlugin_GetTraceClient_Context_Call) Return(context1 context.Context) *SpanReaderPlugin_GetTraceClient_Context_Call { _c.Call.Return(context1) return _c } func (_c *SpanReaderPlugin_GetTraceClient_Context_Call) RunAndReturn(run func() context.Context) *SpanReaderPlugin_GetTraceClient_Context_Call { _c.Call.Return(run) return _c } // Header provides a mock function for the type SpanReaderPlugin_GetTraceClient func (_mock *SpanReaderPlugin_GetTraceClient) Header() (metadata.MD, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Header") } var r0 metadata.MD var r1 error if returnFunc, ok := ret.Get(0).(func() (metadata.MD, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() metadata.MD); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(metadata.MD) } } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // SpanReaderPlugin_GetTraceClient_Header_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Header' type SpanReaderPlugin_GetTraceClient_Header_Call struct { *mock.Call } // Header is a helper method to define mock.On call func (_e *SpanReaderPlugin_GetTraceClient_Expecter) Header() *SpanReaderPlugin_GetTraceClient_Header_Call { return &SpanReaderPlugin_GetTraceClient_Header_Call{Call: _e.mock.On("Header")} } func (_c *SpanReaderPlugin_GetTraceClient_Header_Call) Run(run func()) *SpanReaderPlugin_GetTraceClient_Header_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *SpanReaderPlugin_GetTraceClient_Header_Call) Return(mD metadata.MD, err error) *SpanReaderPlugin_GetTraceClient_Header_Call { _c.Call.Return(mD, err) return _c } func (_c *SpanReaderPlugin_GetTraceClient_Header_Call) RunAndReturn(run func() (metadata.MD, error)) *SpanReaderPlugin_GetTraceClient_Header_Call { _c.Call.Return(run) return _c } // Recv provides a mock function for the type SpanReaderPlugin_GetTraceClient func (_mock *SpanReaderPlugin_GetTraceClient) Recv() (*storage_v1.SpansResponseChunk, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Recv") } var r0 *storage_v1.SpansResponseChunk var r1 error if returnFunc, ok := ret.Get(0).(func() (*storage_v1.SpansResponseChunk, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() *storage_v1.SpansResponseChunk); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*storage_v1.SpansResponseChunk) } } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // SpanReaderPlugin_GetTraceClient_Recv_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Recv' type SpanReaderPlugin_GetTraceClient_Recv_Call struct { *mock.Call } // Recv is a helper method to define mock.On call func (_e *SpanReaderPlugin_GetTraceClient_Expecter) Recv() *SpanReaderPlugin_GetTraceClient_Recv_Call { return &SpanReaderPlugin_GetTraceClient_Recv_Call{Call: _e.mock.On("Recv")} } func (_c *SpanReaderPlugin_GetTraceClient_Recv_Call) Run(run func()) *SpanReaderPlugin_GetTraceClient_Recv_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *SpanReaderPlugin_GetTraceClient_Recv_Call) Return(spansResponseChunk *storage_v1.SpansResponseChunk, err error) *SpanReaderPlugin_GetTraceClient_Recv_Call { _c.Call.Return(spansResponseChunk, err) return _c } func (_c *SpanReaderPlugin_GetTraceClient_Recv_Call) RunAndReturn(run func() (*storage_v1.SpansResponseChunk, error)) *SpanReaderPlugin_GetTraceClient_Recv_Call { _c.Call.Return(run) return _c } // RecvMsg provides a mock function for the type SpanReaderPlugin_GetTraceClient func (_mock *SpanReaderPlugin_GetTraceClient) RecvMsg(m any) error { ret := _mock.Called(m) if len(ret) == 0 { panic("no return value specified for RecvMsg") } var r0 error if returnFunc, ok := ret.Get(0).(func(any) error); ok { r0 = returnFunc(m) } else { r0 = ret.Error(0) } return r0 } // SpanReaderPlugin_GetTraceClient_RecvMsg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RecvMsg' type SpanReaderPlugin_GetTraceClient_RecvMsg_Call struct { *mock.Call } // RecvMsg is a helper method to define mock.On call // - m any func (_e *SpanReaderPlugin_GetTraceClient_Expecter) RecvMsg(m interface{}) *SpanReaderPlugin_GetTraceClient_RecvMsg_Call { return &SpanReaderPlugin_GetTraceClient_RecvMsg_Call{Call: _e.mock.On("RecvMsg", m)} } func (_c *SpanReaderPlugin_GetTraceClient_RecvMsg_Call) Run(run func(m any)) *SpanReaderPlugin_GetTraceClient_RecvMsg_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 any if args[0] != nil { arg0 = args[0].(any) } run( arg0, ) }) return _c } func (_c *SpanReaderPlugin_GetTraceClient_RecvMsg_Call) Return(err error) *SpanReaderPlugin_GetTraceClient_RecvMsg_Call { _c.Call.Return(err) return _c } func (_c *SpanReaderPlugin_GetTraceClient_RecvMsg_Call) RunAndReturn(run func(m any) error) *SpanReaderPlugin_GetTraceClient_RecvMsg_Call { _c.Call.Return(run) return _c } // SendMsg provides a mock function for the type SpanReaderPlugin_GetTraceClient func (_mock *SpanReaderPlugin_GetTraceClient) SendMsg(m any) error { ret := _mock.Called(m) if len(ret) == 0 { panic("no return value specified for SendMsg") } var r0 error if returnFunc, ok := ret.Get(0).(func(any) error); ok { r0 = returnFunc(m) } else { r0 = ret.Error(0) } return r0 } // SpanReaderPlugin_GetTraceClient_SendMsg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMsg' type SpanReaderPlugin_GetTraceClient_SendMsg_Call struct { *mock.Call } // SendMsg is a helper method to define mock.On call // - m any func (_e *SpanReaderPlugin_GetTraceClient_Expecter) SendMsg(m interface{}) *SpanReaderPlugin_GetTraceClient_SendMsg_Call { return &SpanReaderPlugin_GetTraceClient_SendMsg_Call{Call: _e.mock.On("SendMsg", m)} } func (_c *SpanReaderPlugin_GetTraceClient_SendMsg_Call) Run(run func(m any)) *SpanReaderPlugin_GetTraceClient_SendMsg_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 any if args[0] != nil { arg0 = args[0].(any) } run( arg0, ) }) return _c } func (_c *SpanReaderPlugin_GetTraceClient_SendMsg_Call) Return(err error) *SpanReaderPlugin_GetTraceClient_SendMsg_Call { _c.Call.Return(err) return _c } func (_c *SpanReaderPlugin_GetTraceClient_SendMsg_Call) RunAndReturn(run func(m any) error) *SpanReaderPlugin_GetTraceClient_SendMsg_Call { _c.Call.Return(run) return _c } // Trailer provides a mock function for the type SpanReaderPlugin_GetTraceClient func (_mock *SpanReaderPlugin_GetTraceClient) Trailer() metadata.MD { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Trailer") } var r0 metadata.MD if returnFunc, ok := ret.Get(0).(func() metadata.MD); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(metadata.MD) } } return r0 } // SpanReaderPlugin_GetTraceClient_Trailer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Trailer' type SpanReaderPlugin_GetTraceClient_Trailer_Call struct { *mock.Call } // Trailer is a helper method to define mock.On call func (_e *SpanReaderPlugin_GetTraceClient_Expecter) Trailer() *SpanReaderPlugin_GetTraceClient_Trailer_Call { return &SpanReaderPlugin_GetTraceClient_Trailer_Call{Call: _e.mock.On("Trailer")} } func (_c *SpanReaderPlugin_GetTraceClient_Trailer_Call) Run(run func()) *SpanReaderPlugin_GetTraceClient_Trailer_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *SpanReaderPlugin_GetTraceClient_Trailer_Call) Return(mD metadata.MD) *SpanReaderPlugin_GetTraceClient_Trailer_Call { _c.Call.Return(mD) return _c } func (_c *SpanReaderPlugin_GetTraceClient_Trailer_Call) RunAndReturn(run func() metadata.MD) *SpanReaderPlugin_GetTraceClient_Trailer_Call { _c.Call.Return(run) return _c } // NewSpanReaderPlugin_FindTracesClient creates a new instance of SpanReaderPlugin_FindTracesClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewSpanReaderPlugin_FindTracesClient(t interface { mock.TestingT Cleanup(func()) }) *SpanReaderPlugin_FindTracesClient { mock := &SpanReaderPlugin_FindTracesClient{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // SpanReaderPlugin_FindTracesClient is an autogenerated mock type for the SpanReaderPlugin_FindTracesClient type type SpanReaderPlugin_FindTracesClient struct { mock.Mock } type SpanReaderPlugin_FindTracesClient_Expecter struct { mock *mock.Mock } func (_m *SpanReaderPlugin_FindTracesClient) EXPECT() *SpanReaderPlugin_FindTracesClient_Expecter { return &SpanReaderPlugin_FindTracesClient_Expecter{mock: &_m.Mock} } // CloseSend provides a mock function for the type SpanReaderPlugin_FindTracesClient func (_mock *SpanReaderPlugin_FindTracesClient) CloseSend() error { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for CloseSend") } var r0 error if returnFunc, ok := ret.Get(0).(func() error); ok { r0 = returnFunc() } else { r0 = ret.Error(0) } return r0 } // SpanReaderPlugin_FindTracesClient_CloseSend_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CloseSend' type SpanReaderPlugin_FindTracesClient_CloseSend_Call struct { *mock.Call } // CloseSend is a helper method to define mock.On call func (_e *SpanReaderPlugin_FindTracesClient_Expecter) CloseSend() *SpanReaderPlugin_FindTracesClient_CloseSend_Call { return &SpanReaderPlugin_FindTracesClient_CloseSend_Call{Call: _e.mock.On("CloseSend")} } func (_c *SpanReaderPlugin_FindTracesClient_CloseSend_Call) Run(run func()) *SpanReaderPlugin_FindTracesClient_CloseSend_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *SpanReaderPlugin_FindTracesClient_CloseSend_Call) Return(err error) *SpanReaderPlugin_FindTracesClient_CloseSend_Call { _c.Call.Return(err) return _c } func (_c *SpanReaderPlugin_FindTracesClient_CloseSend_Call) RunAndReturn(run func() error) *SpanReaderPlugin_FindTracesClient_CloseSend_Call { _c.Call.Return(run) return _c } // Context provides a mock function for the type SpanReaderPlugin_FindTracesClient func (_mock *SpanReaderPlugin_FindTracesClient) Context() context.Context { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Context") } var r0 context.Context if returnFunc, ok := ret.Get(0).(func() context.Context); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(context.Context) } } return r0 } // SpanReaderPlugin_FindTracesClient_Context_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Context' type SpanReaderPlugin_FindTracesClient_Context_Call struct { *mock.Call } // Context is a helper method to define mock.On call func (_e *SpanReaderPlugin_FindTracesClient_Expecter) Context() *SpanReaderPlugin_FindTracesClient_Context_Call { return &SpanReaderPlugin_FindTracesClient_Context_Call{Call: _e.mock.On("Context")} } func (_c *SpanReaderPlugin_FindTracesClient_Context_Call) Run(run func()) *SpanReaderPlugin_FindTracesClient_Context_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *SpanReaderPlugin_FindTracesClient_Context_Call) Return(context1 context.Context) *SpanReaderPlugin_FindTracesClient_Context_Call { _c.Call.Return(context1) return _c } func (_c *SpanReaderPlugin_FindTracesClient_Context_Call) RunAndReturn(run func() context.Context) *SpanReaderPlugin_FindTracesClient_Context_Call { _c.Call.Return(run) return _c } // Header provides a mock function for the type SpanReaderPlugin_FindTracesClient func (_mock *SpanReaderPlugin_FindTracesClient) Header() (metadata.MD, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Header") } var r0 metadata.MD var r1 error if returnFunc, ok := ret.Get(0).(func() (metadata.MD, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() metadata.MD); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(metadata.MD) } } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // SpanReaderPlugin_FindTracesClient_Header_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Header' type SpanReaderPlugin_FindTracesClient_Header_Call struct { *mock.Call } // Header is a helper method to define mock.On call func (_e *SpanReaderPlugin_FindTracesClient_Expecter) Header() *SpanReaderPlugin_FindTracesClient_Header_Call { return &SpanReaderPlugin_FindTracesClient_Header_Call{Call: _e.mock.On("Header")} } func (_c *SpanReaderPlugin_FindTracesClient_Header_Call) Run(run func()) *SpanReaderPlugin_FindTracesClient_Header_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *SpanReaderPlugin_FindTracesClient_Header_Call) Return(mD metadata.MD, err error) *SpanReaderPlugin_FindTracesClient_Header_Call { _c.Call.Return(mD, err) return _c } func (_c *SpanReaderPlugin_FindTracesClient_Header_Call) RunAndReturn(run func() (metadata.MD, error)) *SpanReaderPlugin_FindTracesClient_Header_Call { _c.Call.Return(run) return _c } // Recv provides a mock function for the type SpanReaderPlugin_FindTracesClient func (_mock *SpanReaderPlugin_FindTracesClient) Recv() (*storage_v1.SpansResponseChunk, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Recv") } var r0 *storage_v1.SpansResponseChunk var r1 error if returnFunc, ok := ret.Get(0).(func() (*storage_v1.SpansResponseChunk, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() *storage_v1.SpansResponseChunk); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*storage_v1.SpansResponseChunk) } } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // SpanReaderPlugin_FindTracesClient_Recv_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Recv' type SpanReaderPlugin_FindTracesClient_Recv_Call struct { *mock.Call } // Recv is a helper method to define mock.On call func (_e *SpanReaderPlugin_FindTracesClient_Expecter) Recv() *SpanReaderPlugin_FindTracesClient_Recv_Call { return &SpanReaderPlugin_FindTracesClient_Recv_Call{Call: _e.mock.On("Recv")} } func (_c *SpanReaderPlugin_FindTracesClient_Recv_Call) Run(run func()) *SpanReaderPlugin_FindTracesClient_Recv_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *SpanReaderPlugin_FindTracesClient_Recv_Call) Return(spansResponseChunk *storage_v1.SpansResponseChunk, err error) *SpanReaderPlugin_FindTracesClient_Recv_Call { _c.Call.Return(spansResponseChunk, err) return _c } func (_c *SpanReaderPlugin_FindTracesClient_Recv_Call) RunAndReturn(run func() (*storage_v1.SpansResponseChunk, error)) *SpanReaderPlugin_FindTracesClient_Recv_Call { _c.Call.Return(run) return _c } // RecvMsg provides a mock function for the type SpanReaderPlugin_FindTracesClient func (_mock *SpanReaderPlugin_FindTracesClient) RecvMsg(m any) error { ret := _mock.Called(m) if len(ret) == 0 { panic("no return value specified for RecvMsg") } var r0 error if returnFunc, ok := ret.Get(0).(func(any) error); ok { r0 = returnFunc(m) } else { r0 = ret.Error(0) } return r0 } // SpanReaderPlugin_FindTracesClient_RecvMsg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RecvMsg' type SpanReaderPlugin_FindTracesClient_RecvMsg_Call struct { *mock.Call } // RecvMsg is a helper method to define mock.On call // - m any func (_e *SpanReaderPlugin_FindTracesClient_Expecter) RecvMsg(m interface{}) *SpanReaderPlugin_FindTracesClient_RecvMsg_Call { return &SpanReaderPlugin_FindTracesClient_RecvMsg_Call{Call: _e.mock.On("RecvMsg", m)} } func (_c *SpanReaderPlugin_FindTracesClient_RecvMsg_Call) Run(run func(m any)) *SpanReaderPlugin_FindTracesClient_RecvMsg_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 any if args[0] != nil { arg0 = args[0].(any) } run( arg0, ) }) return _c } func (_c *SpanReaderPlugin_FindTracesClient_RecvMsg_Call) Return(err error) *SpanReaderPlugin_FindTracesClient_RecvMsg_Call { _c.Call.Return(err) return _c } func (_c *SpanReaderPlugin_FindTracesClient_RecvMsg_Call) RunAndReturn(run func(m any) error) *SpanReaderPlugin_FindTracesClient_RecvMsg_Call { _c.Call.Return(run) return _c } // SendMsg provides a mock function for the type SpanReaderPlugin_FindTracesClient func (_mock *SpanReaderPlugin_FindTracesClient) SendMsg(m any) error { ret := _mock.Called(m) if len(ret) == 0 { panic("no return value specified for SendMsg") } var r0 error if returnFunc, ok := ret.Get(0).(func(any) error); ok { r0 = returnFunc(m) } else { r0 = ret.Error(0) } return r0 } // SpanReaderPlugin_FindTracesClient_SendMsg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMsg' type SpanReaderPlugin_FindTracesClient_SendMsg_Call struct { *mock.Call } // SendMsg is a helper method to define mock.On call // - m any func (_e *SpanReaderPlugin_FindTracesClient_Expecter) SendMsg(m interface{}) *SpanReaderPlugin_FindTracesClient_SendMsg_Call { return &SpanReaderPlugin_FindTracesClient_SendMsg_Call{Call: _e.mock.On("SendMsg", m)} } func (_c *SpanReaderPlugin_FindTracesClient_SendMsg_Call) Run(run func(m any)) *SpanReaderPlugin_FindTracesClient_SendMsg_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 any if args[0] != nil { arg0 = args[0].(any) } run( arg0, ) }) return _c } func (_c *SpanReaderPlugin_FindTracesClient_SendMsg_Call) Return(err error) *SpanReaderPlugin_FindTracesClient_SendMsg_Call { _c.Call.Return(err) return _c } func (_c *SpanReaderPlugin_FindTracesClient_SendMsg_Call) RunAndReturn(run func(m any) error) *SpanReaderPlugin_FindTracesClient_SendMsg_Call { _c.Call.Return(run) return _c } // Trailer provides a mock function for the type SpanReaderPlugin_FindTracesClient func (_mock *SpanReaderPlugin_FindTracesClient) Trailer() metadata.MD { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Trailer") } var r0 metadata.MD if returnFunc, ok := ret.Get(0).(func() metadata.MD); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(metadata.MD) } } return r0 } // SpanReaderPlugin_FindTracesClient_Trailer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Trailer' type SpanReaderPlugin_FindTracesClient_Trailer_Call struct { *mock.Call } // Trailer is a helper method to define mock.On call func (_e *SpanReaderPlugin_FindTracesClient_Expecter) Trailer() *SpanReaderPlugin_FindTracesClient_Trailer_Call { return &SpanReaderPlugin_FindTracesClient_Trailer_Call{Call: _e.mock.On("Trailer")} } func (_c *SpanReaderPlugin_FindTracesClient_Trailer_Call) Run(run func()) *SpanReaderPlugin_FindTracesClient_Trailer_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *SpanReaderPlugin_FindTracesClient_Trailer_Call) Return(mD metadata.MD) *SpanReaderPlugin_FindTracesClient_Trailer_Call { _c.Call.Return(mD) return _c } func (_c *SpanReaderPlugin_FindTracesClient_Trailer_Call) RunAndReturn(run func() metadata.MD) *SpanReaderPlugin_FindTracesClient_Trailer_Call { _c.Call.Return(run) return _c } // NewSpanReaderPluginServer creates a new instance of SpanReaderPluginServer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewSpanReaderPluginServer(t interface { mock.TestingT Cleanup(func()) }) *SpanReaderPluginServer { mock := &SpanReaderPluginServer{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // SpanReaderPluginServer is an autogenerated mock type for the SpanReaderPluginServer type type SpanReaderPluginServer struct { mock.Mock } type SpanReaderPluginServer_Expecter struct { mock *mock.Mock } func (_m *SpanReaderPluginServer) EXPECT() *SpanReaderPluginServer_Expecter { return &SpanReaderPluginServer_Expecter{mock: &_m.Mock} } // FindTraceIDs provides a mock function for the type SpanReaderPluginServer func (_mock *SpanReaderPluginServer) FindTraceIDs(context1 context.Context, findTraceIDsRequest *storage_v1.FindTraceIDsRequest) (*storage_v1.FindTraceIDsResponse, error) { ret := _mock.Called(context1, findTraceIDsRequest) if len(ret) == 0 { panic("no return value specified for FindTraceIDs") } var r0 *storage_v1.FindTraceIDsResponse var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.FindTraceIDsRequest) (*storage_v1.FindTraceIDsResponse, error)); ok { return returnFunc(context1, findTraceIDsRequest) } if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.FindTraceIDsRequest) *storage_v1.FindTraceIDsResponse); ok { r0 = returnFunc(context1, findTraceIDsRequest) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*storage_v1.FindTraceIDsResponse) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *storage_v1.FindTraceIDsRequest) error); ok { r1 = returnFunc(context1, findTraceIDsRequest) } else { r1 = ret.Error(1) } return r0, r1 } // SpanReaderPluginServer_FindTraceIDs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindTraceIDs' type SpanReaderPluginServer_FindTraceIDs_Call struct { *mock.Call } // FindTraceIDs is a helper method to define mock.On call // - context1 context.Context // - findTraceIDsRequest *storage_v1.FindTraceIDsRequest func (_e *SpanReaderPluginServer_Expecter) FindTraceIDs(context1 interface{}, findTraceIDsRequest interface{}) *SpanReaderPluginServer_FindTraceIDs_Call { return &SpanReaderPluginServer_FindTraceIDs_Call{Call: _e.mock.On("FindTraceIDs", context1, findTraceIDsRequest)} } func (_c *SpanReaderPluginServer_FindTraceIDs_Call) Run(run func(context1 context.Context, findTraceIDsRequest *storage_v1.FindTraceIDsRequest)) *SpanReaderPluginServer_FindTraceIDs_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *storage_v1.FindTraceIDsRequest if args[1] != nil { arg1 = args[1].(*storage_v1.FindTraceIDsRequest) } run( arg0, arg1, ) }) return _c } func (_c *SpanReaderPluginServer_FindTraceIDs_Call) Return(findTraceIDsResponse *storage_v1.FindTraceIDsResponse, err error) *SpanReaderPluginServer_FindTraceIDs_Call { _c.Call.Return(findTraceIDsResponse, err) return _c } func (_c *SpanReaderPluginServer_FindTraceIDs_Call) RunAndReturn(run func(context1 context.Context, findTraceIDsRequest *storage_v1.FindTraceIDsRequest) (*storage_v1.FindTraceIDsResponse, error)) *SpanReaderPluginServer_FindTraceIDs_Call { _c.Call.Return(run) return _c } // FindTraces provides a mock function for the type SpanReaderPluginServer func (_mock *SpanReaderPluginServer) FindTraces(findTracesRequest *storage_v1.FindTracesRequest, spanReaderPlugin_FindTracesServer storage_v1.SpanReaderPlugin_FindTracesServer) error { ret := _mock.Called(findTracesRequest, spanReaderPlugin_FindTracesServer) if len(ret) == 0 { panic("no return value specified for FindTraces") } var r0 error if returnFunc, ok := ret.Get(0).(func(*storage_v1.FindTracesRequest, storage_v1.SpanReaderPlugin_FindTracesServer) error); ok { r0 = returnFunc(findTracesRequest, spanReaderPlugin_FindTracesServer) } else { r0 = ret.Error(0) } return r0 } // SpanReaderPluginServer_FindTraces_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindTraces' type SpanReaderPluginServer_FindTraces_Call struct { *mock.Call } // FindTraces is a helper method to define mock.On call // - findTracesRequest *storage_v1.FindTracesRequest // - spanReaderPlugin_FindTracesServer storage_v1.SpanReaderPlugin_FindTracesServer func (_e *SpanReaderPluginServer_Expecter) FindTraces(findTracesRequest interface{}, spanReaderPlugin_FindTracesServer interface{}) *SpanReaderPluginServer_FindTraces_Call { return &SpanReaderPluginServer_FindTraces_Call{Call: _e.mock.On("FindTraces", findTracesRequest, spanReaderPlugin_FindTracesServer)} } func (_c *SpanReaderPluginServer_FindTraces_Call) Run(run func(findTracesRequest *storage_v1.FindTracesRequest, spanReaderPlugin_FindTracesServer storage_v1.SpanReaderPlugin_FindTracesServer)) *SpanReaderPluginServer_FindTraces_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 *storage_v1.FindTracesRequest if args[0] != nil { arg0 = args[0].(*storage_v1.FindTracesRequest) } var arg1 storage_v1.SpanReaderPlugin_FindTracesServer if args[1] != nil { arg1 = args[1].(storage_v1.SpanReaderPlugin_FindTracesServer) } run( arg0, arg1, ) }) return _c } func (_c *SpanReaderPluginServer_FindTraces_Call) Return(err error) *SpanReaderPluginServer_FindTraces_Call { _c.Call.Return(err) return _c } func (_c *SpanReaderPluginServer_FindTraces_Call) RunAndReturn(run func(findTracesRequest *storage_v1.FindTracesRequest, spanReaderPlugin_FindTracesServer storage_v1.SpanReaderPlugin_FindTracesServer) error) *SpanReaderPluginServer_FindTraces_Call { _c.Call.Return(run) return _c } // GetOperations provides a mock function for the type SpanReaderPluginServer func (_mock *SpanReaderPluginServer) GetOperations(context1 context.Context, getOperationsRequest *storage_v1.GetOperationsRequest) (*storage_v1.GetOperationsResponse, error) { ret := _mock.Called(context1, getOperationsRequest) if len(ret) == 0 { panic("no return value specified for GetOperations") } var r0 *storage_v1.GetOperationsResponse var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.GetOperationsRequest) (*storage_v1.GetOperationsResponse, error)); ok { return returnFunc(context1, getOperationsRequest) } if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.GetOperationsRequest) *storage_v1.GetOperationsResponse); ok { r0 = returnFunc(context1, getOperationsRequest) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*storage_v1.GetOperationsResponse) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *storage_v1.GetOperationsRequest) error); ok { r1 = returnFunc(context1, getOperationsRequest) } else { r1 = ret.Error(1) } return r0, r1 } // SpanReaderPluginServer_GetOperations_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetOperations' type SpanReaderPluginServer_GetOperations_Call struct { *mock.Call } // GetOperations is a helper method to define mock.On call // - context1 context.Context // - getOperationsRequest *storage_v1.GetOperationsRequest func (_e *SpanReaderPluginServer_Expecter) GetOperations(context1 interface{}, getOperationsRequest interface{}) *SpanReaderPluginServer_GetOperations_Call { return &SpanReaderPluginServer_GetOperations_Call{Call: _e.mock.On("GetOperations", context1, getOperationsRequest)} } func (_c *SpanReaderPluginServer_GetOperations_Call) Run(run func(context1 context.Context, getOperationsRequest *storage_v1.GetOperationsRequest)) *SpanReaderPluginServer_GetOperations_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *storage_v1.GetOperationsRequest if args[1] != nil { arg1 = args[1].(*storage_v1.GetOperationsRequest) } run( arg0, arg1, ) }) return _c } func (_c *SpanReaderPluginServer_GetOperations_Call) Return(getOperationsResponse *storage_v1.GetOperationsResponse, err error) *SpanReaderPluginServer_GetOperations_Call { _c.Call.Return(getOperationsResponse, err) return _c } func (_c *SpanReaderPluginServer_GetOperations_Call) RunAndReturn(run func(context1 context.Context, getOperationsRequest *storage_v1.GetOperationsRequest) (*storage_v1.GetOperationsResponse, error)) *SpanReaderPluginServer_GetOperations_Call { _c.Call.Return(run) return _c } // GetServices provides a mock function for the type SpanReaderPluginServer func (_mock *SpanReaderPluginServer) GetServices(context1 context.Context, getServicesRequest *storage_v1.GetServicesRequest) (*storage_v1.GetServicesResponse, error) { ret := _mock.Called(context1, getServicesRequest) if len(ret) == 0 { panic("no return value specified for GetServices") } var r0 *storage_v1.GetServicesResponse var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.GetServicesRequest) (*storage_v1.GetServicesResponse, error)); ok { return returnFunc(context1, getServicesRequest) } if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.GetServicesRequest) *storage_v1.GetServicesResponse); ok { r0 = returnFunc(context1, getServicesRequest) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*storage_v1.GetServicesResponse) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *storage_v1.GetServicesRequest) error); ok { r1 = returnFunc(context1, getServicesRequest) } else { r1 = ret.Error(1) } return r0, r1 } // SpanReaderPluginServer_GetServices_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetServices' type SpanReaderPluginServer_GetServices_Call struct { *mock.Call } // GetServices is a helper method to define mock.On call // - context1 context.Context // - getServicesRequest *storage_v1.GetServicesRequest func (_e *SpanReaderPluginServer_Expecter) GetServices(context1 interface{}, getServicesRequest interface{}) *SpanReaderPluginServer_GetServices_Call { return &SpanReaderPluginServer_GetServices_Call{Call: _e.mock.On("GetServices", context1, getServicesRequest)} } func (_c *SpanReaderPluginServer_GetServices_Call) Run(run func(context1 context.Context, getServicesRequest *storage_v1.GetServicesRequest)) *SpanReaderPluginServer_GetServices_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *storage_v1.GetServicesRequest if args[1] != nil { arg1 = args[1].(*storage_v1.GetServicesRequest) } run( arg0, arg1, ) }) return _c } func (_c *SpanReaderPluginServer_GetServices_Call) Return(getServicesResponse *storage_v1.GetServicesResponse, err error) *SpanReaderPluginServer_GetServices_Call { _c.Call.Return(getServicesResponse, err) return _c } func (_c *SpanReaderPluginServer_GetServices_Call) RunAndReturn(run func(context1 context.Context, getServicesRequest *storage_v1.GetServicesRequest) (*storage_v1.GetServicesResponse, error)) *SpanReaderPluginServer_GetServices_Call { _c.Call.Return(run) return _c } // GetTrace provides a mock function for the type SpanReaderPluginServer func (_mock *SpanReaderPluginServer) GetTrace(getTraceRequest *storage_v1.GetTraceRequest, spanReaderPlugin_GetTraceServer storage_v1.SpanReaderPlugin_GetTraceServer) error { ret := _mock.Called(getTraceRequest, spanReaderPlugin_GetTraceServer) if len(ret) == 0 { panic("no return value specified for GetTrace") } var r0 error if returnFunc, ok := ret.Get(0).(func(*storage_v1.GetTraceRequest, storage_v1.SpanReaderPlugin_GetTraceServer) error); ok { r0 = returnFunc(getTraceRequest, spanReaderPlugin_GetTraceServer) } else { r0 = ret.Error(0) } return r0 } // SpanReaderPluginServer_GetTrace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTrace' type SpanReaderPluginServer_GetTrace_Call struct { *mock.Call } // GetTrace is a helper method to define mock.On call // - getTraceRequest *storage_v1.GetTraceRequest // - spanReaderPlugin_GetTraceServer storage_v1.SpanReaderPlugin_GetTraceServer func (_e *SpanReaderPluginServer_Expecter) GetTrace(getTraceRequest interface{}, spanReaderPlugin_GetTraceServer interface{}) *SpanReaderPluginServer_GetTrace_Call { return &SpanReaderPluginServer_GetTrace_Call{Call: _e.mock.On("GetTrace", getTraceRequest, spanReaderPlugin_GetTraceServer)} } func (_c *SpanReaderPluginServer_GetTrace_Call) Run(run func(getTraceRequest *storage_v1.GetTraceRequest, spanReaderPlugin_GetTraceServer storage_v1.SpanReaderPlugin_GetTraceServer)) *SpanReaderPluginServer_GetTrace_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 *storage_v1.GetTraceRequest if args[0] != nil { arg0 = args[0].(*storage_v1.GetTraceRequest) } var arg1 storage_v1.SpanReaderPlugin_GetTraceServer if args[1] != nil { arg1 = args[1].(storage_v1.SpanReaderPlugin_GetTraceServer) } run( arg0, arg1, ) }) return _c } func (_c *SpanReaderPluginServer_GetTrace_Call) Return(err error) *SpanReaderPluginServer_GetTrace_Call { _c.Call.Return(err) return _c } func (_c *SpanReaderPluginServer_GetTrace_Call) RunAndReturn(run func(getTraceRequest *storage_v1.GetTraceRequest, spanReaderPlugin_GetTraceServer storage_v1.SpanReaderPlugin_GetTraceServer) error) *SpanReaderPluginServer_GetTrace_Call { _c.Call.Return(run) return _c } // NewSpanReaderPlugin_GetTraceServer creates a new instance of SpanReaderPlugin_GetTraceServer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewSpanReaderPlugin_GetTraceServer(t interface { mock.TestingT Cleanup(func()) }) *SpanReaderPlugin_GetTraceServer { mock := &SpanReaderPlugin_GetTraceServer{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // SpanReaderPlugin_GetTraceServer is an autogenerated mock type for the SpanReaderPlugin_GetTraceServer type type SpanReaderPlugin_GetTraceServer struct { mock.Mock } type SpanReaderPlugin_GetTraceServer_Expecter struct { mock *mock.Mock } func (_m *SpanReaderPlugin_GetTraceServer) EXPECT() *SpanReaderPlugin_GetTraceServer_Expecter { return &SpanReaderPlugin_GetTraceServer_Expecter{mock: &_m.Mock} } // Context provides a mock function for the type SpanReaderPlugin_GetTraceServer func (_mock *SpanReaderPlugin_GetTraceServer) Context() context.Context { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Context") } var r0 context.Context if returnFunc, ok := ret.Get(0).(func() context.Context); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(context.Context) } } return r0 } // SpanReaderPlugin_GetTraceServer_Context_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Context' type SpanReaderPlugin_GetTraceServer_Context_Call struct { *mock.Call } // Context is a helper method to define mock.On call func (_e *SpanReaderPlugin_GetTraceServer_Expecter) Context() *SpanReaderPlugin_GetTraceServer_Context_Call { return &SpanReaderPlugin_GetTraceServer_Context_Call{Call: _e.mock.On("Context")} } func (_c *SpanReaderPlugin_GetTraceServer_Context_Call) Run(run func()) *SpanReaderPlugin_GetTraceServer_Context_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *SpanReaderPlugin_GetTraceServer_Context_Call) Return(context1 context.Context) *SpanReaderPlugin_GetTraceServer_Context_Call { _c.Call.Return(context1) return _c } func (_c *SpanReaderPlugin_GetTraceServer_Context_Call) RunAndReturn(run func() context.Context) *SpanReaderPlugin_GetTraceServer_Context_Call { _c.Call.Return(run) return _c } // RecvMsg provides a mock function for the type SpanReaderPlugin_GetTraceServer func (_mock *SpanReaderPlugin_GetTraceServer) RecvMsg(m any) error { ret := _mock.Called(m) if len(ret) == 0 { panic("no return value specified for RecvMsg") } var r0 error if returnFunc, ok := ret.Get(0).(func(any) error); ok { r0 = returnFunc(m) } else { r0 = ret.Error(0) } return r0 } // SpanReaderPlugin_GetTraceServer_RecvMsg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RecvMsg' type SpanReaderPlugin_GetTraceServer_RecvMsg_Call struct { *mock.Call } // RecvMsg is a helper method to define mock.On call // - m any func (_e *SpanReaderPlugin_GetTraceServer_Expecter) RecvMsg(m interface{}) *SpanReaderPlugin_GetTraceServer_RecvMsg_Call { return &SpanReaderPlugin_GetTraceServer_RecvMsg_Call{Call: _e.mock.On("RecvMsg", m)} } func (_c *SpanReaderPlugin_GetTraceServer_RecvMsg_Call) Run(run func(m any)) *SpanReaderPlugin_GetTraceServer_RecvMsg_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 any if args[0] != nil { arg0 = args[0].(any) } run( arg0, ) }) return _c } func (_c *SpanReaderPlugin_GetTraceServer_RecvMsg_Call) Return(err error) *SpanReaderPlugin_GetTraceServer_RecvMsg_Call { _c.Call.Return(err) return _c } func (_c *SpanReaderPlugin_GetTraceServer_RecvMsg_Call) RunAndReturn(run func(m any) error) *SpanReaderPlugin_GetTraceServer_RecvMsg_Call { _c.Call.Return(run) return _c } // Send provides a mock function for the type SpanReaderPlugin_GetTraceServer func (_mock *SpanReaderPlugin_GetTraceServer) Send(spansResponseChunk *storage_v1.SpansResponseChunk) error { ret := _mock.Called(spansResponseChunk) if len(ret) == 0 { panic("no return value specified for Send") } var r0 error if returnFunc, ok := ret.Get(0).(func(*storage_v1.SpansResponseChunk) error); ok { r0 = returnFunc(spansResponseChunk) } else { r0 = ret.Error(0) } return r0 } // SpanReaderPlugin_GetTraceServer_Send_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Send' type SpanReaderPlugin_GetTraceServer_Send_Call struct { *mock.Call } // Send is a helper method to define mock.On call // - spansResponseChunk *storage_v1.SpansResponseChunk func (_e *SpanReaderPlugin_GetTraceServer_Expecter) Send(spansResponseChunk interface{}) *SpanReaderPlugin_GetTraceServer_Send_Call { return &SpanReaderPlugin_GetTraceServer_Send_Call{Call: _e.mock.On("Send", spansResponseChunk)} } func (_c *SpanReaderPlugin_GetTraceServer_Send_Call) Run(run func(spansResponseChunk *storage_v1.SpansResponseChunk)) *SpanReaderPlugin_GetTraceServer_Send_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 *storage_v1.SpansResponseChunk if args[0] != nil { arg0 = args[0].(*storage_v1.SpansResponseChunk) } run( arg0, ) }) return _c } func (_c *SpanReaderPlugin_GetTraceServer_Send_Call) Return(err error) *SpanReaderPlugin_GetTraceServer_Send_Call { _c.Call.Return(err) return _c } func (_c *SpanReaderPlugin_GetTraceServer_Send_Call) RunAndReturn(run func(spansResponseChunk *storage_v1.SpansResponseChunk) error) *SpanReaderPlugin_GetTraceServer_Send_Call { _c.Call.Return(run) return _c } // SendHeader provides a mock function for the type SpanReaderPlugin_GetTraceServer func (_mock *SpanReaderPlugin_GetTraceServer) SendHeader(mD metadata.MD) error { ret := _mock.Called(mD) if len(ret) == 0 { panic("no return value specified for SendHeader") } var r0 error if returnFunc, ok := ret.Get(0).(func(metadata.MD) error); ok { r0 = returnFunc(mD) } else { r0 = ret.Error(0) } return r0 } // SpanReaderPlugin_GetTraceServer_SendHeader_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendHeader' type SpanReaderPlugin_GetTraceServer_SendHeader_Call struct { *mock.Call } // SendHeader is a helper method to define mock.On call // - mD metadata.MD func (_e *SpanReaderPlugin_GetTraceServer_Expecter) SendHeader(mD interface{}) *SpanReaderPlugin_GetTraceServer_SendHeader_Call { return &SpanReaderPlugin_GetTraceServer_SendHeader_Call{Call: _e.mock.On("SendHeader", mD)} } func (_c *SpanReaderPlugin_GetTraceServer_SendHeader_Call) Run(run func(mD metadata.MD)) *SpanReaderPlugin_GetTraceServer_SendHeader_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 metadata.MD if args[0] != nil { arg0 = args[0].(metadata.MD) } run( arg0, ) }) return _c } func (_c *SpanReaderPlugin_GetTraceServer_SendHeader_Call) Return(err error) *SpanReaderPlugin_GetTraceServer_SendHeader_Call { _c.Call.Return(err) return _c } func (_c *SpanReaderPlugin_GetTraceServer_SendHeader_Call) RunAndReturn(run func(mD metadata.MD) error) *SpanReaderPlugin_GetTraceServer_SendHeader_Call { _c.Call.Return(run) return _c } // SendMsg provides a mock function for the type SpanReaderPlugin_GetTraceServer func (_mock *SpanReaderPlugin_GetTraceServer) SendMsg(m any) error { ret := _mock.Called(m) if len(ret) == 0 { panic("no return value specified for SendMsg") } var r0 error if returnFunc, ok := ret.Get(0).(func(any) error); ok { r0 = returnFunc(m) } else { r0 = ret.Error(0) } return r0 } // SpanReaderPlugin_GetTraceServer_SendMsg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMsg' type SpanReaderPlugin_GetTraceServer_SendMsg_Call struct { *mock.Call } // SendMsg is a helper method to define mock.On call // - m any func (_e *SpanReaderPlugin_GetTraceServer_Expecter) SendMsg(m interface{}) *SpanReaderPlugin_GetTraceServer_SendMsg_Call { return &SpanReaderPlugin_GetTraceServer_SendMsg_Call{Call: _e.mock.On("SendMsg", m)} } func (_c *SpanReaderPlugin_GetTraceServer_SendMsg_Call) Run(run func(m any)) *SpanReaderPlugin_GetTraceServer_SendMsg_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 any if args[0] != nil { arg0 = args[0].(any) } run( arg0, ) }) return _c } func (_c *SpanReaderPlugin_GetTraceServer_SendMsg_Call) Return(err error) *SpanReaderPlugin_GetTraceServer_SendMsg_Call { _c.Call.Return(err) return _c } func (_c *SpanReaderPlugin_GetTraceServer_SendMsg_Call) RunAndReturn(run func(m any) error) *SpanReaderPlugin_GetTraceServer_SendMsg_Call { _c.Call.Return(run) return _c } // SetHeader provides a mock function for the type SpanReaderPlugin_GetTraceServer func (_mock *SpanReaderPlugin_GetTraceServer) SetHeader(mD metadata.MD) error { ret := _mock.Called(mD) if len(ret) == 0 { panic("no return value specified for SetHeader") } var r0 error if returnFunc, ok := ret.Get(0).(func(metadata.MD) error); ok { r0 = returnFunc(mD) } else { r0 = ret.Error(0) } return r0 } // SpanReaderPlugin_GetTraceServer_SetHeader_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetHeader' type SpanReaderPlugin_GetTraceServer_SetHeader_Call struct { *mock.Call } // SetHeader is a helper method to define mock.On call // - mD metadata.MD func (_e *SpanReaderPlugin_GetTraceServer_Expecter) SetHeader(mD interface{}) *SpanReaderPlugin_GetTraceServer_SetHeader_Call { return &SpanReaderPlugin_GetTraceServer_SetHeader_Call{Call: _e.mock.On("SetHeader", mD)} } func (_c *SpanReaderPlugin_GetTraceServer_SetHeader_Call) Run(run func(mD metadata.MD)) *SpanReaderPlugin_GetTraceServer_SetHeader_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 metadata.MD if args[0] != nil { arg0 = args[0].(metadata.MD) } run( arg0, ) }) return _c } func (_c *SpanReaderPlugin_GetTraceServer_SetHeader_Call) Return(err error) *SpanReaderPlugin_GetTraceServer_SetHeader_Call { _c.Call.Return(err) return _c } func (_c *SpanReaderPlugin_GetTraceServer_SetHeader_Call) RunAndReturn(run func(mD metadata.MD) error) *SpanReaderPlugin_GetTraceServer_SetHeader_Call { _c.Call.Return(run) return _c } // SetTrailer provides a mock function for the type SpanReaderPlugin_GetTraceServer func (_mock *SpanReaderPlugin_GetTraceServer) SetTrailer(mD metadata.MD) { _mock.Called(mD) return } // SpanReaderPlugin_GetTraceServer_SetTrailer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetTrailer' type SpanReaderPlugin_GetTraceServer_SetTrailer_Call struct { *mock.Call } // SetTrailer is a helper method to define mock.On call // - mD metadata.MD func (_e *SpanReaderPlugin_GetTraceServer_Expecter) SetTrailer(mD interface{}) *SpanReaderPlugin_GetTraceServer_SetTrailer_Call { return &SpanReaderPlugin_GetTraceServer_SetTrailer_Call{Call: _e.mock.On("SetTrailer", mD)} } func (_c *SpanReaderPlugin_GetTraceServer_SetTrailer_Call) Run(run func(mD metadata.MD)) *SpanReaderPlugin_GetTraceServer_SetTrailer_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 metadata.MD if args[0] != nil { arg0 = args[0].(metadata.MD) } run( arg0, ) }) return _c } func (_c *SpanReaderPlugin_GetTraceServer_SetTrailer_Call) Return() *SpanReaderPlugin_GetTraceServer_SetTrailer_Call { _c.Call.Return() return _c } func (_c *SpanReaderPlugin_GetTraceServer_SetTrailer_Call) RunAndReturn(run func(mD metadata.MD)) *SpanReaderPlugin_GetTraceServer_SetTrailer_Call { _c.Run(run) return _c } // NewSpanReaderPlugin_FindTracesServer creates a new instance of SpanReaderPlugin_FindTracesServer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewSpanReaderPlugin_FindTracesServer(t interface { mock.TestingT Cleanup(func()) }) *SpanReaderPlugin_FindTracesServer { mock := &SpanReaderPlugin_FindTracesServer{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // SpanReaderPlugin_FindTracesServer is an autogenerated mock type for the SpanReaderPlugin_FindTracesServer type type SpanReaderPlugin_FindTracesServer struct { mock.Mock } type SpanReaderPlugin_FindTracesServer_Expecter struct { mock *mock.Mock } func (_m *SpanReaderPlugin_FindTracesServer) EXPECT() *SpanReaderPlugin_FindTracesServer_Expecter { return &SpanReaderPlugin_FindTracesServer_Expecter{mock: &_m.Mock} } // Context provides a mock function for the type SpanReaderPlugin_FindTracesServer func (_mock *SpanReaderPlugin_FindTracesServer) Context() context.Context { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Context") } var r0 context.Context if returnFunc, ok := ret.Get(0).(func() context.Context); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(context.Context) } } return r0 } // SpanReaderPlugin_FindTracesServer_Context_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Context' type SpanReaderPlugin_FindTracesServer_Context_Call struct { *mock.Call } // Context is a helper method to define mock.On call func (_e *SpanReaderPlugin_FindTracesServer_Expecter) Context() *SpanReaderPlugin_FindTracesServer_Context_Call { return &SpanReaderPlugin_FindTracesServer_Context_Call{Call: _e.mock.On("Context")} } func (_c *SpanReaderPlugin_FindTracesServer_Context_Call) Run(run func()) *SpanReaderPlugin_FindTracesServer_Context_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *SpanReaderPlugin_FindTracesServer_Context_Call) Return(context1 context.Context) *SpanReaderPlugin_FindTracesServer_Context_Call { _c.Call.Return(context1) return _c } func (_c *SpanReaderPlugin_FindTracesServer_Context_Call) RunAndReturn(run func() context.Context) *SpanReaderPlugin_FindTracesServer_Context_Call { _c.Call.Return(run) return _c } // RecvMsg provides a mock function for the type SpanReaderPlugin_FindTracesServer func (_mock *SpanReaderPlugin_FindTracesServer) RecvMsg(m any) error { ret := _mock.Called(m) if len(ret) == 0 { panic("no return value specified for RecvMsg") } var r0 error if returnFunc, ok := ret.Get(0).(func(any) error); ok { r0 = returnFunc(m) } else { r0 = ret.Error(0) } return r0 } // SpanReaderPlugin_FindTracesServer_RecvMsg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RecvMsg' type SpanReaderPlugin_FindTracesServer_RecvMsg_Call struct { *mock.Call } // RecvMsg is a helper method to define mock.On call // - m any func (_e *SpanReaderPlugin_FindTracesServer_Expecter) RecvMsg(m interface{}) *SpanReaderPlugin_FindTracesServer_RecvMsg_Call { return &SpanReaderPlugin_FindTracesServer_RecvMsg_Call{Call: _e.mock.On("RecvMsg", m)} } func (_c *SpanReaderPlugin_FindTracesServer_RecvMsg_Call) Run(run func(m any)) *SpanReaderPlugin_FindTracesServer_RecvMsg_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 any if args[0] != nil { arg0 = args[0].(any) } run( arg0, ) }) return _c } func (_c *SpanReaderPlugin_FindTracesServer_RecvMsg_Call) Return(err error) *SpanReaderPlugin_FindTracesServer_RecvMsg_Call { _c.Call.Return(err) return _c } func (_c *SpanReaderPlugin_FindTracesServer_RecvMsg_Call) RunAndReturn(run func(m any) error) *SpanReaderPlugin_FindTracesServer_RecvMsg_Call { _c.Call.Return(run) return _c } // Send provides a mock function for the type SpanReaderPlugin_FindTracesServer func (_mock *SpanReaderPlugin_FindTracesServer) Send(spansResponseChunk *storage_v1.SpansResponseChunk) error { ret := _mock.Called(spansResponseChunk) if len(ret) == 0 { panic("no return value specified for Send") } var r0 error if returnFunc, ok := ret.Get(0).(func(*storage_v1.SpansResponseChunk) error); ok { r0 = returnFunc(spansResponseChunk) } else { r0 = ret.Error(0) } return r0 } // SpanReaderPlugin_FindTracesServer_Send_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Send' type SpanReaderPlugin_FindTracesServer_Send_Call struct { *mock.Call } // Send is a helper method to define mock.On call // - spansResponseChunk *storage_v1.SpansResponseChunk func (_e *SpanReaderPlugin_FindTracesServer_Expecter) Send(spansResponseChunk interface{}) *SpanReaderPlugin_FindTracesServer_Send_Call { return &SpanReaderPlugin_FindTracesServer_Send_Call{Call: _e.mock.On("Send", spansResponseChunk)} } func (_c *SpanReaderPlugin_FindTracesServer_Send_Call) Run(run func(spansResponseChunk *storage_v1.SpansResponseChunk)) *SpanReaderPlugin_FindTracesServer_Send_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 *storage_v1.SpansResponseChunk if args[0] != nil { arg0 = args[0].(*storage_v1.SpansResponseChunk) } run( arg0, ) }) return _c } func (_c *SpanReaderPlugin_FindTracesServer_Send_Call) Return(err error) *SpanReaderPlugin_FindTracesServer_Send_Call { _c.Call.Return(err) return _c } func (_c *SpanReaderPlugin_FindTracesServer_Send_Call) RunAndReturn(run func(spansResponseChunk *storage_v1.SpansResponseChunk) error) *SpanReaderPlugin_FindTracesServer_Send_Call { _c.Call.Return(run) return _c } // SendHeader provides a mock function for the type SpanReaderPlugin_FindTracesServer func (_mock *SpanReaderPlugin_FindTracesServer) SendHeader(mD metadata.MD) error { ret := _mock.Called(mD) if len(ret) == 0 { panic("no return value specified for SendHeader") } var r0 error if returnFunc, ok := ret.Get(0).(func(metadata.MD) error); ok { r0 = returnFunc(mD) } else { r0 = ret.Error(0) } return r0 } // SpanReaderPlugin_FindTracesServer_SendHeader_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendHeader' type SpanReaderPlugin_FindTracesServer_SendHeader_Call struct { *mock.Call } // SendHeader is a helper method to define mock.On call // - mD metadata.MD func (_e *SpanReaderPlugin_FindTracesServer_Expecter) SendHeader(mD interface{}) *SpanReaderPlugin_FindTracesServer_SendHeader_Call { return &SpanReaderPlugin_FindTracesServer_SendHeader_Call{Call: _e.mock.On("SendHeader", mD)} } func (_c *SpanReaderPlugin_FindTracesServer_SendHeader_Call) Run(run func(mD metadata.MD)) *SpanReaderPlugin_FindTracesServer_SendHeader_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 metadata.MD if args[0] != nil { arg0 = args[0].(metadata.MD) } run( arg0, ) }) return _c } func (_c *SpanReaderPlugin_FindTracesServer_SendHeader_Call) Return(err error) *SpanReaderPlugin_FindTracesServer_SendHeader_Call { _c.Call.Return(err) return _c } func (_c *SpanReaderPlugin_FindTracesServer_SendHeader_Call) RunAndReturn(run func(mD metadata.MD) error) *SpanReaderPlugin_FindTracesServer_SendHeader_Call { _c.Call.Return(run) return _c } // SendMsg provides a mock function for the type SpanReaderPlugin_FindTracesServer func (_mock *SpanReaderPlugin_FindTracesServer) SendMsg(m any) error { ret := _mock.Called(m) if len(ret) == 0 { panic("no return value specified for SendMsg") } var r0 error if returnFunc, ok := ret.Get(0).(func(any) error); ok { r0 = returnFunc(m) } else { r0 = ret.Error(0) } return r0 } // SpanReaderPlugin_FindTracesServer_SendMsg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMsg' type SpanReaderPlugin_FindTracesServer_SendMsg_Call struct { *mock.Call } // SendMsg is a helper method to define mock.On call // - m any func (_e *SpanReaderPlugin_FindTracesServer_Expecter) SendMsg(m interface{}) *SpanReaderPlugin_FindTracesServer_SendMsg_Call { return &SpanReaderPlugin_FindTracesServer_SendMsg_Call{Call: _e.mock.On("SendMsg", m)} } func (_c *SpanReaderPlugin_FindTracesServer_SendMsg_Call) Run(run func(m any)) *SpanReaderPlugin_FindTracesServer_SendMsg_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 any if args[0] != nil { arg0 = args[0].(any) } run( arg0, ) }) return _c } func (_c *SpanReaderPlugin_FindTracesServer_SendMsg_Call) Return(err error) *SpanReaderPlugin_FindTracesServer_SendMsg_Call { _c.Call.Return(err) return _c } func (_c *SpanReaderPlugin_FindTracesServer_SendMsg_Call) RunAndReturn(run func(m any) error) *SpanReaderPlugin_FindTracesServer_SendMsg_Call { _c.Call.Return(run) return _c } // SetHeader provides a mock function for the type SpanReaderPlugin_FindTracesServer func (_mock *SpanReaderPlugin_FindTracesServer) SetHeader(mD metadata.MD) error { ret := _mock.Called(mD) if len(ret) == 0 { panic("no return value specified for SetHeader") } var r0 error if returnFunc, ok := ret.Get(0).(func(metadata.MD) error); ok { r0 = returnFunc(mD) } else { r0 = ret.Error(0) } return r0 } // SpanReaderPlugin_FindTracesServer_SetHeader_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetHeader' type SpanReaderPlugin_FindTracesServer_SetHeader_Call struct { *mock.Call } // SetHeader is a helper method to define mock.On call // - mD metadata.MD func (_e *SpanReaderPlugin_FindTracesServer_Expecter) SetHeader(mD interface{}) *SpanReaderPlugin_FindTracesServer_SetHeader_Call { return &SpanReaderPlugin_FindTracesServer_SetHeader_Call{Call: _e.mock.On("SetHeader", mD)} } func (_c *SpanReaderPlugin_FindTracesServer_SetHeader_Call) Run(run func(mD metadata.MD)) *SpanReaderPlugin_FindTracesServer_SetHeader_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 metadata.MD if args[0] != nil { arg0 = args[0].(metadata.MD) } run( arg0, ) }) return _c } func (_c *SpanReaderPlugin_FindTracesServer_SetHeader_Call) Return(err error) *SpanReaderPlugin_FindTracesServer_SetHeader_Call { _c.Call.Return(err) return _c } func (_c *SpanReaderPlugin_FindTracesServer_SetHeader_Call) RunAndReturn(run func(mD metadata.MD) error) *SpanReaderPlugin_FindTracesServer_SetHeader_Call { _c.Call.Return(run) return _c } // SetTrailer provides a mock function for the type SpanReaderPlugin_FindTracesServer func (_mock *SpanReaderPlugin_FindTracesServer) SetTrailer(mD metadata.MD) { _mock.Called(mD) return } // SpanReaderPlugin_FindTracesServer_SetTrailer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetTrailer' type SpanReaderPlugin_FindTracesServer_SetTrailer_Call struct { *mock.Call } // SetTrailer is a helper method to define mock.On call // - mD metadata.MD func (_e *SpanReaderPlugin_FindTracesServer_Expecter) SetTrailer(mD interface{}) *SpanReaderPlugin_FindTracesServer_SetTrailer_Call { return &SpanReaderPlugin_FindTracesServer_SetTrailer_Call{Call: _e.mock.On("SetTrailer", mD)} } func (_c *SpanReaderPlugin_FindTracesServer_SetTrailer_Call) Run(run func(mD metadata.MD)) *SpanReaderPlugin_FindTracesServer_SetTrailer_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 metadata.MD if args[0] != nil { arg0 = args[0].(metadata.MD) } run( arg0, ) }) return _c } func (_c *SpanReaderPlugin_FindTracesServer_SetTrailer_Call) Return() *SpanReaderPlugin_FindTracesServer_SetTrailer_Call { _c.Call.Return() return _c } func (_c *SpanReaderPlugin_FindTracesServer_SetTrailer_Call) RunAndReturn(run func(mD metadata.MD)) *SpanReaderPlugin_FindTracesServer_SetTrailer_Call { _c.Run(run) return _c } // NewArchiveSpanWriterPluginClient creates a new instance of ArchiveSpanWriterPluginClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewArchiveSpanWriterPluginClient(t interface { mock.TestingT Cleanup(func()) }) *ArchiveSpanWriterPluginClient { mock := &ArchiveSpanWriterPluginClient{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // ArchiveSpanWriterPluginClient is an autogenerated mock type for the ArchiveSpanWriterPluginClient type type ArchiveSpanWriterPluginClient struct { mock.Mock } type ArchiveSpanWriterPluginClient_Expecter struct { mock *mock.Mock } func (_m *ArchiveSpanWriterPluginClient) EXPECT() *ArchiveSpanWriterPluginClient_Expecter { return &ArchiveSpanWriterPluginClient_Expecter{mock: &_m.Mock} } // WriteArchiveSpan provides a mock function for the type ArchiveSpanWriterPluginClient func (_mock *ArchiveSpanWriterPluginClient) WriteArchiveSpan(ctx context.Context, in *storage_v1.WriteSpanRequest, opts ...grpc.CallOption) (*storage_v1.WriteSpanResponse, error) { var tmpRet mock.Arguments if len(opts) > 0 { tmpRet = _mock.Called(ctx, in, opts) } else { tmpRet = _mock.Called(ctx, in) } ret := tmpRet if len(ret) == 0 { panic("no return value specified for WriteArchiveSpan") } var r0 *storage_v1.WriteSpanResponse var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.WriteSpanRequest, ...grpc.CallOption) (*storage_v1.WriteSpanResponse, error)); ok { return returnFunc(ctx, in, opts...) } if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.WriteSpanRequest, ...grpc.CallOption) *storage_v1.WriteSpanResponse); ok { r0 = returnFunc(ctx, in, opts...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*storage_v1.WriteSpanResponse) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *storage_v1.WriteSpanRequest, ...grpc.CallOption) error); ok { r1 = returnFunc(ctx, in, opts...) } else { r1 = ret.Error(1) } return r0, r1 } // ArchiveSpanWriterPluginClient_WriteArchiveSpan_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteArchiveSpan' type ArchiveSpanWriterPluginClient_WriteArchiveSpan_Call struct { *mock.Call } // WriteArchiveSpan is a helper method to define mock.On call // - ctx context.Context // - in *storage_v1.WriteSpanRequest // - opts ...grpc.CallOption func (_e *ArchiveSpanWriterPluginClient_Expecter) WriteArchiveSpan(ctx interface{}, in interface{}, opts ...interface{}) *ArchiveSpanWriterPluginClient_WriteArchiveSpan_Call { return &ArchiveSpanWriterPluginClient_WriteArchiveSpan_Call{Call: _e.mock.On("WriteArchiveSpan", append([]interface{}{ctx, in}, opts...)...)} } func (_c *ArchiveSpanWriterPluginClient_WriteArchiveSpan_Call) Run(run func(ctx context.Context, in *storage_v1.WriteSpanRequest, opts ...grpc.CallOption)) *ArchiveSpanWriterPluginClient_WriteArchiveSpan_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *storage_v1.WriteSpanRequest if args[1] != nil { arg1 = args[1].(*storage_v1.WriteSpanRequest) } var arg2 []grpc.CallOption var variadicArgs []grpc.CallOption if len(args) > 2 { variadicArgs = args[2].([]grpc.CallOption) } arg2 = variadicArgs run( arg0, arg1, arg2..., ) }) return _c } func (_c *ArchiveSpanWriterPluginClient_WriteArchiveSpan_Call) Return(writeSpanResponse *storage_v1.WriteSpanResponse, err error) *ArchiveSpanWriterPluginClient_WriteArchiveSpan_Call { _c.Call.Return(writeSpanResponse, err) return _c } func (_c *ArchiveSpanWriterPluginClient_WriteArchiveSpan_Call) RunAndReturn(run func(ctx context.Context, in *storage_v1.WriteSpanRequest, opts ...grpc.CallOption) (*storage_v1.WriteSpanResponse, error)) *ArchiveSpanWriterPluginClient_WriteArchiveSpan_Call { _c.Call.Return(run) return _c } // NewArchiveSpanWriterPluginServer creates a new instance of ArchiveSpanWriterPluginServer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewArchiveSpanWriterPluginServer(t interface { mock.TestingT Cleanup(func()) }) *ArchiveSpanWriterPluginServer { mock := &ArchiveSpanWriterPluginServer{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // ArchiveSpanWriterPluginServer is an autogenerated mock type for the ArchiveSpanWriterPluginServer type type ArchiveSpanWriterPluginServer struct { mock.Mock } type ArchiveSpanWriterPluginServer_Expecter struct { mock *mock.Mock } func (_m *ArchiveSpanWriterPluginServer) EXPECT() *ArchiveSpanWriterPluginServer_Expecter { return &ArchiveSpanWriterPluginServer_Expecter{mock: &_m.Mock} } // WriteArchiveSpan provides a mock function for the type ArchiveSpanWriterPluginServer func (_mock *ArchiveSpanWriterPluginServer) WriteArchiveSpan(context1 context.Context, writeSpanRequest *storage_v1.WriteSpanRequest) (*storage_v1.WriteSpanResponse, error) { ret := _mock.Called(context1, writeSpanRequest) if len(ret) == 0 { panic("no return value specified for WriteArchiveSpan") } var r0 *storage_v1.WriteSpanResponse var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.WriteSpanRequest) (*storage_v1.WriteSpanResponse, error)); ok { return returnFunc(context1, writeSpanRequest) } if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.WriteSpanRequest) *storage_v1.WriteSpanResponse); ok { r0 = returnFunc(context1, writeSpanRequest) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*storage_v1.WriteSpanResponse) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *storage_v1.WriteSpanRequest) error); ok { r1 = returnFunc(context1, writeSpanRequest) } else { r1 = ret.Error(1) } return r0, r1 } // ArchiveSpanWriterPluginServer_WriteArchiveSpan_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteArchiveSpan' type ArchiveSpanWriterPluginServer_WriteArchiveSpan_Call struct { *mock.Call } // WriteArchiveSpan is a helper method to define mock.On call // - context1 context.Context // - writeSpanRequest *storage_v1.WriteSpanRequest func (_e *ArchiveSpanWriterPluginServer_Expecter) WriteArchiveSpan(context1 interface{}, writeSpanRequest interface{}) *ArchiveSpanWriterPluginServer_WriteArchiveSpan_Call { return &ArchiveSpanWriterPluginServer_WriteArchiveSpan_Call{Call: _e.mock.On("WriteArchiveSpan", context1, writeSpanRequest)} } func (_c *ArchiveSpanWriterPluginServer_WriteArchiveSpan_Call) Run(run func(context1 context.Context, writeSpanRequest *storage_v1.WriteSpanRequest)) *ArchiveSpanWriterPluginServer_WriteArchiveSpan_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *storage_v1.WriteSpanRequest if args[1] != nil { arg1 = args[1].(*storage_v1.WriteSpanRequest) } run( arg0, arg1, ) }) return _c } func (_c *ArchiveSpanWriterPluginServer_WriteArchiveSpan_Call) Return(writeSpanResponse *storage_v1.WriteSpanResponse, err error) *ArchiveSpanWriterPluginServer_WriteArchiveSpan_Call { _c.Call.Return(writeSpanResponse, err) return _c } func (_c *ArchiveSpanWriterPluginServer_WriteArchiveSpan_Call) RunAndReturn(run func(context1 context.Context, writeSpanRequest *storage_v1.WriteSpanRequest) (*storage_v1.WriteSpanResponse, error)) *ArchiveSpanWriterPluginServer_WriteArchiveSpan_Call { _c.Call.Return(run) return _c } // NewArchiveSpanReaderPluginClient creates a new instance of ArchiveSpanReaderPluginClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewArchiveSpanReaderPluginClient(t interface { mock.TestingT Cleanup(func()) }) *ArchiveSpanReaderPluginClient { mock := &ArchiveSpanReaderPluginClient{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // ArchiveSpanReaderPluginClient is an autogenerated mock type for the ArchiveSpanReaderPluginClient type type ArchiveSpanReaderPluginClient struct { mock.Mock } type ArchiveSpanReaderPluginClient_Expecter struct { mock *mock.Mock } func (_m *ArchiveSpanReaderPluginClient) EXPECT() *ArchiveSpanReaderPluginClient_Expecter { return &ArchiveSpanReaderPluginClient_Expecter{mock: &_m.Mock} } // GetArchiveTrace provides a mock function for the type ArchiveSpanReaderPluginClient func (_mock *ArchiveSpanReaderPluginClient) GetArchiveTrace(ctx context.Context, in *storage_v1.GetTraceRequest, opts ...grpc.CallOption) (storage_v1.ArchiveSpanReaderPlugin_GetArchiveTraceClient, error) { var tmpRet mock.Arguments if len(opts) > 0 { tmpRet = _mock.Called(ctx, in, opts) } else { tmpRet = _mock.Called(ctx, in) } ret := tmpRet if len(ret) == 0 { panic("no return value specified for GetArchiveTrace") } var r0 storage_v1.ArchiveSpanReaderPlugin_GetArchiveTraceClient var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.GetTraceRequest, ...grpc.CallOption) (storage_v1.ArchiveSpanReaderPlugin_GetArchiveTraceClient, error)); ok { return returnFunc(ctx, in, opts...) } if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.GetTraceRequest, ...grpc.CallOption) storage_v1.ArchiveSpanReaderPlugin_GetArchiveTraceClient); ok { r0 = returnFunc(ctx, in, opts...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(storage_v1.ArchiveSpanReaderPlugin_GetArchiveTraceClient) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *storage_v1.GetTraceRequest, ...grpc.CallOption) error); ok { r1 = returnFunc(ctx, in, opts...) } else { r1 = ret.Error(1) } return r0, r1 } // ArchiveSpanReaderPluginClient_GetArchiveTrace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetArchiveTrace' type ArchiveSpanReaderPluginClient_GetArchiveTrace_Call struct { *mock.Call } // GetArchiveTrace is a helper method to define mock.On call // - ctx context.Context // - in *storage_v1.GetTraceRequest // - opts ...grpc.CallOption func (_e *ArchiveSpanReaderPluginClient_Expecter) GetArchiveTrace(ctx interface{}, in interface{}, opts ...interface{}) *ArchiveSpanReaderPluginClient_GetArchiveTrace_Call { return &ArchiveSpanReaderPluginClient_GetArchiveTrace_Call{Call: _e.mock.On("GetArchiveTrace", append([]interface{}{ctx, in}, opts...)...)} } func (_c *ArchiveSpanReaderPluginClient_GetArchiveTrace_Call) Run(run func(ctx context.Context, in *storage_v1.GetTraceRequest, opts ...grpc.CallOption)) *ArchiveSpanReaderPluginClient_GetArchiveTrace_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *storage_v1.GetTraceRequest if args[1] != nil { arg1 = args[1].(*storage_v1.GetTraceRequest) } var arg2 []grpc.CallOption var variadicArgs []grpc.CallOption if len(args) > 2 { variadicArgs = args[2].([]grpc.CallOption) } arg2 = variadicArgs run( arg0, arg1, arg2..., ) }) return _c } func (_c *ArchiveSpanReaderPluginClient_GetArchiveTrace_Call) Return(archiveSpanReaderPlugin_GetArchiveTraceClient storage_v1.ArchiveSpanReaderPlugin_GetArchiveTraceClient, err error) *ArchiveSpanReaderPluginClient_GetArchiveTrace_Call { _c.Call.Return(archiveSpanReaderPlugin_GetArchiveTraceClient, err) return _c } func (_c *ArchiveSpanReaderPluginClient_GetArchiveTrace_Call) RunAndReturn(run func(ctx context.Context, in *storage_v1.GetTraceRequest, opts ...grpc.CallOption) (storage_v1.ArchiveSpanReaderPlugin_GetArchiveTraceClient, error)) *ArchiveSpanReaderPluginClient_GetArchiveTrace_Call { _c.Call.Return(run) return _c } // NewArchiveSpanReaderPlugin_GetArchiveTraceClient creates a new instance of ArchiveSpanReaderPlugin_GetArchiveTraceClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewArchiveSpanReaderPlugin_GetArchiveTraceClient(t interface { mock.TestingT Cleanup(func()) }) *ArchiveSpanReaderPlugin_GetArchiveTraceClient { mock := &ArchiveSpanReaderPlugin_GetArchiveTraceClient{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // ArchiveSpanReaderPlugin_GetArchiveTraceClient is an autogenerated mock type for the ArchiveSpanReaderPlugin_GetArchiveTraceClient type type ArchiveSpanReaderPlugin_GetArchiveTraceClient struct { mock.Mock } type ArchiveSpanReaderPlugin_GetArchiveTraceClient_Expecter struct { mock *mock.Mock } func (_m *ArchiveSpanReaderPlugin_GetArchiveTraceClient) EXPECT() *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Expecter { return &ArchiveSpanReaderPlugin_GetArchiveTraceClient_Expecter{mock: &_m.Mock} } // CloseSend provides a mock function for the type ArchiveSpanReaderPlugin_GetArchiveTraceClient func (_mock *ArchiveSpanReaderPlugin_GetArchiveTraceClient) CloseSend() error { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for CloseSend") } var r0 error if returnFunc, ok := ret.Get(0).(func() error); ok { r0 = returnFunc() } else { r0 = ret.Error(0) } return r0 } // ArchiveSpanReaderPlugin_GetArchiveTraceClient_CloseSend_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CloseSend' type ArchiveSpanReaderPlugin_GetArchiveTraceClient_CloseSend_Call struct { *mock.Call } // CloseSend is a helper method to define mock.On call func (_e *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Expecter) CloseSend() *ArchiveSpanReaderPlugin_GetArchiveTraceClient_CloseSend_Call { return &ArchiveSpanReaderPlugin_GetArchiveTraceClient_CloseSend_Call{Call: _e.mock.On("CloseSend")} } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceClient_CloseSend_Call) Run(run func()) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_CloseSend_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceClient_CloseSend_Call) Return(err error) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_CloseSend_Call { _c.Call.Return(err) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceClient_CloseSend_Call) RunAndReturn(run func() error) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_CloseSend_Call { _c.Call.Return(run) return _c } // Context provides a mock function for the type ArchiveSpanReaderPlugin_GetArchiveTraceClient func (_mock *ArchiveSpanReaderPlugin_GetArchiveTraceClient) Context() context.Context { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Context") } var r0 context.Context if returnFunc, ok := ret.Get(0).(func() context.Context); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(context.Context) } } return r0 } // ArchiveSpanReaderPlugin_GetArchiveTraceClient_Context_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Context' type ArchiveSpanReaderPlugin_GetArchiveTraceClient_Context_Call struct { *mock.Call } // Context is a helper method to define mock.On call func (_e *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Expecter) Context() *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Context_Call { return &ArchiveSpanReaderPlugin_GetArchiveTraceClient_Context_Call{Call: _e.mock.On("Context")} } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Context_Call) Run(run func()) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Context_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Context_Call) Return(context1 context.Context) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Context_Call { _c.Call.Return(context1) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Context_Call) RunAndReturn(run func() context.Context) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Context_Call { _c.Call.Return(run) return _c } // Header provides a mock function for the type ArchiveSpanReaderPlugin_GetArchiveTraceClient func (_mock *ArchiveSpanReaderPlugin_GetArchiveTraceClient) Header() (metadata.MD, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Header") } var r0 metadata.MD var r1 error if returnFunc, ok := ret.Get(0).(func() (metadata.MD, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() metadata.MD); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(metadata.MD) } } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // ArchiveSpanReaderPlugin_GetArchiveTraceClient_Header_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Header' type ArchiveSpanReaderPlugin_GetArchiveTraceClient_Header_Call struct { *mock.Call } // Header is a helper method to define mock.On call func (_e *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Expecter) Header() *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Header_Call { return &ArchiveSpanReaderPlugin_GetArchiveTraceClient_Header_Call{Call: _e.mock.On("Header")} } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Header_Call) Run(run func()) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Header_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Header_Call) Return(mD metadata.MD, err error) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Header_Call { _c.Call.Return(mD, err) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Header_Call) RunAndReturn(run func() (metadata.MD, error)) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Header_Call { _c.Call.Return(run) return _c } // Recv provides a mock function for the type ArchiveSpanReaderPlugin_GetArchiveTraceClient func (_mock *ArchiveSpanReaderPlugin_GetArchiveTraceClient) Recv() (*storage_v1.SpansResponseChunk, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Recv") } var r0 *storage_v1.SpansResponseChunk var r1 error if returnFunc, ok := ret.Get(0).(func() (*storage_v1.SpansResponseChunk, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() *storage_v1.SpansResponseChunk); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*storage_v1.SpansResponseChunk) } } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // ArchiveSpanReaderPlugin_GetArchiveTraceClient_Recv_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Recv' type ArchiveSpanReaderPlugin_GetArchiveTraceClient_Recv_Call struct { *mock.Call } // Recv is a helper method to define mock.On call func (_e *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Expecter) Recv() *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Recv_Call { return &ArchiveSpanReaderPlugin_GetArchiveTraceClient_Recv_Call{Call: _e.mock.On("Recv")} } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Recv_Call) Run(run func()) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Recv_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Recv_Call) Return(spansResponseChunk *storage_v1.SpansResponseChunk, err error) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Recv_Call { _c.Call.Return(spansResponseChunk, err) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Recv_Call) RunAndReturn(run func() (*storage_v1.SpansResponseChunk, error)) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Recv_Call { _c.Call.Return(run) return _c } // RecvMsg provides a mock function for the type ArchiveSpanReaderPlugin_GetArchiveTraceClient func (_mock *ArchiveSpanReaderPlugin_GetArchiveTraceClient) RecvMsg(m any) error { ret := _mock.Called(m) if len(ret) == 0 { panic("no return value specified for RecvMsg") } var r0 error if returnFunc, ok := ret.Get(0).(func(any) error); ok { r0 = returnFunc(m) } else { r0 = ret.Error(0) } return r0 } // ArchiveSpanReaderPlugin_GetArchiveTraceClient_RecvMsg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RecvMsg' type ArchiveSpanReaderPlugin_GetArchiveTraceClient_RecvMsg_Call struct { *mock.Call } // RecvMsg is a helper method to define mock.On call // - m any func (_e *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Expecter) RecvMsg(m interface{}) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_RecvMsg_Call { return &ArchiveSpanReaderPlugin_GetArchiveTraceClient_RecvMsg_Call{Call: _e.mock.On("RecvMsg", m)} } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceClient_RecvMsg_Call) Run(run func(m any)) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_RecvMsg_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 any if args[0] != nil { arg0 = args[0].(any) } run( arg0, ) }) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceClient_RecvMsg_Call) Return(err error) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_RecvMsg_Call { _c.Call.Return(err) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceClient_RecvMsg_Call) RunAndReturn(run func(m any) error) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_RecvMsg_Call { _c.Call.Return(run) return _c } // SendMsg provides a mock function for the type ArchiveSpanReaderPlugin_GetArchiveTraceClient func (_mock *ArchiveSpanReaderPlugin_GetArchiveTraceClient) SendMsg(m any) error { ret := _mock.Called(m) if len(ret) == 0 { panic("no return value specified for SendMsg") } var r0 error if returnFunc, ok := ret.Get(0).(func(any) error); ok { r0 = returnFunc(m) } else { r0 = ret.Error(0) } return r0 } // ArchiveSpanReaderPlugin_GetArchiveTraceClient_SendMsg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMsg' type ArchiveSpanReaderPlugin_GetArchiveTraceClient_SendMsg_Call struct { *mock.Call } // SendMsg is a helper method to define mock.On call // - m any func (_e *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Expecter) SendMsg(m interface{}) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_SendMsg_Call { return &ArchiveSpanReaderPlugin_GetArchiveTraceClient_SendMsg_Call{Call: _e.mock.On("SendMsg", m)} } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceClient_SendMsg_Call) Run(run func(m any)) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_SendMsg_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 any if args[0] != nil { arg0 = args[0].(any) } run( arg0, ) }) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceClient_SendMsg_Call) Return(err error) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_SendMsg_Call { _c.Call.Return(err) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceClient_SendMsg_Call) RunAndReturn(run func(m any) error) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_SendMsg_Call { _c.Call.Return(run) return _c } // Trailer provides a mock function for the type ArchiveSpanReaderPlugin_GetArchiveTraceClient func (_mock *ArchiveSpanReaderPlugin_GetArchiveTraceClient) Trailer() metadata.MD { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Trailer") } var r0 metadata.MD if returnFunc, ok := ret.Get(0).(func() metadata.MD); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(metadata.MD) } } return r0 } // ArchiveSpanReaderPlugin_GetArchiveTraceClient_Trailer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Trailer' type ArchiveSpanReaderPlugin_GetArchiveTraceClient_Trailer_Call struct { *mock.Call } // Trailer is a helper method to define mock.On call func (_e *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Expecter) Trailer() *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Trailer_Call { return &ArchiveSpanReaderPlugin_GetArchiveTraceClient_Trailer_Call{Call: _e.mock.On("Trailer")} } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Trailer_Call) Run(run func()) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Trailer_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Trailer_Call) Return(mD metadata.MD) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Trailer_Call { _c.Call.Return(mD) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Trailer_Call) RunAndReturn(run func() metadata.MD) *ArchiveSpanReaderPlugin_GetArchiveTraceClient_Trailer_Call { _c.Call.Return(run) return _c } // NewArchiveSpanReaderPluginServer creates a new instance of ArchiveSpanReaderPluginServer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewArchiveSpanReaderPluginServer(t interface { mock.TestingT Cleanup(func()) }) *ArchiveSpanReaderPluginServer { mock := &ArchiveSpanReaderPluginServer{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // ArchiveSpanReaderPluginServer is an autogenerated mock type for the ArchiveSpanReaderPluginServer type type ArchiveSpanReaderPluginServer struct { mock.Mock } type ArchiveSpanReaderPluginServer_Expecter struct { mock *mock.Mock } func (_m *ArchiveSpanReaderPluginServer) EXPECT() *ArchiveSpanReaderPluginServer_Expecter { return &ArchiveSpanReaderPluginServer_Expecter{mock: &_m.Mock} } // GetArchiveTrace provides a mock function for the type ArchiveSpanReaderPluginServer func (_mock *ArchiveSpanReaderPluginServer) GetArchiveTrace(getTraceRequest *storage_v1.GetTraceRequest, archiveSpanReaderPlugin_GetArchiveTraceServer storage_v1.ArchiveSpanReaderPlugin_GetArchiveTraceServer) error { ret := _mock.Called(getTraceRequest, archiveSpanReaderPlugin_GetArchiveTraceServer) if len(ret) == 0 { panic("no return value specified for GetArchiveTrace") } var r0 error if returnFunc, ok := ret.Get(0).(func(*storage_v1.GetTraceRequest, storage_v1.ArchiveSpanReaderPlugin_GetArchiveTraceServer) error); ok { r0 = returnFunc(getTraceRequest, archiveSpanReaderPlugin_GetArchiveTraceServer) } else { r0 = ret.Error(0) } return r0 } // ArchiveSpanReaderPluginServer_GetArchiveTrace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetArchiveTrace' type ArchiveSpanReaderPluginServer_GetArchiveTrace_Call struct { *mock.Call } // GetArchiveTrace is a helper method to define mock.On call // - getTraceRequest *storage_v1.GetTraceRequest // - archiveSpanReaderPlugin_GetArchiveTraceServer storage_v1.ArchiveSpanReaderPlugin_GetArchiveTraceServer func (_e *ArchiveSpanReaderPluginServer_Expecter) GetArchiveTrace(getTraceRequest interface{}, archiveSpanReaderPlugin_GetArchiveTraceServer interface{}) *ArchiveSpanReaderPluginServer_GetArchiveTrace_Call { return &ArchiveSpanReaderPluginServer_GetArchiveTrace_Call{Call: _e.mock.On("GetArchiveTrace", getTraceRequest, archiveSpanReaderPlugin_GetArchiveTraceServer)} } func (_c *ArchiveSpanReaderPluginServer_GetArchiveTrace_Call) Run(run func(getTraceRequest *storage_v1.GetTraceRequest, archiveSpanReaderPlugin_GetArchiveTraceServer storage_v1.ArchiveSpanReaderPlugin_GetArchiveTraceServer)) *ArchiveSpanReaderPluginServer_GetArchiveTrace_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 *storage_v1.GetTraceRequest if args[0] != nil { arg0 = args[0].(*storage_v1.GetTraceRequest) } var arg1 storage_v1.ArchiveSpanReaderPlugin_GetArchiveTraceServer if args[1] != nil { arg1 = args[1].(storage_v1.ArchiveSpanReaderPlugin_GetArchiveTraceServer) } run( arg0, arg1, ) }) return _c } func (_c *ArchiveSpanReaderPluginServer_GetArchiveTrace_Call) Return(err error) *ArchiveSpanReaderPluginServer_GetArchiveTrace_Call { _c.Call.Return(err) return _c } func (_c *ArchiveSpanReaderPluginServer_GetArchiveTrace_Call) RunAndReturn(run func(getTraceRequest *storage_v1.GetTraceRequest, archiveSpanReaderPlugin_GetArchiveTraceServer storage_v1.ArchiveSpanReaderPlugin_GetArchiveTraceServer) error) *ArchiveSpanReaderPluginServer_GetArchiveTrace_Call { _c.Call.Return(run) return _c } // NewArchiveSpanReaderPlugin_GetArchiveTraceServer creates a new instance of ArchiveSpanReaderPlugin_GetArchiveTraceServer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewArchiveSpanReaderPlugin_GetArchiveTraceServer(t interface { mock.TestingT Cleanup(func()) }) *ArchiveSpanReaderPlugin_GetArchiveTraceServer { mock := &ArchiveSpanReaderPlugin_GetArchiveTraceServer{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // ArchiveSpanReaderPlugin_GetArchiveTraceServer is an autogenerated mock type for the ArchiveSpanReaderPlugin_GetArchiveTraceServer type type ArchiveSpanReaderPlugin_GetArchiveTraceServer struct { mock.Mock } type ArchiveSpanReaderPlugin_GetArchiveTraceServer_Expecter struct { mock *mock.Mock } func (_m *ArchiveSpanReaderPlugin_GetArchiveTraceServer) EXPECT() *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Expecter { return &ArchiveSpanReaderPlugin_GetArchiveTraceServer_Expecter{mock: &_m.Mock} } // Context provides a mock function for the type ArchiveSpanReaderPlugin_GetArchiveTraceServer func (_mock *ArchiveSpanReaderPlugin_GetArchiveTraceServer) Context() context.Context { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Context") } var r0 context.Context if returnFunc, ok := ret.Get(0).(func() context.Context); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(context.Context) } } return r0 } // ArchiveSpanReaderPlugin_GetArchiveTraceServer_Context_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Context' type ArchiveSpanReaderPlugin_GetArchiveTraceServer_Context_Call struct { *mock.Call } // Context is a helper method to define mock.On call func (_e *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Expecter) Context() *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Context_Call { return &ArchiveSpanReaderPlugin_GetArchiveTraceServer_Context_Call{Call: _e.mock.On("Context")} } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Context_Call) Run(run func()) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Context_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Context_Call) Return(context1 context.Context) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Context_Call { _c.Call.Return(context1) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Context_Call) RunAndReturn(run func() context.Context) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Context_Call { _c.Call.Return(run) return _c } // RecvMsg provides a mock function for the type ArchiveSpanReaderPlugin_GetArchiveTraceServer func (_mock *ArchiveSpanReaderPlugin_GetArchiveTraceServer) RecvMsg(m any) error { ret := _mock.Called(m) if len(ret) == 0 { panic("no return value specified for RecvMsg") } var r0 error if returnFunc, ok := ret.Get(0).(func(any) error); ok { r0 = returnFunc(m) } else { r0 = ret.Error(0) } return r0 } // ArchiveSpanReaderPlugin_GetArchiveTraceServer_RecvMsg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RecvMsg' type ArchiveSpanReaderPlugin_GetArchiveTraceServer_RecvMsg_Call struct { *mock.Call } // RecvMsg is a helper method to define mock.On call // - m any func (_e *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Expecter) RecvMsg(m interface{}) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_RecvMsg_Call { return &ArchiveSpanReaderPlugin_GetArchiveTraceServer_RecvMsg_Call{Call: _e.mock.On("RecvMsg", m)} } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceServer_RecvMsg_Call) Run(run func(m any)) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_RecvMsg_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 any if args[0] != nil { arg0 = args[0].(any) } run( arg0, ) }) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceServer_RecvMsg_Call) Return(err error) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_RecvMsg_Call { _c.Call.Return(err) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceServer_RecvMsg_Call) RunAndReturn(run func(m any) error) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_RecvMsg_Call { _c.Call.Return(run) return _c } // Send provides a mock function for the type ArchiveSpanReaderPlugin_GetArchiveTraceServer func (_mock *ArchiveSpanReaderPlugin_GetArchiveTraceServer) Send(spansResponseChunk *storage_v1.SpansResponseChunk) error { ret := _mock.Called(spansResponseChunk) if len(ret) == 0 { panic("no return value specified for Send") } var r0 error if returnFunc, ok := ret.Get(0).(func(*storage_v1.SpansResponseChunk) error); ok { r0 = returnFunc(spansResponseChunk) } else { r0 = ret.Error(0) } return r0 } // ArchiveSpanReaderPlugin_GetArchiveTraceServer_Send_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Send' type ArchiveSpanReaderPlugin_GetArchiveTraceServer_Send_Call struct { *mock.Call } // Send is a helper method to define mock.On call // - spansResponseChunk *storage_v1.SpansResponseChunk func (_e *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Expecter) Send(spansResponseChunk interface{}) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Send_Call { return &ArchiveSpanReaderPlugin_GetArchiveTraceServer_Send_Call{Call: _e.mock.On("Send", spansResponseChunk)} } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Send_Call) Run(run func(spansResponseChunk *storage_v1.SpansResponseChunk)) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Send_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 *storage_v1.SpansResponseChunk if args[0] != nil { arg0 = args[0].(*storage_v1.SpansResponseChunk) } run( arg0, ) }) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Send_Call) Return(err error) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Send_Call { _c.Call.Return(err) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Send_Call) RunAndReturn(run func(spansResponseChunk *storage_v1.SpansResponseChunk) error) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Send_Call { _c.Call.Return(run) return _c } // SendHeader provides a mock function for the type ArchiveSpanReaderPlugin_GetArchiveTraceServer func (_mock *ArchiveSpanReaderPlugin_GetArchiveTraceServer) SendHeader(mD metadata.MD) error { ret := _mock.Called(mD) if len(ret) == 0 { panic("no return value specified for SendHeader") } var r0 error if returnFunc, ok := ret.Get(0).(func(metadata.MD) error); ok { r0 = returnFunc(mD) } else { r0 = ret.Error(0) } return r0 } // ArchiveSpanReaderPlugin_GetArchiveTraceServer_SendHeader_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendHeader' type ArchiveSpanReaderPlugin_GetArchiveTraceServer_SendHeader_Call struct { *mock.Call } // SendHeader is a helper method to define mock.On call // - mD metadata.MD func (_e *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Expecter) SendHeader(mD interface{}) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SendHeader_Call { return &ArchiveSpanReaderPlugin_GetArchiveTraceServer_SendHeader_Call{Call: _e.mock.On("SendHeader", mD)} } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SendHeader_Call) Run(run func(mD metadata.MD)) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SendHeader_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 metadata.MD if args[0] != nil { arg0 = args[0].(metadata.MD) } run( arg0, ) }) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SendHeader_Call) Return(err error) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SendHeader_Call { _c.Call.Return(err) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SendHeader_Call) RunAndReturn(run func(mD metadata.MD) error) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SendHeader_Call { _c.Call.Return(run) return _c } // SendMsg provides a mock function for the type ArchiveSpanReaderPlugin_GetArchiveTraceServer func (_mock *ArchiveSpanReaderPlugin_GetArchiveTraceServer) SendMsg(m any) error { ret := _mock.Called(m) if len(ret) == 0 { panic("no return value specified for SendMsg") } var r0 error if returnFunc, ok := ret.Get(0).(func(any) error); ok { r0 = returnFunc(m) } else { r0 = ret.Error(0) } return r0 } // ArchiveSpanReaderPlugin_GetArchiveTraceServer_SendMsg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMsg' type ArchiveSpanReaderPlugin_GetArchiveTraceServer_SendMsg_Call struct { *mock.Call } // SendMsg is a helper method to define mock.On call // - m any func (_e *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Expecter) SendMsg(m interface{}) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SendMsg_Call { return &ArchiveSpanReaderPlugin_GetArchiveTraceServer_SendMsg_Call{Call: _e.mock.On("SendMsg", m)} } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SendMsg_Call) Run(run func(m any)) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SendMsg_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 any if args[0] != nil { arg0 = args[0].(any) } run( arg0, ) }) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SendMsg_Call) Return(err error) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SendMsg_Call { _c.Call.Return(err) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SendMsg_Call) RunAndReturn(run func(m any) error) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SendMsg_Call { _c.Call.Return(run) return _c } // SetHeader provides a mock function for the type ArchiveSpanReaderPlugin_GetArchiveTraceServer func (_mock *ArchiveSpanReaderPlugin_GetArchiveTraceServer) SetHeader(mD metadata.MD) error { ret := _mock.Called(mD) if len(ret) == 0 { panic("no return value specified for SetHeader") } var r0 error if returnFunc, ok := ret.Get(0).(func(metadata.MD) error); ok { r0 = returnFunc(mD) } else { r0 = ret.Error(0) } return r0 } // ArchiveSpanReaderPlugin_GetArchiveTraceServer_SetHeader_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetHeader' type ArchiveSpanReaderPlugin_GetArchiveTraceServer_SetHeader_Call struct { *mock.Call } // SetHeader is a helper method to define mock.On call // - mD metadata.MD func (_e *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Expecter) SetHeader(mD interface{}) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SetHeader_Call { return &ArchiveSpanReaderPlugin_GetArchiveTraceServer_SetHeader_Call{Call: _e.mock.On("SetHeader", mD)} } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SetHeader_Call) Run(run func(mD metadata.MD)) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SetHeader_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 metadata.MD if args[0] != nil { arg0 = args[0].(metadata.MD) } run( arg0, ) }) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SetHeader_Call) Return(err error) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SetHeader_Call { _c.Call.Return(err) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SetHeader_Call) RunAndReturn(run func(mD metadata.MD) error) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SetHeader_Call { _c.Call.Return(run) return _c } // SetTrailer provides a mock function for the type ArchiveSpanReaderPlugin_GetArchiveTraceServer func (_mock *ArchiveSpanReaderPlugin_GetArchiveTraceServer) SetTrailer(mD metadata.MD) { _mock.Called(mD) return } // ArchiveSpanReaderPlugin_GetArchiveTraceServer_SetTrailer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetTrailer' type ArchiveSpanReaderPlugin_GetArchiveTraceServer_SetTrailer_Call struct { *mock.Call } // SetTrailer is a helper method to define mock.On call // - mD metadata.MD func (_e *ArchiveSpanReaderPlugin_GetArchiveTraceServer_Expecter) SetTrailer(mD interface{}) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SetTrailer_Call { return &ArchiveSpanReaderPlugin_GetArchiveTraceServer_SetTrailer_Call{Call: _e.mock.On("SetTrailer", mD)} } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SetTrailer_Call) Run(run func(mD metadata.MD)) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SetTrailer_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 metadata.MD if args[0] != nil { arg0 = args[0].(metadata.MD) } run( arg0, ) }) return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SetTrailer_Call) Return() *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SetTrailer_Call { _c.Call.Return() return _c } func (_c *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SetTrailer_Call) RunAndReturn(run func(mD metadata.MD)) *ArchiveSpanReaderPlugin_GetArchiveTraceServer_SetTrailer_Call { _c.Run(run) return _c } // NewDependenciesReaderPluginClient creates a new instance of DependenciesReaderPluginClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewDependenciesReaderPluginClient(t interface { mock.TestingT Cleanup(func()) }) *DependenciesReaderPluginClient { mock := &DependenciesReaderPluginClient{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // DependenciesReaderPluginClient is an autogenerated mock type for the DependenciesReaderPluginClient type type DependenciesReaderPluginClient struct { mock.Mock } type DependenciesReaderPluginClient_Expecter struct { mock *mock.Mock } func (_m *DependenciesReaderPluginClient) EXPECT() *DependenciesReaderPluginClient_Expecter { return &DependenciesReaderPluginClient_Expecter{mock: &_m.Mock} } // GetDependencies provides a mock function for the type DependenciesReaderPluginClient func (_mock *DependenciesReaderPluginClient) GetDependencies(ctx context.Context, in *storage_v1.GetDependenciesRequest, opts ...grpc.CallOption) (*storage_v1.GetDependenciesResponse, error) { var tmpRet mock.Arguments if len(opts) > 0 { tmpRet = _mock.Called(ctx, in, opts) } else { tmpRet = _mock.Called(ctx, in) } ret := tmpRet if len(ret) == 0 { panic("no return value specified for GetDependencies") } var r0 *storage_v1.GetDependenciesResponse var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.GetDependenciesRequest, ...grpc.CallOption) (*storage_v1.GetDependenciesResponse, error)); ok { return returnFunc(ctx, in, opts...) } if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.GetDependenciesRequest, ...grpc.CallOption) *storage_v1.GetDependenciesResponse); ok { r0 = returnFunc(ctx, in, opts...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*storage_v1.GetDependenciesResponse) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *storage_v1.GetDependenciesRequest, ...grpc.CallOption) error); ok { r1 = returnFunc(ctx, in, opts...) } else { r1 = ret.Error(1) } return r0, r1 } // DependenciesReaderPluginClient_GetDependencies_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDependencies' type DependenciesReaderPluginClient_GetDependencies_Call struct { *mock.Call } // GetDependencies is a helper method to define mock.On call // - ctx context.Context // - in *storage_v1.GetDependenciesRequest // - opts ...grpc.CallOption func (_e *DependenciesReaderPluginClient_Expecter) GetDependencies(ctx interface{}, in interface{}, opts ...interface{}) *DependenciesReaderPluginClient_GetDependencies_Call { return &DependenciesReaderPluginClient_GetDependencies_Call{Call: _e.mock.On("GetDependencies", append([]interface{}{ctx, in}, opts...)...)} } func (_c *DependenciesReaderPluginClient_GetDependencies_Call) Run(run func(ctx context.Context, in *storage_v1.GetDependenciesRequest, opts ...grpc.CallOption)) *DependenciesReaderPluginClient_GetDependencies_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *storage_v1.GetDependenciesRequest if args[1] != nil { arg1 = args[1].(*storage_v1.GetDependenciesRequest) } var arg2 []grpc.CallOption var variadicArgs []grpc.CallOption if len(args) > 2 { variadicArgs = args[2].([]grpc.CallOption) } arg2 = variadicArgs run( arg0, arg1, arg2..., ) }) return _c } func (_c *DependenciesReaderPluginClient_GetDependencies_Call) Return(getDependenciesResponse *storage_v1.GetDependenciesResponse, err error) *DependenciesReaderPluginClient_GetDependencies_Call { _c.Call.Return(getDependenciesResponse, err) return _c } func (_c *DependenciesReaderPluginClient_GetDependencies_Call) RunAndReturn(run func(ctx context.Context, in *storage_v1.GetDependenciesRequest, opts ...grpc.CallOption) (*storage_v1.GetDependenciesResponse, error)) *DependenciesReaderPluginClient_GetDependencies_Call { _c.Call.Return(run) return _c } // NewDependenciesReaderPluginServer creates a new instance of DependenciesReaderPluginServer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewDependenciesReaderPluginServer(t interface { mock.TestingT Cleanup(func()) }) *DependenciesReaderPluginServer { mock := &DependenciesReaderPluginServer{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // DependenciesReaderPluginServer is an autogenerated mock type for the DependenciesReaderPluginServer type type DependenciesReaderPluginServer struct { mock.Mock } type DependenciesReaderPluginServer_Expecter struct { mock *mock.Mock } func (_m *DependenciesReaderPluginServer) EXPECT() *DependenciesReaderPluginServer_Expecter { return &DependenciesReaderPluginServer_Expecter{mock: &_m.Mock} } // GetDependencies provides a mock function for the type DependenciesReaderPluginServer func (_mock *DependenciesReaderPluginServer) GetDependencies(context1 context.Context, getDependenciesRequest *storage_v1.GetDependenciesRequest) (*storage_v1.GetDependenciesResponse, error) { ret := _mock.Called(context1, getDependenciesRequest) if len(ret) == 0 { panic("no return value specified for GetDependencies") } var r0 *storage_v1.GetDependenciesResponse var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.GetDependenciesRequest) (*storage_v1.GetDependenciesResponse, error)); ok { return returnFunc(context1, getDependenciesRequest) } if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.GetDependenciesRequest) *storage_v1.GetDependenciesResponse); ok { r0 = returnFunc(context1, getDependenciesRequest) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*storage_v1.GetDependenciesResponse) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *storage_v1.GetDependenciesRequest) error); ok { r1 = returnFunc(context1, getDependenciesRequest) } else { r1 = ret.Error(1) } return r0, r1 } // DependenciesReaderPluginServer_GetDependencies_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDependencies' type DependenciesReaderPluginServer_GetDependencies_Call struct { *mock.Call } // GetDependencies is a helper method to define mock.On call // - context1 context.Context // - getDependenciesRequest *storage_v1.GetDependenciesRequest func (_e *DependenciesReaderPluginServer_Expecter) GetDependencies(context1 interface{}, getDependenciesRequest interface{}) *DependenciesReaderPluginServer_GetDependencies_Call { return &DependenciesReaderPluginServer_GetDependencies_Call{Call: _e.mock.On("GetDependencies", context1, getDependenciesRequest)} } func (_c *DependenciesReaderPluginServer_GetDependencies_Call) Run(run func(context1 context.Context, getDependenciesRequest *storage_v1.GetDependenciesRequest)) *DependenciesReaderPluginServer_GetDependencies_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *storage_v1.GetDependenciesRequest if args[1] != nil { arg1 = args[1].(*storage_v1.GetDependenciesRequest) } run( arg0, arg1, ) }) return _c } func (_c *DependenciesReaderPluginServer_GetDependencies_Call) Return(getDependenciesResponse *storage_v1.GetDependenciesResponse, err error) *DependenciesReaderPluginServer_GetDependencies_Call { _c.Call.Return(getDependenciesResponse, err) return _c } func (_c *DependenciesReaderPluginServer_GetDependencies_Call) RunAndReturn(run func(context1 context.Context, getDependenciesRequest *storage_v1.GetDependenciesRequest) (*storage_v1.GetDependenciesResponse, error)) *DependenciesReaderPluginServer_GetDependencies_Call { _c.Call.Return(run) return _c } // NewPluginCapabilitiesClient creates a new instance of PluginCapabilitiesClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewPluginCapabilitiesClient(t interface { mock.TestingT Cleanup(func()) }) *PluginCapabilitiesClient { mock := &PluginCapabilitiesClient{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // PluginCapabilitiesClient is an autogenerated mock type for the PluginCapabilitiesClient type type PluginCapabilitiesClient struct { mock.Mock } type PluginCapabilitiesClient_Expecter struct { mock *mock.Mock } func (_m *PluginCapabilitiesClient) EXPECT() *PluginCapabilitiesClient_Expecter { return &PluginCapabilitiesClient_Expecter{mock: &_m.Mock} } // Capabilities provides a mock function for the type PluginCapabilitiesClient func (_mock *PluginCapabilitiesClient) Capabilities(ctx context.Context, in *storage_v1.CapabilitiesRequest, opts ...grpc.CallOption) (*storage_v1.CapabilitiesResponse, error) { var tmpRet mock.Arguments if len(opts) > 0 { tmpRet = _mock.Called(ctx, in, opts) } else { tmpRet = _mock.Called(ctx, in) } ret := tmpRet if len(ret) == 0 { panic("no return value specified for Capabilities") } var r0 *storage_v1.CapabilitiesResponse var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.CapabilitiesRequest, ...grpc.CallOption) (*storage_v1.CapabilitiesResponse, error)); ok { return returnFunc(ctx, in, opts...) } if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.CapabilitiesRequest, ...grpc.CallOption) *storage_v1.CapabilitiesResponse); ok { r0 = returnFunc(ctx, in, opts...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*storage_v1.CapabilitiesResponse) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *storage_v1.CapabilitiesRequest, ...grpc.CallOption) error); ok { r1 = returnFunc(ctx, in, opts...) } else { r1 = ret.Error(1) } return r0, r1 } // PluginCapabilitiesClient_Capabilities_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Capabilities' type PluginCapabilitiesClient_Capabilities_Call struct { *mock.Call } // Capabilities is a helper method to define mock.On call // - ctx context.Context // - in *storage_v1.CapabilitiesRequest // - opts ...grpc.CallOption func (_e *PluginCapabilitiesClient_Expecter) Capabilities(ctx interface{}, in interface{}, opts ...interface{}) *PluginCapabilitiesClient_Capabilities_Call { return &PluginCapabilitiesClient_Capabilities_Call{Call: _e.mock.On("Capabilities", append([]interface{}{ctx, in}, opts...)...)} } func (_c *PluginCapabilitiesClient_Capabilities_Call) Run(run func(ctx context.Context, in *storage_v1.CapabilitiesRequest, opts ...grpc.CallOption)) *PluginCapabilitiesClient_Capabilities_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *storage_v1.CapabilitiesRequest if args[1] != nil { arg1 = args[1].(*storage_v1.CapabilitiesRequest) } var arg2 []grpc.CallOption var variadicArgs []grpc.CallOption if len(args) > 2 { variadicArgs = args[2].([]grpc.CallOption) } arg2 = variadicArgs run( arg0, arg1, arg2..., ) }) return _c } func (_c *PluginCapabilitiesClient_Capabilities_Call) Return(capabilitiesResponse *storage_v1.CapabilitiesResponse, err error) *PluginCapabilitiesClient_Capabilities_Call { _c.Call.Return(capabilitiesResponse, err) return _c } func (_c *PluginCapabilitiesClient_Capabilities_Call) RunAndReturn(run func(ctx context.Context, in *storage_v1.CapabilitiesRequest, opts ...grpc.CallOption) (*storage_v1.CapabilitiesResponse, error)) *PluginCapabilitiesClient_Capabilities_Call { _c.Call.Return(run) return _c } // NewPluginCapabilitiesServer creates a new instance of PluginCapabilitiesServer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewPluginCapabilitiesServer(t interface { mock.TestingT Cleanup(func()) }) *PluginCapabilitiesServer { mock := &PluginCapabilitiesServer{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // PluginCapabilitiesServer is an autogenerated mock type for the PluginCapabilitiesServer type type PluginCapabilitiesServer struct { mock.Mock } type PluginCapabilitiesServer_Expecter struct { mock *mock.Mock } func (_m *PluginCapabilitiesServer) EXPECT() *PluginCapabilitiesServer_Expecter { return &PluginCapabilitiesServer_Expecter{mock: &_m.Mock} } // Capabilities provides a mock function for the type PluginCapabilitiesServer func (_mock *PluginCapabilitiesServer) Capabilities(context1 context.Context, capabilitiesRequest *storage_v1.CapabilitiesRequest) (*storage_v1.CapabilitiesResponse, error) { ret := _mock.Called(context1, capabilitiesRequest) if len(ret) == 0 { panic("no return value specified for Capabilities") } var r0 *storage_v1.CapabilitiesResponse var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.CapabilitiesRequest) (*storage_v1.CapabilitiesResponse, error)); ok { return returnFunc(context1, capabilitiesRequest) } if returnFunc, ok := ret.Get(0).(func(context.Context, *storage_v1.CapabilitiesRequest) *storage_v1.CapabilitiesResponse); ok { r0 = returnFunc(context1, capabilitiesRequest) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*storage_v1.CapabilitiesResponse) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *storage_v1.CapabilitiesRequest) error); ok { r1 = returnFunc(context1, capabilitiesRequest) } else { r1 = ret.Error(1) } return r0, r1 } // PluginCapabilitiesServer_Capabilities_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Capabilities' type PluginCapabilitiesServer_Capabilities_Call struct { *mock.Call } // Capabilities is a helper method to define mock.On call // - context1 context.Context // - capabilitiesRequest *storage_v1.CapabilitiesRequest func (_e *PluginCapabilitiesServer_Expecter) Capabilities(context1 interface{}, capabilitiesRequest interface{}) *PluginCapabilitiesServer_Capabilities_Call { return &PluginCapabilitiesServer_Capabilities_Call{Call: _e.mock.On("Capabilities", context1, capabilitiesRequest)} } func (_c *PluginCapabilitiesServer_Capabilities_Call) Run(run func(context1 context.Context, capabilitiesRequest *storage_v1.CapabilitiesRequest)) *PluginCapabilitiesServer_Capabilities_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *storage_v1.CapabilitiesRequest if args[1] != nil { arg1 = args[1].(*storage_v1.CapabilitiesRequest) } run( arg0, arg1, ) }) return _c } func (_c *PluginCapabilitiesServer_Capabilities_Call) Return(capabilitiesResponse *storage_v1.CapabilitiesResponse, err error) *PluginCapabilitiesServer_Capabilities_Call { _c.Call.Return(capabilitiesResponse, err) return _c } func (_c *PluginCapabilitiesServer_Capabilities_Call) RunAndReturn(run func(context1 context.Context, capabilitiesRequest *storage_v1.CapabilitiesRequest) (*storage_v1.CapabilitiesResponse, error)) *PluginCapabilitiesServer_Capabilities_Call { _c.Call.Return(run) return _c } ================================================ FILE: internal/proto-gen/storage_v1/storage.pb.go ================================================ // Code generated by protoc-gen-gogo. DO NOT EDIT. // source: storage.proto package storage_v1 import ( context "context" fmt "fmt" _ "github.com/gogo/protobuf/gogoproto" proto "github.com/gogo/protobuf/proto" _ "github.com/gogo/protobuf/types" github_com_gogo_protobuf_types "github.com/gogo/protobuf/types" github_com_jaegertracing_jaeger_idl_model_v1 "github.com/jaegertracing/jaeger-idl/model/v1" v1 "github.com/jaegertracing/jaeger-idl/model/v1" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" io "io" math "math" math_bits "math/bits" time "time" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf var _ = time.Kitchen // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package type GetDependenciesRequest struct { StartTime time.Time `protobuf:"bytes,1,opt,name=start_time,json=startTime,proto3,stdtime" json:"start_time"` EndTime time.Time `protobuf:"bytes,2,opt,name=end_time,json=endTime,proto3,stdtime" json:"end_time"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GetDependenciesRequest) Reset() { *m = GetDependenciesRequest{} } func (m *GetDependenciesRequest) String() string { return proto.CompactTextString(m) } func (*GetDependenciesRequest) ProtoMessage() {} func (*GetDependenciesRequest) Descriptor() ([]byte, []int) { return fileDescriptor_0d2c4ccf1453ffdb, []int{0} } func (m *GetDependenciesRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GetDependenciesRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GetDependenciesRequest.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GetDependenciesRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_GetDependenciesRequest.Merge(m, src) } func (m *GetDependenciesRequest) XXX_Size() int { return m.Size() } func (m *GetDependenciesRequest) XXX_DiscardUnknown() { xxx_messageInfo_GetDependenciesRequest.DiscardUnknown(m) } var xxx_messageInfo_GetDependenciesRequest proto.InternalMessageInfo func (m *GetDependenciesRequest) GetStartTime() time.Time { if m != nil { return m.StartTime } return time.Time{} } func (m *GetDependenciesRequest) GetEndTime() time.Time { if m != nil { return m.EndTime } return time.Time{} } type GetDependenciesResponse struct { Dependencies []v1.DependencyLink `protobuf:"bytes,1,rep,name=dependencies,proto3" json:"dependencies"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GetDependenciesResponse) Reset() { *m = GetDependenciesResponse{} } func (m *GetDependenciesResponse) String() string { return proto.CompactTextString(m) } func (*GetDependenciesResponse) ProtoMessage() {} func (*GetDependenciesResponse) Descriptor() ([]byte, []int) { return fileDescriptor_0d2c4ccf1453ffdb, []int{1} } func (m *GetDependenciesResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GetDependenciesResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GetDependenciesResponse.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GetDependenciesResponse) XXX_Merge(src proto.Message) { xxx_messageInfo_GetDependenciesResponse.Merge(m, src) } func (m *GetDependenciesResponse) XXX_Size() int { return m.Size() } func (m *GetDependenciesResponse) XXX_DiscardUnknown() { xxx_messageInfo_GetDependenciesResponse.DiscardUnknown(m) } var xxx_messageInfo_GetDependenciesResponse proto.InternalMessageInfo func (m *GetDependenciesResponse) GetDependencies() []v1.DependencyLink { if m != nil { return m.Dependencies } return nil } type WriteSpanRequest struct { Span *v1.Span `protobuf:"bytes,1,opt,name=span,proto3" json:"span,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *WriteSpanRequest) Reset() { *m = WriteSpanRequest{} } func (m *WriteSpanRequest) String() string { return proto.CompactTextString(m) } func (*WriteSpanRequest) ProtoMessage() {} func (*WriteSpanRequest) Descriptor() ([]byte, []int) { return fileDescriptor_0d2c4ccf1453ffdb, []int{2} } func (m *WriteSpanRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *WriteSpanRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_WriteSpanRequest.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *WriteSpanRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_WriteSpanRequest.Merge(m, src) } func (m *WriteSpanRequest) XXX_Size() int { return m.Size() } func (m *WriteSpanRequest) XXX_DiscardUnknown() { xxx_messageInfo_WriteSpanRequest.DiscardUnknown(m) } var xxx_messageInfo_WriteSpanRequest proto.InternalMessageInfo func (m *WriteSpanRequest) GetSpan() *v1.Span { if m != nil { return m.Span } return nil } // empty; extensible in the future type WriteSpanResponse struct { XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *WriteSpanResponse) Reset() { *m = WriteSpanResponse{} } func (m *WriteSpanResponse) String() string { return proto.CompactTextString(m) } func (*WriteSpanResponse) ProtoMessage() {} func (*WriteSpanResponse) Descriptor() ([]byte, []int) { return fileDescriptor_0d2c4ccf1453ffdb, []int{3} } func (m *WriteSpanResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *WriteSpanResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_WriteSpanResponse.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *WriteSpanResponse) XXX_Merge(src proto.Message) { xxx_messageInfo_WriteSpanResponse.Merge(m, src) } func (m *WriteSpanResponse) XXX_Size() int { return m.Size() } func (m *WriteSpanResponse) XXX_DiscardUnknown() { xxx_messageInfo_WriteSpanResponse.DiscardUnknown(m) } var xxx_messageInfo_WriteSpanResponse proto.InternalMessageInfo // empty; extensible in the future type CloseWriterRequest struct { XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *CloseWriterRequest) Reset() { *m = CloseWriterRequest{} } func (m *CloseWriterRequest) String() string { return proto.CompactTextString(m) } func (*CloseWriterRequest) ProtoMessage() {} func (*CloseWriterRequest) Descriptor() ([]byte, []int) { return fileDescriptor_0d2c4ccf1453ffdb, []int{4} } func (m *CloseWriterRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *CloseWriterRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_CloseWriterRequest.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *CloseWriterRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_CloseWriterRequest.Merge(m, src) } func (m *CloseWriterRequest) XXX_Size() int { return m.Size() } func (m *CloseWriterRequest) XXX_DiscardUnknown() { xxx_messageInfo_CloseWriterRequest.DiscardUnknown(m) } var xxx_messageInfo_CloseWriterRequest proto.InternalMessageInfo // empty; extensible in the future type CloseWriterResponse struct { XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *CloseWriterResponse) Reset() { *m = CloseWriterResponse{} } func (m *CloseWriterResponse) String() string { return proto.CompactTextString(m) } func (*CloseWriterResponse) ProtoMessage() {} func (*CloseWriterResponse) Descriptor() ([]byte, []int) { return fileDescriptor_0d2c4ccf1453ffdb, []int{5} } func (m *CloseWriterResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *CloseWriterResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_CloseWriterResponse.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *CloseWriterResponse) XXX_Merge(src proto.Message) { xxx_messageInfo_CloseWriterResponse.Merge(m, src) } func (m *CloseWriterResponse) XXX_Size() int { return m.Size() } func (m *CloseWriterResponse) XXX_DiscardUnknown() { xxx_messageInfo_CloseWriterResponse.DiscardUnknown(m) } var xxx_messageInfo_CloseWriterResponse proto.InternalMessageInfo type GetTraceRequest struct { TraceID github_com_jaegertracing_jaeger_idl_model_v1.TraceID `protobuf:"bytes,1,opt,name=trace_id,json=traceId,proto3,customtype=github.com/jaegertracing/jaeger-idl/model/v1.TraceID" json:"trace_id"` StartTime time.Time `protobuf:"bytes,2,opt,name=start_time,json=startTime,proto3,stdtime" json:"start_time"` EndTime time.Time `protobuf:"bytes,3,opt,name=end_time,json=endTime,proto3,stdtime" json:"end_time"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GetTraceRequest) Reset() { *m = GetTraceRequest{} } func (m *GetTraceRequest) String() string { return proto.CompactTextString(m) } func (*GetTraceRequest) ProtoMessage() {} func (*GetTraceRequest) Descriptor() ([]byte, []int) { return fileDescriptor_0d2c4ccf1453ffdb, []int{6} } func (m *GetTraceRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GetTraceRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GetTraceRequest.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GetTraceRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_GetTraceRequest.Merge(m, src) } func (m *GetTraceRequest) XXX_Size() int { return m.Size() } func (m *GetTraceRequest) XXX_DiscardUnknown() { xxx_messageInfo_GetTraceRequest.DiscardUnknown(m) } var xxx_messageInfo_GetTraceRequest proto.InternalMessageInfo func (m *GetTraceRequest) GetStartTime() time.Time { if m != nil { return m.StartTime } return time.Time{} } func (m *GetTraceRequest) GetEndTime() time.Time { if m != nil { return m.EndTime } return time.Time{} } type GetServicesRequest struct { XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GetServicesRequest) Reset() { *m = GetServicesRequest{} } func (m *GetServicesRequest) String() string { return proto.CompactTextString(m) } func (*GetServicesRequest) ProtoMessage() {} func (*GetServicesRequest) Descriptor() ([]byte, []int) { return fileDescriptor_0d2c4ccf1453ffdb, []int{7} } func (m *GetServicesRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GetServicesRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GetServicesRequest.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GetServicesRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_GetServicesRequest.Merge(m, src) } func (m *GetServicesRequest) XXX_Size() int { return m.Size() } func (m *GetServicesRequest) XXX_DiscardUnknown() { xxx_messageInfo_GetServicesRequest.DiscardUnknown(m) } var xxx_messageInfo_GetServicesRequest proto.InternalMessageInfo type GetServicesResponse struct { Services []string `protobuf:"bytes,1,rep,name=services,proto3" json:"services,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GetServicesResponse) Reset() { *m = GetServicesResponse{} } func (m *GetServicesResponse) String() string { return proto.CompactTextString(m) } func (*GetServicesResponse) ProtoMessage() {} func (*GetServicesResponse) Descriptor() ([]byte, []int) { return fileDescriptor_0d2c4ccf1453ffdb, []int{8} } func (m *GetServicesResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GetServicesResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GetServicesResponse.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GetServicesResponse) XXX_Merge(src proto.Message) { xxx_messageInfo_GetServicesResponse.Merge(m, src) } func (m *GetServicesResponse) XXX_Size() int { return m.Size() } func (m *GetServicesResponse) XXX_DiscardUnknown() { xxx_messageInfo_GetServicesResponse.DiscardUnknown(m) } var xxx_messageInfo_GetServicesResponse proto.InternalMessageInfo func (m *GetServicesResponse) GetServices() []string { if m != nil { return m.Services } return nil } type GetOperationsRequest struct { Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` SpanKind string `protobuf:"bytes,2,opt,name=span_kind,json=spanKind,proto3" json:"span_kind,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GetOperationsRequest) Reset() { *m = GetOperationsRequest{} } func (m *GetOperationsRequest) String() string { return proto.CompactTextString(m) } func (*GetOperationsRequest) ProtoMessage() {} func (*GetOperationsRequest) Descriptor() ([]byte, []int) { return fileDescriptor_0d2c4ccf1453ffdb, []int{9} } func (m *GetOperationsRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GetOperationsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GetOperationsRequest.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GetOperationsRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_GetOperationsRequest.Merge(m, src) } func (m *GetOperationsRequest) XXX_Size() int { return m.Size() } func (m *GetOperationsRequest) XXX_DiscardUnknown() { xxx_messageInfo_GetOperationsRequest.DiscardUnknown(m) } var xxx_messageInfo_GetOperationsRequest proto.InternalMessageInfo func (m *GetOperationsRequest) GetService() string { if m != nil { return m.Service } return "" } func (m *GetOperationsRequest) GetSpanKind() string { if m != nil { return m.SpanKind } return "" } type Operation struct { Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` SpanKind string `protobuf:"bytes,2,opt,name=span_kind,json=spanKind,proto3" json:"span_kind,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *Operation) Reset() { *m = Operation{} } func (m *Operation) String() string { return proto.CompactTextString(m) } func (*Operation) ProtoMessage() {} func (*Operation) Descriptor() ([]byte, []int) { return fileDescriptor_0d2c4ccf1453ffdb, []int{10} } func (m *Operation) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *Operation) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_Operation.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *Operation) XXX_Merge(src proto.Message) { xxx_messageInfo_Operation.Merge(m, src) } func (m *Operation) XXX_Size() int { return m.Size() } func (m *Operation) XXX_DiscardUnknown() { xxx_messageInfo_Operation.DiscardUnknown(m) } var xxx_messageInfo_Operation proto.InternalMessageInfo func (m *Operation) GetName() string { if m != nil { return m.Name } return "" } func (m *Operation) GetSpanKind() string { if m != nil { return m.SpanKind } return "" } type GetOperationsResponse struct { OperationNames []string `protobuf:"bytes,1,rep,name=operationNames,proto3" json:"operationNames,omitempty"` Operations []*Operation `protobuf:"bytes,2,rep,name=operations,proto3" json:"operations,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GetOperationsResponse) Reset() { *m = GetOperationsResponse{} } func (m *GetOperationsResponse) String() string { return proto.CompactTextString(m) } func (*GetOperationsResponse) ProtoMessage() {} func (*GetOperationsResponse) Descriptor() ([]byte, []int) { return fileDescriptor_0d2c4ccf1453ffdb, []int{11} } func (m *GetOperationsResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *GetOperationsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_GetOperationsResponse.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *GetOperationsResponse) XXX_Merge(src proto.Message) { xxx_messageInfo_GetOperationsResponse.Merge(m, src) } func (m *GetOperationsResponse) XXX_Size() int { return m.Size() } func (m *GetOperationsResponse) XXX_DiscardUnknown() { xxx_messageInfo_GetOperationsResponse.DiscardUnknown(m) } var xxx_messageInfo_GetOperationsResponse proto.InternalMessageInfo func (m *GetOperationsResponse) GetOperationNames() []string { if m != nil { return m.OperationNames } return nil } func (m *GetOperationsResponse) GetOperations() []*Operation { if m != nil { return m.Operations } return nil } type TraceQueryParameters struct { ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` OperationName string `protobuf:"bytes,2,opt,name=operation_name,json=operationName,proto3" json:"operation_name,omitempty"` Tags map[string]string `protobuf:"bytes,3,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` StartTimeMin time.Time `protobuf:"bytes,4,opt,name=start_time_min,json=startTimeMin,proto3,stdtime" json:"start_time_min"` StartTimeMax time.Time `protobuf:"bytes,5,opt,name=start_time_max,json=startTimeMax,proto3,stdtime" json:"start_time_max"` DurationMin time.Duration `protobuf:"bytes,6,opt,name=duration_min,json=durationMin,proto3,stdduration" json:"duration_min"` DurationMax time.Duration `protobuf:"bytes,7,opt,name=duration_max,json=durationMax,proto3,stdduration" json:"duration_max"` NumTraces int32 `protobuf:"varint,8,opt,name=num_traces,json=numTraces,proto3" json:"num_traces,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *TraceQueryParameters) Reset() { *m = TraceQueryParameters{} } func (m *TraceQueryParameters) String() string { return proto.CompactTextString(m) } func (*TraceQueryParameters) ProtoMessage() {} func (*TraceQueryParameters) Descriptor() ([]byte, []int) { return fileDescriptor_0d2c4ccf1453ffdb, []int{12} } func (m *TraceQueryParameters) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *TraceQueryParameters) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_TraceQueryParameters.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *TraceQueryParameters) XXX_Merge(src proto.Message) { xxx_messageInfo_TraceQueryParameters.Merge(m, src) } func (m *TraceQueryParameters) XXX_Size() int { return m.Size() } func (m *TraceQueryParameters) XXX_DiscardUnknown() { xxx_messageInfo_TraceQueryParameters.DiscardUnknown(m) } var xxx_messageInfo_TraceQueryParameters proto.InternalMessageInfo func (m *TraceQueryParameters) GetServiceName() string { if m != nil { return m.ServiceName } return "" } func (m *TraceQueryParameters) GetOperationName() string { if m != nil { return m.OperationName } return "" } func (m *TraceQueryParameters) GetTags() map[string]string { if m != nil { return m.Tags } return nil } func (m *TraceQueryParameters) GetStartTimeMin() time.Time { if m != nil { return m.StartTimeMin } return time.Time{} } func (m *TraceQueryParameters) GetStartTimeMax() time.Time { if m != nil { return m.StartTimeMax } return time.Time{} } func (m *TraceQueryParameters) GetDurationMin() time.Duration { if m != nil { return m.DurationMin } return 0 } func (m *TraceQueryParameters) GetDurationMax() time.Duration { if m != nil { return m.DurationMax } return 0 } func (m *TraceQueryParameters) GetNumTraces() int32 { if m != nil { return m.NumTraces } return 0 } type FindTracesRequest struct { Query *TraceQueryParameters `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *FindTracesRequest) Reset() { *m = FindTracesRequest{} } func (m *FindTracesRequest) String() string { return proto.CompactTextString(m) } func (*FindTracesRequest) ProtoMessage() {} func (*FindTracesRequest) Descriptor() ([]byte, []int) { return fileDescriptor_0d2c4ccf1453ffdb, []int{13} } func (m *FindTracesRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *FindTracesRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_FindTracesRequest.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *FindTracesRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_FindTracesRequest.Merge(m, src) } func (m *FindTracesRequest) XXX_Size() int { return m.Size() } func (m *FindTracesRequest) XXX_DiscardUnknown() { xxx_messageInfo_FindTracesRequest.DiscardUnknown(m) } var xxx_messageInfo_FindTracesRequest proto.InternalMessageInfo func (m *FindTracesRequest) GetQuery() *TraceQueryParameters { if m != nil { return m.Query } return nil } type SpansResponseChunk struct { Spans []v1.Span `protobuf:"bytes,1,rep,name=spans,proto3" json:"spans"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *SpansResponseChunk) Reset() { *m = SpansResponseChunk{} } func (m *SpansResponseChunk) String() string { return proto.CompactTextString(m) } func (*SpansResponseChunk) ProtoMessage() {} func (*SpansResponseChunk) Descriptor() ([]byte, []int) { return fileDescriptor_0d2c4ccf1453ffdb, []int{14} } func (m *SpansResponseChunk) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *SpansResponseChunk) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_SpansResponseChunk.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *SpansResponseChunk) XXX_Merge(src proto.Message) { xxx_messageInfo_SpansResponseChunk.Merge(m, src) } func (m *SpansResponseChunk) XXX_Size() int { return m.Size() } func (m *SpansResponseChunk) XXX_DiscardUnknown() { xxx_messageInfo_SpansResponseChunk.DiscardUnknown(m) } var xxx_messageInfo_SpansResponseChunk proto.InternalMessageInfo func (m *SpansResponseChunk) GetSpans() []v1.Span { if m != nil { return m.Spans } return nil } type FindTraceIDsRequest struct { Query *TraceQueryParameters `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *FindTraceIDsRequest) Reset() { *m = FindTraceIDsRequest{} } func (m *FindTraceIDsRequest) String() string { return proto.CompactTextString(m) } func (*FindTraceIDsRequest) ProtoMessage() {} func (*FindTraceIDsRequest) Descriptor() ([]byte, []int) { return fileDescriptor_0d2c4ccf1453ffdb, []int{15} } func (m *FindTraceIDsRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *FindTraceIDsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_FindTraceIDsRequest.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *FindTraceIDsRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_FindTraceIDsRequest.Merge(m, src) } func (m *FindTraceIDsRequest) XXX_Size() int { return m.Size() } func (m *FindTraceIDsRequest) XXX_DiscardUnknown() { xxx_messageInfo_FindTraceIDsRequest.DiscardUnknown(m) } var xxx_messageInfo_FindTraceIDsRequest proto.InternalMessageInfo func (m *FindTraceIDsRequest) GetQuery() *TraceQueryParameters { if m != nil { return m.Query } return nil } type FindTraceIDsResponse struct { TraceIDs []github_com_jaegertracing_jaeger_idl_model_v1.TraceID `protobuf:"bytes,1,rep,name=trace_ids,json=traceIds,proto3,customtype=github.com/jaegertracing/jaeger-idl/model/v1.TraceID" json:"trace_ids"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *FindTraceIDsResponse) Reset() { *m = FindTraceIDsResponse{} } func (m *FindTraceIDsResponse) String() string { return proto.CompactTextString(m) } func (*FindTraceIDsResponse) ProtoMessage() {} func (*FindTraceIDsResponse) Descriptor() ([]byte, []int) { return fileDescriptor_0d2c4ccf1453ffdb, []int{16} } func (m *FindTraceIDsResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *FindTraceIDsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_FindTraceIDsResponse.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *FindTraceIDsResponse) XXX_Merge(src proto.Message) { xxx_messageInfo_FindTraceIDsResponse.Merge(m, src) } func (m *FindTraceIDsResponse) XXX_Size() int { return m.Size() } func (m *FindTraceIDsResponse) XXX_DiscardUnknown() { xxx_messageInfo_FindTraceIDsResponse.DiscardUnknown(m) } var xxx_messageInfo_FindTraceIDsResponse proto.InternalMessageInfo // empty; extensible in the future type CapabilitiesRequest struct { XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *CapabilitiesRequest) Reset() { *m = CapabilitiesRequest{} } func (m *CapabilitiesRequest) String() string { return proto.CompactTextString(m) } func (*CapabilitiesRequest) ProtoMessage() {} func (*CapabilitiesRequest) Descriptor() ([]byte, []int) { return fileDescriptor_0d2c4ccf1453ffdb, []int{17} } func (m *CapabilitiesRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *CapabilitiesRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_CapabilitiesRequest.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *CapabilitiesRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_CapabilitiesRequest.Merge(m, src) } func (m *CapabilitiesRequest) XXX_Size() int { return m.Size() } func (m *CapabilitiesRequest) XXX_DiscardUnknown() { xxx_messageInfo_CapabilitiesRequest.DiscardUnknown(m) } var xxx_messageInfo_CapabilitiesRequest proto.InternalMessageInfo type CapabilitiesResponse struct { ArchiveSpanReader bool `protobuf:"varint,1,opt,name=archiveSpanReader,proto3" json:"archiveSpanReader,omitempty"` // Deprecated: Do not use. ArchiveSpanWriter bool `protobuf:"varint,2,opt,name=archiveSpanWriter,proto3" json:"archiveSpanWriter,omitempty"` // Deprecated: Do not use. StreamingSpanWriter bool `protobuf:"varint,3,opt,name=streamingSpanWriter,proto3" json:"streamingSpanWriter,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *CapabilitiesResponse) Reset() { *m = CapabilitiesResponse{} } func (m *CapabilitiesResponse) String() string { return proto.CompactTextString(m) } func (*CapabilitiesResponse) ProtoMessage() {} func (*CapabilitiesResponse) Descriptor() ([]byte, []int) { return fileDescriptor_0d2c4ccf1453ffdb, []int{18} } func (m *CapabilitiesResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } func (m *CapabilitiesResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { return xxx_messageInfo_CapabilitiesResponse.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) if err != nil { return nil, err } return b[:n], nil } } func (m *CapabilitiesResponse) XXX_Merge(src proto.Message) { xxx_messageInfo_CapabilitiesResponse.Merge(m, src) } func (m *CapabilitiesResponse) XXX_Size() int { return m.Size() } func (m *CapabilitiesResponse) XXX_DiscardUnknown() { xxx_messageInfo_CapabilitiesResponse.DiscardUnknown(m) } var xxx_messageInfo_CapabilitiesResponse proto.InternalMessageInfo // Deprecated: Do not use. func (m *CapabilitiesResponse) GetArchiveSpanReader() bool { if m != nil { return m.ArchiveSpanReader } return false } // Deprecated: Do not use. func (m *CapabilitiesResponse) GetArchiveSpanWriter() bool { if m != nil { return m.ArchiveSpanWriter } return false } func (m *CapabilitiesResponse) GetStreamingSpanWriter() bool { if m != nil { return m.StreamingSpanWriter } return false } func init() { proto.RegisterType((*GetDependenciesRequest)(nil), "jaeger.storage.v1.GetDependenciesRequest") proto.RegisterType((*GetDependenciesResponse)(nil), "jaeger.storage.v1.GetDependenciesResponse") proto.RegisterType((*WriteSpanRequest)(nil), "jaeger.storage.v1.WriteSpanRequest") proto.RegisterType((*WriteSpanResponse)(nil), "jaeger.storage.v1.WriteSpanResponse") proto.RegisterType((*CloseWriterRequest)(nil), "jaeger.storage.v1.CloseWriterRequest") proto.RegisterType((*CloseWriterResponse)(nil), "jaeger.storage.v1.CloseWriterResponse") proto.RegisterType((*GetTraceRequest)(nil), "jaeger.storage.v1.GetTraceRequest") proto.RegisterType((*GetServicesRequest)(nil), "jaeger.storage.v1.GetServicesRequest") proto.RegisterType((*GetServicesResponse)(nil), "jaeger.storage.v1.GetServicesResponse") proto.RegisterType((*GetOperationsRequest)(nil), "jaeger.storage.v1.GetOperationsRequest") proto.RegisterType((*Operation)(nil), "jaeger.storage.v1.Operation") proto.RegisterType((*GetOperationsResponse)(nil), "jaeger.storage.v1.GetOperationsResponse") proto.RegisterType((*TraceQueryParameters)(nil), "jaeger.storage.v1.TraceQueryParameters") proto.RegisterMapType((map[string]string)(nil), "jaeger.storage.v1.TraceQueryParameters.TagsEntry") proto.RegisterType((*FindTracesRequest)(nil), "jaeger.storage.v1.FindTracesRequest") proto.RegisterType((*SpansResponseChunk)(nil), "jaeger.storage.v1.SpansResponseChunk") proto.RegisterType((*FindTraceIDsRequest)(nil), "jaeger.storage.v1.FindTraceIDsRequest") proto.RegisterType((*FindTraceIDsResponse)(nil), "jaeger.storage.v1.FindTraceIDsResponse") proto.RegisterType((*CapabilitiesRequest)(nil), "jaeger.storage.v1.CapabilitiesRequest") proto.RegisterType((*CapabilitiesResponse)(nil), "jaeger.storage.v1.CapabilitiesResponse") } func init() { proto.RegisterFile("storage.proto", fileDescriptor_0d2c4ccf1453ffdb) } var fileDescriptor_0d2c4ccf1453ffdb = []byte{ // 1142 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x57, 0x4f, 0x73, 0xdb, 0x44, 0x14, 0x47, 0x89, 0xdd, 0xd8, 0xcf, 0x4e, 0x9b, 0xac, 0x5d, 0xaa, 0x0a, 0x9a, 0x04, 0x41, 0x93, 0xc0, 0x0c, 0x72, 0x62, 0x98, 0x81, 0x81, 0x32, 0x0c, 0x4e, 0x52, 0x13, 0xa0, 0x50, 0x94, 0x4c, 0x3b, 0xc3, 0x9f, 0x7a, 0xd6, 0xd1, 0xa2, 0x2c, 0xb1, 0x56, 0xae, 0xfe, 0x78, 0x92, 0x61, 0x7a, 0xe3, 0x03, 0x70, 0xe4, 0xc4, 0x95, 0x0b, 0x9f, 0x82, 0x53, 0x8f, 0x9c, 0x39, 0x04, 0x26, 0x57, 0x3e, 0x02, 0x97, 0x8e, 0x76, 0x57, 0xb2, 0x64, 0x69, 0x92, 0x34, 0xcd, 0xcd, 0xfb, 0xf6, 0xf7, 0x7e, 0xef, 0xef, 0xbe, 0x27, 0xc3, 0xac, 0x1f, 0xb8, 0x1e, 0xb6, 0x89, 0x31, 0xf4, 0xdc, 0xc0, 0x45, 0xf3, 0x3f, 0x62, 0x62, 0x13, 0xcf, 0x88, 0xa5, 0xa3, 0x75, 0xad, 0x69, 0xbb, 0xb6, 0xcb, 0x6f, 0x5b, 0xd1, 0x2f, 0x01, 0xd4, 0x16, 0x6d, 0xd7, 0xb5, 0x07, 0xa4, 0xc5, 0x4f, 0xfd, 0xf0, 0x87, 0x56, 0x40, 0x1d, 0xe2, 0x07, 0xd8, 0x19, 0x4a, 0xc0, 0xc2, 0x24, 0xc0, 0x0a, 0x3d, 0x1c, 0x50, 0x97, 0xc9, 0xfb, 0x9a, 0xe3, 0x5a, 0x64, 0x20, 0x0e, 0xfa, 0x6f, 0x0a, 0xbc, 0xdc, 0x25, 0xc1, 0x26, 0x19, 0x12, 0x66, 0x11, 0xb6, 0x47, 0x89, 0x6f, 0x92, 0xc7, 0x21, 0xf1, 0x03, 0xb4, 0x01, 0xe0, 0x07, 0xd8, 0x0b, 0x7a, 0x91, 0x01, 0x55, 0x59, 0x52, 0x56, 0x6b, 0x6d, 0xcd, 0x10, 0xe4, 0x46, 0x4c, 0x6e, 0xec, 0xc6, 0xd6, 0x3b, 0x95, 0xa7, 0xc7, 0x8b, 0x2f, 0xfd, 0xf2, 0xcf, 0xa2, 0x62, 0x56, 0xb9, 0x5e, 0x74, 0x83, 0x3e, 0x86, 0x0a, 0x61, 0x96, 0xa0, 0x98, 0x7a, 0x0e, 0x8a, 0x19, 0xc2, 0xac, 0x48, 0xae, 0xf7, 0xe1, 0x46, 0xce, 0x3f, 0x7f, 0xe8, 0x32, 0x9f, 0xa0, 0x2e, 0xd4, 0xad, 0x94, 0x5c, 0x55, 0x96, 0xa6, 0x57, 0x6b, 0xed, 0x5b, 0x86, 0xcc, 0x24, 0x1e, 0xd2, 0xde, 0xa8, 0x6d, 0x24, 0xaa, 0x47, 0x5f, 0x50, 0x76, 0xd0, 0x29, 0x45, 0x26, 0xcc, 0x8c, 0xa2, 0xfe, 0x21, 0xcc, 0x3d, 0xf4, 0x68, 0x40, 0x76, 0x86, 0x98, 0xc5, 0xd1, 0xaf, 0x40, 0xc9, 0x1f, 0x62, 0x26, 0xe3, 0x6e, 0x4c, 0x90, 0x72, 0x24, 0x07, 0xe8, 0x0d, 0x98, 0x4f, 0x29, 0x0b, 0xd7, 0xf4, 0x26, 0xa0, 0x8d, 0x81, 0xeb, 0x13, 0x7e, 0xe3, 0x49, 0x4e, 0xfd, 0x3a, 0x34, 0x32, 0x52, 0x09, 0xfe, 0x5f, 0x81, 0x6b, 0x5d, 0x12, 0xec, 0x7a, 0x78, 0x8f, 0xc4, 0xe6, 0xfb, 0x50, 0x09, 0xa2, 0x73, 0x8f, 0x5a, 0xdc, 0x85, 0x7a, 0xa7, 0x1b, 0x39, 0xfe, 0xf7, 0xf1, 0xe2, 0xbb, 0x36, 0x0d, 0xf6, 0xc3, 0xbe, 0xb1, 0xe7, 0x3a, 0x2d, 0xe1, 0x54, 0x04, 0xa4, 0xcc, 0x96, 0xa7, 0xb7, 0xa9, 0x35, 0x68, 0xf1, 0x12, 0xb7, 0x46, 0xeb, 0x06, 0x27, 0xdd, 0xde, 0x3c, 0x39, 0x5e, 0x9c, 0x91, 0x3f, 0xcd, 0x19, 0x4e, 0xbc, 0x6d, 0x4d, 0x14, 0x78, 0xea, 0xc5, 0x0b, 0x3c, 0x7d, 0x91, 0x02, 0x37, 0x01, 0x75, 0x49, 0xb0, 0x43, 0xbc, 0x11, 0xdd, 0x4b, 0x9a, 0x4f, 0x5f, 0x87, 0x46, 0x46, 0x2a, 0x4b, 0xae, 0x41, 0xc5, 0x97, 0x32, 0x5e, 0xee, 0xaa, 0x99, 0x9c, 0xf5, 0x7b, 0xd0, 0xec, 0x92, 0xe0, 0xab, 0x21, 0x11, 0xdd, 0x9e, 0xf4, 0xb1, 0x0a, 0x33, 0x12, 0xc3, 0x33, 0x59, 0x35, 0xe3, 0x23, 0x7a, 0x05, 0xaa, 0x51, 0x09, 0x7b, 0x07, 0x94, 0x59, 0x3c, 0xfe, 0x88, 0x6e, 0x88, 0xd9, 0xe7, 0x94, 0x59, 0xfa, 0x1d, 0xa8, 0x26, 0x5c, 0x08, 0x41, 0x89, 0x61, 0x27, 0x26, 0xe0, 0xbf, 0x4f, 0xd7, 0x7e, 0x02, 0xd7, 0x27, 0x9c, 0x91, 0x11, 0x2c, 0xc3, 0x55, 0x37, 0x96, 0x7e, 0x89, 0x9d, 0x24, 0x8e, 0x09, 0x29, 0xba, 0x03, 0x90, 0x48, 0x7c, 0x75, 0x8a, 0xb7, 0xf6, 0xab, 0x46, 0x6e, 0x48, 0x18, 0x89, 0x09, 0x33, 0x85, 0xd7, 0x7f, 0x2f, 0x41, 0x93, 0xd7, 0xfb, 0xeb, 0x90, 0x78, 0x47, 0xf7, 0xb1, 0x87, 0x1d, 0x12, 0x10, 0xcf, 0x47, 0xaf, 0x41, 0x5d, 0x46, 0xdf, 0x4b, 0x05, 0x54, 0x93, 0xb2, 0xc8, 0x34, 0xba, 0x9d, 0xf2, 0x50, 0x80, 0x44, 0x70, 0xb3, 0x19, 0x0f, 0xd1, 0x16, 0x94, 0x02, 0x6c, 0xfb, 0xea, 0x34, 0x77, 0x6d, 0xbd, 0xc0, 0xb5, 0x22, 0x07, 0x8c, 0x5d, 0x6c, 0xfb, 0x5b, 0x2c, 0xf0, 0x8e, 0x4c, 0xae, 0x8e, 0x3e, 0x83, 0xab, 0xe3, 0x26, 0xec, 0x39, 0x94, 0xa9, 0xa5, 0xe7, 0xe8, 0xa2, 0x7a, 0xd2, 0x88, 0xf7, 0x28, 0x9b, 0xe4, 0xc2, 0x87, 0x6a, 0xf9, 0x62, 0x5c, 0xf8, 0x10, 0xdd, 0x85, 0x7a, 0x3c, 0x37, 0xb9, 0x57, 0x57, 0x38, 0xd3, 0xcd, 0x1c, 0xd3, 0xa6, 0x04, 0x09, 0xa2, 0x5f, 0x23, 0xa2, 0x5a, 0xac, 0x18, 0xf9, 0x94, 0xe1, 0xc1, 0x87, 0xea, 0xcc, 0x45, 0x78, 0xf0, 0x21, 0xba, 0x05, 0xc0, 0x42, 0xa7, 0xc7, 0xdf, 0xae, 0xaf, 0x56, 0x96, 0x94, 0xd5, 0xb2, 0x59, 0x65, 0xa1, 0xc3, 0x93, 0xec, 0x6b, 0xef, 0x41, 0x35, 0xc9, 0x2c, 0x9a, 0x83, 0xe9, 0x03, 0x72, 0x24, 0x6b, 0x1b, 0xfd, 0x44, 0x4d, 0x28, 0x8f, 0xf0, 0x20, 0x8c, 0x4b, 0x29, 0x0e, 0x1f, 0x4c, 0xbd, 0xaf, 0xe8, 0x26, 0xcc, 0xdf, 0xa5, 0xcc, 0x12, 0x34, 0xf1, 0x93, 0xf9, 0x08, 0xca, 0x8f, 0xa3, 0xba, 0xc9, 0xe9, 0xb7, 0x72, 0xce, 0xe2, 0x9a, 0x42, 0x4b, 0xdf, 0x02, 0x14, 0x4d, 0xc3, 0xa4, 0xe9, 0x37, 0xf6, 0x43, 0x76, 0x80, 0x5a, 0x50, 0x8e, 0x9e, 0x47, 0x3c, 0xa7, 0x8b, 0x46, 0xaa, 0x9c, 0xce, 0x02, 0xa7, 0xef, 0x42, 0x23, 0x71, 0x6d, 0x7b, 0xf3, 0xb2, 0x9c, 0x7b, 0x02, 0xcd, 0x2c, 0xab, 0x7c, 0x98, 0x04, 0xaa, 0xf1, 0xc4, 0x15, 0x2e, 0xd6, 0x3b, 0x9f, 0xbe, 0xe0, 0xc8, 0xad, 0x24, 0x46, 0x2a, 0x72, 0xe6, 0xfa, 0x7c, 0x07, 0xe0, 0x21, 0xee, 0xd3, 0x01, 0x0d, 0xc6, 0xcb, 0x56, 0xff, 0x43, 0x81, 0x66, 0x56, 0x2e, 0xdd, 0x5a, 0x83, 0x79, 0xec, 0xed, 0xed, 0xd3, 0x91, 0x5c, 0x30, 0xd8, 0x22, 0x1e, 0x8f, 0xbc, 0xd2, 0x99, 0x52, 0x15, 0x33, 0x7f, 0x39, 0xa1, 0x21, 0x76, 0x0d, 0xaf, 0x7b, 0x5e, 0x43, 0x5c, 0xa2, 0x35, 0x68, 0xf8, 0x81, 0x47, 0xb0, 0x43, 0x99, 0x9d, 0xd2, 0x89, 0xc6, 0x79, 0xc5, 0x2c, 0xba, 0x6a, 0xff, 0xa9, 0xc0, 0xdc, 0xf8, 0x78, 0x7f, 0x10, 0xda, 0x94, 0xa1, 0x07, 0x50, 0x4d, 0x36, 0x21, 0x7a, 0xbd, 0xa0, 0x2c, 0x93, 0x4b, 0x56, 0x7b, 0xe3, 0x74, 0x90, 0x4c, 0xc1, 0x03, 0x28, 0xf3, 0xb5, 0x89, 0x6e, 0x17, 0xc0, 0xf3, 0x6b, 0x56, 0x5b, 0x3e, 0x0b, 0x26, 0x78, 0xdb, 0x3f, 0xc1, 0xcd, 0x9d, 0x7c, 0x6c, 0x32, 0x98, 0x47, 0x70, 0x2d, 0xf1, 0x44, 0xa0, 0x2e, 0x31, 0xa4, 0x55, 0xa5, 0xfd, 0xdf, 0xb4, 0xc8, 0xa0, 0x28, 0x9a, 0x34, 0xfa, 0x10, 0x2a, 0xf1, 0x87, 0x00, 0xd2, 0x0b, 0x88, 0x26, 0xbe, 0x12, 0xb4, 0xa2, 0x84, 0xe4, 0x5f, 0xde, 0x9a, 0x82, 0xbe, 0x83, 0x5a, 0x6a, 0x9d, 0x16, 0x26, 0x32, 0xbf, 0x84, 0x0b, 0x13, 0x59, 0xb4, 0x95, 0xfb, 0x30, 0x9b, 0x59, 0x76, 0x68, 0xa5, 0x58, 0x31, 0xb7, 0x9b, 0xb5, 0xd5, 0xb3, 0x81, 0xd2, 0xc6, 0xb7, 0x00, 0xe3, 0x39, 0x85, 0x8a, 0xb2, 0x9c, 0x1b, 0x63, 0xe7, 0x4f, 0x4f, 0x0f, 0xea, 0xe9, 0x99, 0x80, 0x96, 0x4f, 0xa3, 0x1f, 0x8f, 0x22, 0x6d, 0xe5, 0x4c, 0x9c, 0x6c, 0xb5, 0x43, 0xb8, 0xf1, 0xc9, 0xe4, 0xb3, 0x93, 0x35, 0xff, 0x5e, 0x7e, 0x7c, 0xa6, 0xee, 0x2f, 0xb1, 0xd3, 0xda, 0x47, 0x19, 0xcb, 0x99, 0x6e, 0x7b, 0xc4, 0x3f, 0x3b, 0xe5, 0xed, 0xe5, 0x37, 0x5d, 0xfb, 0x67, 0x05, 0xd4, 0xec, 0x87, 0x7b, 0xca, 0xf8, 0x3e, 0x37, 0x9e, 0xbe, 0x46, 0x6f, 0x16, 0x1b, 0x2f, 0xf8, 0x6f, 0xa2, 0xbd, 0x75, 0x1e, 0xa8, 0xcc, 0x40, 0x08, 0x48, 0xd8, 0x4c, 0xcf, 0xd7, 0xa8, 0xe4, 0x99, 0x73, 0xe1, 0xd0, 0xc8, 0x0f, 0xea, 0xc2, 0x92, 0x17, 0x0d, 0xee, 0x8e, 0xfa, 0xf4, 0x64, 0x41, 0xf9, 0xeb, 0x64, 0x41, 0xf9, 0xf7, 0x64, 0x41, 0xf9, 0x06, 0x24, 0xbc, 0x37, 0x5a, 0xef, 0x5f, 0xe1, 0x4b, 0xff, 0x9d, 0x67, 0x01, 0x00, 0x00, 0xff, 0xff, 0x6c, 0xd4, 0x66, 0xf9, 0x02, 0x0e, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. var _ context.Context var _ grpc.ClientConn // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. const _ = grpc.SupportPackageIsVersion4 // SpanWriterPluginClient is the client API for SpanWriterPlugin service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type SpanWriterPluginClient interface { // spanstore/Writer WriteSpan(ctx context.Context, in *WriteSpanRequest, opts ...grpc.CallOption) (*WriteSpanResponse, error) Close(ctx context.Context, in *CloseWriterRequest, opts ...grpc.CallOption) (*CloseWriterResponse, error) } type spanWriterPluginClient struct { cc *grpc.ClientConn } func NewSpanWriterPluginClient(cc *grpc.ClientConn) SpanWriterPluginClient { return &spanWriterPluginClient{cc} } func (c *spanWriterPluginClient) WriteSpan(ctx context.Context, in *WriteSpanRequest, opts ...grpc.CallOption) (*WriteSpanResponse, error) { out := new(WriteSpanResponse) err := c.cc.Invoke(ctx, "/jaeger.storage.v1.SpanWriterPlugin/WriteSpan", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *spanWriterPluginClient) Close(ctx context.Context, in *CloseWriterRequest, opts ...grpc.CallOption) (*CloseWriterResponse, error) { out := new(CloseWriterResponse) err := c.cc.Invoke(ctx, "/jaeger.storage.v1.SpanWriterPlugin/Close", in, out, opts...) if err != nil { return nil, err } return out, nil } // SpanWriterPluginServer is the server API for SpanWriterPlugin service. type SpanWriterPluginServer interface { // spanstore/Writer WriteSpan(context.Context, *WriteSpanRequest) (*WriteSpanResponse, error) Close(context.Context, *CloseWriterRequest) (*CloseWriterResponse, error) } // UnimplementedSpanWriterPluginServer can be embedded to have forward compatible implementations. type UnimplementedSpanWriterPluginServer struct { } func (*UnimplementedSpanWriterPluginServer) WriteSpan(ctx context.Context, req *WriteSpanRequest) (*WriteSpanResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method WriteSpan not implemented") } func (*UnimplementedSpanWriterPluginServer) Close(ctx context.Context, req *CloseWriterRequest) (*CloseWriterResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Close not implemented") } func RegisterSpanWriterPluginServer(s *grpc.Server, srv SpanWriterPluginServer) { s.RegisterService(&_SpanWriterPlugin_serviceDesc, srv) } func _SpanWriterPlugin_WriteSpan_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(WriteSpanRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(SpanWriterPluginServer).WriteSpan(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/jaeger.storage.v1.SpanWriterPlugin/WriteSpan", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(SpanWriterPluginServer).WriteSpan(ctx, req.(*WriteSpanRequest)) } return interceptor(ctx, in, info, handler) } func _SpanWriterPlugin_Close_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CloseWriterRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(SpanWriterPluginServer).Close(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/jaeger.storage.v1.SpanWriterPlugin/Close", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(SpanWriterPluginServer).Close(ctx, req.(*CloseWriterRequest)) } return interceptor(ctx, in, info, handler) } var _SpanWriterPlugin_serviceDesc = grpc.ServiceDesc{ ServiceName: "jaeger.storage.v1.SpanWriterPlugin", HandlerType: (*SpanWriterPluginServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "WriteSpan", Handler: _SpanWriterPlugin_WriteSpan_Handler, }, { MethodName: "Close", Handler: _SpanWriterPlugin_Close_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "storage.proto", } // StreamingSpanWriterPluginClient is the client API for StreamingSpanWriterPlugin service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type StreamingSpanWriterPluginClient interface { WriteSpanStream(ctx context.Context, opts ...grpc.CallOption) (StreamingSpanWriterPlugin_WriteSpanStreamClient, error) } type streamingSpanWriterPluginClient struct { cc *grpc.ClientConn } func NewStreamingSpanWriterPluginClient(cc *grpc.ClientConn) StreamingSpanWriterPluginClient { return &streamingSpanWriterPluginClient{cc} } func (c *streamingSpanWriterPluginClient) WriteSpanStream(ctx context.Context, opts ...grpc.CallOption) (StreamingSpanWriterPlugin_WriteSpanStreamClient, error) { stream, err := c.cc.NewStream(ctx, &_StreamingSpanWriterPlugin_serviceDesc.Streams[0], "/jaeger.storage.v1.StreamingSpanWriterPlugin/WriteSpanStream", opts...) if err != nil { return nil, err } x := &streamingSpanWriterPluginWriteSpanStreamClient{stream} return x, nil } type StreamingSpanWriterPlugin_WriteSpanStreamClient interface { Send(*WriteSpanRequest) error CloseAndRecv() (*WriteSpanResponse, error) grpc.ClientStream } type streamingSpanWriterPluginWriteSpanStreamClient struct { grpc.ClientStream } func (x *streamingSpanWriterPluginWriteSpanStreamClient) Send(m *WriteSpanRequest) error { return x.ClientStream.SendMsg(m) } func (x *streamingSpanWriterPluginWriteSpanStreamClient) CloseAndRecv() (*WriteSpanResponse, error) { if err := x.ClientStream.CloseSend(); err != nil { return nil, err } m := new(WriteSpanResponse) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } // StreamingSpanWriterPluginServer is the server API for StreamingSpanWriterPlugin service. type StreamingSpanWriterPluginServer interface { WriteSpanStream(StreamingSpanWriterPlugin_WriteSpanStreamServer) error } // UnimplementedStreamingSpanWriterPluginServer can be embedded to have forward compatible implementations. type UnimplementedStreamingSpanWriterPluginServer struct { } func (*UnimplementedStreamingSpanWriterPluginServer) WriteSpanStream(srv StreamingSpanWriterPlugin_WriteSpanStreamServer) error { return status.Errorf(codes.Unimplemented, "method WriteSpanStream not implemented") } func RegisterStreamingSpanWriterPluginServer(s *grpc.Server, srv StreamingSpanWriterPluginServer) { s.RegisterService(&_StreamingSpanWriterPlugin_serviceDesc, srv) } func _StreamingSpanWriterPlugin_WriteSpanStream_Handler(srv interface{}, stream grpc.ServerStream) error { return srv.(StreamingSpanWriterPluginServer).WriteSpanStream(&streamingSpanWriterPluginWriteSpanStreamServer{stream}) } type StreamingSpanWriterPlugin_WriteSpanStreamServer interface { SendAndClose(*WriteSpanResponse) error Recv() (*WriteSpanRequest, error) grpc.ServerStream } type streamingSpanWriterPluginWriteSpanStreamServer struct { grpc.ServerStream } func (x *streamingSpanWriterPluginWriteSpanStreamServer) SendAndClose(m *WriteSpanResponse) error { return x.ServerStream.SendMsg(m) } func (x *streamingSpanWriterPluginWriteSpanStreamServer) Recv() (*WriteSpanRequest, error) { m := new(WriteSpanRequest) if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err } return m, nil } var _StreamingSpanWriterPlugin_serviceDesc = grpc.ServiceDesc{ ServiceName: "jaeger.storage.v1.StreamingSpanWriterPlugin", HandlerType: (*StreamingSpanWriterPluginServer)(nil), Methods: []grpc.MethodDesc{}, Streams: []grpc.StreamDesc{ { StreamName: "WriteSpanStream", Handler: _StreamingSpanWriterPlugin_WriteSpanStream_Handler, ClientStreams: true, }, }, Metadata: "storage.proto", } // SpanReaderPluginClient is the client API for SpanReaderPlugin service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type SpanReaderPluginClient interface { // spanstore/Reader GetTrace(ctx context.Context, in *GetTraceRequest, opts ...grpc.CallOption) (SpanReaderPlugin_GetTraceClient, error) GetServices(ctx context.Context, in *GetServicesRequest, opts ...grpc.CallOption) (*GetServicesResponse, error) GetOperations(ctx context.Context, in *GetOperationsRequest, opts ...grpc.CallOption) (*GetOperationsResponse, error) FindTraces(ctx context.Context, in *FindTracesRequest, opts ...grpc.CallOption) (SpanReaderPlugin_FindTracesClient, error) FindTraceIDs(ctx context.Context, in *FindTraceIDsRequest, opts ...grpc.CallOption) (*FindTraceIDsResponse, error) } type spanReaderPluginClient struct { cc *grpc.ClientConn } func NewSpanReaderPluginClient(cc *grpc.ClientConn) SpanReaderPluginClient { return &spanReaderPluginClient{cc} } func (c *spanReaderPluginClient) GetTrace(ctx context.Context, in *GetTraceRequest, opts ...grpc.CallOption) (SpanReaderPlugin_GetTraceClient, error) { stream, err := c.cc.NewStream(ctx, &_SpanReaderPlugin_serviceDesc.Streams[0], "/jaeger.storage.v1.SpanReaderPlugin/GetTrace", opts...) if err != nil { return nil, err } x := &spanReaderPluginGetTraceClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type SpanReaderPlugin_GetTraceClient interface { Recv() (*SpansResponseChunk, error) grpc.ClientStream } type spanReaderPluginGetTraceClient struct { grpc.ClientStream } func (x *spanReaderPluginGetTraceClient) Recv() (*SpansResponseChunk, error) { m := new(SpansResponseChunk) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *spanReaderPluginClient) GetServices(ctx context.Context, in *GetServicesRequest, opts ...grpc.CallOption) (*GetServicesResponse, error) { out := new(GetServicesResponse) err := c.cc.Invoke(ctx, "/jaeger.storage.v1.SpanReaderPlugin/GetServices", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *spanReaderPluginClient) GetOperations(ctx context.Context, in *GetOperationsRequest, opts ...grpc.CallOption) (*GetOperationsResponse, error) { out := new(GetOperationsResponse) err := c.cc.Invoke(ctx, "/jaeger.storage.v1.SpanReaderPlugin/GetOperations", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *spanReaderPluginClient) FindTraces(ctx context.Context, in *FindTracesRequest, opts ...grpc.CallOption) (SpanReaderPlugin_FindTracesClient, error) { stream, err := c.cc.NewStream(ctx, &_SpanReaderPlugin_serviceDesc.Streams[1], "/jaeger.storage.v1.SpanReaderPlugin/FindTraces", opts...) if err != nil { return nil, err } x := &spanReaderPluginFindTracesClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type SpanReaderPlugin_FindTracesClient interface { Recv() (*SpansResponseChunk, error) grpc.ClientStream } type spanReaderPluginFindTracesClient struct { grpc.ClientStream } func (x *spanReaderPluginFindTracesClient) Recv() (*SpansResponseChunk, error) { m := new(SpansResponseChunk) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *spanReaderPluginClient) FindTraceIDs(ctx context.Context, in *FindTraceIDsRequest, opts ...grpc.CallOption) (*FindTraceIDsResponse, error) { out := new(FindTraceIDsResponse) err := c.cc.Invoke(ctx, "/jaeger.storage.v1.SpanReaderPlugin/FindTraceIDs", in, out, opts...) if err != nil { return nil, err } return out, nil } // SpanReaderPluginServer is the server API for SpanReaderPlugin service. type SpanReaderPluginServer interface { // spanstore/Reader GetTrace(*GetTraceRequest, SpanReaderPlugin_GetTraceServer) error GetServices(context.Context, *GetServicesRequest) (*GetServicesResponse, error) GetOperations(context.Context, *GetOperationsRequest) (*GetOperationsResponse, error) FindTraces(*FindTracesRequest, SpanReaderPlugin_FindTracesServer) error FindTraceIDs(context.Context, *FindTraceIDsRequest) (*FindTraceIDsResponse, error) } // UnimplementedSpanReaderPluginServer can be embedded to have forward compatible implementations. type UnimplementedSpanReaderPluginServer struct { } func (*UnimplementedSpanReaderPluginServer) GetTrace(req *GetTraceRequest, srv SpanReaderPlugin_GetTraceServer) error { return status.Errorf(codes.Unimplemented, "method GetTrace not implemented") } func (*UnimplementedSpanReaderPluginServer) GetServices(ctx context.Context, req *GetServicesRequest) (*GetServicesResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetServices not implemented") } func (*UnimplementedSpanReaderPluginServer) GetOperations(ctx context.Context, req *GetOperationsRequest) (*GetOperationsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetOperations not implemented") } func (*UnimplementedSpanReaderPluginServer) FindTraces(req *FindTracesRequest, srv SpanReaderPlugin_FindTracesServer) error { return status.Errorf(codes.Unimplemented, "method FindTraces not implemented") } func (*UnimplementedSpanReaderPluginServer) FindTraceIDs(ctx context.Context, req *FindTraceIDsRequest) (*FindTraceIDsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method FindTraceIDs not implemented") } func RegisterSpanReaderPluginServer(s *grpc.Server, srv SpanReaderPluginServer) { s.RegisterService(&_SpanReaderPlugin_serviceDesc, srv) } func _SpanReaderPlugin_GetTrace_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(GetTraceRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(SpanReaderPluginServer).GetTrace(m, &spanReaderPluginGetTraceServer{stream}) } type SpanReaderPlugin_GetTraceServer interface { Send(*SpansResponseChunk) error grpc.ServerStream } type spanReaderPluginGetTraceServer struct { grpc.ServerStream } func (x *spanReaderPluginGetTraceServer) Send(m *SpansResponseChunk) error { return x.ServerStream.SendMsg(m) } func _SpanReaderPlugin_GetServices_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetServicesRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(SpanReaderPluginServer).GetServices(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/jaeger.storage.v1.SpanReaderPlugin/GetServices", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(SpanReaderPluginServer).GetServices(ctx, req.(*GetServicesRequest)) } return interceptor(ctx, in, info, handler) } func _SpanReaderPlugin_GetOperations_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetOperationsRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(SpanReaderPluginServer).GetOperations(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/jaeger.storage.v1.SpanReaderPlugin/GetOperations", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(SpanReaderPluginServer).GetOperations(ctx, req.(*GetOperationsRequest)) } return interceptor(ctx, in, info, handler) } func _SpanReaderPlugin_FindTraces_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(FindTracesRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(SpanReaderPluginServer).FindTraces(m, &spanReaderPluginFindTracesServer{stream}) } type SpanReaderPlugin_FindTracesServer interface { Send(*SpansResponseChunk) error grpc.ServerStream } type spanReaderPluginFindTracesServer struct { grpc.ServerStream } func (x *spanReaderPluginFindTracesServer) Send(m *SpansResponseChunk) error { return x.ServerStream.SendMsg(m) } func _SpanReaderPlugin_FindTraceIDs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(FindTraceIDsRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(SpanReaderPluginServer).FindTraceIDs(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/jaeger.storage.v1.SpanReaderPlugin/FindTraceIDs", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(SpanReaderPluginServer).FindTraceIDs(ctx, req.(*FindTraceIDsRequest)) } return interceptor(ctx, in, info, handler) } var _SpanReaderPlugin_serviceDesc = grpc.ServiceDesc{ ServiceName: "jaeger.storage.v1.SpanReaderPlugin", HandlerType: (*SpanReaderPluginServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "GetServices", Handler: _SpanReaderPlugin_GetServices_Handler, }, { MethodName: "GetOperations", Handler: _SpanReaderPlugin_GetOperations_Handler, }, { MethodName: "FindTraceIDs", Handler: _SpanReaderPlugin_FindTraceIDs_Handler, }, }, Streams: []grpc.StreamDesc{ { StreamName: "GetTrace", Handler: _SpanReaderPlugin_GetTrace_Handler, ServerStreams: true, }, { StreamName: "FindTraces", Handler: _SpanReaderPlugin_FindTraces_Handler, ServerStreams: true, }, }, Metadata: "storage.proto", } // ArchiveSpanWriterPluginClient is the client API for ArchiveSpanWriterPlugin service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type ArchiveSpanWriterPluginClient interface { // spanstore/Writer WriteArchiveSpan(ctx context.Context, in *WriteSpanRequest, opts ...grpc.CallOption) (*WriteSpanResponse, error) } type archiveSpanWriterPluginClient struct { cc *grpc.ClientConn } func NewArchiveSpanWriterPluginClient(cc *grpc.ClientConn) ArchiveSpanWriterPluginClient { return &archiveSpanWriterPluginClient{cc} } func (c *archiveSpanWriterPluginClient) WriteArchiveSpan(ctx context.Context, in *WriteSpanRequest, opts ...grpc.CallOption) (*WriteSpanResponse, error) { out := new(WriteSpanResponse) err := c.cc.Invoke(ctx, "/jaeger.storage.v1.ArchiveSpanWriterPlugin/WriteArchiveSpan", in, out, opts...) if err != nil { return nil, err } return out, nil } // ArchiveSpanWriterPluginServer is the server API for ArchiveSpanWriterPlugin service. type ArchiveSpanWriterPluginServer interface { // spanstore/Writer WriteArchiveSpan(context.Context, *WriteSpanRequest) (*WriteSpanResponse, error) } // UnimplementedArchiveSpanWriterPluginServer can be embedded to have forward compatible implementations. type UnimplementedArchiveSpanWriterPluginServer struct { } func (*UnimplementedArchiveSpanWriterPluginServer) WriteArchiveSpan(ctx context.Context, req *WriteSpanRequest) (*WriteSpanResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method WriteArchiveSpan not implemented") } func RegisterArchiveSpanWriterPluginServer(s *grpc.Server, srv ArchiveSpanWriterPluginServer) { s.RegisterService(&_ArchiveSpanWriterPlugin_serviceDesc, srv) } func _ArchiveSpanWriterPlugin_WriteArchiveSpan_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(WriteSpanRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ArchiveSpanWriterPluginServer).WriteArchiveSpan(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/jaeger.storage.v1.ArchiveSpanWriterPlugin/WriteArchiveSpan", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ArchiveSpanWriterPluginServer).WriteArchiveSpan(ctx, req.(*WriteSpanRequest)) } return interceptor(ctx, in, info, handler) } var _ArchiveSpanWriterPlugin_serviceDesc = grpc.ServiceDesc{ ServiceName: "jaeger.storage.v1.ArchiveSpanWriterPlugin", HandlerType: (*ArchiveSpanWriterPluginServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "WriteArchiveSpan", Handler: _ArchiveSpanWriterPlugin_WriteArchiveSpan_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "storage.proto", } // ArchiveSpanReaderPluginClient is the client API for ArchiveSpanReaderPlugin service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type ArchiveSpanReaderPluginClient interface { // spanstore/Reader GetArchiveTrace(ctx context.Context, in *GetTraceRequest, opts ...grpc.CallOption) (ArchiveSpanReaderPlugin_GetArchiveTraceClient, error) } type archiveSpanReaderPluginClient struct { cc *grpc.ClientConn } func NewArchiveSpanReaderPluginClient(cc *grpc.ClientConn) ArchiveSpanReaderPluginClient { return &archiveSpanReaderPluginClient{cc} } func (c *archiveSpanReaderPluginClient) GetArchiveTrace(ctx context.Context, in *GetTraceRequest, opts ...grpc.CallOption) (ArchiveSpanReaderPlugin_GetArchiveTraceClient, error) { stream, err := c.cc.NewStream(ctx, &_ArchiveSpanReaderPlugin_serviceDesc.Streams[0], "/jaeger.storage.v1.ArchiveSpanReaderPlugin/GetArchiveTrace", opts...) if err != nil { return nil, err } x := &archiveSpanReaderPluginGetArchiveTraceClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type ArchiveSpanReaderPlugin_GetArchiveTraceClient interface { Recv() (*SpansResponseChunk, error) grpc.ClientStream } type archiveSpanReaderPluginGetArchiveTraceClient struct { grpc.ClientStream } func (x *archiveSpanReaderPluginGetArchiveTraceClient) Recv() (*SpansResponseChunk, error) { m := new(SpansResponseChunk) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } // ArchiveSpanReaderPluginServer is the server API for ArchiveSpanReaderPlugin service. type ArchiveSpanReaderPluginServer interface { // spanstore/Reader GetArchiveTrace(*GetTraceRequest, ArchiveSpanReaderPlugin_GetArchiveTraceServer) error } // UnimplementedArchiveSpanReaderPluginServer can be embedded to have forward compatible implementations. type UnimplementedArchiveSpanReaderPluginServer struct { } func (*UnimplementedArchiveSpanReaderPluginServer) GetArchiveTrace(req *GetTraceRequest, srv ArchiveSpanReaderPlugin_GetArchiveTraceServer) error { return status.Errorf(codes.Unimplemented, "method GetArchiveTrace not implemented") } func RegisterArchiveSpanReaderPluginServer(s *grpc.Server, srv ArchiveSpanReaderPluginServer) { s.RegisterService(&_ArchiveSpanReaderPlugin_serviceDesc, srv) } func _ArchiveSpanReaderPlugin_GetArchiveTrace_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(GetTraceRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(ArchiveSpanReaderPluginServer).GetArchiveTrace(m, &archiveSpanReaderPluginGetArchiveTraceServer{stream}) } type ArchiveSpanReaderPlugin_GetArchiveTraceServer interface { Send(*SpansResponseChunk) error grpc.ServerStream } type archiveSpanReaderPluginGetArchiveTraceServer struct { grpc.ServerStream } func (x *archiveSpanReaderPluginGetArchiveTraceServer) Send(m *SpansResponseChunk) error { return x.ServerStream.SendMsg(m) } var _ArchiveSpanReaderPlugin_serviceDesc = grpc.ServiceDesc{ ServiceName: "jaeger.storage.v1.ArchiveSpanReaderPlugin", HandlerType: (*ArchiveSpanReaderPluginServer)(nil), Methods: []grpc.MethodDesc{}, Streams: []grpc.StreamDesc{ { StreamName: "GetArchiveTrace", Handler: _ArchiveSpanReaderPlugin_GetArchiveTrace_Handler, ServerStreams: true, }, }, Metadata: "storage.proto", } // DependenciesReaderPluginClient is the client API for DependenciesReaderPlugin service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type DependenciesReaderPluginClient interface { // dependencystore/Reader GetDependencies(ctx context.Context, in *GetDependenciesRequest, opts ...grpc.CallOption) (*GetDependenciesResponse, error) } type dependenciesReaderPluginClient struct { cc *grpc.ClientConn } func NewDependenciesReaderPluginClient(cc *grpc.ClientConn) DependenciesReaderPluginClient { return &dependenciesReaderPluginClient{cc} } func (c *dependenciesReaderPluginClient) GetDependencies(ctx context.Context, in *GetDependenciesRequest, opts ...grpc.CallOption) (*GetDependenciesResponse, error) { out := new(GetDependenciesResponse) err := c.cc.Invoke(ctx, "/jaeger.storage.v1.DependenciesReaderPlugin/GetDependencies", in, out, opts...) if err != nil { return nil, err } return out, nil } // DependenciesReaderPluginServer is the server API for DependenciesReaderPlugin service. type DependenciesReaderPluginServer interface { // dependencystore/Reader GetDependencies(context.Context, *GetDependenciesRequest) (*GetDependenciesResponse, error) } // UnimplementedDependenciesReaderPluginServer can be embedded to have forward compatible implementations. type UnimplementedDependenciesReaderPluginServer struct { } func (*UnimplementedDependenciesReaderPluginServer) GetDependencies(ctx context.Context, req *GetDependenciesRequest) (*GetDependenciesResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetDependencies not implemented") } func RegisterDependenciesReaderPluginServer(s *grpc.Server, srv DependenciesReaderPluginServer) { s.RegisterService(&_DependenciesReaderPlugin_serviceDesc, srv) } func _DependenciesReaderPlugin_GetDependencies_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetDependenciesRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DependenciesReaderPluginServer).GetDependencies(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/jaeger.storage.v1.DependenciesReaderPlugin/GetDependencies", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DependenciesReaderPluginServer).GetDependencies(ctx, req.(*GetDependenciesRequest)) } return interceptor(ctx, in, info, handler) } var _DependenciesReaderPlugin_serviceDesc = grpc.ServiceDesc{ ServiceName: "jaeger.storage.v1.DependenciesReaderPlugin", HandlerType: (*DependenciesReaderPluginServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "GetDependencies", Handler: _DependenciesReaderPlugin_GetDependencies_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "storage.proto", } // PluginCapabilitiesClient is the client API for PluginCapabilities service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type PluginCapabilitiesClient interface { Capabilities(ctx context.Context, in *CapabilitiesRequest, opts ...grpc.CallOption) (*CapabilitiesResponse, error) } type pluginCapabilitiesClient struct { cc *grpc.ClientConn } func NewPluginCapabilitiesClient(cc *grpc.ClientConn) PluginCapabilitiesClient { return &pluginCapabilitiesClient{cc} } func (c *pluginCapabilitiesClient) Capabilities(ctx context.Context, in *CapabilitiesRequest, opts ...grpc.CallOption) (*CapabilitiesResponse, error) { out := new(CapabilitiesResponse) err := c.cc.Invoke(ctx, "/jaeger.storage.v1.PluginCapabilities/Capabilities", in, out, opts...) if err != nil { return nil, err } return out, nil } // PluginCapabilitiesServer is the server API for PluginCapabilities service. type PluginCapabilitiesServer interface { Capabilities(context.Context, *CapabilitiesRequest) (*CapabilitiesResponse, error) } // UnimplementedPluginCapabilitiesServer can be embedded to have forward compatible implementations. type UnimplementedPluginCapabilitiesServer struct { } func (*UnimplementedPluginCapabilitiesServer) Capabilities(ctx context.Context, req *CapabilitiesRequest) (*CapabilitiesResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Capabilities not implemented") } func RegisterPluginCapabilitiesServer(s *grpc.Server, srv PluginCapabilitiesServer) { s.RegisterService(&_PluginCapabilities_serviceDesc, srv) } func _PluginCapabilities_Capabilities_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CapabilitiesRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(PluginCapabilitiesServer).Capabilities(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/jaeger.storage.v1.PluginCapabilities/Capabilities", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(PluginCapabilitiesServer).Capabilities(ctx, req.(*CapabilitiesRequest)) } return interceptor(ctx, in, info, handler) } var _PluginCapabilities_serviceDesc = grpc.ServiceDesc{ ServiceName: "jaeger.storage.v1.PluginCapabilities", HandlerType: (*PluginCapabilitiesServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "Capabilities", Handler: _PluginCapabilities_Capabilities_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "storage.proto", } func (m *GetDependenciesRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GetDependenciesRequest) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GetDependenciesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } n1, err1 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.EndTime, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.EndTime):]) if err1 != nil { return 0, err1 } i -= n1 i = encodeVarintStorage(dAtA, i, uint64(n1)) i-- dAtA[i] = 0x12 n2, err2 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.StartTime, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTime):]) if err2 != nil { return 0, err2 } i -= n2 i = encodeVarintStorage(dAtA, i, uint64(n2)) i-- dAtA[i] = 0xa return len(dAtA) - i, nil } func (m *GetDependenciesResponse) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GetDependenciesResponse) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GetDependenciesResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.Dependencies) > 0 { for iNdEx := len(m.Dependencies) - 1; iNdEx >= 0; iNdEx-- { { size, err := m.Dependencies[iNdEx].MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintStorage(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } } return len(dAtA) - i, nil } func (m *WriteSpanRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *WriteSpanRequest) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *WriteSpanRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if m.Span != nil { { size, err := m.Span.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintStorage(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *WriteSpanResponse) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *WriteSpanResponse) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *WriteSpanResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } return len(dAtA) - i, nil } func (m *CloseWriterRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *CloseWriterRequest) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *CloseWriterRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } return len(dAtA) - i, nil } func (m *CloseWriterResponse) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *CloseWriterResponse) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *CloseWriterResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } return len(dAtA) - i, nil } func (m *GetTraceRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GetTraceRequest) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GetTraceRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } n4, err4 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.EndTime, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.EndTime):]) if err4 != nil { return 0, err4 } i -= n4 i = encodeVarintStorage(dAtA, i, uint64(n4)) i-- dAtA[i] = 0x1a n5, err5 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.StartTime, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTime):]) if err5 != nil { return 0, err5 } i -= n5 i = encodeVarintStorage(dAtA, i, uint64(n5)) i-- dAtA[i] = 0x12 { size := m.TraceID.Size() i -= size if _, err := m.TraceID.MarshalTo(dAtA[i:]); err != nil { return 0, err } i = encodeVarintStorage(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa return len(dAtA) - i, nil } func (m *GetServicesRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GetServicesRequest) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GetServicesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } return len(dAtA) - i, nil } func (m *GetServicesResponse) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GetServicesResponse) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GetServicesResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.Services) > 0 { for iNdEx := len(m.Services) - 1; iNdEx >= 0; iNdEx-- { i -= len(m.Services[iNdEx]) copy(dAtA[i:], m.Services[iNdEx]) i = encodeVarintStorage(dAtA, i, uint64(len(m.Services[iNdEx]))) i-- dAtA[i] = 0xa } } return len(dAtA) - i, nil } func (m *GetOperationsRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GetOperationsRequest) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GetOperationsRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.SpanKind) > 0 { i -= len(m.SpanKind) copy(dAtA[i:], m.SpanKind) i = encodeVarintStorage(dAtA, i, uint64(len(m.SpanKind))) i-- dAtA[i] = 0x12 } if len(m.Service) > 0 { i -= len(m.Service) copy(dAtA[i:], m.Service) i = encodeVarintStorage(dAtA, i, uint64(len(m.Service))) i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *Operation) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *Operation) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *Operation) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.SpanKind) > 0 { i -= len(m.SpanKind) copy(dAtA[i:], m.SpanKind) i = encodeVarintStorage(dAtA, i, uint64(len(m.SpanKind))) i-- dAtA[i] = 0x12 } if len(m.Name) > 0 { i -= len(m.Name) copy(dAtA[i:], m.Name) i = encodeVarintStorage(dAtA, i, uint64(len(m.Name))) i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *GetOperationsResponse) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *GetOperationsResponse) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *GetOperationsResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.Operations) > 0 { for iNdEx := len(m.Operations) - 1; iNdEx >= 0; iNdEx-- { { size, err := m.Operations[iNdEx].MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintStorage(dAtA, i, uint64(size)) } i-- dAtA[i] = 0x12 } } if len(m.OperationNames) > 0 { for iNdEx := len(m.OperationNames) - 1; iNdEx >= 0; iNdEx-- { i -= len(m.OperationNames[iNdEx]) copy(dAtA[i:], m.OperationNames[iNdEx]) i = encodeVarintStorage(dAtA, i, uint64(len(m.OperationNames[iNdEx]))) i-- dAtA[i] = 0xa } } return len(dAtA) - i, nil } func (m *TraceQueryParameters) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *TraceQueryParameters) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *TraceQueryParameters) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if m.NumTraces != 0 { i = encodeVarintStorage(dAtA, i, uint64(m.NumTraces)) i-- dAtA[i] = 0x40 } n6, err6 := github_com_gogo_protobuf_types.StdDurationMarshalTo(m.DurationMax, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdDuration(m.DurationMax):]) if err6 != nil { return 0, err6 } i -= n6 i = encodeVarintStorage(dAtA, i, uint64(n6)) i-- dAtA[i] = 0x3a n7, err7 := github_com_gogo_protobuf_types.StdDurationMarshalTo(m.DurationMin, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdDuration(m.DurationMin):]) if err7 != nil { return 0, err7 } i -= n7 i = encodeVarintStorage(dAtA, i, uint64(n7)) i-- dAtA[i] = 0x32 n8, err8 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.StartTimeMax, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTimeMax):]) if err8 != nil { return 0, err8 } i -= n8 i = encodeVarintStorage(dAtA, i, uint64(n8)) i-- dAtA[i] = 0x2a n9, err9 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.StartTimeMin, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTimeMin):]) if err9 != nil { return 0, err9 } i -= n9 i = encodeVarintStorage(dAtA, i, uint64(n9)) i-- dAtA[i] = 0x22 if len(m.Tags) > 0 { for k := range m.Tags { v := m.Tags[k] baseI := i i -= len(v) copy(dAtA[i:], v) i = encodeVarintStorage(dAtA, i, uint64(len(v))) i-- dAtA[i] = 0x12 i -= len(k) copy(dAtA[i:], k) i = encodeVarintStorage(dAtA, i, uint64(len(k))) i-- dAtA[i] = 0xa i = encodeVarintStorage(dAtA, i, uint64(baseI-i)) i-- dAtA[i] = 0x1a } } if len(m.OperationName) > 0 { i -= len(m.OperationName) copy(dAtA[i:], m.OperationName) i = encodeVarintStorage(dAtA, i, uint64(len(m.OperationName))) i-- dAtA[i] = 0x12 } if len(m.ServiceName) > 0 { i -= len(m.ServiceName) copy(dAtA[i:], m.ServiceName) i = encodeVarintStorage(dAtA, i, uint64(len(m.ServiceName))) i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *FindTracesRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *FindTracesRequest) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *FindTracesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if m.Query != nil { { size, err := m.Query.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintStorage(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *SpansResponseChunk) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *SpansResponseChunk) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *SpansResponseChunk) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.Spans) > 0 { for iNdEx := len(m.Spans) - 1; iNdEx >= 0; iNdEx-- { { size, err := m.Spans[iNdEx].MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintStorage(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } } return len(dAtA) - i, nil } func (m *FindTraceIDsRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *FindTraceIDsRequest) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *FindTraceIDsRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if m.Query != nil { { size, err := m.Query.MarshalToSizedBuffer(dAtA[:i]) if err != nil { return 0, err } i -= size i = encodeVarintStorage(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } return len(dAtA) - i, nil } func (m *FindTraceIDsResponse) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *FindTraceIDsResponse) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *FindTraceIDsResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if len(m.TraceIDs) > 0 { for iNdEx := len(m.TraceIDs) - 1; iNdEx >= 0; iNdEx-- { { size := m.TraceIDs[iNdEx].Size() i -= size if _, err := m.TraceIDs[iNdEx].MarshalTo(dAtA[i:]); err != nil { return 0, err } i = encodeVarintStorage(dAtA, i, uint64(size)) } i-- dAtA[i] = 0xa } } return len(dAtA) - i, nil } func (m *CapabilitiesRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *CapabilitiesRequest) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *CapabilitiesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } return len(dAtA) - i, nil } func (m *CapabilitiesResponse) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } return dAtA[:n], nil } func (m *CapabilitiesResponse) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } func (m *CapabilitiesResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l if m.XXX_unrecognized != nil { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } if m.StreamingSpanWriter { i-- if m.StreamingSpanWriter { dAtA[i] = 1 } else { dAtA[i] = 0 } i-- dAtA[i] = 0x18 } if m.ArchiveSpanWriter { i-- if m.ArchiveSpanWriter { dAtA[i] = 1 } else { dAtA[i] = 0 } i-- dAtA[i] = 0x10 } if m.ArchiveSpanReader { i-- if m.ArchiveSpanReader { dAtA[i] = 1 } else { dAtA[i] = 0 } i-- dAtA[i] = 0x8 } return len(dAtA) - i, nil } func encodeVarintStorage(dAtA []byte, offset int, v uint64) int { offset -= sovStorage(v) base := offset for v >= 1<<7 { dAtA[offset] = uint8(v&0x7f | 0x80) v >>= 7 offset++ } dAtA[offset] = uint8(v) return base } func (m *GetDependenciesRequest) Size() (n int) { if m == nil { return 0 } var l int _ = l l = github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTime) n += 1 + l + sovStorage(uint64(l)) l = github_com_gogo_protobuf_types.SizeOfStdTime(m.EndTime) n += 1 + l + sovStorage(uint64(l)) if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *GetDependenciesResponse) Size() (n int) { if m == nil { return 0 } var l int _ = l if len(m.Dependencies) > 0 { for _, e := range m.Dependencies { l = e.Size() n += 1 + l + sovStorage(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *WriteSpanRequest) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.Span != nil { l = m.Span.Size() n += 1 + l + sovStorage(uint64(l)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *WriteSpanResponse) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *CloseWriterRequest) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *CloseWriterResponse) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *GetTraceRequest) Size() (n int) { if m == nil { return 0 } var l int _ = l l = m.TraceID.Size() n += 1 + l + sovStorage(uint64(l)) l = github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTime) n += 1 + l + sovStorage(uint64(l)) l = github_com_gogo_protobuf_types.SizeOfStdTime(m.EndTime) n += 1 + l + sovStorage(uint64(l)) if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *GetServicesRequest) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *GetServicesResponse) Size() (n int) { if m == nil { return 0 } var l int _ = l if len(m.Services) > 0 { for _, s := range m.Services { l = len(s) n += 1 + l + sovStorage(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *GetOperationsRequest) Size() (n int) { if m == nil { return 0 } var l int _ = l l = len(m.Service) if l > 0 { n += 1 + l + sovStorage(uint64(l)) } l = len(m.SpanKind) if l > 0 { n += 1 + l + sovStorage(uint64(l)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *Operation) Size() (n int) { if m == nil { return 0 } var l int _ = l l = len(m.Name) if l > 0 { n += 1 + l + sovStorage(uint64(l)) } l = len(m.SpanKind) if l > 0 { n += 1 + l + sovStorage(uint64(l)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *GetOperationsResponse) Size() (n int) { if m == nil { return 0 } var l int _ = l if len(m.OperationNames) > 0 { for _, s := range m.OperationNames { l = len(s) n += 1 + l + sovStorage(uint64(l)) } } if len(m.Operations) > 0 { for _, e := range m.Operations { l = e.Size() n += 1 + l + sovStorage(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *TraceQueryParameters) Size() (n int) { if m == nil { return 0 } var l int _ = l l = len(m.ServiceName) if l > 0 { n += 1 + l + sovStorage(uint64(l)) } l = len(m.OperationName) if l > 0 { n += 1 + l + sovStorage(uint64(l)) } if len(m.Tags) > 0 { for k, v := range m.Tags { _ = k _ = v mapEntrySize := 1 + len(k) + sovStorage(uint64(len(k))) + 1 + len(v) + sovStorage(uint64(len(v))) n += mapEntrySize + 1 + sovStorage(uint64(mapEntrySize)) } } l = github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTimeMin) n += 1 + l + sovStorage(uint64(l)) l = github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTimeMax) n += 1 + l + sovStorage(uint64(l)) l = github_com_gogo_protobuf_types.SizeOfStdDuration(m.DurationMin) n += 1 + l + sovStorage(uint64(l)) l = github_com_gogo_protobuf_types.SizeOfStdDuration(m.DurationMax) n += 1 + l + sovStorage(uint64(l)) if m.NumTraces != 0 { n += 1 + sovStorage(uint64(m.NumTraces)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *FindTracesRequest) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.Query != nil { l = m.Query.Size() n += 1 + l + sovStorage(uint64(l)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *SpansResponseChunk) Size() (n int) { if m == nil { return 0 } var l int _ = l if len(m.Spans) > 0 { for _, e := range m.Spans { l = e.Size() n += 1 + l + sovStorage(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *FindTraceIDsRequest) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.Query != nil { l = m.Query.Size() n += 1 + l + sovStorage(uint64(l)) } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *FindTraceIDsResponse) Size() (n int) { if m == nil { return 0 } var l int _ = l if len(m.TraceIDs) > 0 { for _, e := range m.TraceIDs { l = e.Size() n += 1 + l + sovStorage(uint64(l)) } } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *CapabilitiesRequest) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func (m *CapabilitiesResponse) Size() (n int) { if m == nil { return 0 } var l int _ = l if m.ArchiveSpanReader { n += 2 } if m.ArchiveSpanWriter { n += 2 } if m.StreamingSpanWriter { n += 2 } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } return n } func sovStorage(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } func sozStorage(x uint64) (n int) { return sovStorage(uint64((x << 1) ^ uint64((int64(x) >> 63)))) } func (m *GetDependenciesRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GetDependenciesRequest: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GetDependenciesRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field StartTime", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.StartTime, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field EndTime", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.EndTime, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *GetDependenciesResponse) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GetDependenciesResponse: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GetDependenciesResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Dependencies", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.Dependencies = append(m.Dependencies, v1.DependencyLink{}) if err := m.Dependencies[len(m.Dependencies)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *WriteSpanRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: WriteSpanRequest: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: WriteSpanRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Span", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } if m.Span == nil { m.Span = &v1.Span{} } if err := m.Span.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *WriteSpanResponse) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: WriteSpanResponse: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: WriteSpanResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { default: iNdEx = preIndex skippy, err := skipStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *CloseWriterRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: CloseWriterRequest: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: CloseWriterRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { default: iNdEx = preIndex skippy, err := skipStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *CloseWriterResponse) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: CloseWriterResponse: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: CloseWriterResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { default: iNdEx = preIndex skippy, err := skipStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *GetTraceRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GetTraceRequest: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GetTraceRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field TraceID", wireType) } var byteLen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ byteLen |= int(b&0x7F) << shift if b < 0x80 { break } } if byteLen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + byteLen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } if err := m.TraceID.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field StartTime", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.StartTime, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field EndTime", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.EndTime, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *GetServicesRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GetServicesRequest: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GetServicesRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { default: iNdEx = preIndex skippy, err := skipStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *GetServicesResponse) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GetServicesResponse: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GetServicesResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Services", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.Services = append(m.Services, string(dAtA[iNdEx:postIndex])) iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *GetOperationsRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GetOperationsRequest: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GetOperationsRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Service", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.Service = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field SpanKind", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.SpanKind = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *Operation) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: Operation: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: Operation: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.Name = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field SpanKind", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.SpanKind = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *GetOperationsResponse) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: GetOperationsResponse: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: GetOperationsResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field OperationNames", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.OperationNames = append(m.OperationNames, string(dAtA[iNdEx:postIndex])) iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Operations", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.Operations = append(m.Operations, &Operation{}) if err := m.Operations[len(m.Operations)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *TraceQueryParameters) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: TraceQueryParameters: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: TraceQueryParameters: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field ServiceName", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.ServiceName = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field OperationName", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.OperationName = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Tags", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } if m.Tags == nil { m.Tags = make(map[string]string) } var mapkey string var mapvalue string for iNdEx < postIndex { entryPreIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) if fieldNum == 1 { var stringLenmapkey uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLenmapkey |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLenmapkey := int(stringLenmapkey) if intStringLenmapkey < 0 { return ErrInvalidLengthStorage } postStringIndexmapkey := iNdEx + intStringLenmapkey if postStringIndexmapkey < 0 { return ErrInvalidLengthStorage } if postStringIndexmapkey > l { return io.ErrUnexpectedEOF } mapkey = string(dAtA[iNdEx:postStringIndexmapkey]) iNdEx = postStringIndexmapkey } else if fieldNum == 2 { var stringLenmapvalue uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLenmapvalue |= uint64(b&0x7F) << shift if b < 0x80 { break } } intStringLenmapvalue := int(stringLenmapvalue) if intStringLenmapvalue < 0 { return ErrInvalidLengthStorage } postStringIndexmapvalue := iNdEx + intStringLenmapvalue if postStringIndexmapvalue < 0 { return ErrInvalidLengthStorage } if postStringIndexmapvalue > l { return io.ErrUnexpectedEOF } mapvalue = string(dAtA[iNdEx:postStringIndexmapvalue]) iNdEx = postStringIndexmapvalue } else { iNdEx = entryPreIndex skippy, err := skipStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthStorage } if (iNdEx + skippy) > postIndex { return io.ErrUnexpectedEOF } iNdEx += skippy } } m.Tags[mapkey] = mapvalue iNdEx = postIndex case 4: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field StartTimeMin", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.StartTimeMin, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 5: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field StartTimeMax", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.StartTimeMax, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 6: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field DurationMin", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdDurationUnmarshal(&m.DurationMin, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 7: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field DurationMax", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdDurationUnmarshal(&m.DurationMax, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 8: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field NumTraces", wireType) } m.NumTraces = 0 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ m.NumTraces |= int32(b&0x7F) << shift if b < 0x80 { break } } default: iNdEx = preIndex skippy, err := skipStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *FindTracesRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: FindTracesRequest: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: FindTracesRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Query", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } if m.Query == nil { m.Query = &TraceQueryParameters{} } if err := m.Query.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *SpansResponseChunk) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: SpansResponseChunk: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: SpansResponseChunk: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Spans", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } m.Spans = append(m.Spans, v1.Span{}) if err := m.Spans[len(m.Spans)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *FindTraceIDsRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: FindTraceIDsRequest: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: FindTraceIDsRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Query", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= int(b&0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } if m.Query == nil { m.Query = &TraceQueryParameters{} } if err := m.Query.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *FindTraceIDsResponse) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: FindTraceIDsResponse: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: FindTraceIDsResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field TraceIDs", wireType) } var byteLen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ byteLen |= int(b&0x7F) << shift if b < 0x80 { break } } if byteLen < 0 { return ErrInvalidLengthStorage } postIndex := iNdEx + byteLen if postIndex < 0 { return ErrInvalidLengthStorage } if postIndex > l { return io.ErrUnexpectedEOF } var v github_com_jaegertracing_jaeger_idl_model_v1.TraceID m.TraceIDs = append(m.TraceIDs, v) if err := m.TraceIDs[len(m.TraceIDs)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *CapabilitiesRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: CapabilitiesRequest: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: CapabilitiesRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { default: iNdEx = preIndex skippy, err := skipStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *CapabilitiesResponse) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= uint64(b&0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: CapabilitiesResponse: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: CapabilitiesResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field ArchiveSpanReader", wireType) } var v int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= int(b&0x7F) << shift if b < 0x80 { break } } m.ArchiveSpanReader = bool(v != 0) case 2: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field ArchiveSpanWriter", wireType) } var v int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= int(b&0x7F) << shift if b < 0x80 { break } } m.ArchiveSpanWriter = bool(v != 0) case 3: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field StreamingSpanWriter", wireType) } var v int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowStorage } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= int(b&0x7F) << shift if b < 0x80 { break } } m.StreamingSpanWriter = bool(v != 0) default: iNdEx = preIndex skippy, err := skipStorage(dAtA[iNdEx:]) if err != nil { return err } if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthStorage } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func skipStorage(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 depth := 0 for iNdEx < l { var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowStorage } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } wireType := int(wire & 0x7) switch wireType { case 0: for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowStorage } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } iNdEx++ if dAtA[iNdEx-1] < 0x80 { break } } case 1: iNdEx += 8 case 2: var length int for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowStorage } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ length |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if length < 0 { return 0, ErrInvalidLengthStorage } iNdEx += length case 3: depth++ case 4: if depth == 0 { return 0, ErrUnexpectedEndOfGroupStorage } depth-- case 5: iNdEx += 4 default: return 0, fmt.Errorf("proto: illegal wireType %d", wireType) } if iNdEx < 0 { return 0, ErrInvalidLengthStorage } if depth == 0 { return iNdEx, nil } } return 0, io.ErrUnexpectedEOF } var ( ErrInvalidLengthStorage = fmt.Errorf("proto: negative length found during unmarshaling") ErrIntOverflowStorage = fmt.Errorf("proto: integer overflow") ErrUnexpectedEndOfGroupStorage = fmt.Errorf("proto: unexpected end of group") ) ================================================ FILE: internal/proto-gen/zipkin/zipkin.pb.go ================================================ // Code generated by protoc-gen-gogo. DO NOT EDIT. // source: zipkin.proto package zipkin_proto3 import ( context "context" fmt "fmt" proto "github.com/gogo/protobuf/proto" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" math "math" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package // When present, kind clarifies timestamp, duration and remote_endpoint. When // absent, the span is local or incomplete. Unlike client and server, there // is no direct critical path latency relationship between producer and // consumer spans. type Span_Kind int32 const ( // Default value interpreted as absent. Span_SPAN_KIND_UNSPECIFIED Span_Kind = 0 // The span represents the client side of an RPC operation, implying the // following: // // timestamp is the moment a request was sent to the server. // duration is the delay until a response or an error was received. // remote_endpoint is the server. Span_CLIENT Span_Kind = 1 // The span represents the server side of an RPC operation, implying the // following: // // timestamp is the moment a client request was received. // duration is the delay until a response was sent or an error. // remote_endpoint is the client. Span_SERVER Span_Kind = 2 // The span represents production of a message to a remote broker, implying // the following: // // timestamp is the moment a message was sent to a destination. // duration is the delay sending the message, such as batching. // remote_endpoint is the broker. Span_PRODUCER Span_Kind = 3 // The span represents consumption of a message from a remote broker, not // time spent servicing it. For example, a message processor would be an // in-process child span of a consumer. Consumer spans imply the following: // // timestamp is the moment a message was received from an origin. // duration is the delay consuming the message, such as from backlog. // remote_endpoint is the broker. Span_CONSUMER Span_Kind = 4 ) var Span_Kind_name = map[int32]string{ 0: "SPAN_KIND_UNSPECIFIED", 1: "CLIENT", 2: "SERVER", 3: "PRODUCER", 4: "CONSUMER", } var Span_Kind_value = map[string]int32{ "SPAN_KIND_UNSPECIFIED": 0, "CLIENT": 1, "SERVER": 2, "PRODUCER": 3, "CONSUMER": 4, } func (x Span_Kind) String() string { return proto.EnumName(Span_Kind_name, int32(x)) } func (Span_Kind) EnumDescriptor() ([]byte, []int) { return fileDescriptor_ab863b5fa670a281, []int{0, 0} } // A span is a single-host view of an operation. A trace is a series of spans // (often RPC calls) which nest to form a latency tree. Spans are in the same // trace when they share the same trace ID. The parent_id field establishes the // position of one span in the tree. // // The root span is where parent_id is Absent and usually has the longest // duration in the trace. However, nested asynchronous work can materialize as // child spans whose duration exceed the root span. // // Spans usually represent remote activity such as RPC calls, or messaging // producers and consumers. However, they can also represent in-process // activity in any position of the trace. For example, a root span could // represent a server receiving an initial client request. A root span could // also represent a scheduled job that has no remote context. // // Encoding notes: // // Epoch timestamp are encoded fixed64 as varint would also be 8 bytes, and more // expensive to encode and size. Duration is stored uint64, as often the numbers // are quite small. // // Default values are ok, as only natural numbers are used. For example, zero is // an invalid timestamp and an invalid duration, false values for debug or shared // are ignorable, and zero-length strings also coerce to null. // // The next id is 14. // // Note fields up to 15 take 1 byte to encode. Take care when adding new fields // https://developers.google.com/protocol-buffers/docs/proto3#assigning-tags type Span struct { // Randomly generated, unique identifier for a trace, set on all spans within // it. // // This field is required and encoded as 8 or 16 bytes, in big endian byte // order. TraceId []byte `protobuf:"bytes,1,opt,name=trace_id,json=traceId,proto3" json:"trace_id,omitempty"` // The parent span ID or absent if this the root span in a trace. ParentId []byte `protobuf:"bytes,2,opt,name=parent_id,json=parentId,proto3" json:"parent_id,omitempty"` // Unique identifier for this operation within the trace. // // This field is required and encoded as 8 opaque bytes. Id []byte `protobuf:"bytes,3,opt,name=id,proto3" json:"id,omitempty"` // When present, used to interpret remote_endpoint Kind Span_Kind `protobuf:"varint,4,opt,name=kind,proto3,enum=zipkin.proto3.Span_Kind" json:"kind,omitempty"` // The logical operation this span represents in lowercase (e.g. rpc method). // Leave absent if unknown. // // As these are lookup labels, take care to ensure names are low cardinality. // For example, do not embed variables into the name. Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"` // Epoch microseconds of the start of this span, possibly absent if // incomplete. // // For example, 1502787600000000 corresponds to 2017-08-15 09:00 UTC // // This value should be set directly by instrumentation, using the most // precise value possible. For example, gettimeofday or multiplying epoch // millis by 1000. // // There are three known edge-cases where this could be reported absent. // - A span was allocated but never started (ex not yet received a timestamp) // - The span's start event was lost // - Data about a completed span (ex tags) were sent after the fact Timestamp uint64 `protobuf:"fixed64,6,opt,name=timestamp,proto3" json:"timestamp,omitempty"` // Duration in microseconds of the critical path, if known. Durations of less // than one are rounded up. Duration of children can be longer than their // parents due to asynchronous operations. // // For example 150 milliseconds is 150000 microseconds. Duration uint64 `protobuf:"varint,7,opt,name=duration,proto3" json:"duration,omitempty"` // The host that recorded this span, primarily for query by service name. // // Instrumentation should always record this. Usually, absent implies late // data. The IP address corresponding to this is usually the site local or // advertised service address. When present, the port indicates the listen // port. LocalEndpoint *Endpoint `protobuf:"bytes,8,opt,name=local_endpoint,json=localEndpoint,proto3" json:"local_endpoint,omitempty"` // When an RPC (or messaging) span, indicates the other side of the // connection. // // By recording the remote endpoint, your trace will contain network context // even if the peer is not tracing. For example, you can record the IP from // the "X-Forwarded-For" header or the service name and socket of a remote // peer. RemoteEndpoint *Endpoint `protobuf:"bytes,9,opt,name=remote_endpoint,json=remoteEndpoint,proto3" json:"remote_endpoint,omitempty"` // Associates events that explain latency with the time they happened. Annotations []*Annotation `protobuf:"bytes,10,rep,name=annotations,proto3" json:"annotations,omitempty"` // Tags give your span context for search, viewing and analysis. // // For example, a key "your_app.version" would let you lookup traces by // version. A tag "sql.query" isn't searchable, but it can help in debugging // when viewing a trace. Tags map[string]string `protobuf:"bytes,11,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // True is a request to store this span even if it overrides sampling policy. // // This is true when the "X-B3-Flags" header has a value of 1. Debug bool `protobuf:"varint,12,opt,name=debug,proto3" json:"debug,omitempty"` // True if we are contributing to a span started by another tracer (ex on a // different host). Shared bool `protobuf:"varint,13,opt,name=shared,proto3" json:"shared,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *Span) Reset() { *m = Span{} } func (m *Span) String() string { return proto.CompactTextString(m) } func (*Span) ProtoMessage() {} func (*Span) Descriptor() ([]byte, []int) { return fileDescriptor_ab863b5fa670a281, []int{0} } func (m *Span) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Span.Unmarshal(m, b) } func (m *Span) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_Span.Marshal(b, m, deterministic) } func (m *Span) XXX_Merge(src proto.Message) { xxx_messageInfo_Span.Merge(m, src) } func (m *Span) XXX_Size() int { return xxx_messageInfo_Span.Size(m) } func (m *Span) XXX_DiscardUnknown() { xxx_messageInfo_Span.DiscardUnknown(m) } var xxx_messageInfo_Span proto.InternalMessageInfo func (m *Span) GetTraceId() []byte { if m != nil { return m.TraceId } return nil } func (m *Span) GetParentId() []byte { if m != nil { return m.ParentId } return nil } func (m *Span) GetId() []byte { if m != nil { return m.Id } return nil } func (m *Span) GetKind() Span_Kind { if m != nil { return m.Kind } return Span_SPAN_KIND_UNSPECIFIED } func (m *Span) GetName() string { if m != nil { return m.Name } return "" } func (m *Span) GetTimestamp() uint64 { if m != nil { return m.Timestamp } return 0 } func (m *Span) GetDuration() uint64 { if m != nil { return m.Duration } return 0 } func (m *Span) GetLocalEndpoint() *Endpoint { if m != nil { return m.LocalEndpoint } return nil } func (m *Span) GetRemoteEndpoint() *Endpoint { if m != nil { return m.RemoteEndpoint } return nil } func (m *Span) GetAnnotations() []*Annotation { if m != nil { return m.Annotations } return nil } func (m *Span) GetTags() map[string]string { if m != nil { return m.Tags } return nil } func (m *Span) GetDebug() bool { if m != nil { return m.Debug } return false } func (m *Span) GetShared() bool { if m != nil { return m.Shared } return false } // The network context of a node in the service graph. // // The next id is 5. type Endpoint struct { // Lower-case label of this node in the service graph, such as "favstar". // Leave absent if unknown. // // This is a primary label for trace lookup and aggregation, so it should be // intuitive and consistent. Many use a name from service discovery. ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` // 4 byte representation of the primary IPv4 address associated with this // connection. Absent if unknown. Ipv4 []byte `protobuf:"bytes,2,opt,name=ipv4,proto3" json:"ipv4,omitempty"` // 16 byte representation of the primary IPv6 address associated with this // connection. Absent if unknown. // // Prefer using the ipv4 field for mapped addresses. Ipv6 []byte `protobuf:"bytes,3,opt,name=ipv6,proto3" json:"ipv6,omitempty"` // Depending on context, this could be a listen port or the client-side of a // socket. Absent if unknown. Port int32 `protobuf:"varint,4,opt,name=port,proto3" json:"port,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *Endpoint) Reset() { *m = Endpoint{} } func (m *Endpoint) String() string { return proto.CompactTextString(m) } func (*Endpoint) ProtoMessage() {} func (*Endpoint) Descriptor() ([]byte, []int) { return fileDescriptor_ab863b5fa670a281, []int{1} } func (m *Endpoint) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Endpoint.Unmarshal(m, b) } func (m *Endpoint) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_Endpoint.Marshal(b, m, deterministic) } func (m *Endpoint) XXX_Merge(src proto.Message) { xxx_messageInfo_Endpoint.Merge(m, src) } func (m *Endpoint) XXX_Size() int { return xxx_messageInfo_Endpoint.Size(m) } func (m *Endpoint) XXX_DiscardUnknown() { xxx_messageInfo_Endpoint.DiscardUnknown(m) } var xxx_messageInfo_Endpoint proto.InternalMessageInfo func (m *Endpoint) GetServiceName() string { if m != nil { return m.ServiceName } return "" } func (m *Endpoint) GetIpv4() []byte { if m != nil { return m.Ipv4 } return nil } func (m *Endpoint) GetIpv6() []byte { if m != nil { return m.Ipv6 } return nil } func (m *Endpoint) GetPort() int32 { if m != nil { return m.Port } return 0 } // Associates an event that explains latency with a timestamp. // Unlike log statements, annotations are often codes. Ex. "ws" for WireSend // // The next id is 3. type Annotation struct { // Epoch microseconds of this event. // // For example, 1502787600000000 corresponds to 2017-08-15 09:00 UTC // // This value should be set directly by instrumentation, using the most // precise value possible. For example, gettimeofday or multiplying epoch // millis by 1000. Timestamp uint64 `protobuf:"fixed64,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` // Usually a short tag indicating an event, like "error" // // While possible to add larger data, such as garbage collection details, low // cardinality event names both keep the size of spans down and also are easy // to search against. Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *Annotation) Reset() { *m = Annotation{} } func (m *Annotation) String() string { return proto.CompactTextString(m) } func (*Annotation) ProtoMessage() {} func (*Annotation) Descriptor() ([]byte, []int) { return fileDescriptor_ab863b5fa670a281, []int{2} } func (m *Annotation) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Annotation.Unmarshal(m, b) } func (m *Annotation) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_Annotation.Marshal(b, m, deterministic) } func (m *Annotation) XXX_Merge(src proto.Message) { xxx_messageInfo_Annotation.Merge(m, src) } func (m *Annotation) XXX_Size() int { return xxx_messageInfo_Annotation.Size(m) } func (m *Annotation) XXX_DiscardUnknown() { xxx_messageInfo_Annotation.DiscardUnknown(m) } var xxx_messageInfo_Annotation proto.InternalMessageInfo func (m *Annotation) GetTimestamp() uint64 { if m != nil { return m.Timestamp } return 0 } func (m *Annotation) GetValue() string { if m != nil { return m.Value } return "" } // A list of spans with possibly different trace ids, in no particular order. // // This is used for all transports: POST, Kafka messages etc. No other fields // are expected, This message facilitates the mechanics of encoding a list, as // a field number is required. The name of this type is the same in the OpenApi // aka Swagger specification. https://zipkin.io/zipkin-api/#/default/post_spans type ListOfSpans struct { Spans []*Span `protobuf:"bytes,1,rep,name=spans,proto3" json:"spans,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *ListOfSpans) Reset() { *m = ListOfSpans{} } func (m *ListOfSpans) String() string { return proto.CompactTextString(m) } func (*ListOfSpans) ProtoMessage() {} func (*ListOfSpans) Descriptor() ([]byte, []int) { return fileDescriptor_ab863b5fa670a281, []int{3} } func (m *ListOfSpans) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ListOfSpans.Unmarshal(m, b) } func (m *ListOfSpans) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_ListOfSpans.Marshal(b, m, deterministic) } func (m *ListOfSpans) XXX_Merge(src proto.Message) { xxx_messageInfo_ListOfSpans.Merge(m, src) } func (m *ListOfSpans) XXX_Size() int { return xxx_messageInfo_ListOfSpans.Size(m) } func (m *ListOfSpans) XXX_DiscardUnknown() { xxx_messageInfo_ListOfSpans.DiscardUnknown(m) } var xxx_messageInfo_ListOfSpans proto.InternalMessageInfo func (m *ListOfSpans) GetSpans() []*Span { if m != nil { return m.Spans } return nil } // Response for SpanService/Report RPC. This response currently does not return // any information beyond indicating that the request has finished. That said, // it may be extended in the future. type ReportResponse struct { XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *ReportResponse) Reset() { *m = ReportResponse{} } func (m *ReportResponse) String() string { return proto.CompactTextString(m) } func (*ReportResponse) ProtoMessage() {} func (*ReportResponse) Descriptor() ([]byte, []int) { return fileDescriptor_ab863b5fa670a281, []int{4} } func (m *ReportResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ReportResponse.Unmarshal(m, b) } func (m *ReportResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_ReportResponse.Marshal(b, m, deterministic) } func (m *ReportResponse) XXX_Merge(src proto.Message) { xxx_messageInfo_ReportResponse.Merge(m, src) } func (m *ReportResponse) XXX_Size() int { return xxx_messageInfo_ReportResponse.Size(m) } func (m *ReportResponse) XXX_DiscardUnknown() { xxx_messageInfo_ReportResponse.DiscardUnknown(m) } var xxx_messageInfo_ReportResponse proto.InternalMessageInfo func init() { proto.RegisterEnum("zipkin.proto3.Span_Kind", Span_Kind_name, Span_Kind_value) proto.RegisterType((*Span)(nil), "zipkin.proto3.Span") proto.RegisterMapType((map[string]string)(nil), "zipkin.proto3.Span.TagsEntry") proto.RegisterType((*Endpoint)(nil), "zipkin.proto3.Endpoint") proto.RegisterType((*Annotation)(nil), "zipkin.proto3.Annotation") proto.RegisterType((*ListOfSpans)(nil), "zipkin.proto3.ListOfSpans") proto.RegisterType((*ReportResponse)(nil), "zipkin.proto3.ReportResponse") } func init() { proto.RegisterFile("zipkin.proto", fileDescriptor_ab863b5fa670a281) } var fileDescriptor_ab863b5fa670a281 = []byte{ // 563 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x52, 0xdf, 0x6f, 0xd3, 0x3c, 0x14, 0x9d, 0xdb, 0x34, 0x4b, 0x6e, 0xba, 0x7c, 0x91, 0x3f, 0x7e, 0x78, 0x05, 0xa4, 0x90, 0xa7, 0x20, 0xa1, 0x4a, 0x14, 0x04, 0x13, 0x48, 0x68, 0xa3, 0x0b, 0x52, 0xb4, 0x91, 0x55, 0xce, 0xca, 0x6b, 0xe5, 0x2d, 0x66, 0x58, 0x5b, 0x9d, 0x28, 0xf1, 0x26, 0x8d, 0x3f, 0x9d, 0x27, 0x64, 0x27, 0x74, 0x5b, 0x55, 0xf1, 0x76, 0xce, 0xf1, 0xc9, 0xb1, 0x73, 0xef, 0x81, 0xe1, 0x2f, 0x51, 0x5d, 0x0a, 0x39, 0xae, 0xea, 0x52, 0x95, 0x78, 0xe7, 0x3e, 0x7b, 0x1b, 0xfd, 0xb6, 0xc0, 0xca, 0x2b, 0x26, 0xf1, 0x2e, 0x38, 0xaa, 0x66, 0xe7, 0x7c, 0x21, 0x0a, 0x82, 0x42, 0x14, 0x0f, 0xe9, 0xb6, 0xe1, 0x69, 0x81, 0x9f, 0x81, 0x5b, 0xb1, 0x9a, 0x4b, 0xa5, 0xcf, 0x7a, 0xe6, 0xcc, 0x69, 0x85, 0xb4, 0xc0, 0x3e, 0xf4, 0x44, 0x41, 0xfa, 0x46, 0xed, 0x89, 0x02, 0xbf, 0x06, 0xeb, 0x52, 0xc8, 0x82, 0x58, 0x21, 0x8a, 0xfd, 0x09, 0x19, 0x3f, 0xb8, 0x6e, 0xac, 0xaf, 0x1a, 0x1f, 0x09, 0x59, 0x50, 0xe3, 0xc2, 0x18, 0x2c, 0xc9, 0x96, 0x9c, 0x0c, 0x42, 0x14, 0xbb, 0xd4, 0x60, 0xfc, 0x1c, 0x5c, 0x25, 0x96, 0xbc, 0x51, 0x6c, 0x59, 0x11, 0x3b, 0x44, 0xb1, 0x4d, 0xef, 0x04, 0x3c, 0x02, 0xa7, 0xb8, 0xae, 0x99, 0x12, 0xa5, 0x24, 0xdb, 0x21, 0x8a, 0x2d, 0xba, 0xe2, 0xf8, 0x33, 0xf8, 0x57, 0xe5, 0x39, 0xbb, 0x5a, 0x70, 0x59, 0x54, 0xa5, 0x90, 0x8a, 0x38, 0x21, 0x8a, 0xbd, 0xc9, 0xd3, 0xb5, 0x57, 0x24, 0xdd, 0x31, 0xdd, 0x31, 0xf6, 0xbf, 0x14, 0xef, 0xc3, 0x7f, 0x35, 0x5f, 0x96, 0x8a, 0xdf, 0x05, 0xb8, 0xff, 0x0e, 0xf0, 0x5b, 0xff, 0x2a, 0xe1, 0x13, 0x78, 0x4c, 0xca, 0x52, 0x99, 0xf7, 0x34, 0x04, 0xc2, 0x7e, 0xec, 0x4d, 0x76, 0xd7, 0xbe, 0x3e, 0x58, 0x39, 0xe8, 0x7d, 0x37, 0x7e, 0x03, 0x96, 0x62, 0x17, 0x0d, 0xf1, 0xcc, 0x57, 0x2f, 0x36, 0x8d, 0xee, 0x94, 0x5d, 0x34, 0x89, 0x54, 0xf5, 0x2d, 0x35, 0x56, 0xfc, 0x08, 0x06, 0x05, 0x3f, 0xbb, 0xbe, 0x20, 0xc3, 0x10, 0xc5, 0x0e, 0x6d, 0x09, 0x7e, 0x02, 0x76, 0xf3, 0x93, 0xd5, 0xbc, 0x20, 0x3b, 0x46, 0xee, 0xd8, 0xe8, 0x03, 0xb8, 0xab, 0x00, 0x1c, 0x40, 0xff, 0x92, 0xdf, 0x9a, 0x5d, 0xbb, 0x54, 0x43, 0x1d, 0x76, 0xc3, 0xae, 0xae, 0xb9, 0xd9, 0xb1, 0x4b, 0x5b, 0xf2, 0xb1, 0xb7, 0x87, 0xa2, 0x39, 0x58, 0x7a, 0x69, 0x78, 0x17, 0x1e, 0xe7, 0xb3, 0x83, 0x6c, 0x71, 0x94, 0x66, 0x87, 0x8b, 0x79, 0x96, 0xcf, 0x92, 0x69, 0xfa, 0x35, 0x4d, 0x0e, 0x83, 0x2d, 0x0c, 0x60, 0x4f, 0x8f, 0xd3, 0x24, 0x3b, 0x0d, 0x90, 0xc6, 0x79, 0x42, 0xbf, 0x27, 0x34, 0xe8, 0xe1, 0x21, 0x38, 0x33, 0x7a, 0x72, 0x38, 0x9f, 0x26, 0x34, 0xe8, 0x6b, 0x36, 0x3d, 0xc9, 0xf2, 0xf9, 0xb7, 0x84, 0x06, 0x56, 0x24, 0xc0, 0x59, 0x4d, 0xee, 0x25, 0x0c, 0x1b, 0x5e, 0xdf, 0x88, 0x73, 0xbe, 0x30, 0x8d, 0x68, 0xdf, 0xe5, 0x75, 0x5a, 0xa6, 0x8b, 0x81, 0xc1, 0x12, 0xd5, 0xcd, 0xbb, 0xae, 0x82, 0x06, 0x77, 0xda, 0xfb, 0xae, 0x80, 0x06, 0x6b, 0xad, 0x2a, 0x6b, 0x65, 0x2a, 0x38, 0xa0, 0x06, 0x47, 0xfb, 0x00, 0x77, 0x63, 0x7f, 0x58, 0x31, 0xb4, 0x5e, 0xb1, 0x8d, 0x73, 0x88, 0xf6, 0xc0, 0x3b, 0x16, 0x8d, 0x3a, 0xf9, 0xa1, 0x17, 0xd1, 0xe0, 0x57, 0x30, 0x68, 0x34, 0x20, 0xc8, 0x6c, 0xeb, 0xff, 0x0d, 0xdb, 0xa2, 0xad, 0x23, 0x0a, 0xc0, 0xa7, 0x5c, 0xbf, 0x82, 0xf2, 0xa6, 0x2a, 0x65, 0xc3, 0x27, 0xa7, 0xe0, 0x69, 0x43, 0xde, 0xfe, 0x1c, 0x4e, 0xc0, 0x6e, 0x0d, 0x78, 0xb4, 0x16, 0x73, 0xef, 0xc6, 0xd1, 0x7a, 0x21, 0x1e, 0x66, 0x46, 0x5b, 0x5f, 0x30, 0xf8, 0xad, 0x63, 0xd2, 0x59, 0x66, 0xe8, 0xcc, 0x6e, 0xd1, 0x9f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xfc, 0x97, 0x2e, 0x7e, 0x05, 0x04, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. var _ context.Context var _ grpc.ClientConn // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. const _ = grpc.SupportPackageIsVersion4 // SpanServiceClient is the client API for SpanService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type SpanServiceClient interface { // Report the provided spans to the collector. Analogous to the HTTP POST // /api/v2/spans endpoint. Spans are not required to be complete or belonging // to the same trace. Report(ctx context.Context, in *ListOfSpans, opts ...grpc.CallOption) (*ReportResponse, error) } type spanServiceClient struct { cc *grpc.ClientConn } func NewSpanServiceClient(cc *grpc.ClientConn) SpanServiceClient { return &spanServiceClient{cc} } func (c *spanServiceClient) Report(ctx context.Context, in *ListOfSpans, opts ...grpc.CallOption) (*ReportResponse, error) { out := new(ReportResponse) err := c.cc.Invoke(ctx, "/zipkin.proto3.SpanService/Report", in, out, opts...) if err != nil { return nil, err } return out, nil } // SpanServiceServer is the server API for SpanService service. type SpanServiceServer interface { // Report the provided spans to the collector. Analogous to the HTTP POST // /api/v2/spans endpoint. Spans are not required to be complete or belonging // to the same trace. Report(context.Context, *ListOfSpans) (*ReportResponse, error) } // UnimplementedSpanServiceServer can be embedded to have forward compatible implementations. type UnimplementedSpanServiceServer struct { } func (*UnimplementedSpanServiceServer) Report(ctx context.Context, req *ListOfSpans) (*ReportResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Report not implemented") } func RegisterSpanServiceServer(s *grpc.Server, srv SpanServiceServer) { s.RegisterService(&_SpanService_serviceDesc, srv) } func _SpanService_Report_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListOfSpans) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(SpanServiceServer).Report(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/zipkin.proto3.SpanService/Report", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(SpanServiceServer).Report(ctx, req.(*ListOfSpans)) } return interceptor(ctx, in, info, handler) } var _SpanService_serviceDesc = grpc.ServiceDesc{ ServiceName: "zipkin.proto3.SpanService", HandlerType: (*SpanServiceServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "Report", Handler: _SpanService_Report_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "zipkin.proto", } ================================================ FILE: internal/recoveryhandler/zap.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017-2018 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package recoveryhandler import ( "fmt" "net/http" "github.com/gorilla/handlers" "go.uber.org/zap" ) // zapRecoveryWrapper wraps a zap logger into a gorilla RecoveryLogger type zapRecoveryWrapper struct { logger *zap.Logger } // Println logs an error message with the given fields func (z zapRecoveryWrapper) Println(args ...any) { z.logger.Error(fmt.Sprint(args...)) } // NewRecoveryHandler returns an http.Handler that recovers on panics func NewRecoveryHandler(logger *zap.Logger, printStack bool) func(h http.Handler) http.Handler { zWrapper := zapRecoveryWrapper{logger} return handlers.RecoveryHandler(handlers.RecoveryLogger(zWrapper), handlers.PrintRecoveryStack(printStack)) } ================================================ FILE: internal/recoveryhandler/zap_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package recoveryhandler import ( "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestNewRecoveryHandler(t *testing.T) { logger, log := testutils.NewLogger() handlerFunc := http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { panic("Unexpected error!") }) recovery := NewRecoveryHandler(logger, false)(handlerFunc) req, err := http.NewRequest(http.MethodGet, "/subdir/asdf", http.NoBody) require.NoError(t, err) res := httptest.NewRecorder() recovery.ServeHTTP(res, req) assert.Equal(t, http.StatusInternalServerError, res.Code) assert.Equal(t, map[string]string{ "level": "error", "msg": "Unexpected error!", }, log.JSONLine(0)) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/safeexpvar/safeexpvar.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // 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. package safeexpvar import ( "expvar" ) func SetInt(name string, value int64) { v := expvar.Get(name) if v == nil { v = expvar.NewInt(name) } v.(*expvar.Int).Set(value) } ================================================ FILE: internal/safeexpvar/safeexpvar_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package safeexpvar import ( "expvar" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestSetInt(t *testing.T) { // Test with a new variable name := "metrics-test-1" value := int64(42) SetInt(name, value) // Retrieve the variable and check its value v := expvar.Get(name) assert.NotNil(t, v, "expected variable %s to be created", name) expInt, ok := v.(*expvar.Int) require.True(t, ok, "expected variable %s to be of type *expvar.Int", name) assert.Equal(t, value, expInt.Value()) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/sampling/grpc/grpc_handler.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package grpc import ( "context" "github.com/jaegertracing/jaeger-idl/proto-gen/api_v2" "github.com/jaegertracing/jaeger/internal/sampling/samplingstrategy" ) // Handler is sampling strategy handler for gRPC. type Handler struct { samplingProvider samplingstrategy.Provider } // NewHandler creates a handler that controls sampling strategies for services. func NewHandler(provider samplingstrategy.Provider) Handler { return Handler{ samplingProvider: provider, } } // GetSamplingStrategy returns sampling decision from store. func (s Handler) GetSamplingStrategy(ctx context.Context, param *api_v2.SamplingStrategyParameters) (*api_v2.SamplingStrategyResponse, error) { return s.samplingProvider.GetSamplingStrategy(ctx, param.GetServiceName()) } ================================================ FILE: internal/sampling/grpc/grpc_handler_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package grpc import ( "context" "errors" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger-idl/proto-gen/api_v2" "github.com/jaegertracing/jaeger/internal/testutils" ) type mockSamplingStore struct{} func (mockSamplingStore) GetSamplingStrategy(_ context.Context, serviceName string) (*api_v2.SamplingStrategyResponse, error) { switch serviceName { case "error": return nil, errors.New("some error") case "nil": return nil, nil default: return &api_v2.SamplingStrategyResponse{StrategyType: api_v2.SamplingStrategyType_PROBABILISTIC}, nil } } func (mockSamplingStore) Close() error { return nil } func TestNewGRPCHandler(t *testing.T) { tests := []struct { req *api_v2.SamplingStrategyParameters resp *api_v2.SamplingStrategyResponse err string }{ {req: &api_v2.SamplingStrategyParameters{ServiceName: "error"}, err: "some error"}, {req: &api_v2.SamplingStrategyParameters{ServiceName: "nil"}, resp: nil}, {req: &api_v2.SamplingStrategyParameters{ServiceName: "foo"}, resp: &api_v2.SamplingStrategyResponse{StrategyType: api_v2.SamplingStrategyType_PROBABILISTIC}}, } h := NewHandler(mockSamplingStore{}) for _, test := range tests { resp, err := h.GetSamplingStrategy(context.Background(), test.req) if test.err != "" { require.EqualError(t, err, test.err) require.Nil(t, resp) } else { require.NoError(t, err) assert.Equal(t, test.resp, resp) } } } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/sampling/http/cfgmgr.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package http import ( "context" "github.com/jaegertracing/jaeger-idl/proto-gen/api_v2" "github.com/jaegertracing/jaeger/internal/sampling/samplingstrategy" ) // ConfigManager implements ClientConfigManager. type ConfigManager struct { SamplingProvider samplingstrategy.Provider } // GetSamplingStrategy implements ClientConfigManager.GetSamplingStrategy. func (c *ConfigManager) GetSamplingStrategy(ctx context.Context, serviceName string) (*api_v2.SamplingStrategyResponse, error) { return c.SamplingProvider.GetSamplingStrategy(ctx, serviceName) } ================================================ FILE: internal/sampling/http/cfgmgr_test.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package http import ( "context" "errors" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger-idl/proto-gen/api_v2" ) type mockSamplingProvider struct { samplingResponse *api_v2.SamplingStrategyResponse } func (m *mockSamplingProvider) GetSamplingStrategy(context.Context, string /* serviceName */) (*api_v2.SamplingStrategyResponse, error) { if m.samplingResponse == nil { return nil, errors.New("no mock response provided") } return m.samplingResponse, nil } func (*mockSamplingProvider) Close() error { return nil } func TestConfigManager(t *testing.T) { mgr := &ConfigManager{ SamplingProvider: &mockSamplingProvider{ samplingResponse: &api_v2.SamplingStrategyResponse{}, }, } t.Run("GetSamplingStrategy", func(t *testing.T) { r, err := mgr.GetSamplingStrategy(context.Background(), "foo") require.NoError(t, err) assert.Equal(t, api_v2.SamplingStrategyResponse{}, *r) }) } ================================================ FILE: internal/sampling/http/handler.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package http import ( "context" "errors" "fmt" "net/http" "github.com/jaegertracing/jaeger-idl/proto-gen/api_v2" "github.com/jaegertracing/jaeger/internal/metrics" p2json "github.com/jaegertracing/jaeger/internal/uimodel/converter/v1/json" ) const mimeTypeApplicationJSON = "application/json" var errBadRequest = errors.New("bad request") type ClientConfigManager interface { GetSamplingStrategy(ctx context.Context, serviceName string) (*api_v2.SamplingStrategyResponse, error) } // HandlerParams contains parameters that must be passed to NewHTTPHandler. type HandlerParams struct { ConfigManager ClientConfigManager // required MetricsFactory metrics.Factory // required } // Handler implements endpoints for used by Jaeger clients to retrieve client configuration, // such as sampling strategies. type Handler struct { params HandlerParams metrics struct { // Number of good sampling requests SamplingRequestSuccess metrics.Counter `metric:"http-server.requests" tags:"type=sampling"` // Number of bad requests (400s) BadRequest metrics.Counter `metric:"http-server.errors" tags:"status=4xx,source=all"` // Number of collector proxy failures CollectorProxyFailures metrics.Counter `metric:"http-server.errors" tags:"status=5xx,source=collector-proxy"` // Number of bad responses due to proto conversion BadProtoFailures metrics.Counter `metric:"http-server.errors" tags:"status=5xx,source=proto"` // Number of failed response writes from http server WriteFailures metrics.Counter `metric:"http-server.errors" tags:"status=5xx,source=write"` } } // NewHandler creates new HTTPHandler. func NewHandler(params HandlerParams) *Handler { handler := &Handler{params: params} metrics.MustInit(&handler.metrics, params.MetricsFactory, nil) return handler } // RegisterRoutes registers configuration handlers with HTTP Router. func (h *Handler) RegisterRoutes(router *http.ServeMux) { router.HandleFunc( "/", func(w http.ResponseWriter, r *http.Request) { h.serveSamplingHTTP(w, r, h.encodeProto) }, ) } func (h *Handler) serviceFromRequest(w http.ResponseWriter, r *http.Request) (string, error) { services := r.URL.Query()["service"] if len(services) != 1 { h.metrics.BadRequest.Inc(1) http.Error(w, "'service' parameter must be provided once", http.StatusBadRequest) return "", errBadRequest } return services[0], nil } func (h *Handler) writeJSON(w http.ResponseWriter, jsonData []byte) error { w.Header().Add("Content-Type", mimeTypeApplicationJSON) if _, err := w.Write(jsonData); err != nil { h.metrics.WriteFailures.Inc(1) return err } return nil } func (h *Handler) serveSamplingHTTP( w http.ResponseWriter, r *http.Request, encoder func(strategy *api_v2.SamplingStrategyResponse) ([]byte, error), ) { service, err := h.serviceFromRequest(w, r) if err != nil { return } resp, err := h.params.ConfigManager.GetSamplingStrategy(r.Context(), service) if err != nil { h.metrics.CollectorProxyFailures.Inc(1) http.Error(w, fmt.Sprintf("collector error: %+v", err), http.StatusInternalServerError) return } jsonBytes, err := encoder(resp) if err != nil { http.Error(w, "cannot marshall to JSON", http.StatusInternalServerError) return } if err = h.writeJSON(w, jsonBytes); err != nil { return } } func (h *Handler) encodeProto(strategy *api_v2.SamplingStrategyResponse) ([]byte, error) { str, err := p2json.SamplingStrategyResponseToJSON(strategy) if err != nil { h.metrics.BadProtoFailures.Inc(1) return nil, fmt.Errorf("SamplingStrategyResponseToJSON failed: %w", err) } h.metrics.SamplingRequestSuccess.Inc(1) return []byte(str), nil } ================================================ FILE: internal/sampling/http/handler_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package http import ( "bytes" "errors" "io" "net/http" "net/http/httptest" "testing" "github.com/gogo/protobuf/jsonpb" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger-idl/proto-gen/api_v2" "github.com/jaegertracing/jaeger/internal/metricstest" ) // parseSamplingResponse parses a JSON sampling strategy response // using the same jsonpb.Unmarshal logic as the OTel Jaeger Remote Sampler SDK. // See: https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/samplers/jaegerremote/sampler_remote.go func parseSamplingResponse(t *testing.T, body []byte) *api_v2.SamplingStrategyResponse { t.Helper() strategy := new(api_v2.SamplingStrategyResponse) require.NoError(t, jsonpb.Unmarshal(bytes.NewReader(body), strategy)) return strategy } type testServer struct { metricsFactory *metricstest.Factory samplingProvider *mockSamplingProvider server *httptest.Server handler *Handler } func withServer( mockSamplingResponse *api_v2.SamplingStrategyResponse, testFn func(server *testServer), ) { metricsFactory := metricstest.NewFactory(0) samplingProvider := &mockSamplingProvider{samplingResponse: mockSamplingResponse} cfgMgr := &ConfigManager{ SamplingProvider: samplingProvider, } handler := NewHandler(HandlerParams{ ConfigManager: cfgMgr, MetricsFactory: metricsFactory, }) httpMux := http.NewServeMux() handler.RegisterRoutes(httpMux) server := httptest.NewServer(httpMux) defer server.Close() testFn(&testServer{ metricsFactory: metricsFactory, samplingProvider: samplingProvider, server: server, handler: handler, }) } func TestHTTPHandler(t *testing.T) { withServer(rateLimiting(42), func(ts *testServer) { resp, err := http.Get(ts.server.URL + "/?service=Y") require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) body, err := io.ReadAll(resp.Body) require.NoError(t, err) require.NoError(t, resp.Body.Close()) assert.JSONEq(t, `{"strategyType":"RATE_LIMITING","rateLimitingSampling":{"maxTracesPerSecond":42}}`, string(body)) objResp := parseSamplingResponse(t, body) assert.Equal(t, ts.samplingProvider.samplingResponse, objResp) // handler must emit metrics ts.metricsFactory.AssertCounterMetrics(t, metricstest.ExpectedMetric{Name: "http-server.requests", Tags: map[string]string{"type": "sampling"}, Value: 1}) }) } // TestOTelSDKCompatibility verifies that the response from // the RegisterRoutes endpoint can be parsed by the same jsonpb.Unmarshal // logic used in the OpenTelemetry Jaeger Remote Sampler SDK: // https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/samplers/jaegerremote/sampler_remote.go // // The SDK's Parse function does: // // strategy := new(jaeger_api_v2.SamplingStrategyResponse) // if err := jsonpb.Unmarshal(bytes.NewReader(response), strategy); err != nil { ... } // // Gogo's jsonpb module can parse both string-based and numeric enum formats. // Cf. https://github.com/open-telemetry/opentelemetry-go-contrib/issues/3184 func TestOTelSDKCompatibility(t *testing.T) { tests := []struct { name string response *api_v2.SamplingStrategyResponse }{ { name: "rate limiting strategy", response: rateLimiting(42), }, { name: "probabilistic strategy", response: probabilistic(0.5), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { withServer(test.response, func(ts *testServer) { resp, err := http.Get(ts.server.URL + "/?service=Y") require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) body, err := io.ReadAll(resp.Body) require.NoError(t, err) require.NoError(t, resp.Body.Close()) // Parse the response the same way as the OTel Jaeger Remote Sampler SDK. // See: https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/samplers/jaegerremote/sampler_remote.go objResp := parseSamplingResponse(t, body) assert.Equal(t, test.response.GetStrategyType(), objResp.GetStrategyType()) // even though one of these strategies is nil, the generated code // still allows to call next method on it and return default value. assert.InDelta(t, test.response.GetProbabilisticSampling().GetSamplingRate(), objResp.GetProbabilisticSampling().GetSamplingRate(), 0) assert.Equal(t, test.response.GetRateLimitingSampling().GetMaxTracesPerSecond(), objResp.GetRateLimitingSampling().GetMaxTracesPerSecond()) }) }) } } func TestHTTPHandlerErrors(t *testing.T) { testCases := []struct { description string mockSamplingResponse *api_v2.SamplingStrategyResponse url string statusCode int body string metrics []metricstest.ExpectedMetric }{ { description: "no service name", url: "", statusCode: http.StatusBadRequest, body: "'service' parameter must be provided once\n", metrics: []metricstest.ExpectedMetric{ {Name: "http-server.errors", Tags: map[string]string{"source": "all", "status": "4xx"}, Value: 1}, }, }, { description: "sampling endpoint too many service names", url: "?service=Y&service=Y", statusCode: http.StatusBadRequest, body: "'service' parameter must be provided once\n", metrics: []metricstest.ExpectedMetric{ {Name: "http-server.errors", Tags: map[string]string{"source": "all", "status": "4xx"}, Value: 1}, }, }, { description: "sampler collector error", url: "?service=Y", statusCode: http.StatusInternalServerError, body: "collector error: no mock response provided\n", metrics: []metricstest.ExpectedMetric{ {Name: "http-server.errors", Tags: map[string]string{"source": "collector-proxy", "status": "5xx"}, Value: 1}, }, }, } for _, tc := range testCases { testCase := tc // capture loop var t.Run(testCase.description, func(t *testing.T) { withServer(testCase.mockSamplingResponse, func(ts *testServer) { resp, err := http.Get(ts.server.URL + testCase.url) require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, testCase.statusCode, resp.StatusCode) if testCase.body != "" { body, err := io.ReadAll(resp.Body) require.NoError(t, err) assert.Equal(t, testCase.body, string(body)) } if len(testCase.metrics) > 0 { ts.metricsFactory.AssertCounterMetrics(t, testCase.metrics...) } }) }) } t.Run("failure to write a response", func(t *testing.T) { withServer(probabilistic(0.001), func(ts *testServer) { handler := ts.handler req := httptest.NewRequest(http.MethodGet, "http://localhost:80/?service=X", http.NoBody) w := &mockWriter{header: make(http.Header)} handler.serveSamplingHTTP(w, req, handler.encodeProto) ts.metricsFactory.AssertCounterMetrics(t, metricstest.ExpectedMetric{Name: "http-server.errors", Tags: map[string]string{"source": "write", "status": "5xx"}, Value: 1}) }) }) } func TestEncodeErrors(t *testing.T) { withServer(nil, func(server *testServer) { _, err := server.handler.encodeProto(nil) require.ErrorContains(t, err, "SamplingStrategyResponseToJSON failed") server.metricsFactory.AssertCounterMetrics(t, []metricstest.ExpectedMetric{ {Name: "http-server.errors", Tags: map[string]string{"source": "proto", "status": "5xx"}, Value: 1}, }...) }) } func rateLimiting(rate int32) *api_v2.SamplingStrategyResponse { return &api_v2.SamplingStrategyResponse{ StrategyType: api_v2.SamplingStrategyType_RATE_LIMITING, RateLimitingSampling: &api_v2.RateLimitingSamplingStrategy{ MaxTracesPerSecond: rate, }, } } func probabilistic(probability float64) *api_v2.SamplingStrategyResponse { return &api_v2.SamplingStrategyResponse{ StrategyType: api_v2.SamplingStrategyType_PROBABILISTIC, ProbabilisticSampling: &api_v2.ProbabilisticSamplingStrategy{ SamplingRate: probability, }, } } type mockWriter struct { header http.Header } func (w *mockWriter) Header() http.Header { return w.header } func (*mockWriter) Write([]byte) (int, error) { return 0, errors.New("write error") } func (*mockWriter) WriteHeader(int) {} ================================================ FILE: internal/sampling/http/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package http import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/sampling/samplingstrategy/adaptive/README.md ================================================ # Adaptive Sampling Adaptive sampling works in Jaeger collector by observing the spans received from services and recalculating sampling probabilities for each service/endpoint combination to ensure that the volume of collected traces matches the desired target of traces per second. When a new service or endpoint is detected, it is initially sampled with "initial-sampling-probability" until enough data is collected to calculate the rate appropriate for the traffic going through the endpoint. Adaptive sampling requires a storage backend to store the observed traffic data and computed probabilities. At the moment memory (for all-in-one deployment), cassandra, badger, elasticsearch and opensearch are supported as sampling storage backends. Note: adaptive sampling in Jaeger backend *does not actually do the sampling*. The sampling is performed by OTEL SDKs, and sampling decisions are propagated through trace context. The job of Adaptive Sampling is to dynamically calculate sampling probabilities and expose them as sampling strategies via Jaeger's Remote Sampling protocol. References: * [Documentation](https://www.jaegertracing.io/docs/latest/sampling/#adaptive-sampling) * [Blog post](https://medium.com/jaegertracing/adaptive-sampling-in-jaeger-50f336f4334) ## Implementation details There are three main components of the Adaptive Sampling: Aggregator, Post-aggregator (could use a better name), and Provider. ### Aggregator *Aggregator* is a component that runs in the ingestion pipeline (e.g. as a trace processor in OTEL Collector). It looks at all spans passing through that instance of the collector and looks for root spans. Each root span indicates a new trace being generated, so the aggregation aggregates the count of those traces (grouped by service name and span name) and periodically flushes those aggregates (called "throughput") to storage. ### Post-aggregator *Post-aggregator* is the main logic responsible for _adaptive_ part of this sampling strategy implementation. Its main job is to load all throughput from storage (because multiple instances of collector could've written different aggregates), aggregate it into a final output, and compute the desired sampling probabilities, which are also written into storage. In a typical production usage Jaeger deployment consists of many collectors. Each collector runs an independent aggregator, because they do not require coordination as long as there is a shared storage. Each collector also runs post-aggregator, however only one of those should be combining the output of all aggregators and producing the final sampling probabilities. This is achieved by using a simple leader-follower election with the help of the storage backend. The leader post-aggregator does the main job of the computation, while the follower-aggregators are only loading the throughput from storage and aggregate it in memory, so that each of them is ready to assume the role of the leader if needed, but they do not compute the probabilities or write them back into storage. ### Provider *Provider* is responsible for providing the sampling strategy to the SDKs when they poll the `/sampling` endpoint. It periodically reads the computed sampling probabilities from storage and translates them into sampling strategy output expected by the Jaeger Remote Sampling protocol. ================================================ FILE: internal/sampling/samplingstrategy/adaptive/aggregator.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package adaptive import ( "strconv" "sync" "time" "go.uber.org/zap" spanmodel "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/hostname" "github.com/jaegertracing/jaeger/internal/leaderelection" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/sampling/samplingstrategy" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore/model" ) const ( maxProbabilities = 10 ) // aggregator is a kind of trace processor that watches for root spans // and calculates how many traces per service / per endpoint are being // produced. It periodically flushes these stats ("throughput") to storage. // // It also invokes PostAggregator which actually computes adaptive sampling // probabilities based on the observed throughput. type aggregator struct { sync.Mutex operationsCounter metrics.Counter servicesCounter metrics.Counter currentThroughput serviceOperationThroughput postAggregator *PostAggregator aggregationInterval time.Duration storage samplingstore.Store stop chan struct{} bgFinished sync.WaitGroup } // NewAggregator creates a throughput aggregator that simply emits metrics // about the number of operations seen over the aggregationInterval. func NewAggregator(options Options, logger *zap.Logger, metricsFactory metrics.Factory, participant leaderelection.ElectionParticipant, store samplingstore.Store) (samplingstrategy.Aggregator, error) { hostId, err := hostname.AsIdentifier() if err != nil { return nil, err } logger.Info("Using unique participantName in adaptive sampling", zap.String("participantName", hostId)) postAggregator, err := newPostAggregator(options, hostId, store, participant, metricsFactory, logger) if err != nil { return nil, err } return &aggregator{ operationsCounter: metricsFactory.Counter(metrics.Options{Name: "sampling_operations"}), servicesCounter: metricsFactory.Counter(metrics.Options{Name: "sampling_services"}), currentThroughput: make(serviceOperationThroughput), aggregationInterval: options.CalculationInterval, postAggregator: postAggregator, storage: store, stop: make(chan struct{}), }, nil } func (a *aggregator) runAggregationLoop() { ticker := time.NewTicker(a.aggregationInterval) for { select { case <-ticker.C: a.Lock() a.saveThroughput() a.currentThroughput = make(serviceOperationThroughput) a.postAggregator.runCalculation() a.Unlock() case <-a.stop: ticker.Stop() return } } } func (a *aggregator) saveThroughput() { totalOperations := 0 var throughputSlice []*model.Throughput for _, opThroughput := range a.currentThroughput { totalOperations += len(opThroughput) for _, throughput := range opThroughput { throughputSlice = append(throughputSlice, throughput) } } a.operationsCounter.Inc(int64(totalOperations)) a.servicesCounter.Inc(int64(len(a.currentThroughput))) a.storage.InsertThroughput(throughputSlice) } func (a *aggregator) RecordThroughput(service, operation string, samplerType spanmodel.SamplerType, probability float64) { a.Lock() defer a.Unlock() if _, ok := a.currentThroughput[service]; !ok { a.currentThroughput[service] = make(map[string]*model.Throughput) } throughput, ok := a.currentThroughput[service][operation] if !ok { throughput = &model.Throughput{ Service: service, Operation: operation, Probabilities: make(map[string]struct{}), } a.currentThroughput[service][operation] = throughput } probStr := TruncateFloat(probability) if len(throughput.Probabilities) != maxProbabilities { throughput.Probabilities[probStr] = struct{}{} } // Only if we see probabilistically sampled root spans do we increment the throughput counter, // for lowerbound sampled spans, we don't increment at all but we still save a count of 0 as // the throughput so that the adaptive sampling processor is made aware of the endpoint. if samplerType == spanmodel.SamplerTypeProbabilistic { throughput.Count++ } } func (a *aggregator) Start() { a.postAggregator.Start() a.bgFinished.Go(func() { a.runAggregationLoop() }) } func (a *aggregator) Close() error { close(a.stop) a.bgFinished.Wait() return nil } func (a *aggregator) HandleRootSpan(span *spanmodel.Span) { // simply checking parentId to determine if a span is a root span is not sufficient. However, // we can be sure that only a root span will have sampler tags. if span.ParentSpanID() != spanmodel.NewSpanID(0) { return } service := span.Process.ServiceName if service == "" || span.OperationName == "" { return } samplerType, samplerParam := getSamplerParams(span, a.postAggregator.logger) if samplerType == spanmodel.SamplerTypeUnrecognized { return } a.RecordThroughput(service, span.OperationName, samplerType, samplerParam) } // GetSamplerParams returns the sampler.type and sampler.param value if they are valid. func getSamplerParams(s *spanmodel.Span, logger *zap.Logger) (spanmodel.SamplerType, float64) { samplerType := s.GetSamplerType() if samplerType == spanmodel.SamplerTypeUnrecognized { return spanmodel.SamplerTypeUnrecognized, 0 } tag, ok := spanmodel.KeyValues(s.Tags).FindByKey(spanmodel.SamplerParamKey) if !ok { return spanmodel.SamplerTypeUnrecognized, 0 } samplerParam, err := samplerParamToFloat(tag) if err != nil { logger. With(zap.String("traceID", s.TraceID.String())). With(zap.String("spanID", s.SpanID.String())). Warn("sampler.param tag is not a number", zap.Any("tag", tag)) return spanmodel.SamplerTypeUnrecognized, 0 } return samplerType, samplerParam } func samplerParamToFloat(samplerParamTag spanmodel.KeyValue) (float64, error) { // The param could be represented as a string, an int, or a float switch samplerParamTag.VType { case spanmodel.Float64Type: return samplerParamTag.Float64(), nil case spanmodel.Int64Type: return float64(samplerParamTag.Int64()), nil default: return strconv.ParseFloat(samplerParamTag.AsString(), 64) } } ================================================ FILE: internal/sampling/samplingstrategy/adaptive/aggregator_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package adaptive import ( "net/http" "strconv" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" epmocks "github.com/jaegertracing/jaeger/internal/leaderelection/mocks" "github.com/jaegertracing/jaeger/internal/metricstest" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore/mocks" ) func TestAggregator(t *testing.T) { t.Skip("Skipping flaky unit test") metricsFactory := metricstest.NewFactory(0) mockStorage := &mocks.Store{} mockStorage.On("InsertThroughput", mock.AnythingOfType("[]*model.Throughput")).Return(nil) mockEP := &epmocks.ElectionParticipant{} mockEP.On("Start").Return(nil) mockEP.On("Close").Return(nil) mockEP.On("IsLeader").Return(true) testOpts := Options{ CalculationInterval: 1 * time.Second, AggregationBuckets: 1, BucketsForCalculation: 1, } logger := zap.NewNop() a, err := NewAggregator(testOpts, logger, metricsFactory, mockEP, mockStorage) require.NoError(t, err) a.RecordThroughput("A", http.MethodGet, model.SamplerTypeProbabilistic, 0.001) a.RecordThroughput("B", http.MethodPost, model.SamplerTypeProbabilistic, 0.001) a.RecordThroughput("C", http.MethodGet, model.SamplerTypeProbabilistic, 0.001) a.RecordThroughput("A", http.MethodPost, model.SamplerTypeProbabilistic, 0.001) a.RecordThroughput("A", http.MethodGet, model.SamplerTypeProbabilistic, 0.001) a.RecordThroughput("A", http.MethodGet, model.SamplerTypeLowerBound, 0.001) a.Start() defer a.Close() for range 10000 { counters, _ := metricsFactory.Snapshot() if _, ok := counters["sampling_operations"]; ok { break } time.Sleep(1 * time.Millisecond) } metricsFactory.AssertCounterMetrics(t, []metricstest.ExpectedMetric{ {Name: "sampling_operations", Value: 4}, {Name: "sampling_services", Value: 3}, }...) } func TestIncrementThroughput(t *testing.T) { metricsFactory := metricstest.NewFactory(0) mockStorage := &mocks.Store{} mockEP := &epmocks.ElectionParticipant{} testOpts := Options{ CalculationInterval: 1 * time.Second, AggregationBuckets: 1, BucketsForCalculation: 1, } logger := zap.NewNop() a, err := NewAggregator(testOpts, logger, metricsFactory, mockEP, mockStorage) require.NoError(t, err) // 20 different probabilities for i := range 20 { a.RecordThroughput("A", http.MethodGet, model.SamplerTypeProbabilistic, 0.001*float64(i)) } assert.Len(t, a.(*aggregator).currentThroughput["A"][http.MethodGet].Probabilities, 10) a, err = NewAggregator(testOpts, logger, metricsFactory, mockEP, mockStorage) require.NoError(t, err) // 20 of the same probabilities for range 20 { a.RecordThroughput("A", http.MethodGet, model.SamplerTypeProbabilistic, 0.001) } assert.Len(t, a.(*aggregator).currentThroughput["A"][http.MethodGet].Probabilities, 1) } func TestLowerboundThroughput(t *testing.T) { metricsFactory := metricstest.NewFactory(0) mockStorage := &mocks.Store{} mockEP := &epmocks.ElectionParticipant{} testOpts := Options{ CalculationInterval: 1 * time.Second, AggregationBuckets: 1, BucketsForCalculation: 1, } logger := zap.NewNop() a, err := NewAggregator(testOpts, logger, metricsFactory, mockEP, mockStorage) require.NoError(t, err) a.RecordThroughput("A", http.MethodGet, model.SamplerTypeLowerBound, 0.001) assert.EqualValues(t, 0, a.(*aggregator).currentThroughput["A"][http.MethodGet].Count) assert.Empty(t, a.(*aggregator).currentThroughput["A"][http.MethodGet].Probabilities["0.001000"]) } func TestRecordThroughput(t *testing.T) { metricsFactory := metricstest.NewFactory(0) mockStorage := &mocks.Store{} mockEP := &epmocks.ElectionParticipant{} testOpts := Options{ CalculationInterval: 1 * time.Second, AggregationBuckets: 1, BucketsForCalculation: 1, } logger := zap.NewNop() a, err := NewAggregator(testOpts, logger, metricsFactory, mockEP, mockStorage) require.NoError(t, err) // Testing non-root span span := &model.Span{References: []model.SpanRef{{SpanID: model.NewSpanID(1), RefType: model.ChildOf}}} a.HandleRootSpan(span) require.Empty(t, a.(*aggregator).currentThroughput) // Testing span with service name but no operation span.References = []model.SpanRef{} span.Process = &model.Process{ ServiceName: "A", } a.HandleRootSpan(span) require.Empty(t, a.(*aggregator).currentThroughput) // Testing span with service name and operation but no probabilistic sampling tags span.OperationName = http.MethodGet a.HandleRootSpan(span) require.Empty(t, a.(*aggregator).currentThroughput) // Testing span with service name, operation, and probabilistic sampling tags span.Tags = model.KeyValues{ model.String("sampler.type", "probabilistic"), model.String("sampler.param", "0.001"), } a.HandleRootSpan(span) assert.EqualValues(t, 1, a.(*aggregator).currentThroughput["A"][http.MethodGet].Count) } func TestRecordThroughputFunc(t *testing.T) { metricsFactory := metricstest.NewFactory(0) mockStorage := &mocks.Store{} mockEP := &epmocks.ElectionParticipant{} logger := zap.NewNop() testOpts := Options{ CalculationInterval: 1 * time.Second, AggregationBuckets: 1, BucketsForCalculation: 1, } a, err := NewAggregator(testOpts, logger, metricsFactory, mockEP, mockStorage) require.NoError(t, err) // Testing non-root span span := &model.Span{References: []model.SpanRef{{SpanID: model.NewSpanID(1), RefType: model.ChildOf}}} a.HandleRootSpan(span) require.Empty(t, a.(*aggregator).currentThroughput) // Testing span with service name but no operation span.References = []model.SpanRef{} span.Process = &model.Process{ ServiceName: "A", } a.HandleRootSpan(span) require.Empty(t, a.(*aggregator).currentThroughput) // Testing span with service name and operation but no probabilistic sampling tags span.OperationName = http.MethodGet a.HandleRootSpan(span) require.Empty(t, a.(*aggregator).currentThroughput) // Testing span with service name, operation, and probabilistic sampling tags span.Tags = model.KeyValues{ model.String("sampler.type", "probabilistic"), model.String("sampler.param", "0.001"), } a.HandleRootSpan(span) assert.EqualValues(t, 1, a.(*aggregator).currentThroughput["A"][http.MethodGet].Count) } func TestGetSamplerParams(t *testing.T) { logger := zap.NewNop() tests := []struct { tags model.KeyValues expectedType model.SamplerType expectedParam float64 }{ { tags: model.KeyValues{ model.String("sampler.type", "probabilistic"), model.String("sampler.param", "1e-05"), }, expectedType: model.SamplerTypeProbabilistic, expectedParam: 0.00001, }, { tags: model.KeyValues{ model.String("sampler.type", "probabilistic"), model.Float64("sampler.param", 0.10404450002098709), }, expectedType: model.SamplerTypeProbabilistic, expectedParam: 0.10404450002098709, }, { tags: model.KeyValues{ model.String("sampler.type", "probabilistic"), model.String("sampler.param", "0.10404450002098709"), }, expectedType: model.SamplerTypeProbabilistic, expectedParam: 0.10404450002098709, }, { tags: model.KeyValues{ model.String("sampler.type", "probabilistic"), model.Int64("sampler.param", 1), }, expectedType: model.SamplerTypeProbabilistic, expectedParam: 1.0, }, { tags: model.KeyValues{ model.String("sampler.type", "ratelimiting"), model.String("sampler.param", "1"), }, expectedType: model.SamplerTypeRateLimiting, expectedParam: 1, }, { tags: model.KeyValues{ model.Float64("sampler.type", 1.5), }, expectedType: model.SamplerTypeUnrecognized, expectedParam: 0, }, { tags: model.KeyValues{ model.String("sampler.type", "probabilistic"), }, expectedType: model.SamplerTypeUnrecognized, expectedParam: 0, }, { tags: model.KeyValues{}, expectedType: model.SamplerTypeUnrecognized, expectedParam: 0, }, { tags: model.KeyValues{ model.String("sampler.type", "lowerbound"), model.String("sampler.param", "1"), }, expectedType: model.SamplerTypeLowerBound, expectedParam: 1, }, { tags: model.KeyValues{ model.String("sampler.type", "lowerbound"), model.Int64("sampler.param", 1), }, expectedType: model.SamplerTypeLowerBound, expectedParam: 1, }, { tags: model.KeyValues{ model.String("sampler.type", "lowerbound"), model.Float64("sampler.param", 0.5), }, expectedType: model.SamplerTypeLowerBound, expectedParam: 0.5, }, { tags: model.KeyValues{ model.String("sampler.type", "lowerbound"), model.String("sampler.param", "not_a_number"), }, expectedType: model.SamplerTypeUnrecognized, expectedParam: 0, }, { tags: model.KeyValues{ model.String("sampler.type", "not_a_type"), model.String("sampler.param", "not_a_number"), }, expectedType: model.SamplerTypeUnrecognized, expectedParam: 0, }, } for i, test := range tests { tt := test t.Run(strconv.Itoa(i), func(t *testing.T) { span := &model.Span{} span.Tags = tt.tags actualType, actualParam := getSamplerParams(span, logger) assert.Equal(t, tt.expectedType, actualType) assert.InDelta(t, tt.expectedParam, actualParam, 0.01) }) } } ================================================ FILE: internal/sampling/samplingstrategy/adaptive/cache.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package adaptive // SamplingCacheEntry keeps track of the probability and whether a service-operation is observed // using adaptive sampling. type SamplingCacheEntry struct { Probability float64 UsingAdaptive bool } // SamplingCache is a nested map: service -> operation -> cache entry. type SamplingCache map[string]map[string]*SamplingCacheEntry // Set adds a new entry for given service/operation. func (s SamplingCache) Set(service, operation string, entry *SamplingCacheEntry) { if _, ok := s[service]; !ok { s[service] = make(map[string]*SamplingCacheEntry) } s[service][operation] = entry } // Get retrieves the entry for given service/operation. Returns nil if not found. func (s SamplingCache) Get(service, operation string) *SamplingCacheEntry { v, ok := s[service] if !ok { return nil } return v[operation] } ================================================ FILE: internal/sampling/samplingstrategy/adaptive/cache_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package adaptive import ( "testing" "github.com/stretchr/testify/assert" ) func TestSamplingCache(t *testing.T) { var ( c = SamplingCache{} service = "svc" operation = "op" ) c.Set(service, operation, &SamplingCacheEntry{}) assert.NotNil(t, c.Get(service, operation)) assert.Nil(t, c.Get("blah", "blah")) } ================================================ FILE: internal/sampling/samplingstrategy/adaptive/calculationstrategy/interface.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package calculationstrategy // ProbabilityCalculator calculates the new probability given the current and target QPS type ProbabilityCalculator interface { Calculate(targetQPS, curQPS, prevProbability float64) (newProbability float64) } ================================================ FILE: internal/sampling/samplingstrategy/adaptive/calculationstrategy/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package calculationstrategy import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/sampling/samplingstrategy/adaptive/calculationstrategy/percentage_increase_capped_calculator.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package calculationstrategy const ( defaultPercentageIncreaseCap = 0.5 ) // PercentageIncreaseCappedCalculator is a probability calculator that caps the probability // increase to a certain percentage of the previous probability. // // Given prevProb = 0.1, newProb = 0.5, and cap = 0.5: // (0.5 - 0.1)/0.1 = 400% increase. Given that our cap is 50%, the probability can only // increase to 0.15. // // Given prevProb = 0.4, newProb = 0.5, and cap = 0.5: // (0.5 - 0.4)/0.4 = 25% increase. Given that this is below our cap of 50%, the probability // can increase to 0.5. type PercentageIncreaseCappedCalculator struct { percentageIncreaseCap float64 } // NewPercentageIncreaseCappedCalculator returns a new percentage increase capped calculator. func NewPercentageIncreaseCappedCalculator(percentageIncreaseCap float64) PercentageIncreaseCappedCalculator { if percentageIncreaseCap == 0 { percentageIncreaseCap = defaultPercentageIncreaseCap } return PercentageIncreaseCappedCalculator{ percentageIncreaseCap: percentageIncreaseCap, } } // Calculate calculates the new probability. func (c PercentageIncreaseCappedCalculator) Calculate(targetQPS, curQPS, prevProbability float64) float64 { factor := targetQPS / curQPS newProbability := prevProbability * factor // If curQPS is lower than the targetQPS, we need to increase the probability slowly to // defend against oversampling. // Else if curQPS is higher than the targetQPS, jump directly to the newProbability to // defend against oversampling. if factor > 1.0 { percentIncrease := (newProbability - prevProbability) / prevProbability if percentIncrease > c.percentageIncreaseCap { newProbability = prevProbability + (prevProbability * c.percentageIncreaseCap) } } return newProbability } ================================================ FILE: internal/sampling/samplingstrategy/adaptive/calculationstrategy/percentage_increase_capped_calculator_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package calculationstrategy import ( "testing" "github.com/stretchr/testify/assert" ) func TestPercentageIncreaseCappedCalculator(t *testing.T) { calculator := NewPercentageIncreaseCappedCalculator(0) tests := []struct { targetQPS float64 curQPS float64 oldProbability float64 expectedProbability float64 testName string }{ {1.0, 2.0, 0.1, 0.05, "test1"}, {1.0, 0.5, 0.1, 0.15, "test2"}, {1.0, 0.8, 0.1, 0.125, "test3"}, } for _, tt := range tests { probability := calculator.Calculate(tt.targetQPS, tt.curQPS, tt.oldProbability) assert.InDelta(t, tt.expectedProbability, probability, 1e-4, tt.testName) } } ================================================ FILE: internal/sampling/samplingstrategy/adaptive/floatutils.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package adaptive import ( "math" "strconv" ) // TruncateFloat truncates float to 6 decimal positions and converts to string. func TruncateFloat(v float64) string { return strconv.FormatFloat(v, 'f', 6, 64) } // FloatEquals compares two floats with 10 decimal positions precision. func FloatEquals(a, b float64) bool { return math.Abs(a-b) < 1e-10 } ================================================ FILE: internal/sampling/samplingstrategy/adaptive/floatutils_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package adaptive import ( "testing" "github.com/stretchr/testify/assert" ) func TestTruncateFloat(t *testing.T) { tests := []struct { prob float64 expected string }{ {prob: 1, expected: "1.000000"}, {prob: 0.00001, expected: "0.000010"}, {prob: 0.00230234, expected: "0.002302"}, {prob: 0.1040445000, expected: "0.104044"}, {prob: 0.10404450002098709, expected: "0.104045"}, } for _, test := range tests { assert.Equal(t, test.expected, TruncateFloat(test.prob)) } } func TestFloatEquals(t *testing.T) { tests := []struct { f1 float64 f2 float64 equal bool }{ {f1: 0.123456789123, f2: 0.123456789123, equal: true}, {f1: 0.123456789123, f2: 0.123456789111, equal: true}, {f1: 0.123456780000, f2: 0.123456781111, equal: false}, } for _, test := range tests { assert.Equal(t, test.equal, FloatEquals(test.f1, test.f2)) } } ================================================ FILE: internal/sampling/samplingstrategy/adaptive/options.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package adaptive import ( "time" ) // Options holds configuration for the adaptive sampling strategy store. // The abbreviation SPS refers to "samples-per-second", which is the target // of the optimization/control implemented by the adaptive sampling. type Options struct { // TargetSamplesPerSecond is the global target rate of samples per operation. // TODO implement manual overrides per service/operation. TargetSamplesPerSecond float64 `mapstructure:"target_samples_per_second"` // DeltaTolerance is the acceptable amount of deviation between the observed and the desired (target) // throughput for an operation, expressed as a ratio. For example, the value of 0.3 (30% deviation) // means that if abs((actual-expected) / expected) < 0.3, then the actual sampling rate is "close enough" // and the system does not need to send an updated sampling probability (the "control signal" u(t) // in the PID Controller terminology) to the sampler in the application. // // Increase this to reduce the amount of fluctuation in the calculated probabilities. DeltaTolerance float64 `mapstructure:"delta_tolerance"` // CalculationInterval determines how often new probabilities are calculated. E.g. if it is 1 minute, // new sampling probabilities are calculated once a minute and each bucket will contain 1 minute worth // of aggregated throughput data. CalculationInterval time.Duration `mapstructure:"calculation_interval"` // AggregationBuckets is the total number of aggregated throughput buckets kept in memory, ie. if // the CalculationInterval is 1 minute (each bucket contains 1 minute of thoughput data) and the // AggregationBuckets is 3, the adaptive sampling processor will keep at most 3 buckets in memory for // all operations. // TODO(wjang): Expand on why this is needed when BucketsForCalculation seems to suffice. AggregationBuckets int `mapstructure:"aggregation_buckets"` // BucketsForCalculation determines how many previous buckets used in calculating the weighted QPS, // ie. if BucketsForCalculation is 1, only the most recent bucket will be used in calculating the weighted QPS. BucketsForCalculation int `mapstructure:"calculation_buckets"` // Delay is the amount of time to delay probability generation by, ie. if the CalculationInterval // is 1 minute, the number of buckets is 10, and the delay is 2 minutes, then at one time // we'll have [now()-12m,now()-2m] range of throughput data in memory to base the calculations // off of. This delay is necessary to counteract the rate at which the jaeger clients poll for // the latest sampling probabilities. The default client poll rate is 1 minute, which means that // during any 1 minute interval, the clients will be fetching new probabilities in a uniformly // distributed manner throughout the 1 minute window. By setting the delay to 2 minutes, we can // guarantee that all clients can use the latest calculated probabilities for at least 1 minute. Delay time.Duration `mapstructure:"calculation_delay"` // InitialSamplingProbability is the initial sampling probability for all new operations. InitialSamplingProbability float64 `mapstructure:"initial_sampling_probability"` // MinSamplingProbability is the minimum sampling probability for all operations. ie. the calculated sampling // probability will be in the range [MinSamplingProbability, 1.0]. MinSamplingProbability float64 `mapstructure:"min_sampling_probability"` // MinSamplesPerSecond determines the min number of traces that are sampled per second. // For example, if the value is 0.01666666666 (one every minute), then the sampling processor will do // its best to sample at least one trace a minute for an operation. This is useful for low QPS operations // that may never be sampled by the probabilistic sampler. MinSamplesPerSecond float64 `mapstructure:"min_samples_per_second"` // LeaderLeaseRefreshInterval is the duration to sleep if this processor is elected leader before // attempting to renew the lease on the leader lock. NB. This should be less than FollowerLeaseRefreshInterval // to reduce lock thrashing. LeaderLeaseRefreshInterval time.Duration `mapstructure:"leader_lease_refresh_interval"` // FollowerLeaseRefreshInterval is the duration to sleep if this processor is a follower // (ie. failed to gain the leader lock). FollowerLeaseRefreshInterval time.Duration `mapstructure:"follower_lease_refresh_interval"` } func DefaultOptions() Options { return Options{ TargetSamplesPerSecond: 1, DeltaTolerance: 0.3, BucketsForCalculation: 1, CalculationInterval: time.Minute, AggregationBuckets: 10, Delay: time.Minute * 2, InitialSamplingProbability: 0.001, MinSamplingProbability: 1e-5, // one in 100k requests MinSamplesPerSecond: 1.0 / float64(time.Minute/time.Second), // once every 1 minute LeaderLeaseRefreshInterval: 5 * time.Second, FollowerLeaseRefreshInterval: 60 * time.Second, } } ================================================ FILE: internal/sampling/samplingstrategy/adaptive/options_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package adaptive import ( "testing" "time" "github.com/stretchr/testify/assert" ) func TestDefaultOptions(t *testing.T) { opts := DefaultOptions() assert.NotNil(t, opts) assert.InDelta(t, 1.0, opts.TargetSamplesPerSecond, 0.01) assert.InDelta(t, 0.3, opts.DeltaTolerance, 0.01) assert.Equal(t, 1, opts.BucketsForCalculation) assert.Equal(t, time.Minute, opts.CalculationInterval) assert.Equal(t, 10, opts.AggregationBuckets) assert.Equal(t, time.Minute*2, opts.Delay) assert.InDelta(t, 0.001, opts.InitialSamplingProbability, 0.0001) assert.InDelta(t, 1e-5, opts.MinSamplingProbability, 1e-6) assert.InDelta(t, 1.0/float64(time.Minute/time.Second), opts.MinSamplesPerSecond, 0.0001) assert.Equal(t, 5*time.Second, opts.LeaderLeaseRefreshInterval) assert.Equal(t, 60*time.Second, opts.FollowerLeaseRefreshInterval) } ================================================ FILE: internal/sampling/samplingstrategy/adaptive/package_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package adaptive import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/sampling/samplingstrategy/adaptive/post_aggregator.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package adaptive import ( "errors" "math" "math/rand" "sync" "time" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/leaderelection" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/sampling/samplingstrategy/adaptive/calculationstrategy" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore/model" ) const ( maxSamplingProbability = 1.0 getThroughputErrMsg = "failed to get throughput from storage" // The number of past entries for samplingCache the leader keeps in memory serviceCacheSize = 25 ) var ( errNonZero = errors.New("CalculationInterval and AggregationBuckets must be greater than 0") errBucketsForCalculation = errors.New("BucketsForCalculation cannot be less than 1") ) // nested map: service -> operation -> throughput. type serviceOperationThroughput map[string]map[string]*model.Throughput func (t serviceOperationThroughput) get(service, operation string) (*model.Throughput, bool) { svcThroughput, ok := t[service] if ok { v, ok := svcThroughput[operation] return v, ok } return nil, false } // nested map: service -> operation -> buckets of QPS values. type serviceOperationQPS map[string]map[string][]float64 type throughputBucket struct { throughput serviceOperationThroughput interval time.Duration endTime time.Time } // PostAggregator retrieves service throughput over a lookback interval and calculates sampling probabilities // per operation such that each operation is sampled at a specified target QPS. It achieves this by // retrieving discrete buckets of operation throughput and doing a weighted average of the throughput // and generating a probability to match the targetQPS. type PostAggregator struct { Options mu sync.RWMutex electionParticipant leaderelection.ElectionParticipant storage samplingstore.Store logger *zap.Logger hostname string // probabilities contains the latest calculated sampling probabilities for service operations. probabilities model.ServiceOperationProbabilities // qps contains the latest calculated qps for service operations; the calculation is essentially // throughput / CalculationInterval. qps model.ServiceOperationQPS // throughputs is an array (of `AggregationBuckets` size) that stores the aggregated throughput. // The latest throughput is stored at the head of the slice. throughputs []*throughputBucket weightVectorCache *WeightVectorCache probabilityCalculator calculationstrategy.ProbabilityCalculator serviceCache []SamplingCache shutdown chan struct{} operationsCalculatedGauge metrics.Gauge calculateProbabilitiesLatency metrics.Timer lastCheckedTime time.Time } // newPostAggregator creates a new sampling postAggregator that generates sampling rates for service operations. func newPostAggregator( opts Options, hostname string, storage samplingstore.Store, electionParticipant leaderelection.ElectionParticipant, metricsFactory metrics.Factory, logger *zap.Logger, ) (*PostAggregator, error) { if opts.CalculationInterval == 0 || opts.AggregationBuckets == 0 { return nil, errNonZero } if opts.BucketsForCalculation < 1 { return nil, errBucketsForCalculation } metricsFactory = metricsFactory.Namespace(metrics.NSOptions{Name: "adaptive_sampling_processor"}) return &PostAggregator{ Options: opts, storage: storage, probabilities: make(model.ServiceOperationProbabilities), qps: make(model.ServiceOperationQPS), hostname: hostname, logger: logger, electionParticipant: electionParticipant, // TODO make weightsCache and probabilityCalculator configurable weightVectorCache: NewWeightVectorCache(), probabilityCalculator: calculationstrategy.NewPercentageIncreaseCappedCalculator(1.0), serviceCache: []SamplingCache{}, operationsCalculatedGauge: metricsFactory.Gauge(metrics.Options{Name: "operations_calculated"}), calculateProbabilitiesLatency: metricsFactory.Timer(metrics.TimerOptions{Name: "calculate_probabilities"}), shutdown: make(chan struct{}), }, nil } // Start initializes and starts the sampling postAggregator which regularly calculates sampling probabilities. func (p *PostAggregator) Start() { p.logger.Info("starting adaptive sampling postAggregator") // NB: the first tick will be slightly delayed by the initializeThroughput call. p.lastCheckedTime = time.Now().Add(p.Delay * -1) p.initializeThroughput(p.lastCheckedTime) } func (p *PostAggregator) isLeader() bool { return p.electionParticipant.IsLeader() } // addJitter adds a random amount of time. Without jitter, if the host holding the leader // lock were to die, then all other collectors can potentially wait for a full cycle before // trying to acquire the lock. With jitter, we can reduce the average amount of time before a // new leader is elected. Furthermore, jitter can be used to spread out read load on storage. func addJitter(jitterAmount time.Duration) time.Duration { half := jitterAmount / 2 if half <= 0 { return jitterAmount } return half + time.Duration(rand.Int63n(int64(half))) } func (p *PostAggregator) runCalculation() { endTime := time.Now().Add(p.Delay * -1) startTime := p.lastCheckedTime throughput, err := p.storage.GetThroughput(startTime, endTime) if err != nil { p.logger.Error(getThroughputErrMsg, zap.Error(err)) return } aggregatedThroughput := p.aggregateThroughput(throughput) p.prependThroughputBucket(&throughputBucket{ throughput: aggregatedThroughput, interval: endTime.Sub(startTime), endTime: endTime, }) p.lastCheckedTime = endTime // Load the latest throughput so that if this host ever becomes leader, it // has the throughput ready in memory. However, only run the actual calculations // if this host becomes leader. // TODO fill the throughput buffer only when we're leader if p.isLeader() { startTime := time.Now() probabilities, qps := p.calculateProbabilitiesAndQPS() p.mu.Lock() p.probabilities = probabilities p.qps = qps p.mu.Unlock() // NB: This has the potential of running into a race condition if the CalculationInterval // is set to an extremely low value. The worst case scenario is that probabilities is calculated // and swapped more than once before generateStrategyResponses() and saveProbabilities() are called. // This will result in one or more batches of probabilities not being saved which is completely // fine. This race condition should not ever occur anyway since the calculation interval will // be way longer than the time to run the calculations. p.calculateProbabilitiesLatency.Record(time.Since(startTime)) p.saveProbabilitiesAndQPS() } } func (p *PostAggregator) saveProbabilitiesAndQPS() { p.mu.RLock() defer p.mu.RUnlock() if err := p.storage.InsertProbabilitiesAndQPS(p.hostname, p.probabilities, p.qps); err != nil { p.logger.Warn("could not save probabilities", zap.Error(err)) } } func (p *PostAggregator) prependThroughputBucket(bucket *throughputBucket) { p.throughputs = append([]*throughputBucket{bucket}, p.throughputs...) if len(p.throughputs) > p.AggregationBuckets { p.throughputs = p.throughputs[0:p.AggregationBuckets] } } // aggregateThroughput aggregates operation throughput from different buckets into one. // All input buckets represent a single time range, but there are many of them because // they are all independently generated by different collector instances from inbound span traffic. func (*PostAggregator) aggregateThroughput(throughputs []*model.Throughput) serviceOperationThroughput { aggregatedThroughput := make(serviceOperationThroughput) for _, throughput := range throughputs { service := throughput.Service operation := throughput.Operation if _, ok := aggregatedThroughput[service]; !ok { aggregatedThroughput[service] = make(map[string]*model.Throughput) } if t, ok := aggregatedThroughput[service][operation]; ok { t.Count += throughput.Count t.Probabilities = merge(t.Probabilities, throughput.Probabilities) } else { copyThroughput := model.Throughput{ Service: throughput.Service, Operation: throughput.Operation, Count: throughput.Count, Probabilities: copySet(throughput.Probabilities), } aggregatedThroughput[service][operation] = ©Throughput } } return aggregatedThroughput } func copySet(in map[string]struct{}) map[string]struct{} { out := make(map[string]struct{}, len(in)) for key := range in { out[key] = struct{}{} } return out } func (p *PostAggregator) initializeThroughput(endTime time.Time) { for i := 0; i < p.AggregationBuckets; i++ { startTime := endTime.Add(p.CalculationInterval * -1) throughput, err := p.storage.GetThroughput(startTime, endTime) if err != nil && p.logger != nil { p.logger.Error(getThroughputErrMsg, zap.Error(err)) return } if len(throughput) == 0 { return } aggregatedThroughput := p.aggregateThroughput(throughput) p.throughputs = append(p.throughputs, &throughputBucket{ throughput: aggregatedThroughput, interval: p.CalculationInterval, endTime: endTime, }) endTime = startTime } } // throughputToQPS converts raw throughput counts for all accumulated buckets to QPS values. func (p *PostAggregator) throughputToQPS() serviceOperationQPS { // TODO previous qps buckets have already been calculated, just need to calculate latest batch // and append them where necessary and throw out the oldest batch. // Edge case #buckets < p.AggregationBuckets, then we shouldn't throw out qps := make(serviceOperationQPS) for _, bucket := range p.throughputs { for svc, operations := range bucket.throughput { if _, ok := qps[svc]; !ok { qps[svc] = make(map[string][]float64) } for op, throughput := range operations { if len(qps[svc][op]) >= p.BucketsForCalculation { continue } qps[svc][op] = append(qps[svc][op], calculateQPS(throughput.Count, bucket.interval)) } } } return qps } func calculateQPS(count int64, interval time.Duration) float64 { seconds := float64(interval) / float64(time.Second) return float64(count) / seconds } // calculateWeightedQPS calculates the weighted qps of the slice allQPS where weights are biased // towards more recent qps. This function assumes that the most recent qps is at the head of the slice. func (p *PostAggregator) calculateWeightedQPS(allQPS []float64) float64 { if len(allQPS) == 0 { return 0 } weights := p.weightVectorCache.GetWeights(len(allQPS)) var qps float64 for i := range allQPS { // #nosec G602 GetWeights always returns a slice of the same length as allQPS qps += allQPS[i] * weights[i] } return qps } func (p *PostAggregator) prependServiceCache() { p.serviceCache = append([]SamplingCache{make(SamplingCache)}, p.serviceCache...) if len(p.serviceCache) > serviceCacheSize { p.serviceCache = p.serviceCache[0:serviceCacheSize] } } func (p *PostAggregator) calculateProbabilitiesAndQPS() (model.ServiceOperationProbabilities, model.ServiceOperationQPS) { p.prependServiceCache() retProbabilities := make(model.ServiceOperationProbabilities) retQPS := make(model.ServiceOperationQPS) svcOpQPS := p.throughputToQPS() totalOperations := int64(0) for svc, opQPS := range svcOpQPS { if _, ok := retProbabilities[svc]; !ok { retProbabilities[svc] = make(map[string]float64) } if _, ok := retQPS[svc]; !ok { retQPS[svc] = make(map[string]float64) } for op, qps := range opQPS { totalOperations++ avgQPS := p.calculateWeightedQPS(qps) retQPS[svc][op] = avgQPS retProbabilities[svc][op] = p.calculateProbability(svc, op, avgQPS) } } p.operationsCalculatedGauge.Update(totalOperations) return retProbabilities, retQPS } func (p *PostAggregator) calculateProbability(service, operation string, qps float64) float64 { oldProbability := p.InitialSamplingProbability // TODO: is this loop overly expensive? p.mu.RLock() if opProbabilities, ok := p.probabilities[service]; ok { if probability, ok := opProbabilities[operation]; ok { oldProbability = probability } } latestThroughput := p.throughputs[0].throughput p.mu.RUnlock() usingAdaptiveSampling := p.isUsingAdaptiveSampling(oldProbability, service, operation, latestThroughput) p.serviceCache[0].Set(service, operation, &SamplingCacheEntry{ Probability: oldProbability, UsingAdaptive: usingAdaptiveSampling, }) // Short circuit if the qps is close enough to targetQPS or if the service doesn't appear to be using // adaptive sampling. if p.withinTolerance(qps, p.TargetSamplesPerSecond) || !usingAdaptiveSampling { return oldProbability } var newProbability float64 if FloatEquals(qps, 0) { // Edge case; we double the sampling probability if the QPS is 0 so that we force the service // to at least sample one span probabilistically. newProbability = oldProbability * 2.0 } else { newProbability = p.probabilityCalculator.Calculate(p.TargetSamplesPerSecond, qps, oldProbability) } return math.Min(maxSamplingProbability, math.Max(p.MinSamplingProbability, newProbability)) } // is actual value within p.DeltaTolerance percentage of expected value. func (p *PostAggregator) withinTolerance(actual, expected float64) bool { return math.Abs(actual-expected)/expected < p.DeltaTolerance } // merge (union) string set p2 into string set p1 func merge(p1 map[string]struct{}, p2 map[string]struct{}) map[string]struct{} { for k := range p2 { p1[k] = struct{}{} } return p1 } func (p *PostAggregator) isUsingAdaptiveSampling( probability float64, service string, operation string, throughput serviceOperationThroughput, ) bool { if FloatEquals(probability, p.InitialSamplingProbability) { // If the service is seen for the first time, assume it's using adaptive sampling (ie prob == initialProb). // Even if this isn't the case, the next time around this loop, the newly calculated probability will not equal // the initialProb so the logic will fall through. return true } if opThroughput, ok := throughput.get(service, operation); ok { f := TruncateFloat(probability) _, ok := opThroughput.Probabilities[f] return ok } // By this point, we know that there's no recorded throughput for this operation for this round // of calculation. Check the previous bucket to see if this operation was using adaptive sampling // before. if len(p.serviceCache) > 1 { if e := p.serviceCache[1].Get(service, operation); e != nil { return e.UsingAdaptive && !FloatEquals(e.Probability, p.InitialSamplingProbability) } } return false } ================================================ FILE: internal/sampling/samplingstrategy/adaptive/post_aggregator_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package adaptive import ( "errors" "net/http" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.uber.org/zap" epmocks "github.com/jaegertracing/jaeger/internal/leaderelection/mocks" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/metricstest" "github.com/jaegertracing/jaeger/internal/sampling/samplingstrategy/adaptive/calculationstrategy" smocks "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore/mocks" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore/model" "github.com/jaegertracing/jaeger/internal/testutils" ) func testThroughputs() []*model.Throughput { return []*model.Throughput{ {Service: "svcA", Operation: http.MethodGet, Count: 4, Probabilities: map[string]struct{}{"0.1": {}}}, {Service: "svcA", Operation: http.MethodGet, Count: 4, Probabilities: map[string]struct{}{"0.2": {}}}, {Service: "svcA", Operation: http.MethodPut, Count: 5, Probabilities: map[string]struct{}{"0.1": {}}}, {Service: "svcB", Operation: http.MethodGet, Count: 3, Probabilities: map[string]struct{}{"0.1": {}}}, } } func testThroughputBuckets() []*throughputBucket { return []*throughputBucket{ { throughput: serviceOperationThroughput{ "svcA": map[string]*model.Throughput{ http.MethodGet: {Count: 45}, http.MethodPut: {Count: 60}, }, "svcB": map[string]*model.Throughput{ http.MethodGet: {Count: 30}, http.MethodPut: {Count: 15}, }, }, interval: 60 * time.Second, }, { throughput: serviceOperationThroughput{ "svcA": map[string]*model.Throughput{ http.MethodGet: {Count: 30}, }, "svcB": map[string]*model.Throughput{ http.MethodGet: {Count: 45}, }, }, interval: 60 * time.Second, }, } } func errTestStorage() error { return errors.New("storage error") } // testProbabilityCalculator is a test implementation of ProbabilityCalculator // that calculates new probability by multiplying the old probability by the // ratio of target QPS to current QPS. type testProbabilityCalculator struct{} // Calculate implements the ProbabilityCalculator interface for testing. func (testProbabilityCalculator) Calculate(targetQPS, qps, oldProbability float64) float64 { factor := targetQPS / qps return oldProbability * factor } func testCalculator() calculationstrategy.ProbabilityCalculator { return testProbabilityCalculator{} } func TestAggregateThroughputInputsImmutability(t *testing.T) { p := &PostAggregator{} in := testThroughputs() _ = p.aggregateThroughput(in) assert.Equal(t, in, testThroughputs()) } func TestAggregateThroughput(t *testing.T) { p := &PostAggregator{} aggregatedThroughput := p.aggregateThroughput(testThroughputs()) require.Len(t, aggregatedThroughput, 2) throughput, ok := aggregatedThroughput["svcA"] require.True(t, ok) require.Len(t, throughput, 2) opThroughput, ok := throughput[http.MethodGet] require.True(t, ok) assert.Equal(t, int64(8), opThroughput.Count) assert.Equal(t, map[string]struct{}{"0.1": {}, "0.2": {}}, opThroughput.Probabilities) opThroughput, ok = throughput[http.MethodPut] require.True(t, ok) assert.Equal(t, int64(5), opThroughput.Count) assert.Equal(t, map[string]struct{}{"0.1": {}}, opThroughput.Probabilities) throughput, ok = aggregatedThroughput["svcB"] require.True(t, ok) require.Len(t, throughput, 1) opThroughput, ok = throughput[http.MethodGet] require.True(t, ok) assert.Equal(t, int64(3), opThroughput.Count) assert.Equal(t, map[string]struct{}{"0.1": {}}, opThroughput.Probabilities) } func TestInitializeThroughput(t *testing.T) { mockStorage := &smocks.Store{} mockStorage.On("GetThroughput", time.Time{}.Add(time.Minute*19), time.Time{}.Add(time.Minute*20)). Return(testThroughputs(), nil) mockStorage.On("GetThroughput", time.Time{}.Add(time.Minute*18), time.Time{}.Add(time.Minute*19)). Return([]*model.Throughput{{Service: "svcA", Operation: http.MethodGet, Count: 7}}, nil) mockStorage.On("GetThroughput", time.Time{}.Add(time.Minute*17), time.Time{}.Add(time.Minute*18)). Return([]*model.Throughput{}, nil) p := &PostAggregator{storage: mockStorage, Options: Options{CalculationInterval: time.Minute, AggregationBuckets: 3}} p.initializeThroughput(time.Time{}.Add(time.Minute * 20)) require.Len(t, p.throughputs, 2) require.Len(t, p.throughputs[0].throughput, 2) assert.Equal(t, time.Minute, p.throughputs[0].interval) assert.Equal(t, p.throughputs[0].endTime, time.Time{}.Add(time.Minute*20)) require.Len(t, p.throughputs[1].throughput, 1) assert.Equal(t, time.Minute, p.throughputs[1].interval) assert.Equal(t, p.throughputs[1].endTime, time.Time{}.Add(time.Minute*19)) } func TestInitializeThroughputFailure(t *testing.T) { mockStorage := &smocks.Store{} mockStorage.On("GetThroughput", time.Time{}.Add(time.Minute*19), time.Time{}.Add(time.Minute*20)). Return(nil, errTestStorage()) p := &PostAggregator{storage: mockStorage, Options: Options{CalculationInterval: time.Minute, AggregationBuckets: 1}} p.initializeThroughput(time.Time{}.Add(time.Minute * 20)) assert.Empty(t, p.throughputs) } func TestCalculateQPS(t *testing.T) { qps := calculateQPS(int64(90), 60*time.Second) assert.InDelta(t, 1.5, qps, 0.01) qps = calculateQPS(int64(45), 60*time.Second) assert.InDelta(t, 0.75, qps, 0.01) } func TestGenerateOperationQPS(t *testing.T) { p := &PostAggregator{throughputs: testThroughputBuckets(), Options: Options{BucketsForCalculation: 10, AggregationBuckets: 10}} svcOpQPS := p.throughputToQPS() assert.Len(t, svcOpQPS, 2) opQPS, ok := svcOpQPS["svcA"] require.True(t, ok) require.Len(t, opQPS, 2) assert.Equal(t, []float64{0.75, 0.5}, opQPS[http.MethodGet]) assert.Equal(t, []float64{1.0}, opQPS[http.MethodPut]) opQPS, ok = svcOpQPS["svcB"] require.True(t, ok) require.Len(t, opQPS, 2) assert.Equal(t, []float64{0.5, 0.75}, opQPS[http.MethodGet]) assert.Equal(t, []float64{0.25}, opQPS[http.MethodPut]) // Test using the previous QPS if the throughput is not provided p.prependThroughputBucket( &throughputBucket{ throughput: serviceOperationThroughput{ "svcA": map[string]*model.Throughput{ http.MethodGet: {Count: 30}, }, }, interval: 60 * time.Second, }, ) svcOpQPS = p.throughputToQPS() require.Len(t, svcOpQPS, 2) opQPS, ok = svcOpQPS["svcA"] require.True(t, ok) require.Len(t, opQPS, 2) assert.Equal(t, []float64{0.5, 0.75, 0.5}, opQPS[http.MethodGet]) assert.Equal(t, []float64{1.0}, opQPS[http.MethodPut]) opQPS, ok = svcOpQPS["svcB"] require.True(t, ok) require.Len(t, opQPS, 2) assert.Equal(t, []float64{0.5, 0.75}, opQPS[http.MethodGet]) assert.Equal(t, []float64{0.25}, opQPS[http.MethodPut]) } func TestGenerateOperationQPS_UseMostRecentBucketOnly(t *testing.T) { p := &PostAggregator{throughputs: testThroughputBuckets(), Options: Options{BucketsForCalculation: 1, AggregationBuckets: 10}} svcOpQPS := p.throughputToQPS() assert.Len(t, svcOpQPS, 2) opQPS, ok := svcOpQPS["svcA"] require.True(t, ok) require.Len(t, opQPS, 2) assert.Equal(t, []float64{0.75}, opQPS[http.MethodGet]) assert.Equal(t, []float64{1.0}, opQPS[http.MethodPut]) p.prependThroughputBucket( &throughputBucket{ throughput: serviceOperationThroughput{ "svcA": map[string]*model.Throughput{ http.MethodGet: {Count: 30}, }, }, interval: 60 * time.Second, }, ) svcOpQPS = p.throughputToQPS() require.Len(t, svcOpQPS, 2) opQPS, ok = svcOpQPS["svcA"] require.True(t, ok) require.Len(t, opQPS, 2) assert.Equal(t, []float64{0.5}, opQPS[http.MethodGet]) assert.Equal(t, []float64{1.0}, opQPS[http.MethodPut]) } func TestCalculateWeightedQPS(t *testing.T) { p := PostAggregator{weightVectorCache: NewWeightVectorCache()} assert.InDelta(t, 0.86735, p.calculateWeightedQPS([]float64{0.8, 1.2, 1.0}), 0.001) assert.InDelta(t, 0.95197, p.calculateWeightedQPS([]float64{1.0, 1.0, 0.0, 0.0}), 0.001) assert.InDelta(t, 0.0, p.calculateWeightedQPS([]float64{}), 0.01) } func TestCalculateProbability(t *testing.T) { throughputs := []*throughputBucket{ { throughput: serviceOperationThroughput{ "svcA": map[string]*model.Throughput{ http.MethodGet: {Probabilities: map[string]struct{}{"0.500000": {}}}, }, }, }, } probabilities := model.ServiceOperationProbabilities{ "svcA": map[string]float64{ http.MethodGet: 0.5, }, } cfg := Options{ TargetSamplesPerSecond: 1.0, DeltaTolerance: 0.2, InitialSamplingProbability: 0.001, MinSamplingProbability: 0.00001, } p := &PostAggregator{ Options: cfg, probabilities: probabilities, probabilityCalculator: testCalculator(), throughputs: throughputs, serviceCache: []SamplingCache{{"svcA": {}, "svcB": {}}}, } tests := []struct { service string operation string qps float64 expectedProbability float64 errMsg string }{ {"svcA", http.MethodGet, 2.0, 0.25, "modify existing probability"}, {"svcA", http.MethodPut, 2.0, 0.0005, "modify default probability"}, {"svcB", http.MethodGet, 0.9, 0.001, "qps within equivalence threshold"}, {"svcB", http.MethodPut, 0.000001, 1.0, "test max probability"}, {"svcB", http.MethodDelete, 1000000000, 0.00001, "test min probability"}, {"svcB", http.MethodDelete, 0.0, 0.002, "test 0 qps"}, } for _, test := range tests { probability := p.calculateProbability(test.service, test.operation, test.qps) assert.InDelta(t, test.expectedProbability, probability, 1e-6, test.errMsg) } } func TestCalculateProbabilitiesAndQPS(t *testing.T) { prevProbabilities := model.ServiceOperationProbabilities{ "svcB": map[string]float64{ http.MethodGet: 0.16, http.MethodPut: 0.03, }, } qps := model.ServiceOperationQPS{ "svcB": map[string]float64{ http.MethodGet: 0.625, }, } mets := metricstest.NewFactory(0) p := &PostAggregator{ Options: Options{ TargetSamplesPerSecond: 1.0, DeltaTolerance: 0.2, InitialSamplingProbability: 0.001, BucketsForCalculation: 10, }, throughputs: testThroughputBuckets(), probabilities: prevProbabilities, qps: qps, weightVectorCache: NewWeightVectorCache(), probabilityCalculator: testCalculator(), operationsCalculatedGauge: mets.Gauge(metrics.Options{Name: "test"}), } probabilities, qps := p.calculateProbabilitiesAndQPS() require.Len(t, probabilities, 2) assert.Equal(t, map[string]float64{http.MethodGet: 0.00136, http.MethodPut: 0.001}, probabilities["svcA"]) assert.Equal(t, map[string]float64{http.MethodGet: 0.16, http.MethodPut: 0.03}, probabilities["svcB"]) require.Len(t, qps, 2) assert.Equal(t, map[string]float64{http.MethodGet: 0.7352941176470588, http.MethodPut: 1}, qps["svcA"]) assert.Equal(t, map[string]float64{http.MethodGet: 0.5147058823529411, http.MethodPut: 0.25}, qps["svcB"]) _, gauges := mets.Backend.Snapshot() assert.EqualValues(t, 4, gauges["test"]) } func TestRunCalculationLoop(t *testing.T) { logger := zap.NewNop() mockStorage := &smocks.Store{} mockStorage.On("GetThroughput", mock.AnythingOfType("time.Time"), mock.AnythingOfType("time.Time")). Return(testThroughputs(), nil) mockStorage.On("GetLatestProbabilities").Return(model.ServiceOperationProbabilities{}, errTestStorage()) mockStorage.On("InsertProbabilitiesAndQPS", mock.AnythingOfType("string"), mock.AnythingOfType("model.ServiceOperationProbabilities"), mock.AnythingOfType("model.ServiceOperationQPS")).Return(errTestStorage()) mockStorage.On("InsertThroughput", mock.AnythingOfType("[]*model.Throughput")).Return(errTestStorage()) mockEP := &epmocks.ElectionParticipant{} mockEP.On("Start").Return(nil) mockEP.On("Close").Return(nil) mockEP.On("IsLeader").Return(true) cfg := Options{ TargetSamplesPerSecond: 1.0, DeltaTolerance: 0.1, InitialSamplingProbability: 0.001, CalculationInterval: time.Millisecond * 5, AggregationBuckets: 2, Delay: time.Millisecond * 5, LeaderLeaseRefreshInterval: time.Millisecond, FollowerLeaseRefreshInterval: time.Second, BucketsForCalculation: 10, } agg, err := NewAggregator(cfg, logger, metrics.NullFactory, mockEP, mockStorage) require.NoError(t, err) agg.Start() defer agg.Close() for range 1000 { agg.(*aggregator).Lock() probabilities := agg.(*aggregator).postAggregator.probabilities agg.(*aggregator).Unlock() if len(probabilities) != 0 { break } time.Sleep(time.Millisecond) } postAgg := agg.(*aggregator).postAggregator postAgg.mu.Lock() probabilities := postAgg.probabilities postAgg.mu.Unlock() require.Len(t, probabilities["svcA"], 2) } func TestRunCalculationLoop_GetThroughputError(t *testing.T) { logger, logBuffer := testutils.NewLogger() mockStorage := &smocks.Store{} mockStorage.On("GetThroughput", mock.AnythingOfType("time.Time"), mock.AnythingOfType("time.Time")). Return(nil, errTestStorage()) mockStorage.On("GetLatestProbabilities").Return(model.ServiceOperationProbabilities{}, errTestStorage()) mockStorage.On("InsertProbabilitiesAndQPS", mock.AnythingOfType("string"), mock.AnythingOfType("model.ServiceOperationProbabilities"), mock.AnythingOfType("model.ServiceOperationQPS")).Return(errTestStorage()) mockStorage.On("InsertThroughput", mock.AnythingOfType("[]*model.Throughput")).Return(errTestStorage()) mockEP := &epmocks.ElectionParticipant{} mockEP.On("Start").Return(nil) mockEP.On("Close").Return(nil) mockEP.On("IsLeader").Return(false) cfg := Options{ CalculationInterval: time.Millisecond * 5, AggregationBuckets: 2, BucketsForCalculation: 10, } agg, err := NewAggregator(cfg, logger, metrics.NullFactory, mockEP, mockStorage) require.NoError(t, err) agg.Start() for range 1000 { // match logs specific to getThroughputErrMsg. We expect to see more than 2, once during // initialization and one or more times during the loop. if match, _ := testutils.LogMatcher(2, getThroughputErrMsg, logBuffer.Lines()); match { break } time.Sleep(time.Millisecond) } match, errMsg := testutils.LogMatcher(2, getThroughputErrMsg, logBuffer.Lines()) assert.True(t, match, errMsg) require.NoError(t, agg.Close()) } func TestPrependBucket(t *testing.T) { p := &PostAggregator{Options: Options{AggregationBuckets: 1}} p.prependThroughputBucket(&throughputBucket{interval: time.Minute}) require.Len(t, p.throughputs, 1) assert.Equal(t, time.Minute, p.throughputs[0].interval) p.prependThroughputBucket(&throughputBucket{interval: 2 * time.Minute}) require.Len(t, p.throughputs, 1) assert.Equal(t, 2*time.Minute, p.throughputs[0].interval) } func TestConstructorFailure(t *testing.T) { logger := zap.NewNop() cfg := Options{ TargetSamplesPerSecond: 1.0, DeltaTolerance: 0.2, InitialSamplingProbability: 0.001, CalculationInterval: time.Second * 5, AggregationBuckets: 0, } _, err := newPostAggregator(cfg, "host", nil, nil, metrics.NullFactory, logger) require.EqualError(t, err, "CalculationInterval and AggregationBuckets must be greater than 0") cfg.CalculationInterval = 0 _, err = newPostAggregator(cfg, "host", nil, nil, metrics.NullFactory, logger) require.EqualError(t, err, "CalculationInterval and AggregationBuckets must be greater than 0") cfg.CalculationInterval = time.Millisecond cfg.AggregationBuckets = 1 cfg.BucketsForCalculation = -1 _, err = newPostAggregator(cfg, "host", nil, nil, metrics.NullFactory, logger) require.EqualError(t, err, "BucketsForCalculation cannot be less than 1") } func TestUsingAdaptiveSampling(t *testing.T) { p := &PostAggregator{} throughput := serviceOperationThroughput{ "svc": map[string]*model.Throughput{ "op": {Probabilities: map[string]struct{}{"0.010000": {}}}, }, } tests := []struct { expected bool probability float64 service string operation string }{ {expected: true, probability: 0.01, service: "svc", operation: "op"}, {expected: true, probability: 0.0099999384, service: "svc", operation: "op"}, {expected: false, probability: 0.01, service: "non-svc"}, {expected: false, probability: 0.01, service: "svc", operation: "non-op"}, {expected: false, probability: 0.01, service: "svc", operation: "non-op"}, {expected: false, probability: 0.02, service: "svc", operation: "op"}, {expected: false, probability: 0.0100009384, service: "svc", operation: "op"}, } for _, test := range tests { assert.Equal(t, test.expected, p.isUsingAdaptiveSampling(test.probability, test.service, test.operation, throughput)) } } func TestPrependServiceCache(t *testing.T) { p := &PostAggregator{} for range serviceCacheSize * 2 { p.prependServiceCache() } assert.Len(t, p.serviceCache, serviceCacheSize) } func TestCalculateProbabilitiesAndQPSMultiple(t *testing.T) { buckets := []*throughputBucket{ { throughput: serviceOperationThroughput{ "svcA": map[string]*model.Throughput{ http.MethodGet: {Count: 3, Probabilities: map[string]struct{}{"0.001000": {}}}, http.MethodPut: {Count: 60, Probabilities: map[string]struct{}{"0.001000": {}}}, }, "svcB": map[string]*model.Throughput{ http.MethodPut: {Count: 15, Probabilities: map[string]struct{}{"0.001000": {}}}, }, }, interval: 60 * time.Second, }, } p := &PostAggregator{ Options: Options{ TargetSamplesPerSecond: 1.0, DeltaTolerance: 0.002, InitialSamplingProbability: 0.001, BucketsForCalculation: 5, AggregationBuckets: 10, }, throughputs: buckets, probabilities: make(model.ServiceOperationProbabilities), qps: make(model.ServiceOperationQPS), weightVectorCache: NewWeightVectorCache(), probabilityCalculator: calculationstrategy.NewPercentageIncreaseCappedCalculator(1.0), serviceCache: []SamplingCache{}, operationsCalculatedGauge: metrics.NullFactory.Gauge(metrics.Options{}), } probabilities, qps := p.calculateProbabilitiesAndQPS() require.Len(t, probabilities, 2) assert.Equal(t, map[string]float64{http.MethodGet: 0.002, http.MethodPut: 0.001}, probabilities["svcA"]) assert.Equal(t, map[string]float64{http.MethodPut: 0.002}, probabilities["svcB"]) p.probabilities = probabilities p.qps = qps // svcA:GET is no longer reported, we should not increase it's probability since we don't know if it's adaptively sampled // until we get at least a lowerbound span or a probability span with the right probability. // svcB:PUT is only reporting lowerbound, we should boost it's probability p.prependThroughputBucket(&throughputBucket{ throughput: serviceOperationThroughput{ "svcA": map[string]*model.Throughput{ http.MethodPut: {Count: 60, Probabilities: map[string]struct{}{"0.001000": {}}}, }, "svcB": map[string]*model.Throughput{ http.MethodGet: {Count: 30, Probabilities: map[string]struct{}{"0.001000": {}}}, http.MethodPut: {Count: 0, Probabilities: map[string]struct{}{"0.002000": {}}}, }, }, interval: 60 * time.Second, }) probabilities, qps = p.calculateProbabilitiesAndQPS() require.Len(t, probabilities, 2) assert.Equal(t, map[string]float64{http.MethodGet: 0.002, http.MethodPut: 0.001}, probabilities["svcA"]) assert.Equal(t, map[string]float64{http.MethodPut: 0.004, http.MethodGet: 0.002}, probabilities["svcB"]) p.probabilities = probabilities p.qps = qps // svcA:GET is lower bound sampled, increase its probability // svcB:PUT is not reported but we should boost it's probability since the previous calculation showed that // it's using adaptive sampling p.prependThroughputBucket(&throughputBucket{ throughput: serviceOperationThroughput{ "svcA": map[string]*model.Throughput{ http.MethodGet: {Count: 0, Probabilities: map[string]struct{}{"0.002000": {}}}, http.MethodPut: {Count: 60, Probabilities: map[string]struct{}{"0.001000": {}}}, }, "svcB": map[string]*model.Throughput{ http.MethodGet: {Count: 30, Probabilities: map[string]struct{}{"0.001000": {}}}, }, }, interval: 60 * time.Second, }) probabilities, qps = p.calculateProbabilitiesAndQPS() require.Len(t, probabilities, 2) assert.Equal(t, map[string]float64{http.MethodGet: 0.004, http.MethodPut: 0.001}, probabilities["svcA"]) assert.Equal(t, map[string]float64{http.MethodPut: 0.008, http.MethodGet: 0.002}, probabilities["svcB"]) p.probabilities = probabilities p.qps = qps // svcA:GET is finally adaptively probabilistically sampled! // svcB:PUT stopped using adaptive sampling p.prependThroughputBucket(&throughputBucket{ throughput: serviceOperationThroughput{ "svcA": map[string]*model.Throughput{ http.MethodGet: {Count: 1, Probabilities: map[string]struct{}{"0.004000": {}}}, http.MethodPut: {Count: 60, Probabilities: map[string]struct{}{"0.001000": {}}}, }, "svcB": map[string]*model.Throughput{ http.MethodGet: {Count: 30, Probabilities: map[string]struct{}{"0.001000": {}}}, http.MethodPut: {Count: 15, Probabilities: map[string]struct{}{"0.001000": {}}}, }, }, interval: 60 * time.Second, }) probabilities, qps = p.calculateProbabilitiesAndQPS() require.Len(t, probabilities, 2) assert.Equal(t, map[string]float64{http.MethodGet: 0.008, http.MethodPut: 0.001}, probabilities["svcA"]) assert.Equal(t, map[string]float64{http.MethodPut: 0.008, http.MethodGet: 0.002}, probabilities["svcB"]) p.probabilities = probabilities p.qps = qps // svcA:GET didn't report anything p.prependThroughputBucket(&throughputBucket{ throughput: serviceOperationThroughput{ "svcA": map[string]*model.Throughput{ http.MethodPut: {Count: 30, Probabilities: map[string]struct{}{"0.001000": {}}}, }, "svcB": map[string]*model.Throughput{ http.MethodGet: {Count: 30, Probabilities: map[string]struct{}{"0.001000": {}}}, http.MethodPut: {Count: 15, Probabilities: map[string]struct{}{"0.001000": {}}}, }, }, interval: 60 * time.Second, }) probabilities, qps = p.calculateProbabilitiesAndQPS() require.Len(t, probabilities, 2) assert.Equal(t, map[string]float64{http.MethodGet: 0.016, http.MethodPut: 0.001468867216804201}, probabilities["svcA"]) assert.Equal(t, map[string]float64{http.MethodPut: 0.008, http.MethodGet: 0.002}, probabilities["svcB"]) p.probabilities = probabilities p.qps = qps // svcA:GET didn't report anything // svcB:PUT starts to use adaptive sampling again p.prependThroughputBucket(&throughputBucket{ throughput: serviceOperationThroughput{ "svcA": map[string]*model.Throughput{ http.MethodPut: {Count: 30, Probabilities: map[string]struct{}{"0.001000": {}}}, }, "svcB": map[string]*model.Throughput{ http.MethodGet: {Count: 30, Probabilities: map[string]struct{}{"0.001000": {}}}, http.MethodPut: {Count: 1, Probabilities: map[string]struct{}{"0.008000": {}}}, }, }, interval: 60 * time.Second, }) probabilities, qps = p.calculateProbabilitiesAndQPS() require.Len(t, probabilities, 2) assert.Equal(t, map[string]float64{http.MethodGet: 0.032, http.MethodPut: 0.001468867216804201}, probabilities["svcA"]) assert.Equal(t, map[string]float64{http.MethodPut: 0.016, http.MethodGet: 0.002}, probabilities["svcB"]) p.probabilities = probabilities p.qps = qps // svcA:GET didn't report anything // svcB:PUT didn't report anything p.prependThroughputBucket(&throughputBucket{ throughput: serviceOperationThroughput{ "svcA": map[string]*model.Throughput{ http.MethodPut: {Count: 30, Probabilities: map[string]struct{}{"0.001000": {}}}, }, "svcB": map[string]*model.Throughput{ http.MethodGet: {Count: 15, Probabilities: map[string]struct{}{"0.001000": {}}}, }, }, interval: 60 * time.Second, }) probabilities, qps = p.calculateProbabilitiesAndQPS() require.Len(t, probabilities, 2) assert.Equal(t, map[string]float64{http.MethodGet: 0.064, http.MethodPut: 0.001468867216804201}, probabilities["svcA"]) assert.Equal(t, map[string]float64{http.MethodPut: 0.032, http.MethodGet: 0.002}, probabilities["svcB"]) p.probabilities = probabilities p.qps = qps // svcA:GET didn't report anything // svcB:PUT didn't report anything p.prependThroughputBucket(&throughputBucket{ throughput: serviceOperationThroughput{ "svcA": map[string]*model.Throughput{ http.MethodPut: {Count: 20, Probabilities: map[string]struct{}{"0.001000": {}}}, }, "svcB": map[string]*model.Throughput{ http.MethodGet: {Count: 10, Probabilities: map[string]struct{}{"0.001000": {}}}, }, }, interval: 60 * time.Second, }) probabilities, qps = p.calculateProbabilitiesAndQPS() require.Len(t, probabilities, 2) assert.Equal(t, map[string]float64{http.MethodGet: 0.128, http.MethodPut: 0.001468867216804201}, probabilities["svcA"]) assert.Equal(t, map[string]float64{http.MethodPut: 0.064, http.MethodGet: 0.002}, probabilities["svcB"]) p.probabilities = probabilities p.qps = qps // svcA:GET didn't report anything // svcB:PUT didn't report anything p.prependThroughputBucket(&throughputBucket{ throughput: serviceOperationThroughput{ "svcA": map[string]*model.Throughput{ http.MethodPut: {Count: 20, Probabilities: map[string]struct{}{"0.001000": {}}}, http.MethodGet: {Count: 120, Probabilities: map[string]struct{}{"0.128000": {}}}, }, "svcB": map[string]*model.Throughput{ http.MethodPut: {Count: 60, Probabilities: map[string]struct{}{"0.064000": {}}}, http.MethodGet: {Count: 10, Probabilities: map[string]struct{}{"0.001000": {}}}, }, }, interval: 60 * time.Second, }) probabilities, qps = p.calculateProbabilitiesAndQPS() require.Len(t, probabilities, 2) assert.Equal(t, map[string]float64{http.MethodGet: 0.0882586677054928, http.MethodPut: 0.001468867216804201}, probabilities["svcA"]) assert.Equal(t, map[string]float64{http.MethodPut: 0.09587513707888091, http.MethodGet: 0.002}, probabilities["svcB"]) p.probabilities = probabilities p.qps = qps } func TestAddJitter(t *testing.T) { // zero duration: must not panic and must return 0 assert.Equal(t, 0*time.Nanosecond, addJitter(0)) // 1ns duration: jitterAmount/2 == 0, must not panic and must return 1ns assert.Equal(t, time.Nanosecond, addJitter(time.Nanosecond)) // normal durations: result must be in [jitterAmount/2, jitterAmount) for _, d := range []time.Duration{2 * time.Millisecond, time.Second, 20 * time.Second, 60 * time.Second} { for range 100 { got := addJitter(d) assert.GreaterOrEqual(t, got, d/2, "addJitter(%v) below lower bound", d) assert.Less(t, got, d, "addJitter(%v) at or above upper bound", d) } } } ================================================ FILE: internal/sampling/samplingstrategy/adaptive/provider.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package adaptive import ( "context" "sync" "time" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/proto-gen/api_v2" "github.com/jaegertracing/jaeger/internal/leaderelection" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore/model" ) const defaultFollowerProbabilityInterval = 20 * time.Second // Provider is responsible for providing sampling strategies for services. // It periodically loads sampling probabilities from storage and converts them // into sampling strategies that are cached and served to clients. // Provider relies on sampling probabilities being periodically updated by the // aggregator & post-aggregator. type Provider struct { Options mu sync.RWMutex electionParticipant leaderelection.ElectionParticipant storage samplingstore.Store logger *zap.Logger // probabilities contains the latest calculated sampling probabilities for service operations. probabilities model.ServiceOperationProbabilities // strategyResponses is the cache of the sampling strategies for every service, in protobuf format. strategyResponses map[string]*api_v2.SamplingStrategyResponse // followerRefreshInterval determines how often the follower processor updates its probabilities. // Given only the leader writes probabilities, the followers need to fetch the probabilities into // cache. followerRefreshInterval time.Duration shutdown chan struct{} bgFinished sync.WaitGroup } // NewProvider creates a strategy store that holds adaptive sampling strategies. func NewProvider(options Options, logger *zap.Logger, participant leaderelection.ElectionParticipant, store samplingstore.Store) *Provider { return &Provider{ Options: options, storage: store, probabilities: make(model.ServiceOperationProbabilities), strategyResponses: make(map[string]*api_v2.SamplingStrategyResponse), logger: logger, electionParticipant: participant, followerRefreshInterval: defaultFollowerProbabilityInterval, shutdown: make(chan struct{}), } } // Start initializes and starts the sampling service which regularly loads sampling probabilities and generates strategies. func (p *Provider) Start() error { p.logger.Info("starting adaptive sampling service") p.loadProbabilities() p.generateStrategyResponses() p.bgFinished.Go(func() { p.runUpdateProbabilitiesLoop() }) return nil } func (p *Provider) loadProbabilities() { // TODO GetLatestProbabilities API can be changed to return the latest measured qps for initialization probabilities, err := p.storage.GetLatestProbabilities() if err != nil { p.logger.Warn("failed to initialize probabilities", zap.Error(err)) return } p.mu.Lock() defer p.mu.Unlock() p.probabilities = probabilities } // runUpdateProbabilitiesLoop is a loop that reads probabilities from storage. // The follower updates its local cache with the latest probabilities and serves them. func (p *Provider) runUpdateProbabilitiesLoop() { select { case <-time.After(addJitter(p.followerRefreshInterval)): // continue after jitter delay case <-p.shutdown: return } ticker := time.NewTicker(p.followerRefreshInterval) defer ticker.Stop() for { select { case <-ticker.C: // Only load probabilities if this strategy_store doesn't hold the leader lock if !p.isLeader() { p.loadProbabilities() p.generateStrategyResponses() } case <-p.shutdown: return } } } func (p *Provider) isLeader() bool { return p.electionParticipant.IsLeader() } // generateStrategyResponses generates and caches SamplingStrategyResponse from the calculated sampling probabilities. func (p *Provider) generateStrategyResponses() { p.mu.RLock() strategies := make(map[string]*api_v2.SamplingStrategyResponse) for svc, opProbabilities := range p.probabilities { opStrategies := make([]*api_v2.OperationSamplingStrategy, len(opProbabilities)) var idx int for op, probability := range opProbabilities { opStrategies[idx] = &api_v2.OperationSamplingStrategy{ Operation: op, ProbabilisticSampling: &api_v2.ProbabilisticSamplingStrategy{ SamplingRate: probability, }, } idx++ } strategy := p.generateDefaultSamplingStrategyResponse() strategy.OperationSampling.PerOperationStrategies = opStrategies strategies[svc] = strategy } p.mu.RUnlock() p.mu.Lock() defer p.mu.Unlock() p.strategyResponses = strategies } func (p *Provider) generateDefaultSamplingStrategyResponse() *api_v2.SamplingStrategyResponse { return &api_v2.SamplingStrategyResponse{ StrategyType: api_v2.SamplingStrategyType_PROBABILISTIC, OperationSampling: &api_v2.PerOperationSamplingStrategies{ DefaultSamplingProbability: p.InitialSamplingProbability, DefaultLowerBoundTracesPerSecond: p.MinSamplesPerSecond, }, } } // GetSamplingStrategy implements protobuf endpoint for retrieving sampling strategy for a service. func (p *Provider) GetSamplingStrategy(_ context.Context, service string) (*api_v2.SamplingStrategyResponse, error) { p.mu.RLock() defer p.mu.RUnlock() if strategy, ok := p.strategyResponses[service]; ok { return strategy, nil } return p.generateDefaultSamplingStrategyResponse(), nil } // Close stops the service from loading probabilities and generating strategies. func (p *Provider) Close() error { p.logger.Info("stopping adaptive sampling service") close(p.shutdown) p.bgFinished.Wait() return nil } ================================================ FILE: internal/sampling/samplingstrategy/adaptive/provider_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package adaptive import ( "context" "net/http" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/proto-gen/api_v2" epmocks "github.com/jaegertracing/jaeger/internal/leaderelection/mocks" smocks "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore/mocks" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore/model" ) func TestProviderLoadProbabilities(t *testing.T) { mockStorage := &smocks.Store{} mockStorage.On("GetLatestProbabilities").Return(make(model.ServiceOperationProbabilities), nil) p := &Provider{storage: mockStorage} require.Nil(t, p.probabilities) p.loadProbabilities() require.NotNil(t, p.probabilities) } func TestProviderRunUpdateProbabilitiesLoop(t *testing.T) { mockStorage := &smocks.Store{} mockStorage.On("GetLatestProbabilities").Return(make(model.ServiceOperationProbabilities), nil) mockEP := &epmocks.ElectionParticipant{} mockEP.On("Start").Return(nil) mockEP.On("Close").Return(nil) mockEP.On("IsLeader").Return(false) p := &Provider{ storage: mockStorage, shutdown: make(chan struct{}), followerRefreshInterval: time.Millisecond, electionParticipant: mockEP, } defer close(p.shutdown) require.Nil(t, p.probabilities) require.Nil(t, p.strategyResponses) go p.runUpdateProbabilitiesLoop() for range 1000 { p.mu.RLock() if p.probabilities != nil && p.strategyResponses != nil { p.mu.RUnlock() break } p.mu.RUnlock() time.Sleep(time.Millisecond) } p.mu.RLock() assert.NotNil(t, p.probabilities) assert.NotNil(t, p.strategyResponses) p.mu.RUnlock() } func TestProviderRealisticRunCalculationLoop(t *testing.T) { t.Skip("Skipped realistic calculation loop test") logger := zap.NewNop() // NB: This is an extremely long test since it uses near realistic (1/6th scale) processor config values testThroughputs := []*model.Throughput{ {Service: "svcA", Operation: http.MethodGet, Count: 10}, {Service: "svcA", Operation: http.MethodPost, Count: 9}, {Service: "svcA", Operation: http.MethodPut, Count: 5}, {Service: "svcA", Operation: http.MethodDelete, Count: 20}, } mockStorage := &smocks.Store{} mockStorage.On("GetThroughput", mock.AnythingOfType("time.Time"), mock.AnythingOfType("time.Time")). Return(testThroughputs, nil) mockStorage.On("GetLatestProbabilities").Return(make(model.ServiceOperationProbabilities), nil) mockStorage.On("InsertProbabilitiesAndQPS", "host", mock.AnythingOfType("model.ServiceOperationProbabilities"), mock.AnythingOfType("model.ServiceOperationQPS")).Return(nil) mockEP := &epmocks.ElectionParticipant{} mockEP.On("Start").Return(nil) mockEP.On("Close").Return(nil) mockEP.On("IsLeader").Return(true) cfg := Options{ TargetSamplesPerSecond: 1.0, DeltaTolerance: 0.2, InitialSamplingProbability: 0.001, CalculationInterval: time.Second * 10, AggregationBuckets: 1, Delay: time.Second * 10, } s := NewProvider(cfg, logger, mockEP, mockStorage) s.Start() for range 100 { strategy, _ := s.GetSamplingStrategy(context.Background(), "svcA") if len(strategy.OperationSampling.PerOperationStrategies) != 0 { break } time.Sleep(250 * time.Millisecond) } s.Close() strategy, err := s.GetSamplingStrategy(context.Background(), "svcA") require.NoError(t, err) require.Len(t, strategy.OperationSampling.PerOperationStrategies, 4) strategies := strategy.OperationSampling.PerOperationStrategies for _, s := range strategies { switch s.Operation { case http.MethodGet: assert.InDelta(t, 0.001, s.ProbabilisticSampling.SamplingRate, 1e-4, "Already at 1QPS, no probability change") case http.MethodPost: assert.InDelta(t, 0.001, s.ProbabilisticSampling.SamplingRate, 1e-4, "Within epsilon of 1QPS, no probability change") case http.MethodPut: assert.InEpsilon(t, 0.002, s.ProbabilisticSampling.SamplingRate, 0.025, "Under sampled, double probability") case http.MethodDelete: assert.InEpsilon(t, 0.0005, s.ProbabilisticSampling.SamplingRate, 0.025, "Over sampled, halve probability") default: t.Errorf("Unexpected operation: %s", s.Operation) } } } func TestProviderGenerateStrategyResponses(t *testing.T) { probabilities := model.ServiceOperationProbabilities{ "svcA": map[string]float64{ http.MethodGet: 0.5, }, } p := &Provider{ probabilities: probabilities, Options: Options{ InitialSamplingProbability: 0.001, MinSamplesPerSecond: 0.0001, }, } p.generateStrategyResponses() expectedResponse := map[string]*api_v2.SamplingStrategyResponse{ "svcA": { StrategyType: api_v2.SamplingStrategyType_PROBABILISTIC, OperationSampling: &api_v2.PerOperationSamplingStrategies{ DefaultSamplingProbability: 0.001, DefaultLowerBoundTracesPerSecond: 0.0001, PerOperationStrategies: []*api_v2.OperationSamplingStrategy{ { Operation: http.MethodGet, ProbabilisticSampling: &api_v2.ProbabilisticSamplingStrategy{ SamplingRate: 0.5, }, }, }, }, }, } assert.Equal(t, expectedResponse, p.strategyResponses) } ================================================ FILE: internal/sampling/samplingstrategy/adaptive/weightvectorcache.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package adaptive import ( "math" "sync" ) // WeightVectorCache stores normalizing weight vectors of different lengths. // The head of each weight vector contains the largest weight. type WeightVectorCache struct { mu sync.Mutex cache map[int][]float64 } // NewWeightVectorCache returns a new weights vector cache. func NewWeightVectorCache() *WeightVectorCache { // TODO allow users to plugin different weighting algorithms return &WeightVectorCache{ cache: make(map[int][]float64), } } // GetWeights returns weights for the specified length { w(i) = i ^ 4, i=1..L }, normalized. func (c *WeightVectorCache) GetWeights(length int) []float64 { c.mu.Lock() defer c.mu.Unlock() if weights, ok := c.cache[length]; ok { return weights } weights := make([]float64, 0, length) var sum float64 for i := length; i > 0; i-- { w := math.Pow(float64(i), 4) weights = append(weights, w) sum += w } // normalize for i := range length { weights[i] /= sum } c.cache[length] = weights return weights } ================================================ FILE: internal/sampling/samplingstrategy/adaptive/weightvectorcache_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package adaptive import ( "testing" "github.com/stretchr/testify/assert" ) func TestGetWeights(t *testing.T) { c := NewWeightVectorCache() weights := c.GetWeights(1) assert.Len(t, weights, 1) weights = c.GetWeights(3) assert.Len(t, weights, 3) assert.InDelta(t, 0.8265306122448979, weights[0], 0.001) weights = c.GetWeights(5) assert.Len(t, weights, 5) assert.InDelta(t, 0.6384, weights[0], 0.001) assert.InDelta(t, 0.0010, weights[4], 0.001) } ================================================ FILE: internal/sampling/samplingstrategy/aggregator.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package samplingstrategy import ( "io" "github.com/jaegertracing/jaeger-idl/model/v1" ) // Aggregator defines an interface used to aggregate operation throughput. type Aggregator interface { // Close() from io.Closer stops the aggregator from aggregating throughput. io.Closer // The HandleRootSpan function processes a span, checking if it's a root span. // If it is, it extracts sampler parameters, then calls RecordThroughput. HandleRootSpan(span *model.Span) // RecordThroughput records throughput for an operation for aggregation. RecordThroughput(service, operation string, samplerType model.SamplerType, probability float64) // Start starts aggregating operation throughput. Start() } ================================================ FILE: internal/sampling/samplingstrategy/empty_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package samplingstrategy import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/sampling/samplingstrategy/factory.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package samplingstrategy import ( "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/storage/v1" ) // Factory defines an interface for a factory that can create implementations of different sampling strategy components. // Implementations are also encouraged to implement storage.Configurable interface. // // # See also // // storage.Configurable type Factory interface { // Initialize performs internal initialization of the factory. Initialize(metricsFactory metrics.Factory, ssFactory storage.SamplingStoreFactory, logger *zap.Logger) error // CreateStrategyProvider initializes and returns Provider and optionallty Aggregator. CreateStrategyProvider() (Provider, Aggregator, error) // Close closes the factory Close() error } ================================================ FILE: internal/sampling/samplingstrategy/file/constants.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package file import ( "github.com/jaegertracing/jaeger-idl/proto-gen/api_v2" ) const ( // samplerTypeProbabilistic is the type of sampler that samples traces // with a certain fixed probability. samplerTypeProbabilistic = "probabilistic" // samplerTypeRateLimiting is the type of sampler that samples // only up to a fixed number of traces per second. samplerTypeRateLimiting = "ratelimiting" // DefaultSamplingProbability is the default value for "DefaultSamplingProbability" // used by the Strategy Store in case no DefaultSamplingProbability is defined DefaultSamplingProbability = 0.001 ) // defaultStrategy is the default sampling strategy the Strategy Store will return // if none is provided. func defaultStrategyResponse(defaultSamplingProbability float64) *api_v2.SamplingStrategyResponse { return &api_v2.SamplingStrategyResponse{ StrategyType: api_v2.SamplingStrategyType_PROBABILISTIC, ProbabilisticSampling: &api_v2.ProbabilisticSamplingStrategy{ SamplingRate: defaultSamplingProbability, }, } } func defaultStrategies(defaultSamplingProbability float64) *storedStrategies { s := &storedStrategies{ serviceStrategies: make(map[string]*api_v2.SamplingStrategyResponse), } s.defaultStrategy = defaultStrategyResponse(defaultSamplingProbability) return s } ================================================ FILE: internal/sampling/samplingstrategy/file/fixtures/TestServiceNoPerOperationStrategiesDeprecatedBehavior_ServiceA.json ================================================ { "probabilisticSampling": { "samplingRate": 1 }, "operationSampling": { "defaultSamplingProbability": 1, "perOperationStrategies": [ { "operation": "/health", "probabilisticSampling": { "samplingRate": 0.1 } } ] } } ================================================ FILE: internal/sampling/samplingstrategy/file/fixtures/TestServiceNoPerOperationStrategiesDeprecatedBehavior_ServiceB.json ================================================ { "strategyType": 1, "rateLimitingSampling": { "maxTracesPerSecond": 3 } } ================================================ FILE: internal/sampling/samplingstrategy/file/fixtures/TestServiceNoPerOperationStrategies_ServiceA.json ================================================ { "probabilisticSampling": { "samplingRate": 1 }, "operationSampling": { "defaultSamplingProbability": 1, "perOperationStrategies": [ { "operation": "/health", "probabilisticSampling": { "samplingRate": 0.1 } } ] } } ================================================ FILE: internal/sampling/samplingstrategy/file/fixtures/TestServiceNoPerOperationStrategies_ServiceB.json ================================================ { "strategyType": 1, "rateLimitingSampling": { "maxTracesPerSecond": 3 }, "operationSampling": { "defaultSamplingProbability": 0.2, "perOperationStrategies": [ { "operation": "/health", "probabilisticSampling": { "samplingRate": 0.1 } } ] } } ================================================ FILE: internal/sampling/samplingstrategy/file/fixtures/bad_strategies.json ================================================ "nonsense" ================================================ FILE: internal/sampling/samplingstrategy/file/fixtures/missing-service-types.json ================================================ { "default_strategy": { "type": "probabilistic", "param": 0.5 }, "service_strategies": [ { "service": "foo", "operation_strategies": [ { "operation": "op1", "type": "probabilistic", "param": 0.2 } ] }, { "service": "bar", "operation_strategies": [ { "operation": "op3", "type": "probabilistic", "param": 0.3 }, { "operation": "op5", "type": "probabilistic", "param": 0.4 } ] } ] } ================================================ FILE: internal/sampling/samplingstrategy/file/fixtures/operation_strategies.json ================================================ { "default_strategy": { "type": "probabilistic", "param": 0.5, "operation_strategies": [ { "operation": "op0", "type": "probabilistic", "param": 0.2 }, { "operation": "op6", "type": "probabilistic", "param": 0 }, { "operation": "spam", "type": "ratelimiting", "param": 1 }, { "operation": "op7", "type": "probabilistic", "param": 1 } ] }, "service_strategies": [ { "service": "foo", "type": "probabilistic", "param": 0.8, "operation_strategies": [ { "operation": "op6", "type": "probabilistic", "param": 0.5 }, { "operation": "op1", "type": "probabilistic", "param": 0.2 }, { "operation": "op2", "type": "ratelimiting", "param": 10 } ] }, { "service": "bar", "type": "ratelimiting", "param": 5, "operation_strategies": [ { "operation": "op3", "type": "probabilistic", "param": 0.3 }, { "operation": "op4", "type": "ratelimiting", "param": 100 }, { "operation": "op5", "type": "probabilistic", "param": 0.4 } ] } ] } ================================================ FILE: internal/sampling/samplingstrategy/file/fixtures/service_no_per_operation.json ================================================ { "service_strategies": [ { "service": "ServiceA", "type": "probabilistic", "param": 1.0 }, { "service": "ServiceB", "type": "ratelimiting", "param": 3 } ], "default_strategy": { "type": "probabilistic", "param": 0.2, "operation_strategies": [ { "operation": "/health", "type": "probabilistic", "param": 0.1 } ] } } ================================================ FILE: internal/sampling/samplingstrategy/file/fixtures/strategies.json ================================================ { "default_strategy": { "type": "probabilistic", "param": 0.5 }, "service_strategies": [ { "service": "foo", "type": "probabilistic", "param": 0.8 }, { "service": "bar", "type": "ratelimiting", "param": 5 } ] } ================================================ FILE: internal/sampling/samplingstrategy/file/options.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package file import ( "time" ) // Options holds configuration for the static sampling strategy store. type Options struct { // StrategiesFile is the path for the sampling strategies file in JSON format StrategiesFile string // ReloadInterval is the time interval to check and reload sampling strategies file ReloadInterval time.Duration // DefaultSamplingProbability is the sampling probability used by the Strategy Store for static sampling DefaultSamplingProbability float64 } ================================================ FILE: internal/sampling/samplingstrategy/file/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package file import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/sampling/samplingstrategy/file/provider.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package file import ( "bytes" "context" "encoding/json" "fmt" "net/http" "net/url" "os" "path/filepath" "sync/atomic" "time" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/proto-gen/api_v2" "github.com/jaegertracing/jaeger/internal/sampling/samplingstrategy" ) // null represents "null" JSON value and // it un-marshals to nil pointer. var nullJSON = []byte("null") type samplingProvider struct { logger *zap.Logger storedStrategies atomic.Value // holds *storedStrategies cancelFunc context.CancelFunc options Options } type storedStrategies struct { defaultStrategy *api_v2.SamplingStrategyResponse serviceStrategies map[string]*api_v2.SamplingStrategyResponse } type strategyLoader func() ([]byte, error) // NewProvider creates a strategy store that holds static sampling strategies. func NewProvider(options Options, logger *zap.Logger) (samplingstrategy.Provider, error) { ctx, cancelFunc := context.WithCancel(context.Background()) h := &samplingProvider{ logger: logger, cancelFunc: cancelFunc, options: options, } h.storedStrategies.Store(defaultStrategies(options.DefaultSamplingProbability)) if options.StrategiesFile == "" { h.logger.Info("No sampling strategies source provided, using defaults") return h, nil } loadFn := h.samplingStrategyLoader(options.StrategiesFile) strategies, err := loadStrategies(loadFn) if err != nil { return nil, err } else if strategies == nil { h.logger.Info("No sampling strategies found or URL is unavailable, using defaults") return h, nil } h.parseStrategies(strategies) if options.ReloadInterval > 0 { go h.autoUpdateStrategies(ctx, loadFn) } return h, nil } // GetSamplingStrategy implements StrategyStore#GetSamplingStrategy. func (h *samplingProvider) GetSamplingStrategy(_ context.Context, serviceName string) (*api_v2.SamplingStrategyResponse, error) { storedStrategies := h.storedStrategies.Load().(*storedStrategies) serviceStrategies := storedStrategies.serviceStrategies if strategy, ok := serviceStrategies[serviceName]; ok { return strategy, nil } h.logger.Debug("sampling strategy not found, using default", zap.String("service", serviceName)) return storedStrategies.defaultStrategy, nil } // Close stops updating the strategies func (h *samplingProvider) Close() error { h.cancelFunc() return nil } func (h *samplingProvider) downloadSamplingStrategies(samplingURL string) ([]byte, error) { h.logger.Info("Downloading sampling strategies", zap.String("url", samplingURL)) ctx, cx := context.WithTimeout(context.Background(), time.Second) defer cx() req, err := http.NewRequestWithContext(ctx, http.MethodGet, samplingURL, http.NoBody) if err != nil { return nil, fmt.Errorf("cannot construct HTTP request: %w", err) } resp, err := http.DefaultClient.Do(req) //nolint:gosec // G704 - URL from config if err != nil { return nil, fmt.Errorf("failed to download sampling strategies: %w", err) } defer resp.Body.Close() buf := new(bytes.Buffer) if _, err = buf.ReadFrom(resp.Body); err != nil { return nil, fmt.Errorf("failed to read sampling strategies HTTP response body: %w", err) } if resp.StatusCode == http.StatusServiceUnavailable { return nullJSON, nil } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf( "receiving %s while downloading strategies file: %s", resp.Status, buf.String(), ) } return buf.Bytes(), nil } func isURL(str string) bool { u, err := url.Parse(str) return err == nil && u.Scheme != "" && u.Host != "" } func (h *samplingProvider) samplingStrategyLoader(strategiesFile string) strategyLoader { if isURL(strategiesFile) { return func() ([]byte, error) { return h.downloadSamplingStrategies(strategiesFile) } } return func() ([]byte, error) { currBytes, err := os.ReadFile(filepath.Clean(strategiesFile)) if err != nil { return nil, fmt.Errorf("failed to read strategies file %s: %w", strategiesFile, err) } return currBytes, nil } } func (h *samplingProvider) autoUpdateStrategies(ctx context.Context, loader strategyLoader) { lastValue := string(nullJSON) ticker := time.NewTicker(h.options.ReloadInterval) defer ticker.Stop() for { select { case <-ticker.C: lastValue = h.reloadSamplingStrategy(loader, lastValue) case <-ctx.Done(): return } } } func (h *samplingProvider) reloadSamplingStrategy(loadFn strategyLoader, lastValue string) string { newValue, err := loadFn() if err != nil { h.logger.Error("failed to re-load sampling strategies", zap.Error(err)) return lastValue } if lastValue == string(newValue) { return lastValue } if err := h.updateSamplingStrategy(newValue); err != nil { h.logger.Error("failed to update sampling strategies", zap.Error(err)) return lastValue } return string(newValue) } func (h *samplingProvider) updateSamplingStrategy(dataBytes []byte) error { var strategies strategies if err := json.Unmarshal(dataBytes, &strategies); err != nil { return fmt.Errorf("failed to unmarshal sampling strategies: %w", err) } h.parseStrategies(&strategies) h.logger.Info("Updated sampling strategies:" + string(dataBytes)) return nil } // TODO good candidate for a global util function func loadStrategies(loadFn strategyLoader) (*strategies, error) { strategyBytes, err := loadFn() if err != nil { return nil, err } var strategies *strategies if err := json.Unmarshal(strategyBytes, &strategies); err != nil { return nil, fmt.Errorf("failed to unmarshal strategies: %w", err) } return strategies, nil } func (h *samplingProvider) parseStrategies(strategies *strategies) { newStore := defaultStrategies(h.options.DefaultSamplingProbability) if strategies.DefaultStrategy != nil { newStore.defaultStrategy = h.parseServiceStrategies(strategies.DefaultStrategy) } for _, s := range strategies.ServiceStrategies { newStore.serviceStrategies[s.Service] = h.parseServiceStrategies(s) // Config for this service may not have per-operation strategies, // but if the default strategy has them they should still apply. if newStore.defaultStrategy.OperationSampling == nil { // Default strategy doens't have them either, nothing to do. continue } opS := newStore.serviceStrategies[s.Service].OperationSampling if opS == nil { // Service does not have its own per-operation rules, so copy (by value) from the default strategy. newOpS := *newStore.defaultStrategy.OperationSampling // If the service's own default is probabilistic, then its sampling rate should take precedence. if newStore.serviceStrategies[s.Service].ProbabilisticSampling != nil { newOpS.DefaultSamplingProbability = newStore.serviceStrategies[s.Service].ProbabilisticSampling.SamplingRate } newStore.serviceStrategies[s.Service].OperationSampling = &newOpS continue } // If the service did have its own per-operation strategies, then merge them with the default ones. opS.PerOperationStrategies = mergePerOperationSamplingStrategies( opS.PerOperationStrategies, newStore.defaultStrategy.OperationSampling.PerOperationStrategies) } h.storedStrategies.Store(newStore) } // mergePerOperationSamplingStrategies merges two operation strategies a and b, where a takes precedence over b. func mergePerOperationSamplingStrategies( a, b []*api_v2.OperationSamplingStrategy, ) []*api_v2.OperationSamplingStrategy { m := make(map[string]bool) for _, aOp := range a { m[aOp.Operation] = true } for _, bOp := range b { if m[bOp.Operation] { continue } a = append(a, bOp) } return a } func (h *samplingProvider) parseServiceStrategies(strategy *serviceStrategy) *api_v2.SamplingStrategyResponse { resp := h.parseStrategy(&strategy.strategy) if len(strategy.OperationStrategies) == 0 { return resp } opS := &api_v2.PerOperationSamplingStrategies{ DefaultSamplingProbability: h.options.DefaultSamplingProbability, } if resp.StrategyType == api_v2.SamplingStrategyType_PROBABILISTIC { opS.DefaultSamplingProbability = resp.ProbabilisticSampling.SamplingRate } for _, operationStrategy := range strategy.OperationStrategies { s, ok := h.parseOperationStrategy(operationStrategy, opS) if !ok { continue } opS.PerOperationStrategies = append(opS.PerOperationStrategies, &api_v2.OperationSamplingStrategy{ Operation: operationStrategy.Operation, ProbabilisticSampling: s.ProbabilisticSampling, }) } resp.OperationSampling = opS return resp } func (h *samplingProvider) parseOperationStrategy( strategy *operationStrategy, parent *api_v2.PerOperationSamplingStrategies, ) (s *api_v2.SamplingStrategyResponse, ok bool) { s = h.parseStrategy(&strategy.strategy) if s.StrategyType == api_v2.SamplingStrategyType_RATE_LIMITING { // TODO OperationSamplingStrategy only supports probabilistic sampling h.logger.Warn( fmt.Sprintf( "Operation strategies only supports probabilistic sampling at the moment,"+ "'%s' defaulting to probabilistic sampling with probability %f", strategy.Operation, parent.DefaultSamplingProbability), zap.Any("strategy", strategy)) return nil, false } return s, true } func (h *samplingProvider) parseStrategy(strategy *strategy) *api_v2.SamplingStrategyResponse { switch strategy.Type { case samplerTypeProbabilistic: return &api_v2.SamplingStrategyResponse{ StrategyType: api_v2.SamplingStrategyType_PROBABILISTIC, ProbabilisticSampling: &api_v2.ProbabilisticSamplingStrategy{ SamplingRate: strategy.Param, }, } case samplerTypeRateLimiting: return &api_v2.SamplingStrategyResponse{ StrategyType: api_v2.SamplingStrategyType_RATE_LIMITING, RateLimitingSampling: &api_v2.RateLimitingSamplingStrategy{ MaxTracesPerSecond: int32(strategy.Param), }, } default: h.logger.Warn("Failed to parse sampling strategy", zap.Any("strategy", strategy)) return defaultStrategyResponse(h.options.DefaultSamplingProbability) } } ================================================ FILE: internal/sampling/samplingstrategy/file/provider_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package file import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "sync/atomic" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zaptest/observer" "github.com/jaegertracing/jaeger-idl/proto-gen/api_v2" "github.com/jaegertracing/jaeger/internal/testutils" ) const snapshotLocation = "./fixtures/" // Snapshots can be regenerated via: // // REGENERATE_SNAPSHOTS=true go test -v ./plugin/sampling/strategyprovider/static/provider_test.go var regenerateSnapshots = os.Getenv("REGENERATE_SNAPSHOTS") == "true" // strategiesJSON returns the strategy with // a given probability. func strategiesJSON(probability float32) string { strategy := fmt.Sprintf(` { "default_strategy": { "type": "probabilistic", "param": 0.5 }, "service_strategies": [ { "service": "foo", "type": "probabilistic", "param": %.1f }, { "service": "bar", "type": "ratelimiting", "param": 5 } ] } `, probability, ) return strategy } // Returns strategies in JSON format. Used for testing // URL option for sampling strategies. func mockStrategyServer(t *testing.T) (*httptest.Server, *atomic.Pointer[string]) { var strategy atomic.Pointer[string] value := strategiesJSON(0.8) strategy.Store(&value) f := func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/bad-content": w.Write([]byte("bad-content")) return case "/bad-status": w.WriteHeader(http.StatusNotFound) return case "/service-unavailable": w.WriteHeader(http.StatusServiceUnavailable) return default: w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") w.Write([]byte(*strategy.Load())) } } mockserver := httptest.NewServer(http.HandlerFunc(f)) t.Cleanup(func() { mockserver.Close() }) return mockserver, &strategy } func TestStrategyStoreWithFile(t *testing.T) { _, err := NewProvider(Options{StrategiesFile: "fileNotFound.json", DefaultSamplingProbability: DefaultSamplingProbability}, zap.NewNop()) require.ErrorContains(t, err, "failed to read strategies file fileNotFound.json") _, err = NewProvider(Options{StrategiesFile: "fixtures/bad_strategies.json", DefaultSamplingProbability: DefaultSamplingProbability}, zap.NewNop()) require.EqualError(t, err, "failed to unmarshal strategies: json: cannot unmarshal string into Go value of type file.strategies") // Test default strategy logger, buf := testutils.NewLogger() provider, err := NewProvider(Options{DefaultSamplingProbability: DefaultSamplingProbability}, logger) require.NoError(t, err) assert.Contains(t, buf.String(), "No sampling strategies source provided, using defaults") s, err := provider.GetSamplingStrategy(context.Background(), "foo") require.NoError(t, err) assert.Equal(t, makeResponse(api_v2.SamplingStrategyType_PROBABILISTIC, 0.001), *s) // Test reading strategies from a file provider, err = NewProvider(Options{StrategiesFile: "fixtures/strategies.json"}, logger) require.NoError(t, err) s, err = provider.GetSamplingStrategy(context.Background(), "foo") require.NoError(t, err) assert.Equal(t, makeResponse(api_v2.SamplingStrategyType_PROBABILISTIC, 0.8), *s) s, err = provider.GetSamplingStrategy(context.Background(), "bar") require.NoError(t, err) assert.Equal(t, makeResponse(api_v2.SamplingStrategyType_RATE_LIMITING, 5), *s) s, err = provider.GetSamplingStrategy(context.Background(), "default") require.NoError(t, err) assert.Equal(t, makeResponse(api_v2.SamplingStrategyType_PROBABILISTIC, 0.5), *s) } func TestStrategyStoreWithURL(t *testing.T) { // Test default strategy when URL is temporarily unavailable. logger, buf := testutils.NewLogger() mockServer, _ := mockStrategyServer(t) provider, err := NewProvider(Options{StrategiesFile: mockServer.URL + "/service-unavailable", DefaultSamplingProbability: DefaultSamplingProbability}, logger) require.NoError(t, err) assert.Contains(t, buf.String(), "No sampling strategies found or URL is unavailable, using defaults") s, err := provider.GetSamplingStrategy(context.Background(), "foo") require.NoError(t, err) assert.Equal(t, makeResponse(api_v2.SamplingStrategyType_PROBABILISTIC, 0.001), *s) // Test downloading strategies from a URL. provider, err = NewProvider(Options{StrategiesFile: mockServer.URL}, logger) require.NoError(t, err) s, err = provider.GetSamplingStrategy(context.Background(), "foo") require.NoError(t, err) assert.Equal(t, makeResponse(api_v2.SamplingStrategyType_PROBABILISTIC, 0.8), *s) s, err = provider.GetSamplingStrategy(context.Background(), "bar") require.NoError(t, err) assert.Equal(t, makeResponse(api_v2.SamplingStrategyType_RATE_LIMITING, 5), *s) } func TestPerOperationSamplingStrategies(t *testing.T) { tests := []struct { options Options }{ {Options{ StrategiesFile: "fixtures/operation_strategies.json", DefaultSamplingProbability: DefaultSamplingProbability, }}, } for _, tc := range tests { logger, buf := testutils.NewLogger() provider, err := NewProvider(tc.options, logger) assert.Contains(t, buf.String(), "Operation strategies only supports probabilistic sampling at the moment,"+ "'op2' defaulting to probabilistic sampling with probability 0.8") assert.Contains(t, buf.String(), "Operation strategies only supports probabilistic sampling at the moment,"+ "'op4' defaulting to probabilistic sampling with probability 0.001") require.NoError(t, err) expected := makeResponse(api_v2.SamplingStrategyType_PROBABILISTIC, 0.8) s, err := provider.GetSamplingStrategy(context.Background(), "foo") require.NoError(t, err) assert.Equal(t, api_v2.SamplingStrategyType_PROBABILISTIC, s.StrategyType) assert.Equal(t, *expected.ProbabilisticSampling, *s.ProbabilisticSampling) require.NotNil(t, s.OperationSampling) opSampling := s.OperationSampling assert.InDelta(t, 0.8, opSampling.DefaultSamplingProbability, 0.01) require.Len(t, opSampling.PerOperationStrategies, 4) assert.Equal(t, "op6", opSampling.PerOperationStrategies[0].Operation) assert.InDelta(t, 0.5, opSampling.PerOperationStrategies[0].ProbabilisticSampling.SamplingRate, 0.01) assert.Equal(t, "op1", opSampling.PerOperationStrategies[1].Operation) assert.InDelta(t, 0.2, opSampling.PerOperationStrategies[1].ProbabilisticSampling.SamplingRate, 0.01) assert.Equal(t, "op0", opSampling.PerOperationStrategies[2].Operation) assert.InDelta(t, 0.2, opSampling.PerOperationStrategies[2].ProbabilisticSampling.SamplingRate, 0.01) assert.Equal(t, "op7", opSampling.PerOperationStrategies[3].Operation) assert.InDelta(t, 1.0, opSampling.PerOperationStrategies[3].ProbabilisticSampling.SamplingRate, 0.01) expected = makeResponse(api_v2.SamplingStrategyType_RATE_LIMITING, 5) s, err = provider.GetSamplingStrategy(context.Background(), "bar") require.NoError(t, err) assert.Equal(t, api_v2.SamplingStrategyType_RATE_LIMITING, s.StrategyType) assert.Equal(t, *expected.RateLimitingSampling, *s.RateLimitingSampling) require.NotNil(t, s.OperationSampling) opSampling = s.OperationSampling assert.InDelta(t, 0.001, opSampling.DefaultSamplingProbability, 1e-4) require.Len(t, opSampling.PerOperationStrategies, 5) assert.Equal(t, "op3", opSampling.PerOperationStrategies[0].Operation) assert.InDelta(t, 0.3, opSampling.PerOperationStrategies[0].ProbabilisticSampling.SamplingRate, 0.01) assert.Equal(t, "op5", opSampling.PerOperationStrategies[1].Operation) assert.InDelta(t, 0.4, opSampling.PerOperationStrategies[1].ProbabilisticSampling.SamplingRate, 0.01) assert.Equal(t, "op0", opSampling.PerOperationStrategies[2].Operation) assert.InDelta(t, 0.2, opSampling.PerOperationStrategies[2].ProbabilisticSampling.SamplingRate, 0.01) assert.Equal(t, "op6", opSampling.PerOperationStrategies[3].Operation) assert.InDelta(t, 0.0, opSampling.PerOperationStrategies[3].ProbabilisticSampling.SamplingRate, 0.01) assert.Equal(t, "op7", opSampling.PerOperationStrategies[4].Operation) assert.InDelta(t, 1.0, opSampling.PerOperationStrategies[4].ProbabilisticSampling.SamplingRate, 0.01) s, err = provider.GetSamplingStrategy(context.Background(), "default") require.NoError(t, err) expectedRsp := makeResponse(api_v2.SamplingStrategyType_PROBABILISTIC, 0.5) expectedRsp.OperationSampling = &api_v2.PerOperationSamplingStrategies{ DefaultSamplingProbability: 0.5, PerOperationStrategies: []*api_v2.OperationSamplingStrategy{ { Operation: "op0", ProbabilisticSampling: &api_v2.ProbabilisticSamplingStrategy{ SamplingRate: 0.2, }, }, { Operation: "op6", ProbabilisticSampling: &api_v2.ProbabilisticSamplingStrategy{ SamplingRate: 0, }, }, { Operation: "op7", ProbabilisticSampling: &api_v2.ProbabilisticSamplingStrategy{ SamplingRate: 1, }, }, }, } assert.Equal(t, expectedRsp, *s) } } func TestMissingServiceSamplingStrategyTypes(t *testing.T) { logger, buf := testutils.NewLogger() provider, err := NewProvider(Options{StrategiesFile: "fixtures/missing-service-types.json", DefaultSamplingProbability: DefaultSamplingProbability}, logger) assert.Contains(t, buf.String(), "Failed to parse sampling strategy") require.NoError(t, err) expected := makeResponse(api_v2.SamplingStrategyType_PROBABILISTIC, DefaultSamplingProbability) s, err := provider.GetSamplingStrategy(context.Background(), "foo") require.NoError(t, err) assert.Equal(t, api_v2.SamplingStrategyType_PROBABILISTIC, s.StrategyType) assert.Equal(t, *expected.ProbabilisticSampling, *s.ProbabilisticSampling) require.NotNil(t, s.OperationSampling) opSampling := s.OperationSampling assert.InDelta(t, DefaultSamplingProbability, opSampling.DefaultSamplingProbability, 1e-4) require.Len(t, opSampling.PerOperationStrategies, 1) assert.Equal(t, "op1", opSampling.PerOperationStrategies[0].Operation) assert.InDelta(t, 0.2, opSampling.PerOperationStrategies[0].ProbabilisticSampling.SamplingRate, 0.001) expected = makeResponse(api_v2.SamplingStrategyType_PROBABILISTIC, DefaultSamplingProbability) s, err = provider.GetSamplingStrategy(context.Background(), "bar") require.NoError(t, err) assert.Equal(t, api_v2.SamplingStrategyType_PROBABILISTIC, s.StrategyType) assert.Equal(t, *expected.ProbabilisticSampling, *s.ProbabilisticSampling) require.NotNil(t, s.OperationSampling) opSampling = s.OperationSampling assert.InDelta(t, 0.001, opSampling.DefaultSamplingProbability, 1e-4) require.Len(t, opSampling.PerOperationStrategies, 2) assert.Equal(t, "op3", opSampling.PerOperationStrategies[0].Operation) assert.InDelta(t, 0.3, opSampling.PerOperationStrategies[0].ProbabilisticSampling.SamplingRate, 0.01) assert.Equal(t, "op5", opSampling.PerOperationStrategies[1].Operation) assert.InDelta(t, 0.4, opSampling.PerOperationStrategies[1].ProbabilisticSampling.SamplingRate, 0.01) s, err = provider.GetSamplingStrategy(context.Background(), "default") require.NoError(t, err) assert.Equal(t, makeResponse(api_v2.SamplingStrategyType_PROBABILISTIC, 0.5), *s) } func TestParseStrategy(t *testing.T) { tests := []struct { strategy serviceStrategy expected api_v2.SamplingStrategyResponse }{ { strategy: serviceStrategy{ Service: "svc", strategy: strategy{Type: "probabilistic", Param: 0.2}, }, expected: makeResponse(api_v2.SamplingStrategyType_PROBABILISTIC, 0.2), }, { strategy: serviceStrategy{ Service: "svc", strategy: strategy{Type: "ratelimiting", Param: 3.5}, }, expected: makeResponse(api_v2.SamplingStrategyType_RATE_LIMITING, 3), }, } logger, buf := testutils.NewLogger() provider := &samplingProvider{options: Options{DefaultSamplingProbability: DefaultSamplingProbability}, logger: logger} for _, test := range tests { tt := test t.Run("", func(t *testing.T) { assert.Equal(t, tt.expected, *provider.parseStrategy(&tt.strategy.strategy)) }) } assert.Empty(t, buf.String()) // Test nonexistent strategy type actual := *provider.parseStrategy(&strategy{Type: "blah", Param: 3.5}) expected := makeResponse(api_v2.SamplingStrategyType_PROBABILISTIC, provider.options.DefaultSamplingProbability) assert.Equal(t, expected, actual) assert.Contains(t, buf.String(), "Failed to parse sampling strategy") } func makeResponse(samplerType api_v2.SamplingStrategyType, param float64) (resp api_v2.SamplingStrategyResponse) { resp.StrategyType = samplerType switch samplerType { case api_v2.SamplingStrategyType_PROBABILISTIC: resp.ProbabilisticSampling = &api_v2.ProbabilisticSamplingStrategy{ SamplingRate: param, } case api_v2.SamplingStrategyType_RATE_LIMITING: resp.RateLimitingSampling = &api_v2.RateLimitingSamplingStrategy{ MaxTracesPerSecond: int32(param), } default: } return resp } func TestAutoUpdateStrategyWithFile(t *testing.T) { tempFile, _ := os.Create(t.TempDir() + "for_go_test_*.json") require.NoError(t, tempFile.Close()) // copy known fixture content into temp file which we can later overwrite srcFile, dstFile := "fixtures/strategies.json", tempFile.Name() srcBytes, err := os.ReadFile(srcFile) require.NoError(t, err) require.NoError(t, os.WriteFile(dstFile, srcBytes, 0o644)) ss, err := NewProvider(Options{ StrategiesFile: dstFile, ReloadInterval: time.Millisecond * 10, }, zap.NewNop()) require.NoError(t, err) provider := ss.(*samplingProvider) defer provider.Close() // confirm baseline value s, err := provider.GetSamplingStrategy(context.Background(), "foo") require.NoError(t, err) assert.Equal(t, makeResponse(api_v2.SamplingStrategyType_PROBABILISTIC, 0.8), *s) // verify that reloading is a no-op value := provider.reloadSamplingStrategy(provider.samplingStrategyLoader(dstFile), string(srcBytes)) assert.Equal(t, string(srcBytes), value) // update file with new probability of 0.9 newStr := strings.Replace(string(srcBytes), "0.8", "0.9", 1) require.NoError(t, os.WriteFile(dstFile, []byte(newStr), 0o644)) // wait for reload timer for range 1000 { // wait up to 1sec s, err = provider.GetSamplingStrategy(context.Background(), "foo") require.NoError(t, err) if s.ProbabilisticSampling != nil && s.ProbabilisticSampling.SamplingRate == 0.9 { break } time.Sleep(1 * time.Millisecond) } assert.Equal(t, makeResponse(api_v2.SamplingStrategyType_PROBABILISTIC, 0.9), *s) } func TestAutoUpdateStrategyWithURL(t *testing.T) { mockServer, mockStrategy := mockStrategyServer(t) ss, err := NewProvider(Options{ DefaultSamplingProbability: DefaultSamplingProbability, StrategiesFile: mockServer.URL, ReloadInterval: 10 * time.Millisecond, }, zap.NewNop()) require.NoError(t, err) provider := ss.(*samplingProvider) defer provider.Close() // confirm baseline value s, err := provider.GetSamplingStrategy(context.Background(), "foo") require.NoError(t, err) assert.Equal(t, makeResponse(api_v2.SamplingStrategyType_PROBABILISTIC, 0.8), *s) // verify that reloading in no-op value := provider.reloadSamplingStrategy( provider.samplingStrategyLoader(mockServer.URL), *mockStrategy.Load(), ) assert.Equal(t, *mockStrategy.Load(), value) // update original strategies with new probability of 0.9 { v09 := strategiesJSON(0.9) mockStrategy.Store(&v09) } // wait for reload timer for range 1000 { // wait up to 1sec s, err = provider.GetSamplingStrategy(context.Background(), "foo") require.NoError(t, err) if s.ProbabilisticSampling != nil && s.ProbabilisticSampling.SamplingRate == 0.9 { break } time.Sleep(1 * time.Millisecond) } assert.Equal(t, makeResponse(api_v2.SamplingStrategyType_PROBABILISTIC, 0.9), *s) } func TestAutoUpdateStrategyErrors(t *testing.T) { tempFile, _ := os.Create(t.TempDir() + "for_go_test_*.json") require.NoError(t, tempFile.Close()) zapCore, logs := observer.New(zap.InfoLevel) logger := zap.New(zapCore) s, err := NewProvider(Options{ StrategiesFile: "fixtures/strategies.json", ReloadInterval: time.Hour, }, logger) require.NoError(t, err) provider := s.(*samplingProvider) defer provider.Close() // check invalid file path or read failure assert.Equal(t, "blah", provider.reloadSamplingStrategy(provider.samplingStrategyLoader(tempFile.Name()+"bad-path"), "blah")) assert.Len(t, logs.FilterMessage("failed to re-load sampling strategies").All(), 1) // check bad file content require.NoError(t, os.WriteFile(tempFile.Name(), []byte("bad value"), 0o644)) assert.Equal(t, "blah", provider.reloadSamplingStrategy(provider.samplingStrategyLoader(tempFile.Name()), "blah")) assert.Len(t, logs.FilterMessage("failed to update sampling strategies").All(), 1) // check invalid url assert.Equal(t, "duh", provider.reloadSamplingStrategy(provider.samplingStrategyLoader("bad-url"), "duh")) assert.Len(t, logs.FilterMessage("failed to re-load sampling strategies").All(), 2) // check status code other than 200 mockServer, _ := mockStrategyServer(t) assert.Equal(t, "duh", provider.reloadSamplingStrategy(provider.samplingStrategyLoader(mockServer.URL+"/bad-status"), "duh")) assert.Len(t, logs.FilterMessage("failed to re-load sampling strategies").All(), 3) // check bad content from url assert.Equal(t, "duh", provider.reloadSamplingStrategy(provider.samplingStrategyLoader(mockServer.URL+"/bad-content"), "duh")) assert.Len(t, logs.FilterMessage("failed to update sampling strategies").All(), 2) } func TestServiceNoPerOperationStrategies(t *testing.T) { // given setup of strategy provider with no specific per operation sampling strategies // and option "sampling.strategies.bugfix-5270=true" provider, err := NewProvider(Options{ StrategiesFile: "fixtures/service_no_per_operation.json", }, zap.NewNop()) require.NoError(t, err) for _, service := range []string{"ServiceA", "ServiceB"} { t.Run(service, func(t *testing.T) { strategy, err := provider.GetSamplingStrategy(context.Background(), service) require.NoError(t, err) strategyJson, err := json.MarshalIndent(strategy, "", " ") require.NoError(t, err) testName := strings.ReplaceAll(t.Name(), "/", "_") snapshotFile := filepath.Join(snapshotLocation, testName+".json") expectedServiceResponse, err := os.ReadFile(snapshotFile) require.NoError(t, err) assert.JSONEq(t, string(expectedServiceResponse), string(strategyJson), "comparing against stored snapshot. Use REGENERATE_SNAPSHOTS=true to rebuild snapshots.") if regenerateSnapshots { os.WriteFile(snapshotFile, strategyJson, 0o644) } }) } } func TestSamplingStrategyLoader(t *testing.T) { provider := &samplingProvider{logger: zap.NewNop()} // invalid file path loader := provider.samplingStrategyLoader("not-exists") _, err := loader() require.ErrorContains(t, err, "failed to read strategies file not-exists") // status code other than 200 mockServer, _ := mockStrategyServer(t) loader = provider.samplingStrategyLoader(mockServer.URL + "/bad-status") _, err = loader() require.ErrorContains(t, err, "receiving 404 Not Found while downloading strategies file") // should download content from URL loader = provider.samplingStrategyLoader(mockServer.URL + "/bad-content") content, err := loader() require.NoError(t, err) assert.Equal(t, "bad-content", string(content)) } ================================================ FILE: internal/sampling/samplingstrategy/file/strategy.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package file // strategy defines a sampling strategy. Type can be "probabilistic" or "ratelimiting" // and Param will represent "sampling probability" and "max traces per second" respectively. type strategy struct { Type string `json:"type"` Param float64 `json:"param"` } // operationStrategy defines an operation specific sampling strategy. type operationStrategy struct { Operation string `json:"operation"` strategy } // serviceStrategy defines a service specific sampling strategy. type serviceStrategy struct { Service string `json:"service"` OperationStrategies []*operationStrategy `json:"operation_strategies"` strategy } // strategies holds a default sampling strategy and service specific sampling strategies. type strategies struct { DefaultStrategy *serviceStrategy `json:"default_strategy"` ServiceStrategies []*serviceStrategy `json:"service_strategies"` } ================================================ FILE: internal/sampling/samplingstrategy/provider.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package samplingstrategy import ( "context" "io" "github.com/jaegertracing/jaeger-idl/proto-gen/api_v2" ) // Provider keeps track of service specific sampling strategies. type Provider interface { // Close() from io.Closer stops the processor from calculating probabilities. io.Closer // GetSamplingStrategy retrieves the sampling strategy for the specified service. GetSamplingStrategy(ctx context.Context, serviceName string) (*api_v2.SamplingStrategyResponse, error) } ================================================ FILE: internal/storage/cassandra/config/config.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package config import ( "context" "errors" "fmt" "time" gocql "github.com/apache/cassandra-gocql-driver/v2" "github.com/apache/cassandra-gocql-driver/v2/snappy" "github.com/asaskevich/govalidator" "go.opentelemetry.io/collector/config/configtls" ) // Configuration describes the configuration properties needed to connect to a Cassandra cluster. type Configuration struct { Schema Schema `mapstructure:"schema"` Connection Connection `mapstructure:"connection"` Query Query `mapstructure:"query"` } type Connection struct { // Servers contains a list of hosts that are used to connect to the cluster. Servers []string `mapstructure:"servers" valid:"required,url"` // LocalDC contains the name of the local Data Center (DC) for DC-aware host selection LocalDC string `mapstructure:"local_dc"` // The port used when dialing to a cluster. Port int `mapstructure:"port"` // DisableAutoDiscovery, if set to true, will disable the cluster's auto-discovery features. DisableAutoDiscovery bool `mapstructure:"disable_auto_discovery"` // ConnectionsPerHost contains the maximum number of open connections for each host on the cluster. ConnectionsPerHost int `mapstructure:"connections_per_host"` // ReconnectInterval contains the regular interval after which the driver tries to connect to // nodes that are down. ReconnectInterval time.Duration `mapstructure:"reconnect_interval"` // SocketKeepAlive contains the keep alive period for the default dialer to the cluster. SocketKeepAlive time.Duration `mapstructure:"socket_keep_alive"` // TLS contains the TLS configuration for the connection to the cluster. TLS configtls.ClientConfig `mapstructure:"tls"` // Timeout contains the maximum time spent to connect to a cluster. Timeout time.Duration `mapstructure:"timeout"` // Authenticator contains the details of the authentication mechanism that is used for // connecting to a cluster. Authenticator Authenticator `mapstructure:"auth"` // ProtoVersion contains the version of the native protocol to use when connecting to a cluster. ProtoVersion int `mapstructure:"proto_version"` } type Schema struct { // Keyspace contains the namespace where Jaeger data will be stored. Keyspace string `mapstructure:"keyspace"` // DisableCompression, if set to true, disables the use of the default Snappy Compression // while connecting to the Cassandra Cluster. This is useful for connecting to clusters, like Azure Cosmos DB, // that do not support SnappyCompression. DisableCompression bool `mapstructure:"disable_compression"` // CreateSchema tells if the schema ahould be created during session initialization based on the configs provided CreateSchema bool `mapstructure:"create" valid:"optional"` // Datacenter is the name for network topology Datacenter string `mapstructure:"datacenter" valid:"optional"` // TraceTTL is Time To Live (TTL) for the trace data. Should at least be 1 second TraceTTL time.Duration `mapstructure:"trace_ttl" valid:"optional"` // DependenciesTTL is Time To Live (TTL) for dependencies data. Should at least be 1 second DependenciesTTL time.Duration `mapstructure:"dependencies_ttl" valid:"optional"` // Replication factor for the db ReplicationFactor int `mapstructure:"replication_factor" valid:"optional"` // CompactionWindow is the size of the window for TimeWindowCompactionStrategy. // All SSTables within that window are grouped together into one SSTable. // Ideally, operators should select a compaction window size that produces approximately less than 50 windows. // For example, if writing with a 90 day TTL, a 3 day window would be a reasonable choice. CompactionWindow time.Duration `mapstructure:"compaction_window" valid:"optional"` } type Query struct { // Timeout contains the maximum time spent executing a query. Timeout time.Duration `mapstructure:"timeout"` // MaxRetryAttempts indicates the maximum number of times a query will be retried for execution. MaxRetryAttempts int `mapstructure:"max_retry_attempts"` // Consistency specifies the consistency level which needs to be satisified before responding // to a query. Consistency string `mapstructure:"consistency"` } // Authenticator holds the authentication properties needed to connect to a Cassandra cluster. type Authenticator struct { Basic BasicAuthenticator `mapstructure:"basic"` // TODO: add more auth types } // BasicAuthenticator holds the username and password for a password authenticator for a Cassandra cluster. type BasicAuthenticator struct { Username string `mapstructure:"username"` Password string `mapstructure:"password" json:"-"` AllowedAuthenticators []string `mapstructure:"allowed_authenticators"` } func DefaultConfiguration() Configuration { return Configuration{ Schema: Schema{ CreateSchema: false, Keyspace: "jaeger_dc1", Datacenter: "dc1", TraceTTL: 2 * 24 * time.Hour, DependenciesTTL: 2 * 24 * time.Hour, ReplicationFactor: 1, CompactionWindow: 2 * time.Hour, }, Connection: Connection{ Servers: []string{"127.0.0.1"}, Port: 9042, ProtoVersion: 4, ConnectionsPerHost: 2, ReconnectInterval: 60 * time.Second, }, Query: Query{ MaxRetryAttempts: 3, }, } } // ApplyDefaults copies settings from source unless its own value is non-zero. func (c *Configuration) ApplyDefaults(source *Configuration) { if c.Schema.Keyspace == "" { c.Schema.Keyspace = source.Schema.Keyspace } if c.Schema.Datacenter == "" { c.Schema.Datacenter = source.Schema.Datacenter } if c.Schema.TraceTTL == 0 { c.Schema.TraceTTL = source.Schema.TraceTTL } if c.Schema.DependenciesTTL == 0 { c.Schema.DependenciesTTL = source.Schema.DependenciesTTL } if c.Schema.ReplicationFactor == 0 { c.Schema.ReplicationFactor = source.Schema.ReplicationFactor } if c.Schema.CompactionWindow == 0 { c.Schema.CompactionWindow = source.Schema.CompactionWindow } if c.Connection.ConnectionsPerHost == 0 { c.Connection.ConnectionsPerHost = source.Connection.ConnectionsPerHost } if c.Connection.ReconnectInterval == 0 { c.Connection.ReconnectInterval = source.Connection.ReconnectInterval } if c.Connection.Port == 0 { c.Connection.Port = source.Connection.Port } if c.Connection.ProtoVersion == 0 { c.Connection.ProtoVersion = source.Connection.ProtoVersion } if c.Connection.SocketKeepAlive == 0 { c.Connection.SocketKeepAlive = source.Connection.SocketKeepAlive } if c.Query.MaxRetryAttempts == 0 { c.Query.MaxRetryAttempts = source.Query.MaxRetryAttempts } if c.Query.Timeout == 0 { c.Query.Timeout = source.Query.Timeout } } // NewCluster creates a new gocql cluster from the configuration func (c *Configuration) NewCluster() (*gocql.ClusterConfig, error) { cluster := gocql.NewCluster(c.Connection.Servers...) cluster.Keyspace = c.Schema.Keyspace cluster.NumConns = c.Connection.ConnectionsPerHost cluster.ConnectTimeout = c.Connection.Timeout cluster.ReconnectInterval = c.Connection.ReconnectInterval cluster.SocketKeepalive = c.Connection.SocketKeepAlive cluster.Timeout = c.Query.Timeout if c.Connection.ProtoVersion > 0 { cluster.ProtoVersion = c.Connection.ProtoVersion } if c.Query.MaxRetryAttempts > 1 { cluster.RetryPolicy = &gocql.SimpleRetryPolicy{NumRetries: c.Query.MaxRetryAttempts - 1} } if c.Connection.Port != 0 { cluster.Port = c.Connection.Port } if !c.Schema.DisableCompression { cluster.Compressor = &snappy.SnappyCompressor{} } if c.Query.Consistency == "" { cluster.Consistency = gocql.LocalOne } else { cluster.Consistency = gocql.ParseConsistency(c.Query.Consistency) } fallbackHostSelectionPolicy := gocql.RoundRobinHostPolicy() if c.Connection.LocalDC != "" { fallbackHostSelectionPolicy = gocql.DCAwareRoundRobinPolicy(c.Connection.LocalDC) } cluster.PoolConfig.HostSelectionPolicy = gocql.TokenAwareHostPolicy(fallbackHostSelectionPolicy, gocql.ShuffleReplicas()) if c.Connection.Authenticator.Basic.Username != "" && c.Connection.Authenticator.Basic.Password != "" { cluster.Authenticator = gocql.PasswordAuthenticator{ Username: c.Connection.Authenticator.Basic.Username, Password: c.Connection.Authenticator.Basic.Password, AllowedAuthenticators: c.Connection.Authenticator.Basic.AllowedAuthenticators, } } if !c.Connection.TLS.Insecure { tlsCfg, err := c.Connection.TLS.LoadTLSConfig(context.Background()) if err != nil { return nil, err } cluster.SslOpts = &gocql.SslOptions{ Config: tlsCfg, } } // If tunneling connection to C*, disable cluster autodiscovery features. if c.Connection.DisableAutoDiscovery { cluster.DisableInitialHostLookup = true cluster.IgnorePeerAddr = true } return cluster, nil } func (c *Configuration) String() string { return fmt.Sprintf("%+v", *c) } func isValidTTL(duration time.Duration) bool { return duration == 0 || duration >= time.Second } func (c *Configuration) Validate() error { _, err := govalidator.ValidateStruct(c) if err != nil { return err } if !isValidTTL(c.Schema.TraceTTL) { return errors.New("trace_ttl can either be 0 or greater than or equal to 1 second") } if !isValidTTL(c.Schema.DependenciesTTL) { return errors.New("dependencies_ttl can either be 0 or greater than or equal to 1 second") } if c.Schema.CompactionWindow < time.Minute { return errors.New("compaction_window should at least be 1 minute") } return nil } ================================================ FILE: internal/storage/cassandra/config/config_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package config import ( "testing" "time" gocql "github.com/apache/cassandra-gocql-driver/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestValidate_ReturnsErrorWhenInvalid(t *testing.T) { tests := []struct { name string cfg *Configuration }{ { name: "missing required fields", cfg: &Configuration{}, }, { name: "require fields in invalid format", cfg: &Configuration{ Connection: Connection{ Servers: []string{"not a url"}, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { err := test.cfg.Validate() require.Error(t, err) }) } } func TestValidate_DoesNotReturnErrorWhenRequiredFieldsSet(t *testing.T) { cfg := Configuration{ Connection: Connection{ Servers: []string{"localhost:9200"}, }, Schema: Schema{ CompactionWindow: time.Minute, }, } err := cfg.Validate() require.NoError(t, err) } func TestNewClusterWithDefaults(t *testing.T) { cfg := DefaultConfiguration() cl, err := cfg.NewCluster() require.NoError(t, err) assert.NotEmpty(t, cl.Keyspace) } func TestNewClusterWithOverrides(t *testing.T) { cfg := DefaultConfiguration() cfg.Query.Consistency = "LOCAL_QUORUM" cfg.Connection.LocalDC = "local_dc" cfg.Connection.Authenticator.Basic.Username = "username" cfg.Connection.Authenticator.Basic.Password = "password" cfg.Connection.TLS.Insecure = false cfg.Connection.DisableAutoDiscovery = true cl, err := cfg.NewCluster() require.NoError(t, err) assert.NotEmpty(t, cl.Keyspace) assert.Equal(t, gocql.LocalQuorum, cl.Consistency) assert.NotNil(t, cl.PoolConfig.HostSelectionPolicy, "local_dc") require.IsType(t, gocql.PasswordAuthenticator{}, cl.Authenticator) auth := cl.Authenticator.(gocql.PasswordAuthenticator) assert.Equal(t, "username", auth.Username) assert.Equal(t, "password", auth.Password) assert.NotNil(t, cl.SslOpts) assert.True(t, cl.DisableInitialHostLookup) } func TestApplyDefaults(t *testing.T) { cfg1 := DefaultConfiguration() cfg2 := Configuration{} cfg2.ApplyDefaults(&cfg1) assert.Equal(t, cfg2.Schema, cfg1.Schema) assert.Equal(t, cfg2.Query, cfg1.Query) assert.NotEqual(t, cfg2.Connection.Servers, cfg1.Connection.Servers, "servers not copied") cfg1.Connection.Servers = nil assert.Equal(t, cfg2.Connection, cfg1.Connection) } func TestToString(t *testing.T) { cfg := DefaultConfiguration() cfg.Schema.Keyspace = "test" s := cfg.String() assert.Contains(t, s, "Keyspace:test") } func TestConfigSchemaValidation(t *testing.T) { cfg := DefaultConfiguration() err := cfg.Validate() require.NoError(t, err) cfg.Schema.TraceTTL = time.Millisecond err = cfg.Validate() require.Error(t, err) cfg.Schema.TraceTTL = time.Second cfg.Schema.CompactionWindow = time.Minute - 1 err = cfg.Validate() require.Error(t, err) cfg.Schema.CompactionWindow = time.Minute cfg.Schema.DependenciesTTL = time.Second - 1 err = cfg.Validate() require.Error(t, err) } ================================================ FILE: internal/storage/cassandra/config/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package config import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/cassandra/empty_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package cassandra import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/cassandra/gocql/empty_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package gocql import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/cassandra/gocql/gocql.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package gocql import ( gocql "github.com/apache/cassandra-gocql-driver/v2" "github.com/jaegertracing/jaeger/internal/storage/cassandra" ) // CQLSession is a wrapper around gocql.Session. type CQLSession struct { session *gocql.Session } // WrapCQLSession creates a Session out of *gocql.Session. func WrapCQLSession(session *gocql.Session) CQLSession { return CQLSession{session: session} } // Query delegates to gocql.Session#Query and wraps the result as Query. func (s CQLSession) Query(stmt string, values ...any) cassandra.Query { return WrapCQLQuery(s.session.Query(stmt, values...)) } // Close delegates to gocql.Session#Close. func (s CQLSession) Close() { s.session.Close() } // --- // CQLQuery is a wrapper around gocql.Query. type CQLQuery struct { query *gocql.Query } // WrapCQLQuery creates a Query out of *gocql.Query. func WrapCQLQuery(query *gocql.Query) CQLQuery { return CQLQuery{query: query} } // Exec delegates to gocql.Query#Exec. func (q CQLQuery) Exec() error { return q.query.Exec() } // ScanCAS delegates to gocql.Query#ScanCAS. func (q CQLQuery) ScanCAS(dest ...any) (bool, error) { return q.query.ScanCAS(dest...) } // Iter delegates to gocql.Query#Iter and wraps the result as Iterator. func (q CQLQuery) Iter() cassandra.Iterator { return WrapCQLIterator(q.query.Iter()) } // Bind delegates to gocql.Query#Bind and wraps the result as Query. func (q CQLQuery) Bind(v ...any) cassandra.Query { return WrapCQLQuery(q.query.Bind(v...)) } // Consistency delegates to gocql.Query#Consistency and wraps the result as Query. func (q CQLQuery) Consistency(level cassandra.Consistency) cassandra.Query { return WrapCQLQuery(q.query.Consistency(gocql.Consistency(level))) } // String returns string representation of this query. func (q CQLQuery) String() string { return q.query.String() } // PageSize delegates to gocql.Query#PageSize and wraps the result as Query. func (q CQLQuery) PageSize(n int) cassandra.Query { return WrapCQLQuery(q.query.PageSize(n)) } // --- // CQLIterator is a wrapper around gocql.Iter. type CQLIterator struct { iter *gocql.Iter } // WrapCQLIterator creates an Iterator out of *gocql.Iter. func WrapCQLIterator(iter *gocql.Iter) CQLIterator { return CQLIterator{iter: iter} } // Scan delegates to gocql.Iter#Scan. func (i CQLIterator) Scan(dest ...any) bool { return i.iter.Scan(dest...) } // Close delegates to gocql.Iter#Close. func (i CQLIterator) Close() error { return i.iter.Close() } ================================================ FILE: internal/storage/cassandra/gocql/testutils/udt.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package testutils import ( "testing" gocql "github.com/apache/cassandra-gocql-driver/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // UDTField describes a field in a gocql User Defined Type type UDTField struct { Name string Type gocql.Type ValIn []byte // value to attempt to marshal Err bool // is error expected? } // UDTTestCase describes a test for a UDT type UDTTestCase struct { Obj gocql.UDTMarshaler ObjName string New func() gocql.UDTUnmarshaler Fields []UDTField } // Run runs a test case func (testCase UDTTestCase) Run(t *testing.T) { for _, ff := range testCase.Fields { field := ff // capture loop var t.Run(testCase.ObjName+"-"+field.Name, func(t *testing.T) { // Create TypeInfo using NewNativeType // For error test cases with invalid type, use TypeVarchar as a default fieldType := field.Type if fieldType == 0 { fieldType = gocql.TypeVarchar } typeInfo := gocql.NewNativeType(0x03, fieldType, "") data, err := testCase.Obj.MarshalUDT(field.Name, typeInfo) if field.Err { require.Error(t, err) } else { require.NoError(t, err) assert.Equal(t, field.ValIn, data) } obj := testCase.New() err = obj.UnmarshalUDT(field.Name, typeInfo, field.ValIn) if field.Err { require.Error(t, err) } else { require.NoError(t, err) } }) } } ================================================ FILE: internal/storage/cassandra/gocql/testutils/udt_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package testutils_test import ( "testing" gocql "github.com/apache/cassandra-gocql-driver/v2" gocqlutils "github.com/jaegertracing/jaeger/internal/storage/cassandra/gocql/testutils" "github.com/jaegertracing/jaeger/internal/testutils" ) // CustomUDT is a custom type that implements gocql.UDTMarshaler and gocql.UDTUnmarshaler interfaces. type CustomUDT struct { Field1 int Field2 string } // MarshalUDT implements the gocql.UDTMarshaler interface. func (c *CustomUDT) MarshalUDT(name string, info gocql.TypeInfo) ([]byte, error) { switch name { case "Field1": return gocql.Marshal(info, c.Field1) case "Field2": return gocql.Marshal(info, c.Field2) default: return nil, gocql.ErrNotFound } } // UnmarshalUDT implements the gocql.UDTUnmarshaler interface. func (c *CustomUDT) UnmarshalUDT(name string, info gocql.TypeInfo, data []byte) error { switch name { case "Field1": return gocql.Unmarshal(info, data, &c.Field1) case "Field2": return gocql.Unmarshal(info, data, &c.Field2) default: return gocql.ErrNotFound } } func TestUDTTestCase(t *testing.T) { udtInstance := &CustomUDT{ Field1: 1, Field2: "test", } // Define UDT fields for testing udtFields := []gocqlutils.UDTField{ { Name: "Field1", Type: gocql.TypeBigInt, ValIn: []byte{0, 0, 0, 0, 0, 0, 0, 1}, Err: false, }, { Name: "Field2", Type: gocql.TypeVarchar, ValIn: []byte("test"), Err: false, }, { Name: "InvalidField", Type: gocql.TypeBigInt, ValIn: []byte("test"), Err: true, }, { // Type is omitted (zero value), should default to TypeVarchar Name: "InvalidFieldType", ValIn: []byte("test"), Err: true, }, } // Create a UDTTestCase testCase := gocqlutils.UDTTestCase{ Obj: udtInstance, ObjName: "CustomUDT", New: func() gocql.UDTUnmarshaler { return &CustomUDT{} }, Fields: udtFields, } testCase.Run(t) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/cassandra/metrics/table.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package metrics import ( "fmt" "time" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/storage/cassandra" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore/spanstoremetrics" ) // Table is a collection of metrics about Cassandra write operations. type Table struct { spanstoremetrics.WriteMetrics } // NewTable takes a metrics scope and creates a table metrics struct func NewTable(factory metrics.Factory, tableName string) *Table { t := spanstoremetrics.WriteMetrics{} metrics.Init(&t, factory.Namespace(metrics.NSOptions{Name: "", Tags: map[string]string{"table": tableName}}), nil) return &Table{t} } // Exec executes an update query and reports metrics/logs about it. func (t *Table) Exec(query cassandra.UpdateQuery, logger *zap.Logger) error { start := time.Now() err := query.Exec() t.Emit(err, time.Since(start)) if err != nil { queryString := query.String() if logger != nil { logger.Error("Failed to exec query", zap.String("query", queryString), zap.Error(err)) } return fmt.Errorf("failed to Exec query '%s': %w", queryString, err) } return nil } ================================================ FILE: internal/storage/cassandra/metrics/table_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package metrics import ( "errors" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/metricstest" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestTableEmit(t *testing.T) { testCases := []struct { err error counts map[string]int64 gauges map[string]int64 }{ { err: nil, counts: map[string]int64{ "attempts|table=a_table": 1, "inserts|table=a_table": 1, }, gauges: map[string]int64{ "latency-ok|table=a_table.P999": 50, "latency-ok|table=a_table.P50": 50, "latency-ok|table=a_table.P75": 50, "latency-ok|table=a_table.P90": 50, "latency-ok|table=a_table.P95": 50, "latency-ok|table=a_table.P99": 50, }, }, { err: errors.New("some error"), counts: map[string]int64{ "attempts|table=a_table": 1, "errors|table=a_table": 1, }, gauges: map[string]int64{ "latency-err|table=a_table.P999": 50, "latency-err|table=a_table.P50": 50, "latency-err|table=a_table.P75": 50, "latency-err|table=a_table.P90": 50, "latency-err|table=a_table.P95": 50, "latency-err|table=a_table.P99": 50, }, }, } for _, tc := range testCases { mf := metricstest.NewFactory(time.Second) tm := NewTable(mf, "a_table") tm.Emit(tc.err, 50*time.Millisecond) counts, gauges := mf.Snapshot() assert.Equal(t, tc.counts, counts) assert.Equal(t, tc.gauges, gauges) mf.Stop() } } func TestTableExec(t *testing.T) { testCases := []struct { q insertQuery log bool counts map[string]int64 }{ { q: insertQuery{}, counts: map[string]int64{ "attempts|table=a_table": 1, "inserts|table=a_table": 1, }, }, { q: insertQuery{ str: "SELECT * FROM something", err: errors.New("failed"), }, counts: map[string]int64{ "attempts|table=a_table": 1, "errors|table=a_table": 1, }, }, { q: insertQuery{ str: "SELECT * FROM something", err: errors.New("failed"), }, log: true, counts: map[string]int64{ "attempts|table=a_table": 1, "errors|table=a_table": 1, }, }, } for _, tc := range testCases { mf := metricstest.NewFactory(0) tm := NewTable(mf, "a_table") logger, logBuf := testutils.NewLogger() useLogger := logger if !tc.log { useLogger = nil } err := tm.Exec(tc.q, useLogger) if tc.q.err == nil { require.NoError(t, err) assert.Empty(t, logBuf.Bytes()) } else { require.Error(t, err, tc.q.err.Error()) if tc.log { assert.Equal(t, map[string]string{ "level": "error", "msg": "Failed to exec query", "query": "SELECT * FROM something", "error": "failed", }, logBuf.JSONLine(0)) } else { assert.Empty(t, logBuf.Bytes()) } } counts, _ := mf.Snapshot() assert.Equal(t, tc.counts, counts) } } type insertQuery struct { err error str string } func (q insertQuery) Exec() error { return q.err } func (q insertQuery) String() string { return q.str } func (insertQuery) ScanCAS(...any /* dest */) (bool, error) { return true, nil } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/cassandra/mocks/mocks.go ================================================ // Copyright (c) The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // // Run 'make generate-mocks' to regenerate. // Code generated by mockery; DO NOT EDIT. // github.com/vektra/mockery // template: testify package mocks import ( "github.com/jaegertracing/jaeger/internal/storage/cassandra" mock "github.com/stretchr/testify/mock" ) // NewSession creates a new instance of Session. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewSession(t interface { mock.TestingT Cleanup(func()) }) *Session { mock := &Session{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // Session is an autogenerated mock type for the Session type type Session struct { mock.Mock } type Session_Expecter struct { mock *mock.Mock } func (_m *Session) EXPECT() *Session_Expecter { return &Session_Expecter{mock: &_m.Mock} } // Close provides a mock function for the type Session func (_mock *Session) Close() { _mock.Called() return } // Session_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' type Session_Close_Call struct { *mock.Call } // Close is a helper method to define mock.On call func (_e *Session_Expecter) Close() *Session_Close_Call { return &Session_Close_Call{Call: _e.mock.On("Close")} } func (_c *Session_Close_Call) Run(run func()) *Session_Close_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *Session_Close_Call) Return() *Session_Close_Call { _c.Call.Return() return _c } func (_c *Session_Close_Call) RunAndReturn(run func()) *Session_Close_Call { _c.Run(run) return _c } // Query provides a mock function for the type Session func (_mock *Session) Query(stmt string, values ...any) cassandra.Query { var tmpRet mock.Arguments if len(values) > 0 { tmpRet = _mock.Called(stmt, values) } else { tmpRet = _mock.Called(stmt) } ret := tmpRet if len(ret) == 0 { panic("no return value specified for Query") } var r0 cassandra.Query if returnFunc, ok := ret.Get(0).(func(string, ...any) cassandra.Query); ok { r0 = returnFunc(stmt, values...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(cassandra.Query) } } return r0 } // Session_Query_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Query' type Session_Query_Call struct { *mock.Call } // Query is a helper method to define mock.On call // - stmt string // - values ...any func (_e *Session_Expecter) Query(stmt interface{}, values ...interface{}) *Session_Query_Call { return &Session_Query_Call{Call: _e.mock.On("Query", append([]interface{}{stmt}, values...)...)} } func (_c *Session_Query_Call) Run(run func(stmt string, values ...any)) *Session_Query_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } var arg1 []any var variadicArgs []any if len(args) > 1 { variadicArgs = args[1].([]any) } arg1 = variadicArgs run( arg0, arg1..., ) }) return _c } func (_c *Session_Query_Call) Return(query cassandra.Query) *Session_Query_Call { _c.Call.Return(query) return _c } func (_c *Session_Query_Call) RunAndReturn(run func(stmt string, values ...any) cassandra.Query) *Session_Query_Call { _c.Call.Return(run) return _c } // NewQuery creates a new instance of Query. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewQuery(t interface { mock.TestingT Cleanup(func()) }) *Query { mock := &Query{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // Query is an autogenerated mock type for the Query type type Query struct { mock.Mock } type Query_Expecter struct { mock *mock.Mock } func (_m *Query) EXPECT() *Query_Expecter { return &Query_Expecter{mock: &_m.Mock} } // Bind provides a mock function for the type Query func (_mock *Query) Bind(v ...any) cassandra.Query { var tmpRet mock.Arguments if len(v) > 0 { tmpRet = _mock.Called(v) } else { tmpRet = _mock.Called() } ret := tmpRet if len(ret) == 0 { panic("no return value specified for Bind") } var r0 cassandra.Query if returnFunc, ok := ret.Get(0).(func(...any) cassandra.Query); ok { r0 = returnFunc(v...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(cassandra.Query) } } return r0 } // Query_Bind_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Bind' type Query_Bind_Call struct { *mock.Call } // Bind is a helper method to define mock.On call // - v ...any func (_e *Query_Expecter) Bind(v ...interface{}) *Query_Bind_Call { return &Query_Bind_Call{Call: _e.mock.On("Bind", append([]interface{}{}, v...)...)} } func (_c *Query_Bind_Call) Run(run func(v ...any)) *Query_Bind_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 []any var variadicArgs []any if len(args) > 0 { variadicArgs = args[0].([]any) } arg0 = variadicArgs run( arg0..., ) }) return _c } func (_c *Query_Bind_Call) Return(query cassandra.Query) *Query_Bind_Call { _c.Call.Return(query) return _c } func (_c *Query_Bind_Call) RunAndReturn(run func(v ...any) cassandra.Query) *Query_Bind_Call { _c.Call.Return(run) return _c } // Consistency provides a mock function for the type Query func (_mock *Query) Consistency(level cassandra.Consistency) cassandra.Query { ret := _mock.Called(level) if len(ret) == 0 { panic("no return value specified for Consistency") } var r0 cassandra.Query if returnFunc, ok := ret.Get(0).(func(cassandra.Consistency) cassandra.Query); ok { r0 = returnFunc(level) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(cassandra.Query) } } return r0 } // Query_Consistency_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Consistency' type Query_Consistency_Call struct { *mock.Call } // Consistency is a helper method to define mock.On call // - level cassandra.Consistency func (_e *Query_Expecter) Consistency(level interface{}) *Query_Consistency_Call { return &Query_Consistency_Call{Call: _e.mock.On("Consistency", level)} } func (_c *Query_Consistency_Call) Run(run func(level cassandra.Consistency)) *Query_Consistency_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 cassandra.Consistency if args[0] != nil { arg0 = args[0].(cassandra.Consistency) } run( arg0, ) }) return _c } func (_c *Query_Consistency_Call) Return(query cassandra.Query) *Query_Consistency_Call { _c.Call.Return(query) return _c } func (_c *Query_Consistency_Call) RunAndReturn(run func(level cassandra.Consistency) cassandra.Query) *Query_Consistency_Call { _c.Call.Return(run) return _c } // Exec provides a mock function for the type Query func (_mock *Query) Exec() error { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Exec") } var r0 error if returnFunc, ok := ret.Get(0).(func() error); ok { r0 = returnFunc() } else { r0 = ret.Error(0) } return r0 } // Query_Exec_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Exec' type Query_Exec_Call struct { *mock.Call } // Exec is a helper method to define mock.On call func (_e *Query_Expecter) Exec() *Query_Exec_Call { return &Query_Exec_Call{Call: _e.mock.On("Exec")} } func (_c *Query_Exec_Call) Run(run func()) *Query_Exec_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *Query_Exec_Call) Return(err error) *Query_Exec_Call { _c.Call.Return(err) return _c } func (_c *Query_Exec_Call) RunAndReturn(run func() error) *Query_Exec_Call { _c.Call.Return(run) return _c } // Iter provides a mock function for the type Query func (_mock *Query) Iter() cassandra.Iterator { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Iter") } var r0 cassandra.Iterator if returnFunc, ok := ret.Get(0).(func() cassandra.Iterator); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(cassandra.Iterator) } } return r0 } // Query_Iter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Iter' type Query_Iter_Call struct { *mock.Call } // Iter is a helper method to define mock.On call func (_e *Query_Expecter) Iter() *Query_Iter_Call { return &Query_Iter_Call{Call: _e.mock.On("Iter")} } func (_c *Query_Iter_Call) Run(run func()) *Query_Iter_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *Query_Iter_Call) Return(iterator cassandra.Iterator) *Query_Iter_Call { _c.Call.Return(iterator) return _c } func (_c *Query_Iter_Call) RunAndReturn(run func() cassandra.Iterator) *Query_Iter_Call { _c.Call.Return(run) return _c } // PageSize provides a mock function for the type Query func (_mock *Query) PageSize(n int) cassandra.Query { ret := _mock.Called(n) if len(ret) == 0 { panic("no return value specified for PageSize") } var r0 cassandra.Query if returnFunc, ok := ret.Get(0).(func(int) cassandra.Query); ok { r0 = returnFunc(n) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(cassandra.Query) } } return r0 } // Query_PageSize_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PageSize' type Query_PageSize_Call struct { *mock.Call } // PageSize is a helper method to define mock.On call // - n int func (_e *Query_Expecter) PageSize(n interface{}) *Query_PageSize_Call { return &Query_PageSize_Call{Call: _e.mock.On("PageSize", n)} } func (_c *Query_PageSize_Call) Run(run func(n int)) *Query_PageSize_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 int if args[0] != nil { arg0 = args[0].(int) } run( arg0, ) }) return _c } func (_c *Query_PageSize_Call) Return(query cassandra.Query) *Query_PageSize_Call { _c.Call.Return(query) return _c } func (_c *Query_PageSize_Call) RunAndReturn(run func(n int) cassandra.Query) *Query_PageSize_Call { _c.Call.Return(run) return _c } // ScanCAS provides a mock function for the type Query func (_mock *Query) ScanCAS(dest ...any) (bool, error) { var tmpRet mock.Arguments if len(dest) > 0 { tmpRet = _mock.Called(dest) } else { tmpRet = _mock.Called() } ret := tmpRet if len(ret) == 0 { panic("no return value specified for ScanCAS") } var r0 bool var r1 error if returnFunc, ok := ret.Get(0).(func(...any) (bool, error)); ok { return returnFunc(dest...) } if returnFunc, ok := ret.Get(0).(func(...any) bool); ok { r0 = returnFunc(dest...) } else { r0 = ret.Get(0).(bool) } if returnFunc, ok := ret.Get(1).(func(...any) error); ok { r1 = returnFunc(dest...) } else { r1 = ret.Error(1) } return r0, r1 } // Query_ScanCAS_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ScanCAS' type Query_ScanCAS_Call struct { *mock.Call } // ScanCAS is a helper method to define mock.On call // - dest ...any func (_e *Query_Expecter) ScanCAS(dest ...interface{}) *Query_ScanCAS_Call { return &Query_ScanCAS_Call{Call: _e.mock.On("ScanCAS", append([]interface{}{}, dest...)...)} } func (_c *Query_ScanCAS_Call) Run(run func(dest ...any)) *Query_ScanCAS_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 []any var variadicArgs []any if len(args) > 0 { variadicArgs = args[0].([]any) } arg0 = variadicArgs run( arg0..., ) }) return _c } func (_c *Query_ScanCAS_Call) Return(b bool, err error) *Query_ScanCAS_Call { _c.Call.Return(b, err) return _c } func (_c *Query_ScanCAS_Call) RunAndReturn(run func(dest ...any) (bool, error)) *Query_ScanCAS_Call { _c.Call.Return(run) return _c } // String provides a mock function for the type Query func (_mock *Query) String() string { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for String") } var r0 string if returnFunc, ok := ret.Get(0).(func() string); ok { r0 = returnFunc() } else { r0 = ret.Get(0).(string) } return r0 } // Query_String_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'String' type Query_String_Call struct { *mock.Call } // String is a helper method to define mock.On call func (_e *Query_Expecter) String() *Query_String_Call { return &Query_String_Call{Call: _e.mock.On("String")} } func (_c *Query_String_Call) Run(run func()) *Query_String_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *Query_String_Call) Return(s string) *Query_String_Call { _c.Call.Return(s) return _c } func (_c *Query_String_Call) RunAndReturn(run func() string) *Query_String_Call { _c.Call.Return(run) return _c } // NewIterator creates a new instance of Iterator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewIterator(t interface { mock.TestingT Cleanup(func()) }) *Iterator { mock := &Iterator{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // Iterator is an autogenerated mock type for the Iterator type type Iterator struct { mock.Mock } type Iterator_Expecter struct { mock *mock.Mock } func (_m *Iterator) EXPECT() *Iterator_Expecter { return &Iterator_Expecter{mock: &_m.Mock} } // Close provides a mock function for the type Iterator func (_mock *Iterator) Close() error { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Close") } var r0 error if returnFunc, ok := ret.Get(0).(func() error); ok { r0 = returnFunc() } else { r0 = ret.Error(0) } return r0 } // Iterator_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' type Iterator_Close_Call struct { *mock.Call } // Close is a helper method to define mock.On call func (_e *Iterator_Expecter) Close() *Iterator_Close_Call { return &Iterator_Close_Call{Call: _e.mock.On("Close")} } func (_c *Iterator_Close_Call) Run(run func()) *Iterator_Close_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *Iterator_Close_Call) Return(err error) *Iterator_Close_Call { _c.Call.Return(err) return _c } func (_c *Iterator_Close_Call) RunAndReturn(run func() error) *Iterator_Close_Call { _c.Call.Return(run) return _c } // Scan provides a mock function for the type Iterator func (_mock *Iterator) Scan(dest ...any) bool { var tmpRet mock.Arguments if len(dest) > 0 { tmpRet = _mock.Called(dest) } else { tmpRet = _mock.Called() } ret := tmpRet if len(ret) == 0 { panic("no return value specified for Scan") } var r0 bool if returnFunc, ok := ret.Get(0).(func(...any) bool); ok { r0 = returnFunc(dest...) } else { r0 = ret.Get(0).(bool) } return r0 } // Iterator_Scan_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Scan' type Iterator_Scan_Call struct { *mock.Call } // Scan is a helper method to define mock.On call // - dest ...any func (_e *Iterator_Expecter) Scan(dest ...interface{}) *Iterator_Scan_Call { return &Iterator_Scan_Call{Call: _e.mock.On("Scan", append([]interface{}{}, dest...)...)} } func (_c *Iterator_Scan_Call) Run(run func(dest ...any)) *Iterator_Scan_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 []any var variadicArgs []any if len(args) > 0 { variadicArgs = args[0].([]any) } arg0 = variadicArgs run( arg0..., ) }) return _c } func (_c *Iterator_Scan_Call) Return(b bool) *Iterator_Scan_Call { _c.Call.Return(b) return _c } func (_c *Iterator_Scan_Call) RunAndReturn(run func(dest ...any) bool) *Iterator_Scan_Call { _c.Call.Return(run) return _c } ================================================ FILE: internal/storage/cassandra/session.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package cassandra // Consistency is Cassandra's consistency level for queries. type Consistency uint16 const ( // Any ... Any Consistency = 0x00 // One ... One Consistency = 0x01 // Two ... Two Consistency = 0x02 // Three ... Three Consistency = 0x03 // Quorum ... Quorum Consistency = 0x04 // All ... All Consistency = 0x05 // LocalQuorum ... LocalQuorum Consistency = 0x06 // EachQuorum ... EachQuorum Consistency = 0x07 // LocalOne ... LocalOne Consistency = 0x0A ) // Session is an abstraction of gocql.Session type Session interface { Query(stmt string, values ...any) Query Close() } // UpdateQuery is a subset of Query just for updates type UpdateQuery interface { Exec() error String() string // ScanCAS executes a lightweight transaction (i.e. an UPDATE or INSERT // statement containing an IF clause). If the transaction fails because // the existing values did not match, the previous values will be stored // in dest. ScanCAS(dest ...any) (bool, error) } // Query is an abstraction of gocql.Query type Query interface { UpdateQuery Iter() Iterator Bind(v ...any) Query Consistency(level Consistency) Query PageSize(int) Query } // Iterator is an abstraction of gocql.Iter type Iterator interface { Scan(dest ...any) bool Close() error } ================================================ FILE: internal/storage/distributedlock/cassandra/lock.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package cassandra import ( "errors" "fmt" "time" "github.com/jaegertracing/jaeger/internal/storage/cassandra" ) // Lock is a distributed lock based off Cassandra. type Lock struct { session cassandra.Session tenantID string } const ( defaultTTL = 60 * time.Second leasesTable = `leases` cqlInsertLock = `INSERT INTO ` + leasesTable + ` (name, owner) VALUES (?,?) IF NOT EXISTS USING TTL ?;` cqlUpdateLock = `UPDATE ` + leasesTable + ` USING TTL ? SET owner = ? WHERE name = ? IF owner = ?;` cqlDeleteLock = `DELETE FROM ` + leasesTable + ` WHERE name = ? IF owner = ?;` ) var errLockOwnership = errors.New("this host does not own the resource lock") // NewLock creates a new instance of a distributed locking mechanism based off Cassandra. func NewLock(session cassandra.Session, tenantID string) *Lock { return &Lock{ session: session, tenantID: tenantID, } } // Acquire acquires a lease around a given resource. NB. Cassandra only allows ttl of seconds granularity func (l *Lock) Acquire(resource string, ttl time.Duration) (bool, error) { if ttl == 0 { ttl = defaultTTL } ttlSec := int(ttl.Seconds()) var name, owner string applied, err := l.session.Query(cqlInsertLock, resource, l.tenantID, ttlSec).ScanCAS(&name, &owner) if err != nil { return false, fmt.Errorf("failed to acquire resource lock due to cassandra error: %w", err) } if applied { // The lock was successfully created return true, nil } if owner == l.tenantID { // This host already owns the lock, extend the lease if err = l.extendLease(resource, ttl); err != nil { return false, fmt.Errorf("failed to extend lease on resource lock: %w", err) } return true, nil } return false, nil } // Forfeit forfeits an existing lease around a given resource. func (l *Lock) Forfeit(resource string) (bool, error) { var name, owner string applied, err := l.session.Query(cqlDeleteLock, resource, l.tenantID).ScanCAS(&name, &owner) if err != nil { return false, fmt.Errorf("failed to forfeit resource lock due to cassandra error: %w", err) } if applied { // The lock was successfully deleted return true, nil } return false, fmt.Errorf("failed to forfeit resource lock: %w", errLockOwnership) } // extendLease will attempt to extend the lease of an existing lock on a given resource. func (l *Lock) extendLease(resource string, ttl time.Duration) error { ttlSec := int(ttl.Seconds()) var owner string applied, err := l.session.Query(cqlUpdateLock, ttlSec, l.tenantID, resource, l.tenantID).ScanCAS(&owner) if err != nil { return err } if applied { return nil } return errLockOwnership } ================================================ FILE: internal/storage/distributedlock/cassandra/lock_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package cassandra import ( "errors" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/storage/cassandra/mocks" "github.com/jaegertracing/jaeger/internal/testutils" ) var ( localhost = "localhost" samplingLock = "sampling_lock" ) type cqlLockTest struct { session *mocks.Session lock *Lock } func withCQLLock(fn func(r *cqlLockTest)) { session := &mocks.Session{} r := &cqlLockTest{ session: session, lock: NewLock(session, localhost), } fn(r) } func TestExtendLease(t *testing.T) { testCases := []struct { caption string applied bool errScan error expectedErrMsg string }{ { caption: "cassandra error", applied: false, errScan: errors.New("Failed to update lock"), expectedErrMsg: "Failed to update lock", }, { caption: "successfully extended lease", applied: true, errScan: nil, expectedErrMsg: "", }, { caption: "failed to extend lease", applied: false, errScan: nil, expectedErrMsg: "this host does not own the resource lock", }, } for _, tc := range testCases { testCase := tc // capture loop var t.Run(testCase.caption, func(t *testing.T) { withCQLLock(func(s *cqlLockTest) { query := &mocks.Query{} query.On("ScanCAS", mock.Anything).Return(testCase.applied, testCase.errScan) s.session.On( "Query", mock.AnythingOfType("string"), []any{ 60, localhost, samplingLock, localhost, }, ).Return(query) err := s.lock.extendLease(samplingLock, time.Second*60) if testCase.expectedErrMsg == "" { require.NoError(t, err) } else { require.EqualError(t, err, testCase.expectedErrMsg) } }) }) } } func TestAcquire(t *testing.T) { testCases := []struct { caption string insertLockApplied bool retVals []string acquired bool updateLockApplied bool errScan error expectedErrMsg string }{ { caption: "cassandra error", insertLockApplied: false, retVals: []string{"", ""}, acquired: false, errScan: errors.New("Failed to create lock"), expectedErrMsg: "failed to acquire resource lock due to cassandra error: Failed to create lock", }, { caption: "successfully created lock", insertLockApplied: true, acquired: true, retVals: []string{samplingLock, localhost}, errScan: nil, expectedErrMsg: "", }, { caption: "lock already exists and belongs to localhost", insertLockApplied: false, acquired: true, retVals: []string{samplingLock, localhost}, updateLockApplied: true, errScan: nil, expectedErrMsg: "", }, { caption: "lock already exists and belongs to localhost but is lost", insertLockApplied: false, acquired: false, retVals: []string{samplingLock, localhost}, updateLockApplied: false, errScan: nil, expectedErrMsg: "failed to extend lease on resource lock: this host does not own the resource lock", }, { caption: "failed to acquire lock", insertLockApplied: false, acquired: false, retVals: []string{samplingLock, "otherhost"}, errScan: nil, expectedErrMsg: "", }, } for _, tc := range testCases { testCase := tc // capture loop var t.Run(testCase.caption, func(t *testing.T) { withCQLLock(func(s *cqlLockTest) { assignPtr := func(vals ...string) any { return mock.MatchedBy(func(args []any) bool { if len(args) != len(vals) { return false } for i, arg := range args { ptr, ok := arg.(*string) if !ok { return false } *ptr = vals[i] } return true }) } firstQuery := &mocks.Query{} firstQuery.On("ScanCAS", assignPtr(testCase.retVals...)). Return(testCase.insertLockApplied, testCase.errScan) secondQuery := &mocks.Query{} secondQuery.On("ScanCAS", mock.Anything).Return(testCase.updateLockApplied, nil) s.session.On("Query", stringMatcher("INSERT INTO leases"), mock.Anything).Return(firstQuery) s.session.On("Query", stringMatcher("UPDATE leases"), mock.Anything).Return(secondQuery) acquired, err := s.lock.Acquire(samplingLock, 0) if testCase.expectedErrMsg == "" { require.NoError(t, err) } else { require.EqualError(t, err, testCase.expectedErrMsg) } assert.Equal(t, testCase.acquired, acquired) }) }) } } func TestForfeit(t *testing.T) { testCases := []struct { caption string applied bool retVals []string errScan error expectedErrMsg string }{ { caption: "cassandra error", applied: false, retVals: []string{"", ""}, errScan: errors.New("Failed to delete lock"), expectedErrMsg: "failed to forfeit resource lock due to cassandra error: Failed to delete lock", }, { caption: "successfully forfeited lock", applied: true, retVals: []string{samplingLock, localhost}, errScan: nil, expectedErrMsg: "", }, { caption: "failed to delete lock", applied: false, retVals: []string{samplingLock, "otherhost"}, errScan: nil, expectedErrMsg: "failed to forfeit resource lock: this host does not own the resource lock", }, } for _, tc := range testCases { testCase := tc // capture loop var t.Run(testCase.caption, func(t *testing.T) { withCQLLock(func(s *cqlLockTest) { query := &mocks.Query{} query.On("ScanCAS", mock.Anything).Return(testCase.applied, testCase.errScan) s.session.On("Query", mock.AnythingOfType("string"), []any{samplingLock, localhost}).Return(query) applied, err := s.lock.Forfeit(samplingLock) if testCase.expectedErrMsg == "" { require.NoError(t, err) } else { require.EqualError(t, err, testCase.expectedErrMsg) } assert.Equal(t, testCase.applied, applied) }) }) } } // stringMatcher can match a string argument when it contains a specific substring q func stringMatcher(q string) any { matchFunc := func(s string) bool { return strings.Contains(s, q) } return mock.MatchedBy(matchFunc) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/elasticsearch/client/basic_auth.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package client import "encoding/base64" // BasicAuth encode username and password to be used with basic authentication header func BasicAuth(username, password string) string { if username == "" || password == "" { return "" } return base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) } ================================================ FILE: internal/storage/elasticsearch/client/basic_auth_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package client import ( "testing" "github.com/stretchr/testify/assert" ) func TestBasicAuth(t *testing.T) { tests := []struct { name string username string password string expectedResult string }{ { name: "user and password", username: "admin", password: "qwerty123456", expectedResult: "YWRtaW46cXdlcnR5MTIzNDU2", }, { name: "username empty", username: "", password: "qwerty123456", expectedResult: "", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { result := BasicAuth(test.username, test.password) assert.Equal(t, test.expectedResult, result) }) } } ================================================ FILE: internal/storage/elasticsearch/client/client.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package client import ( "bytes" "context" "fmt" "io" "net/http" ) // ResponseError holds information about a request error type ResponseError struct { // Error returned by the http client Err error // StatusCode is the http code returned by the server (if any) StatusCode int // Body is the bytes readed in the response (if any) Body []byte } // Error returns the error string of the Err field func (r ResponseError) Error() string { return r.Err.Error() } func (r ResponseError) prefixMessage(message string) ResponseError { return ResponseError{ Err: fmt.Errorf("%s, %w", message, r.Err), StatusCode: r.StatusCode, Body: r.Body, } } func newResponseError(err error, code int, body []byte) ResponseError { return ResponseError{ Err: err, StatusCode: code, Body: body, } } // Client executes requests against Elasticsearch using direct HTTP calls, // without using the official Go client for ES. type Client struct { // Http client. Client *http.Client // ES server endpoint. Endpoint string // Basic authentication string. BasicAuth string } type elasticRequest struct { endpoint string body []byte method string } func (c *Client) request(esRequest elasticRequest) ([]byte, error) { var reader *bytes.Buffer var r *http.Request var err error if len(esRequest.body) > 0 { reader = bytes.NewBuffer(esRequest.body) r, err = http.NewRequestWithContext(context.Background(), esRequest.method, fmt.Sprintf("%s/%s", c.Endpoint, esRequest.endpoint), reader) } else { r, err = http.NewRequestWithContext(context.Background(), esRequest.method, fmt.Sprintf("%s/%s", c.Endpoint, esRequest.endpoint), http.NoBody) } if err != nil { return []byte{}, err } c.setAuthorization(r) r.Header.Add("Content-Type", "application/json") res, err := c.Client.Do(r) //nolint:gosec // G704 - URL from ES config if err != nil { return []byte{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return []byte{}, c.handleFailedRequest(res) } body, err := io.ReadAll(res.Body) if err != nil { return []byte{}, err } return body, nil } func (c *Client) setAuthorization(r *http.Request) { if c.BasicAuth != "" { r.Header.Add("Authorization", "Basic "+c.BasicAuth) } } func (*Client) handleFailedRequest(res *http.Response) error { if res.Body != nil { bodyBytes, err := io.ReadAll(res.Body) if err != nil { return newResponseError(fmt.Errorf("request failed and failed to read response body, status code: %d, %w", res.StatusCode, err), res.StatusCode, nil) } body := string(bodyBytes) return newResponseError(fmt.Errorf("request failed, status code: %d, body: %s", res.StatusCode, body), res.StatusCode, bodyBytes) } return newResponseError(fmt.Errorf("request failed, status code: %d", res.StatusCode), res.StatusCode, nil) } ================================================ FILE: internal/storage/elasticsearch/client/cluster_client.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package client import ( "encoding/json" "fmt" "net/http" "strconv" "strings" ) var _ ClusterAPI = (*ClusterClient)(nil) // ClusterClient is a client used to get ES cluster information type ClusterClient struct { Client } // Version returns the major version of the ES cluster func (c *ClusterClient) Version() (uint, error) { type clusterInfo struct { Version map[string]any `json:"version"` TagLine string `json:"tagline"` } body, err := c.request(elasticRequest{ endpoint: "", method: http.MethodGet, }) if err != nil { return 0, err } var info clusterInfo err = json.Unmarshal(body, &info) if err != nil { return 0, err } versionField := info.Version["number"] versionNumber, isString := versionField.(string) if !isString { return 0, fmt.Errorf("invalid version format: %v", versionField) } version := strings.Split(versionNumber, ".") major, err := strconv.ParseUint(version[0], 10, 32) if err != nil { return 0, fmt.Errorf("invalid version format: %s", version[0]) } if strings.Contains(info.TagLine, "OpenSearch") && (major == 1 || major == 2 || major == 3) { return 7, nil } return uint(major), nil } ================================================ FILE: internal/storage/elasticsearch/client/cluster_client_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package client import ( "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const badVersionType = ` { "name" : "opensearch-node1", "cluster_name" : "opensearch-cluster", "cluster_uuid" : "1StaUGrGSx61r41d-1nDiw", "version" : { "distribution" : "opensearch", "number" : true, "build_type" : "tar", "build_hash" : "34550c5b17124ddc59458ef774f6b43a086522e3", "build_date" : "2021-07-02T23:22:21.383695Z", "build_snapshot" : false, "lucene_version" : "8.8.2", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "The OpenSearch Project: https://opensearch.org/" } ` const badVersionNoNumber = ` { "name" : "opensearch-node1", "cluster_name" : "opensearch-cluster", "cluster_uuid" : "1StaUGrGSx61r41d-1nDiw", "version" : { "distribution" : "opensearch", "number" : "thisisnotanumber", "build_type" : "tar", "build_hash" : "34550c5b17124ddc59458ef774f6b43a086522e3", "build_date" : "2021-07-02T23:22:21.383695Z", "build_snapshot" : false, "lucene_version" : "8.8.2", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "The OpenSearch Project: https://opensearch.org/" } ` const opensearch1 = ` { "name" : "opensearch-node1", "cluster_name" : "opensearch-cluster", "cluster_uuid" : "1StaUGrGSx61r41d-1nDiw", "version" : { "distribution" : "opensearch", "number" : "1.0.0", "build_type" : "tar", "build_hash" : "34550c5b17124ddc59458ef774f6b43a086522e3", "build_date" : "2021-07-02T23:22:21.383695Z", "build_snapshot" : false, "lucene_version" : "8.8.2", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "The OpenSearch Project: https://opensearch.org/" } ` const opensearch2 = ` { "name" : "opensearch-node1", "cluster_name" : "opensearch-cluster", "cluster_uuid" : "1StaUGrGSx61r41d-1nDiw", "version" : { "distribution" : "opensearch", "number" : "2.3.0", "build_type" : "tar", "build_hash" : "34550c5b17124ddc59458ef774f6b43a086522e3", "build_date" : "2021-07-02T23:22:21.383695Z", "build_snapshot" : false, "lucene_version" : "8.8.2", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "The OpenSearch Project: https://opensearch.org/" } ` const opensearch3 = ` { "name" : "opensearch-node1", "cluster_name" : "opensearch-cluster", "cluster_uuid" : "1StaUGrGSx61r41d-1nDiw", "version" : { "distribution" : "opensearch", "number" : "3.0.0", "build_type" : "tar", "build_hash" : "34550c5b17124ddc59458ef774f6b43a086522e3", "build_date" : "2025-05-06T23:22:21.383695Z", "build_snapshot" : false, "lucene_version" : "10.0.0", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "The OpenSearch Project: https://opensearch.org/" } ` const elasticsearch7 = ` { "name" : "elasticsearch-0", "cluster_name" : "clustername", "cluster_uuid" : "HUtdg7bRTomSFaOk7Wzt8w", "version" : { "number" : "7.6.1", "build_flavor" : "default", "build_type" : "docker", "build_hash" : "aa751e09be0a5072e8570670309b1f12348f023b", "build_date" : "2020-02-29T00:15:25.529771Z", "build_snapshot" : false, "lucene_version" : "8.4.0", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search" } ` const elasticsearch8 = ` { "name" : "elasticsearch-0", "version" : { "number" : "8.0.0" }, "tagline" : "You Know, for Search" } ` const elasticsearch6 = ` { "name" : "elasticsearch-0", "cluster_name" : "clustername", "cluster_uuid" : "HUtdg7bRTomSFaOk7Wzt8w", "version" : { "number" : "6.8.0", "build_flavor" : "default", "build_type" : "docker", "build_hash" : "aa751e09be0a5072e8570670309b1f12348f023b", "build_date" : "2020-02-29T00:15:25.529771Z", "build_snapshot" : false, "lucene_version" : "8.4.0", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search" } ` func TestVersion(t *testing.T) { tests := []struct { name string responseCode int response string errContains string expectedResult uint }{ { name: "success with elasticsearch 6", responseCode: http.StatusOK, response: elasticsearch6, expectedResult: 6, }, { name: "success with elasticsearch 7", responseCode: http.StatusOK, response: elasticsearch7, expectedResult: 7, }, { name: "success with elasticsearch 8", responseCode: http.StatusOK, response: elasticsearch8, expectedResult: 8, }, { name: "success with opensearch 1", responseCode: http.StatusOK, response: opensearch1, expectedResult: 7, }, { name: "success with opensearch 2", responseCode: http.StatusOK, response: opensearch2, expectedResult: 7, }, { name: "success with opensearch 3", responseCode: http.StatusOK, response: opensearch3, expectedResult: 7, }, { name: "client error", responseCode: http.StatusBadRequest, response: esErrResponse, errContains: "request failed, status code: 400", }, { name: "bad version", responseCode: http.StatusOK, response: badVersionType, errContains: "invalid version format: true", }, { name: "version not a number", responseCode: http.StatusOK, response: badVersionNoNumber, errContains: "invalid version format: thisisnotanumber", }, { name: "unmarshal error", responseCode: http.StatusOK, response: "thisisaninvalidjson", errContains: "invalid character", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { assert.Equal(t, http.MethodGet, req.Method) assert.Equal(t, "Basic foobar", req.Header.Get("Authorization")) res.WriteHeader(test.responseCode) res.Write([]byte(test.response)) })) defer testServer.Close() c := &ClusterClient{ Client: Client{ Client: testServer.Client(), Endpoint: testServer.URL, BasicAuth: "foobar", }, } result, err := c.Version() if test.errContains != "" { require.ErrorContains(t, err, test.errContains) return } require.NoError(t, err) assert.Equal(t, test.expectedResult, result) }) } } ================================================ FILE: internal/storage/elasticsearch/client/ilm_client.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package client import ( "context" "errors" "fmt" "net/http" "time" "github.com/cenkalti/backoff/v5" "go.uber.org/zap" ) const ( maxTries = 3 maxElapsedTime = 5 * time.Second ) var _ IndexManagementLifecycleAPI = (*ILMClient)(nil) // ILMClient is a client used to manipulate Index lifecycle management policies. type ILMClient struct { Client MasterTimeoutSeconds int Logger *zap.Logger } // Exists verify if a ILM policy exists func (i ILMClient) Exists(name string) (bool, error) { operation := func() ([]byte, error) { bytes, err := i.request(elasticRequest{ endpoint: "_ilm/policy/" + name, method: http.MethodGet, }) if err != nil { i.Logger.Warn("Retryable error while getting ILM policy", zap.String("name", name), zap.Error(err), ) return bytes, err } return bytes, nil } _, err := backoff.Retry( context.TODO(), operation, backoff.WithMaxTries(maxTries), backoff.WithMaxElapsedTime(maxElapsedTime), ) var respError ResponseError if errors.As(err, &respError) { if respError.StatusCode == http.StatusNotFound { return false, nil } } if err != nil { return false, fmt.Errorf("failed to get ILM policy: %s, %w", name, err) } return true, nil } ================================================ FILE: internal/storage/elasticsearch/client/ilm_client_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package client import ( "net/http" "net/http/httptest" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" ) func TestExists(t *testing.T) { t.Parallel() tests := []struct { name string responseCode int response string errContains string expectedResult bool }{ { name: "found", responseCode: http.StatusOK, expectedResult: true, }, { name: "not found", responseCode: http.StatusNotFound, response: esErrResponse, }, { name: "client error", responseCode: http.StatusBadRequest, response: esErrResponse, errContains: "failed to get ILM policy: jaeger-ilm-policy", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { assert.True(t, strings.HasSuffix(req.URL.String(), "_ilm/policy/jaeger-ilm-policy")) assert.Equal(t, http.MethodGet, req.Method) assert.Equal(t, "Basic foobar", req.Header.Get("Authorization")) res.WriteHeader(test.responseCode) res.Write([]byte(test.response)) })) defer testServer.Close() c := &ILMClient{ Client: Client{ Client: testServer.Client(), Endpoint: testServer.URL, BasicAuth: "foobar", }, Logger: zap.NewNop(), } result, err := c.Exists("jaeger-ilm-policy") if test.errContains != "" { require.ErrorContains(t, err, test.errContains) } assert.Equal(t, test.expectedResult, result) }) } } func TestExists_Retries(t *testing.T) { t.Parallel() var callCount int testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { callCount++ assert.True(t, strings.HasSuffix(req.URL.String(), "_ilm/policy/jaeger-ilm-policy")) assert.Equal(t, http.MethodGet, req.Method) assert.Equal(t, "Basic foobar", req.Header.Get("Authorization")) if callCount < maxTries { res.WriteHeader(http.StatusInternalServerError) res.Write([]byte(`{"error": "server error"}`)) return } res.WriteHeader(http.StatusOK) })) defer testServer.Close() c := &ILMClient{ Client: Client{ Client: testServer.Client(), Endpoint: testServer.URL, BasicAuth: "foobar", }, Logger: zap.NewNop(), } result, err := c.Exists("jaeger-ilm-policy") require.NoError(t, err) assert.True(t, result) assert.Equal(t, maxTries, callCount, "should retry twice before succeeding") } ================================================ FILE: internal/storage/elasticsearch/client/index_client.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package client import ( "encoding/json" "errors" "fmt" "net/http" "strconv" "strings" "time" ) // Index represents ES index. type Index struct { // Index name. Index string // Index creation time. CreationTime time.Time // Aliases Aliases map[string]bool } // Alias represents ES alias. type Alias struct { // Index name. Index string // Alias name. Name string // IsWriteIndex option IsWriteIndex bool } var _ IndexAPI = (*IndicesClient)(nil) // IndicesClient is a client used to manipulate indices. type IndicesClient struct { Client MasterTimeoutSeconds int IgnoreUnavailableIndex bool } // GetJaegerIndices queries all Jaeger indices including the archive and rollover. // Jaeger daily indices are: // - jaeger-span-2019-01-01 // - jaeger-service-2019-01-01 // - jaeger-dependencies-2019-01-01 // - jaeger-span-archive // // Rollover indices: // - aliases: jaeger-span-read, jaeger-span-write, jaeger-service-read, jaeger-service-write // - indices: jaeger-span-000001, jaeger-service-000001 etc. // - aliases: jaeger-span-archive-read, jaeger-span-archive-write // - indices: jaeger-span-archive-000001 func (i *IndicesClient) GetJaegerIndices(prefix string) ([]Index, error) { prefix += "jaeger-*" body, err := i.request(elasticRequest{ endpoint: prefix + "?flat_settings=true&filter_path=*.aliases,*.settings", method: http.MethodGet, }) if err != nil { return nil, fmt.Errorf("failed to query indices: %w", err) } type indexInfo struct { Aliases map[string]any `json:"aliases"` Settings map[string]string `json:"settings"` } var indicesInfo map[string]indexInfo if err = json.Unmarshal(body, &indicesInfo); err != nil { return nil, fmt.Errorf("failed to query indices and unmarshall response body: %q: %w", body, err) } var indices []Index for k, v := range indicesInfo { aliases := map[string]bool{} for alias := range v.Aliases { aliases[alias] = true } // ignoring error, ES should return valid date creationDate, _ := strconv.ParseInt(v.Settings["index.creation_date"], 10, 64) indices = append(indices, Index{ Index: k, CreationTime: time.Unix(0, int64(time.Millisecond)*creationDate), Aliases: aliases, }) } return indices, nil } // execute delete request func (i *IndicesClient) indexDeleteRequest(concatIndices string) error { _, err := i.request(elasticRequest{ endpoint: fmt.Sprintf("%s?master_timeout=%ds&ignore_unavailable=%t", concatIndices, i.MasterTimeoutSeconds, i.IgnoreUnavailableIndex), method: http.MethodDelete, }) if err != nil { var responseError ResponseError if errors.As(err, &responseError) { if responseError.StatusCode != http.StatusOK { return responseError.prefixMessage("failed to delete indices: " + concatIndices) } } return fmt.Errorf("failed to delete indices: %w", err) } return nil } // DeleteIndices deletes specified set of indices. func (i *IndicesClient) DeleteIndices(indices []Index) error { concatIndices := "" for j, index := range indices { // verify the length of the concatIndices // An HTTP line is should not be larger than 4096 bytes // a line contains other than concatIndices data in the request, ie: master_timeout // for a safer side check the line length should not exceed 4000 if (len(concatIndices) + len(index.Index)) > 4000 { err := i.indexDeleteRequest(concatIndices) if err != nil { return err } concatIndices = "" } concatIndices += index.Index concatIndices += "," // if it is last index, delete request should be executed if j == len(indices)-1 { return i.indexDeleteRequest(concatIndices) } } return nil } // CreateIndex an ES index func (i *IndicesClient) CreateIndex(index string) error { _, err := i.request(elasticRequest{ endpoint: index, method: http.MethodPut, }) if err != nil { var responseError ResponseError if errors.As(err, &responseError) { if responseError.StatusCode != http.StatusOK { return responseError.prefixMessage("failed to create index: " + index) } } return fmt.Errorf("failed to create index: %w", err) } return nil } // CreateAlias an ES specific set of index aliases func (i *IndicesClient) CreateAlias(aliases []Alias) error { err := i.aliasAction("add", aliases) if err != nil { var responseError ResponseError if errors.As(err, &responseError) { if responseError.StatusCode != http.StatusOK { return responseError.prefixMessage("failed to create aliases: " + i.aliasesString(aliases)) } } return fmt.Errorf("failed to create aliases: %w", err) } return nil } // DeleteAlias an ES specific set of index aliases func (i *IndicesClient) DeleteAlias(aliases []Alias) error { err := i.aliasAction("remove", aliases) if err != nil { var responseError ResponseError if errors.As(err, &responseError) { if responseError.StatusCode != http.StatusOK { return responseError.prefixMessage("failed to delete aliases: " + i.aliasesString(aliases)) } } return fmt.Errorf("failed to delete aliases: %w", err) } return nil } // AliasExists check whether an alias exists or not func (i *IndicesClient) AliasExists(alias string) (bool, error) { _, err := i.request(elasticRequest{ endpoint: "_alias/" + alias, method: http.MethodHead, }) if err != nil { var responseError ResponseError if errors.As(err, &responseError) { if responseError.StatusCode == http.StatusNotFound { return false, nil } } return false, fmt.Errorf("failed to check if alias exists: %w", err) } return true, nil } // IndexExists check whether an index exists or not func (i *IndicesClient) IndexExists(index string) (bool, error) { _, err := i.request(elasticRequest{ endpoint: index, method: http.MethodHead, }) if err != nil { var responseError ResponseError if errors.As(err, &responseError) { if responseError.StatusCode == http.StatusNotFound { return false, nil } } return false, fmt.Errorf("failed to check if index exists: %w", err) } return true, nil } func (*IndicesClient) aliasesString(aliases []Alias) string { var builder strings.Builder for _, alias := range aliases { fmt.Fprintf(&builder, "[index: %s, alias: %s],", alias.Index, alias.Name) } concatAliases := builder.String() return strings.Trim(concatAliases, ",") } func (i *IndicesClient) aliasAction(action string, aliases []Alias) error { actions := []map[string]any{} for _, alias := range aliases { options := map[string]any{ "index": alias.Index, "alias": alias.Name, } if alias.IsWriteIndex { options["is_write_index"] = true } actions = append(actions, map[string]any{ action: options, }) } body := map[string]any{ "actions": actions, } bodyBytes, err := json.Marshal(body) if err != nil { return err } _, err = i.request(elasticRequest{ endpoint: "_aliases", method: http.MethodPost, body: bodyBytes, }) return err } func (i IndicesClient) version() (uint, error) { cl := ClusterClient{Client: i.Client} return cl.Version() } // CreateTemplate an ES index template func (i IndicesClient) CreateTemplate(template, name string) error { endpointFmt := "_template/%s" if v, err := i.version(); err != nil { return err } else if v >= 8 { endpointFmt = "_index_template/%s" } _, err := i.request(elasticRequest{ endpoint: fmt.Sprintf(endpointFmt, name), method: http.MethodPut, body: []byte(template), }) if err != nil { var responseError ResponseError if errors.As(err, &responseError) { if responseError.StatusCode != http.StatusOK { return responseError.prefixMessage("failed to create template: " + name) } } return fmt.Errorf("failed to create template: %w", err) } return nil } // Rollover create a rollover for certain index/alias func (i IndicesClient) Rollover(rolloverTarget string, conditions map[string]any) error { esReq := elasticRequest{ endpoint: rolloverTarget + "/_rollover/", method: http.MethodPost, } if len(conditions) > 0 { body := map[string]any{ "conditions": conditions, } bodyBytes, err := json.Marshal(body) if err != nil { return err } esReq.body = bodyBytes } _, err := i.request(esReq) if err != nil { var responseError ResponseError if errors.As(err, &responseError) { if responseError.StatusCode != http.StatusOK { return responseError.prefixMessage("failed to create rollover target: " + rolloverTarget) } } return fmt.Errorf("failed to create rollover: %w", err) } return nil } ================================================ FILE: internal/storage/elasticsearch/client/index_client_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package client import ( "fmt" "io" "net/http" "net/http/httptest" "sort" "strconv" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const esIndexResponse = ` { "%sjaeger-service-2021-08-06" : { "aliases" : { }, "settings" : { "index.creation_date" : "1628259381266", "index.mapper.dynamic" : "false", "index.mapping.nested_fields.limit" : "50", "index.number_of_replicas" : "1", "index.number_of_shards" : "5", "index.provided_name" : "jaeger-service-2021-08-06", "index.requests.cache.enable" : "true", "index.uuid" : "2kKdvrvAT7qXetRzmWhjYQ", "index.version.created" : "5061099" } }, "%sjaeger-span-2021-08-06" : { "aliases" : { }, "settings" : { "index.creation_date" : "1628259381326", "index.mapper.dynamic" : "false", "index.mapping.nested_fields.limit" : "50", "index.number_of_replicas" : "1", "index.number_of_shards" : "5", "index.provided_name" : "jaeger-span-2021-08-06", "index.requests.cache.enable" : "true", "index.uuid" : "zySRY_FfRFa5YMWxNsNViA", "index.version.created" : "5061099" } }, "%sjaeger-span-000001" : { "aliases" : { "jaeger-span-read" : { }, "jaeger-span-write" : { } }, "settings" : { "index.creation_date" : "1628259381326" } } }` const esErrResponse = `{"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"request [/jaeger-*] contains unrecognized parameter: [help]"}],"type":"illegal_argument_exception","reason":"request [/jaeger-*] contains unrecognized parameter: [help]"},"status":400}` func TestClientGetIndices(t *testing.T) { tests := []struct { name string prefix string responseCode int response string errContains string indices []Index }{ { name: "no error", responseCode: http.StatusOK, response: esIndexResponse, indices: []Index{ { Index: "jaeger-service-2021-08-06", CreationTime: time.Unix(0, int64(time.Millisecond)*1628259381266), Aliases: map[string]bool{}, }, { Index: "jaeger-span-000001", CreationTime: time.Unix(0, int64(time.Millisecond)*1628259381326), Aliases: map[string]bool{"jaeger-span-read": true, "jaeger-span-write": true}, }, { Index: "jaeger-span-2021-08-06", CreationTime: time.Unix(0, int64(time.Millisecond)*1628259381326), Aliases: map[string]bool{}, }, }, }, { name: "no error with prefix", prefix: "foo-", responseCode: http.StatusOK, response: esIndexResponse, indices: []Index{ { Index: "foo-jaeger-service-2021-08-06", CreationTime: time.Unix(0, int64(time.Millisecond)*1628259381266), Aliases: map[string]bool{}, }, { Index: "foo-jaeger-span-000001", CreationTime: time.Unix(0, int64(time.Millisecond)*1628259381326), Aliases: map[string]bool{"jaeger-span-read": true, "jaeger-span-write": true}, }, { Index: "foo-jaeger-span-2021-08-06", CreationTime: time.Unix(0, int64(time.Millisecond)*1628259381326), Aliases: map[string]bool{}, }, }, }, { name: "client error", responseCode: http.StatusBadRequest, response: esErrResponse, errContains: "failed to query indices: request failed, status code: 400", }, { name: "unmarshall error", responseCode: http.StatusOK, response: "AAA", errContains: `failed to query indices and unmarshall response body: "AAA"`, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, _ *http.Request) { res.WriteHeader(test.responseCode) response := test.response if test.errContains == "" { // Formatted string only applies to "success" response bodies. response = fmt.Sprintf(test.response, test.prefix, test.prefix, test.prefix) } res.Write([]byte(response)) })) defer testServer.Close() c := &IndicesClient{ Client: Client{ Client: testServer.Client(), Endpoint: testServer.URL, }, } indices, err := c.GetJaegerIndices(test.prefix) if test.errContains != "" { require.ErrorContains(t, err, test.errContains) assert.Nil(t, indices) } else { require.NoError(t, err) sort.Slice(indices, func(i, j int) bool { return indices[i].Index < indices[j].Index }) assert.Equal(t, test.indices, indices) } }) } } func getIndicesList(size int) []Index { indicesList := []Index{} for count := 1; count <= size/2; count++ { indicesList = append(indicesList, Index{Index: fmt.Sprintf("jaeger-span-%06d", count)}, Index{Index: fmt.Sprintf("jaeger-service-%06d", count)}, ) } return indicesList } func TestClientDeleteIndices(t *testing.T) { masterTimeoutSeconds := 1 maxURLPathLength := 4000 ignoreUnavailableIndex := true tests := []struct { name string responseCode int response string errContains string indices []Index triggerAPI bool }{ { name: "no indices", responseCode: http.StatusOK, indices: []Index{}, triggerAPI: false, }, { name: "one index", responseCode: http.StatusOK, indices: []Index{{Index: "jaeger-span-000001"}}, triggerAPI: true, }, { name: "moderate indices", responseCode: http.StatusOK, response: "", indices: getIndicesList(20), triggerAPI: true, }, { name: "long indices", responseCode: http.StatusOK, response: "", indices: getIndicesList(600), triggerAPI: true, }, { name: "client error", responseCode: http.StatusBadRequest, response: esErrResponse, errContains: "failed to delete indices: jaeger-span-000001", indices: []Index{{Index: "jaeger-span-000001"}}, triggerAPI: true, }, { name: "client error in long indices", responseCode: http.StatusBadRequest, response: esErrResponse, errContains: "failed to delete indices: jaeger-span-000001", indices: getIndicesList(600), triggerAPI: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { deletedIndicesCount := 0 apiTriggered := false testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { apiTriggered = true assert.Equal(t, http.MethodDelete, req.Method) assert.Equal(t, "Basic foobar", req.Header.Get("Authorization")) assert.Equal(t, fmt.Sprintf("%ds", masterTimeoutSeconds), req.URL.Query().Get("master_timeout")) assert.Equal(t, strconv.FormatBool(ignoreUnavailableIndex), req.URL.Query().Get("ignore_unavailable")) assert.LessOrEqual(t, len(req.URL.Path), maxURLPathLength) // removes leading '/' and trailing ',' // example: /jaeger-span-000001, => jaeger-span-000001 rawIndices := strings.TrimPrefix(req.URL.Path, "/") rawIndices = strings.TrimSuffix(rawIndices, ",") if len(test.indices) == 1 { assert.Equal(t, test.indices[0].Index, rawIndices) } deletedIndices := strings.Split(rawIndices, ",") deletedIndicesCount += len(deletedIndices) res.WriteHeader(test.responseCode) res.Write([]byte(test.response)) })) defer testServer.Close() c := &IndicesClient{ Client: Client{ Client: testServer.Client(), Endpoint: testServer.URL, BasicAuth: "foobar", }, MasterTimeoutSeconds: masterTimeoutSeconds, IgnoreUnavailableIndex: ignoreUnavailableIndex, } err := c.DeleteIndices(test.indices) assert.Equal(t, test.triggerAPI, apiTriggered) if test.errContains != "" { assert.ErrorContains(t, err, test.errContains) } else { assert.Len(t, test.indices, deletedIndicesCount) } }) } } func TestIndexExists(t *testing.T) { t.Run("index exists", func(t *testing.T) { testIndexOrAliasExistence(t, "index") }) } func TestAliasExists(t *testing.T) { t.Run("alias exists", func(t *testing.T) { testIndexOrAliasExistence(t, "alias") }) } func testIndexOrAliasExistence(t *testing.T, existence string) { maxURLPathLength := 4000 type indexOrAliasExistence struct { name string exists bool responseCode int expectedErr string } tests := []indexOrAliasExistence{ { name: "exists", responseCode: http.StatusOK, exists: true, }, { name: "not exists", responseCode: http.StatusNotFound, exists: false, }, } switch existence { case "index": test := indexOrAliasExistence{ name: "generic error", responseCode: http.StatusBadRequest, expectedErr: "failed to check if index exists: request failed, status code: 400", } tests = append(tests, test) case "alias": test := indexOrAliasExistence{ name: "generic error", responseCode: http.StatusBadRequest, expectedErr: "failed to check if alias exists: request failed, status code: 400", } tests = append(tests, test) default: } for _, test := range tests { t.Run(test.name, func(t *testing.T) { apiTriggered := false testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { apiTriggered = true assert.Equal(t, http.MethodHead, req.Method) assert.Equal(t, "Basic foobar", req.Header.Get("Authorization")) assert.LessOrEqual(t, len(req.URL.Path), maxURLPathLength) res.WriteHeader(test.responseCode) })) defer testServer.Close() c := &IndicesClient{ Client: Client{ Client: testServer.Client(), Endpoint: testServer.URL, BasicAuth: "foobar", }, } var exists bool var err error switch existence { case "index": exists, err = c.IndexExists("jaeger-span") case "alias": exists, err = c.AliasExists("jaeger-span") default: } if test.expectedErr != "" { require.ErrorContains(t, err, test.expectedErr) } else { require.NoError(t, err) } assert.True(t, apiTriggered) assert.Equal(t, test.exists, exists) }) } } func TestClientRequestError(t *testing.T) { c := &IndicesClient{ Client: Client{ Endpoint: "%", }, } indices, err := c.GetJaegerIndices("") require.Error(t, err) assert.Nil(t, indices) } func TestClientDoError(t *testing.T) { c := &IndicesClient{ Client: Client{ Client: &http.Client{}, Endpoint: "localhost:1", }, } indices, err := c.GetJaegerIndices("") require.Error(t, err) assert.Nil(t, indices) } func TestClientCreateIndex(t *testing.T) { indexName := "jaeger-span" tests := []struct { name string responseCode int response string errContains string }{ { name: "success", responseCode: http.StatusOK, }, { name: "client error", responseCode: http.StatusBadRequest, response: esErrResponse, errContains: "failed to create index: jaeger-span", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { assert.True(t, strings.HasSuffix(req.URL.String(), "jaeger-span")) assert.Equal(t, http.MethodPut, req.Method) assert.Equal(t, "Basic foobar", req.Header.Get("Authorization")) res.WriteHeader(test.responseCode) res.Write([]byte(test.response)) })) defer testServer.Close() c := &IndicesClient{ Client: Client{ Client: testServer.Client(), Endpoint: testServer.URL, BasicAuth: "foobar", }, } err := c.CreateIndex(indexName) if test.errContains != "" { assert.ErrorContains(t, err, test.errContains) } }) } } func TestClientCreateAliases(t *testing.T) { aliases := []Alias{ { Index: "jaeger-span", Name: "jaeger-span-read", }, { Index: "jaeger-span", Name: "jaeger-span-write", IsWriteIndex: true, }, } expectedRequestBody := `{"actions":[{"add":{"alias":"jaeger-span-read","index":"jaeger-span"}},{"add":{"alias":"jaeger-span-write","index":"jaeger-span","is_write_index":true}}]}` tests := []struct { name string responseCode int response string errContains string }{ { name: "success", responseCode: http.StatusOK, }, { name: "client error", responseCode: http.StatusBadRequest, response: esErrResponse, errContains: "failed to create aliases: [index: jaeger-span, alias: jaeger-span-read],[index: jaeger-span, alias: jaeger-span-write]", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { assert.True(t, strings.HasSuffix(req.URL.String(), "_aliases")) assert.Equal(t, http.MethodPost, req.Method) assert.Equal(t, "Basic foobar", req.Header.Get("Authorization")) body, err := io.ReadAll(req.Body) if assert.NoError(t, err) { assert.Equal(t, expectedRequestBody, string(body)) res.WriteHeader(test.responseCode) res.Write([]byte(test.response)) } })) defer testServer.Close() c := &IndicesClient{ Client: Client{ Client: testServer.Client(), Endpoint: testServer.URL, BasicAuth: "foobar", }, } err := c.CreateAlias(aliases) if test.errContains != "" { assert.ErrorContains(t, err, test.errContains) } }) } } func TestClientDeleteAliases(t *testing.T) { aliases := []Alias{ { Index: "jaeger-span", Name: "jaeger-span-read", }, { Index: "jaeger-span", Name: "jaeger-span-write", IsWriteIndex: true, }, } expectedRequestBody := `{"actions":[{"remove":{"alias":"jaeger-span-read","index":"jaeger-span"}},{"remove":{"alias":"jaeger-span-write","index":"jaeger-span","is_write_index":true}}]}` tests := []struct { name string responseCode int response string errContains string }{ { name: "success", responseCode: http.StatusOK, }, { name: "client error", responseCode: http.StatusBadRequest, response: esErrResponse, errContains: "failed to delete aliases: [index: jaeger-span, alias: jaeger-span-read],[index: jaeger-span, alias: jaeger-span-write]", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { assert.True(t, strings.HasSuffix(req.URL.String(), "_aliases")) assert.Equal(t, http.MethodPost, req.Method) assert.Equal(t, "Basic foobar", req.Header.Get("Authorization")) body, err := io.ReadAll(req.Body) assert.NoError(t, err) assert.Equal(t, expectedRequestBody, string(body)) res.WriteHeader(test.responseCode) res.Write([]byte(test.response)) })) defer testServer.Close() c := &IndicesClient{ Client: Client{ Client: testServer.Client(), Endpoint: testServer.URL, BasicAuth: "foobar", }, } err := c.DeleteAlias(aliases) if test.errContains != "" { assert.ErrorContains(t, err, test.errContains) } }) } } func TestClientCreateTemplate(t *testing.T) { templateName := "jaeger-template" templateContent := "template content" tests := []struct { name string versionResp string responseCode int response string errContains string }{ { name: "success/v7", versionResp: elasticsearch7, responseCode: http.StatusOK, }, { name: "success/v8", versionResp: elasticsearch8, responseCode: http.StatusOK, }, { name: "client error", versionResp: elasticsearch7, responseCode: http.StatusBadRequest, response: esErrResponse, errContains: "failed to create template: jaeger-template", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { if req.URL.String() == "/" { // ES version check res.WriteHeader(http.StatusOK) res.Write([]byte(test.versionResp)) return } assert.True(t, strings.HasSuffix(req.URL.String(), "_template/jaeger-template")) assert.Equal(t, http.MethodPut, req.Method) assert.Equal(t, "Basic foobar", req.Header.Get("Authorization")) body, err := io.ReadAll(req.Body) assert.NoError(t, err) assert.Equal(t, templateContent, string(body)) res.WriteHeader(test.responseCode) res.Write([]byte(test.response)) })) defer testServer.Close() c := &IndicesClient{ Client: Client{ Client: testServer.Client(), Endpoint: testServer.URL, BasicAuth: "foobar", }, } err := c.CreateTemplate(templateContent, templateName) if test.errContains != "" { assert.ErrorContains(t, err, test.errContains) } }) } } func TestRollover(t *testing.T) { expectedRequestBody := "{\"conditions\":{\"max_age\":\"2d\"}}" mapConditions := map[string]any{ "max_age": "2d", } tests := []struct { name string responseCode int response string errContains string }{ { name: "success", responseCode: http.StatusOK, }, { name: "client error", responseCode: http.StatusBadRequest, response: esErrResponse, errContains: "failed to create rollover target: jaeger-span", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { assert.True(t, strings.HasSuffix(req.URL.String(), "jaeger-span/_rollover/")) assert.Equal(t, http.MethodPost, req.Method) assert.Equal(t, "Basic foobar", req.Header.Get("Authorization")) body, err := io.ReadAll(req.Body) assert.NoError(t, err) assert.Equal(t, expectedRequestBody, string(body)) res.WriteHeader(test.responseCode) res.Write([]byte(test.response)) })) defer testServer.Close() c := &IndicesClient{ Client: Client{ Client: testServer.Client(), Endpoint: testServer.URL, BasicAuth: "foobar", }, } err := c.Rollover("jaeger-span", mapConditions) if test.errContains != "" { assert.ErrorContains(t, err, test.errContains) } }) } } ================================================ FILE: internal/storage/elasticsearch/client/interfaces.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package client type IndexAPI interface { GetJaegerIndices(prefix string) ([]Index, error) IndexExists(index string) (bool, error) AliasExists(alias string) (bool, error) DeleteIndices(indices []Index) error CreateIndex(index string) error CreateAlias(aliases []Alias) error DeleteAlias(aliases []Alias) error CreateTemplate(template, name string) error Rollover(rolloverTarget string, conditions map[string]any) error } type ClusterAPI interface { Version() (uint, error) } type IndexManagementLifecycleAPI interface { Exists(name string) (bool, error) } ================================================ FILE: internal/storage/elasticsearch/client/mocks/mocks.go ================================================ // Copyright (c) The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // // Run 'make generate-mocks' to regenerate. // Code generated by mockery; DO NOT EDIT. // github.com/vektra/mockery // template: testify package mocks import ( "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/client" mock "github.com/stretchr/testify/mock" ) // NewIndexAPI creates a new instance of IndexAPI. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewIndexAPI(t interface { mock.TestingT Cleanup(func()) }) *IndexAPI { mock := &IndexAPI{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // IndexAPI is an autogenerated mock type for the IndexAPI type type IndexAPI struct { mock.Mock } type IndexAPI_Expecter struct { mock *mock.Mock } func (_m *IndexAPI) EXPECT() *IndexAPI_Expecter { return &IndexAPI_Expecter{mock: &_m.Mock} } // AliasExists provides a mock function for the type IndexAPI func (_mock *IndexAPI) AliasExists(alias string) (bool, error) { ret := _mock.Called(alias) if len(ret) == 0 { panic("no return value specified for AliasExists") } var r0 bool var r1 error if returnFunc, ok := ret.Get(0).(func(string) (bool, error)); ok { return returnFunc(alias) } if returnFunc, ok := ret.Get(0).(func(string) bool); ok { r0 = returnFunc(alias) } else { r0 = ret.Get(0).(bool) } if returnFunc, ok := ret.Get(1).(func(string) error); ok { r1 = returnFunc(alias) } else { r1 = ret.Error(1) } return r0, r1 } // IndexAPI_AliasExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AliasExists' type IndexAPI_AliasExists_Call struct { *mock.Call } // AliasExists is a helper method to define mock.On call // - alias string func (_e *IndexAPI_Expecter) AliasExists(alias interface{}) *IndexAPI_AliasExists_Call { return &IndexAPI_AliasExists_Call{Call: _e.mock.On("AliasExists", alias)} } func (_c *IndexAPI_AliasExists_Call) Run(run func(alias string)) *IndexAPI_AliasExists_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *IndexAPI_AliasExists_Call) Return(b bool, err error) *IndexAPI_AliasExists_Call { _c.Call.Return(b, err) return _c } func (_c *IndexAPI_AliasExists_Call) RunAndReturn(run func(alias string) (bool, error)) *IndexAPI_AliasExists_Call { _c.Call.Return(run) return _c } // CreateAlias provides a mock function for the type IndexAPI func (_mock *IndexAPI) CreateAlias(aliases []client.Alias) error { ret := _mock.Called(aliases) if len(ret) == 0 { panic("no return value specified for CreateAlias") } var r0 error if returnFunc, ok := ret.Get(0).(func([]client.Alias) error); ok { r0 = returnFunc(aliases) } else { r0 = ret.Error(0) } return r0 } // IndexAPI_CreateAlias_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAlias' type IndexAPI_CreateAlias_Call struct { *mock.Call } // CreateAlias is a helper method to define mock.On call // - aliases []client.Alias func (_e *IndexAPI_Expecter) CreateAlias(aliases interface{}) *IndexAPI_CreateAlias_Call { return &IndexAPI_CreateAlias_Call{Call: _e.mock.On("CreateAlias", aliases)} } func (_c *IndexAPI_CreateAlias_Call) Run(run func(aliases []client.Alias)) *IndexAPI_CreateAlias_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 []client.Alias if args[0] != nil { arg0 = args[0].([]client.Alias) } run( arg0, ) }) return _c } func (_c *IndexAPI_CreateAlias_Call) Return(err error) *IndexAPI_CreateAlias_Call { _c.Call.Return(err) return _c } func (_c *IndexAPI_CreateAlias_Call) RunAndReturn(run func(aliases []client.Alias) error) *IndexAPI_CreateAlias_Call { _c.Call.Return(run) return _c } // CreateIndex provides a mock function for the type IndexAPI func (_mock *IndexAPI) CreateIndex(index string) error { ret := _mock.Called(index) if len(ret) == 0 { panic("no return value specified for CreateIndex") } var r0 error if returnFunc, ok := ret.Get(0).(func(string) error); ok { r0 = returnFunc(index) } else { r0 = ret.Error(0) } return r0 } // IndexAPI_CreateIndex_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateIndex' type IndexAPI_CreateIndex_Call struct { *mock.Call } // CreateIndex is a helper method to define mock.On call // - index string func (_e *IndexAPI_Expecter) CreateIndex(index interface{}) *IndexAPI_CreateIndex_Call { return &IndexAPI_CreateIndex_Call{Call: _e.mock.On("CreateIndex", index)} } func (_c *IndexAPI_CreateIndex_Call) Run(run func(index string)) *IndexAPI_CreateIndex_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *IndexAPI_CreateIndex_Call) Return(err error) *IndexAPI_CreateIndex_Call { _c.Call.Return(err) return _c } func (_c *IndexAPI_CreateIndex_Call) RunAndReturn(run func(index string) error) *IndexAPI_CreateIndex_Call { _c.Call.Return(run) return _c } // CreateTemplate provides a mock function for the type IndexAPI func (_mock *IndexAPI) CreateTemplate(template string, name string) error { ret := _mock.Called(template, name) if len(ret) == 0 { panic("no return value specified for CreateTemplate") } var r0 error if returnFunc, ok := ret.Get(0).(func(string, string) error); ok { r0 = returnFunc(template, name) } else { r0 = ret.Error(0) } return r0 } // IndexAPI_CreateTemplate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateTemplate' type IndexAPI_CreateTemplate_Call struct { *mock.Call } // CreateTemplate is a helper method to define mock.On call // - template string // - name string func (_e *IndexAPI_Expecter) CreateTemplate(template interface{}, name interface{}) *IndexAPI_CreateTemplate_Call { return &IndexAPI_CreateTemplate_Call{Call: _e.mock.On("CreateTemplate", template, name)} } func (_c *IndexAPI_CreateTemplate_Call) Run(run func(template string, name string)) *IndexAPI_CreateTemplate_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } var arg1 string if args[1] != nil { arg1 = args[1].(string) } run( arg0, arg1, ) }) return _c } func (_c *IndexAPI_CreateTemplate_Call) Return(err error) *IndexAPI_CreateTemplate_Call { _c.Call.Return(err) return _c } func (_c *IndexAPI_CreateTemplate_Call) RunAndReturn(run func(template string, name string) error) *IndexAPI_CreateTemplate_Call { _c.Call.Return(run) return _c } // DeleteAlias provides a mock function for the type IndexAPI func (_mock *IndexAPI) DeleteAlias(aliases []client.Alias) error { ret := _mock.Called(aliases) if len(ret) == 0 { panic("no return value specified for DeleteAlias") } var r0 error if returnFunc, ok := ret.Get(0).(func([]client.Alias) error); ok { r0 = returnFunc(aliases) } else { r0 = ret.Error(0) } return r0 } // IndexAPI_DeleteAlias_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteAlias' type IndexAPI_DeleteAlias_Call struct { *mock.Call } // DeleteAlias is a helper method to define mock.On call // - aliases []client.Alias func (_e *IndexAPI_Expecter) DeleteAlias(aliases interface{}) *IndexAPI_DeleteAlias_Call { return &IndexAPI_DeleteAlias_Call{Call: _e.mock.On("DeleteAlias", aliases)} } func (_c *IndexAPI_DeleteAlias_Call) Run(run func(aliases []client.Alias)) *IndexAPI_DeleteAlias_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 []client.Alias if args[0] != nil { arg0 = args[0].([]client.Alias) } run( arg0, ) }) return _c } func (_c *IndexAPI_DeleteAlias_Call) Return(err error) *IndexAPI_DeleteAlias_Call { _c.Call.Return(err) return _c } func (_c *IndexAPI_DeleteAlias_Call) RunAndReturn(run func(aliases []client.Alias) error) *IndexAPI_DeleteAlias_Call { _c.Call.Return(run) return _c } // DeleteIndices provides a mock function for the type IndexAPI func (_mock *IndexAPI) DeleteIndices(indices []client.Index) error { ret := _mock.Called(indices) if len(ret) == 0 { panic("no return value specified for DeleteIndices") } var r0 error if returnFunc, ok := ret.Get(0).(func([]client.Index) error); ok { r0 = returnFunc(indices) } else { r0 = ret.Error(0) } return r0 } // IndexAPI_DeleteIndices_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteIndices' type IndexAPI_DeleteIndices_Call struct { *mock.Call } // DeleteIndices is a helper method to define mock.On call // - indices []client.Index func (_e *IndexAPI_Expecter) DeleteIndices(indices interface{}) *IndexAPI_DeleteIndices_Call { return &IndexAPI_DeleteIndices_Call{Call: _e.mock.On("DeleteIndices", indices)} } func (_c *IndexAPI_DeleteIndices_Call) Run(run func(indices []client.Index)) *IndexAPI_DeleteIndices_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 []client.Index if args[0] != nil { arg0 = args[0].([]client.Index) } run( arg0, ) }) return _c } func (_c *IndexAPI_DeleteIndices_Call) Return(err error) *IndexAPI_DeleteIndices_Call { _c.Call.Return(err) return _c } func (_c *IndexAPI_DeleteIndices_Call) RunAndReturn(run func(indices []client.Index) error) *IndexAPI_DeleteIndices_Call { _c.Call.Return(run) return _c } // GetJaegerIndices provides a mock function for the type IndexAPI func (_mock *IndexAPI) GetJaegerIndices(prefix string) ([]client.Index, error) { ret := _mock.Called(prefix) if len(ret) == 0 { panic("no return value specified for GetJaegerIndices") } var r0 []client.Index var r1 error if returnFunc, ok := ret.Get(0).(func(string) ([]client.Index, error)); ok { return returnFunc(prefix) } if returnFunc, ok := ret.Get(0).(func(string) []client.Index); ok { r0 = returnFunc(prefix) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]client.Index) } } if returnFunc, ok := ret.Get(1).(func(string) error); ok { r1 = returnFunc(prefix) } else { r1 = ret.Error(1) } return r0, r1 } // IndexAPI_GetJaegerIndices_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetJaegerIndices' type IndexAPI_GetJaegerIndices_Call struct { *mock.Call } // GetJaegerIndices is a helper method to define mock.On call // - prefix string func (_e *IndexAPI_Expecter) GetJaegerIndices(prefix interface{}) *IndexAPI_GetJaegerIndices_Call { return &IndexAPI_GetJaegerIndices_Call{Call: _e.mock.On("GetJaegerIndices", prefix)} } func (_c *IndexAPI_GetJaegerIndices_Call) Run(run func(prefix string)) *IndexAPI_GetJaegerIndices_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *IndexAPI_GetJaegerIndices_Call) Return(indexs []client.Index, err error) *IndexAPI_GetJaegerIndices_Call { _c.Call.Return(indexs, err) return _c } func (_c *IndexAPI_GetJaegerIndices_Call) RunAndReturn(run func(prefix string) ([]client.Index, error)) *IndexAPI_GetJaegerIndices_Call { _c.Call.Return(run) return _c } // IndexExists provides a mock function for the type IndexAPI func (_mock *IndexAPI) IndexExists(index string) (bool, error) { ret := _mock.Called(index) if len(ret) == 0 { panic("no return value specified for IndexExists") } var r0 bool var r1 error if returnFunc, ok := ret.Get(0).(func(string) (bool, error)); ok { return returnFunc(index) } if returnFunc, ok := ret.Get(0).(func(string) bool); ok { r0 = returnFunc(index) } else { r0 = ret.Get(0).(bool) } if returnFunc, ok := ret.Get(1).(func(string) error); ok { r1 = returnFunc(index) } else { r1 = ret.Error(1) } return r0, r1 } // IndexAPI_IndexExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IndexExists' type IndexAPI_IndexExists_Call struct { *mock.Call } // IndexExists is a helper method to define mock.On call // - index string func (_e *IndexAPI_Expecter) IndexExists(index interface{}) *IndexAPI_IndexExists_Call { return &IndexAPI_IndexExists_Call{Call: _e.mock.On("IndexExists", index)} } func (_c *IndexAPI_IndexExists_Call) Run(run func(index string)) *IndexAPI_IndexExists_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *IndexAPI_IndexExists_Call) Return(b bool, err error) *IndexAPI_IndexExists_Call { _c.Call.Return(b, err) return _c } func (_c *IndexAPI_IndexExists_Call) RunAndReturn(run func(index string) (bool, error)) *IndexAPI_IndexExists_Call { _c.Call.Return(run) return _c } // Rollover provides a mock function for the type IndexAPI func (_mock *IndexAPI) Rollover(rolloverTarget string, conditions map[string]any) error { ret := _mock.Called(rolloverTarget, conditions) if len(ret) == 0 { panic("no return value specified for Rollover") } var r0 error if returnFunc, ok := ret.Get(0).(func(string, map[string]any) error); ok { r0 = returnFunc(rolloverTarget, conditions) } else { r0 = ret.Error(0) } return r0 } // IndexAPI_Rollover_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Rollover' type IndexAPI_Rollover_Call struct { *mock.Call } // Rollover is a helper method to define mock.On call // - rolloverTarget string // - conditions map[string]any func (_e *IndexAPI_Expecter) Rollover(rolloverTarget interface{}, conditions interface{}) *IndexAPI_Rollover_Call { return &IndexAPI_Rollover_Call{Call: _e.mock.On("Rollover", rolloverTarget, conditions)} } func (_c *IndexAPI_Rollover_Call) Run(run func(rolloverTarget string, conditions map[string]any)) *IndexAPI_Rollover_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } var arg1 map[string]any if args[1] != nil { arg1 = args[1].(map[string]any) } run( arg0, arg1, ) }) return _c } func (_c *IndexAPI_Rollover_Call) Return(err error) *IndexAPI_Rollover_Call { _c.Call.Return(err) return _c } func (_c *IndexAPI_Rollover_Call) RunAndReturn(run func(rolloverTarget string, conditions map[string]any) error) *IndexAPI_Rollover_Call { _c.Call.Return(run) return _c } // NewClusterAPI creates a new instance of ClusterAPI. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewClusterAPI(t interface { mock.TestingT Cleanup(func()) }) *ClusterAPI { mock := &ClusterAPI{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // ClusterAPI is an autogenerated mock type for the ClusterAPI type type ClusterAPI struct { mock.Mock } type ClusterAPI_Expecter struct { mock *mock.Mock } func (_m *ClusterAPI) EXPECT() *ClusterAPI_Expecter { return &ClusterAPI_Expecter{mock: &_m.Mock} } // Version provides a mock function for the type ClusterAPI func (_mock *ClusterAPI) Version() (uint, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Version") } var r0 uint var r1 error if returnFunc, ok := ret.Get(0).(func() (uint, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() uint); ok { r0 = returnFunc() } else { r0 = ret.Get(0).(uint) } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // ClusterAPI_Version_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Version' type ClusterAPI_Version_Call struct { *mock.Call } // Version is a helper method to define mock.On call func (_e *ClusterAPI_Expecter) Version() *ClusterAPI_Version_Call { return &ClusterAPI_Version_Call{Call: _e.mock.On("Version")} } func (_c *ClusterAPI_Version_Call) Run(run func()) *ClusterAPI_Version_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *ClusterAPI_Version_Call) Return(v uint, err error) *ClusterAPI_Version_Call { _c.Call.Return(v, err) return _c } func (_c *ClusterAPI_Version_Call) RunAndReturn(run func() (uint, error)) *ClusterAPI_Version_Call { _c.Call.Return(run) return _c } // NewIndexManagementLifecycleAPI creates a new instance of IndexManagementLifecycleAPI. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewIndexManagementLifecycleAPI(t interface { mock.TestingT Cleanup(func()) }) *IndexManagementLifecycleAPI { mock := &IndexManagementLifecycleAPI{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // IndexManagementLifecycleAPI is an autogenerated mock type for the IndexManagementLifecycleAPI type type IndexManagementLifecycleAPI struct { mock.Mock } type IndexManagementLifecycleAPI_Expecter struct { mock *mock.Mock } func (_m *IndexManagementLifecycleAPI) EXPECT() *IndexManagementLifecycleAPI_Expecter { return &IndexManagementLifecycleAPI_Expecter{mock: &_m.Mock} } // Exists provides a mock function for the type IndexManagementLifecycleAPI func (_mock *IndexManagementLifecycleAPI) Exists(name string) (bool, error) { ret := _mock.Called(name) if len(ret) == 0 { panic("no return value specified for Exists") } var r0 bool var r1 error if returnFunc, ok := ret.Get(0).(func(string) (bool, error)); ok { return returnFunc(name) } if returnFunc, ok := ret.Get(0).(func(string) bool); ok { r0 = returnFunc(name) } else { r0 = ret.Get(0).(bool) } if returnFunc, ok := ret.Get(1).(func(string) error); ok { r1 = returnFunc(name) } else { r1 = ret.Error(1) } return r0, r1 } // IndexManagementLifecycleAPI_Exists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Exists' type IndexManagementLifecycleAPI_Exists_Call struct { *mock.Call } // Exists is a helper method to define mock.On call // - name string func (_e *IndexManagementLifecycleAPI_Expecter) Exists(name interface{}) *IndexManagementLifecycleAPI_Exists_Call { return &IndexManagementLifecycleAPI_Exists_Call{Call: _e.mock.On("Exists", name)} } func (_c *IndexManagementLifecycleAPI_Exists_Call) Run(run func(name string)) *IndexManagementLifecycleAPI_Exists_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *IndexManagementLifecycleAPI_Exists_Call) Return(b bool, err error) *IndexManagementLifecycleAPI_Exists_Call { _c.Call.Return(b, err) return _c } func (_c *IndexManagementLifecycleAPI_Exists_Call) RunAndReturn(run func(name string) (bool, error)) *IndexManagementLifecycleAPI_Exists_Call { _c.Call.Return(run) return _c } ================================================ FILE: internal/storage/elasticsearch/client/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package client import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/elasticsearch/client.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "context" "io" "github.com/olivere/elastic/v7" ) // Client is an abstraction for elastic.Client type Client interface { IndexExists(index string) IndicesExistsService CreateIndex(index string) IndicesCreateService CreateTemplate(id string) TemplateCreateService Index() IndexService Search(indices ...string) SearchService MultiSearch() MultiSearchService DeleteIndex(index string) IndicesDeleteService io.Closer GetVersion() uint } // IndicesExistsService is an abstraction for elastic.IndicesExistsService type IndicesExistsService interface { Do(ctx context.Context) (bool, error) } // IndicesCreateService is an abstraction for elastic.IndicesCreateService type IndicesCreateService interface { Body(mapping string) IndicesCreateService Do(ctx context.Context) (*elastic.IndicesCreateResult, error) } // IndicesDeleteService is an abstraction for elastic.IndicesDeleteService type IndicesDeleteService interface { Do(ctx context.Context) (*elastic.IndicesDeleteResponse, error) } // TemplateCreateService is an abstraction for creating a mapping type TemplateCreateService interface { Body(mapping string) TemplateCreateService Do(ctx context.Context) (*elastic.IndicesPutTemplateResponse, error) } // IndexService is an abstraction for elastic BulkService type IndexService interface { Index(index string) IndexService Type(typ string) IndexService Id(id string) IndexService BodyJson(body any) IndexService Add() } // SearchService is an abstraction for elastic.SearchService type SearchService interface { Size(size int) SearchService Aggregation(name string, aggregation elastic.Aggregation) SearchService IgnoreUnavailable(ignoreUnavailable bool) SearchService Query(query elastic.Query) SearchService Do(ctx context.Context) (*elastic.SearchResult, error) } // MultiSearchService is an abstraction for elastic.MultiSearchService type MultiSearchService interface { Add(requests ...*elastic.SearchRequest) MultiSearchService Index(indices ...string) MultiSearchService Do(ctx context.Context) (*elastic.MultiSearchResult, error) } ================================================ FILE: internal/storage/elasticsearch/config/auth_helper.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package config import ( "context" "encoding/base64" "errors" "fmt" "time" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/auth" "github.com/jaegertracing/jaeger/internal/auth/apikey" "github.com/jaegertracing/jaeger/internal/auth/bearertoken" ) // initTokenAuthWithTime initializes token authentication injectable time for testing func initTokenAuthWithTime(tokenAuth *TokenAuthentication, scheme string, logger *zap.Logger, timeFn func() time.Time) (*auth.Method, error) { if tokenAuth == nil || (tokenAuth.FilePath == "" && !tokenAuth.AllowFromContext) { return nil, nil } if tokenAuth.FilePath != "" && tokenAuth.AllowFromContext { logger.Warn("Both token file and context propagation are enabled - context token will take precedence over file-based token", zap.String("auth_scheme", scheme)) } var tokenFn func() string var fromCtx func(context.Context) (string, bool) // File-based token setup if tokenAuth.FilePath != "" { tf, err := auth.TokenProviderWithTime(tokenAuth.FilePath, tokenAuth.ReloadInterval, logger, timeFn) if err != nil { return nil, err } tokenFn = tf } // Context-based token setup if tokenAuth.AllowFromContext { switch scheme { case "Bearer": fromCtx = bearertoken.GetBearerToken case "APIKey": fromCtx = apikey.GetAPIKey default: } } return &auth.Method{ Scheme: scheme, TokenFn: tokenFn, FromCtx: fromCtx, }, nil } // Simplified init functions - directly call shared implementation func initBearerAuth(tokenAuth *TokenAuthentication, logger *zap.Logger) (*auth.Method, error) { if tokenAuth == nil { return nil, nil } return initTokenAuthWithTime(tokenAuth, "Bearer", logger, time.Now) } func initAPIKeyAuth(tokenAuth *TokenAuthentication, logger *zap.Logger) (*auth.Method, error) { if tokenAuth == nil { return nil, nil } return initTokenAuthWithTime(tokenAuth, "APIKey", logger, time.Now) } // Keep initBasicAuth unchanged func initBasicAuth(basicAuth *BasicAuthentication, logger *zap.Logger) (*auth.Method, error) { return initBasicAuthWithTime(basicAuth, logger, time.Now) } func initBasicAuthWithTime(basicAuth *BasicAuthentication, logger *zap.Logger, timeFn func() time.Time) (*auth.Method, error) { if basicAuth == nil { return nil, nil } if basicAuth.Password != "" && basicAuth.PasswordFilePath != "" { return nil, errors.New("both Password and PasswordFilePath are set") } username := basicAuth.Username if username == "" { return nil, nil } var tokenFn func() string // Handle password from file or static password if basicAuth.PasswordFilePath != "" { // Use TokenProvider for password loading passwordFn, err := auth.TokenProviderWithTime(basicAuth.PasswordFilePath, basicAuth.ReloadInterval, logger, timeFn) if err != nil { return nil, fmt.Errorf("failed to load password from file: %w", err) } // Pre-encode credentials in TokenFn tokenFn = func() string { password := passwordFn() if password == "" { return "" } credentials := username + ":" + password return base64.StdEncoding.EncodeToString([]byte(credentials)) } } else { // Static password - pre-encode once password := basicAuth.Password credentials := username + ":" + password encodedCredentials := base64.StdEncoding.EncodeToString([]byte(credentials)) tokenFn = func() string { return encodedCredentials } } return &auth.Method{ Scheme: "Basic", TokenFn: tokenFn, // Returns base64-encoded credentials }, nil } ================================================ FILE: internal/storage/elasticsearch/config/auth_helper_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package config import ( "encoding/base64" "os" "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zaptest/observer" "github.com/jaegertracing/jaeger/internal/auth" ) // TestInitBearerAuth tests bearer token authentication initialization func TestInitBearerAuth(t *testing.T) { logger := zap.NewNop() tempDir := t.TempDir() bearerFile := filepath.Join(tempDir, "bearer-token") require.NoError(t, os.WriteFile(bearerFile, []byte("test-bearer"), 0o600)) tests := []struct { name string bearerAuth *TokenAuthentication expectError bool expectNil bool validate func(t *testing.T, method *auth.Method) }{ { name: "Valid file-based bearer auth", bearerAuth: &TokenAuthentication{ FilePath: bearerFile, }, expectError: false, expectNil: false, validate: func(t *testing.T, method *auth.Method) { require.NotNil(t, method) assert.Equal(t, "Bearer", method.Scheme) assert.NotNil(t, method.TokenFn) assert.Equal(t, "test-bearer", method.TokenFn()) assert.Nil(t, method.FromCtx) }, }, { name: "Valid context-based bearer auth", bearerAuth: &TokenAuthentication{ AllowFromContext: true, }, expectError: false, expectNil: false, validate: func(t *testing.T, method *auth.Method) { require.NotNil(t, method) assert.Equal(t, "Bearer", method.Scheme) assert.Nil(t, method.TokenFn) assert.NotNil(t, method.FromCtx) }, }, { name: "Nil bearer auth returns nil", bearerAuth: nil, expectNil: true, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { method, err := initBearerAuth(tc.bearerAuth, logger) switch { case tc.expectError: require.Error(t, err) assert.Nil(t, method) case tc.expectNil: require.NoError(t, err) assert.Nil(t, method) default: require.NoError(t, err) tc.validate(t, method) } }) } } // TestInitAPIKeyAuth tests API key authentication initialization func TestInitAPIKeyAuth(t *testing.T) { logger := zap.NewNop() tempDir := t.TempDir() apiKeyFile := filepath.Join(tempDir, "api-key") require.NoError(t, os.WriteFile(apiKeyFile, []byte("test-apikey"), 0o600)) tests := []struct { name string apiKeyAuth *TokenAuthentication expectError bool expectNil bool validate func(t *testing.T, method *auth.Method) }{ { name: "Valid file-based API key auth", apiKeyAuth: &TokenAuthentication{ FilePath: apiKeyFile, }, validate: func(t *testing.T, method *auth.Method) { require.NotNil(t, method) assert.Equal(t, "APIKey", method.Scheme) assert.NotNil(t, method.TokenFn) assert.Equal(t, "test-apikey", method.TokenFn()) assert.Nil(t, method.FromCtx) }, }, { name: "Valid context-based API key auth", apiKeyAuth: &TokenAuthentication{ AllowFromContext: true, }, validate: func(t *testing.T, method *auth.Method) { require.NotNil(t, method) assert.Equal(t, "APIKey", method.Scheme) assert.Nil(t, method.TokenFn) assert.NotNil(t, method.FromCtx) }, }, { name: "Nil API key auth returns nil", apiKeyAuth: nil, expectNil: true, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { method, err := initAPIKeyAuth(tc.apiKeyAuth, logger) switch { case tc.expectError: require.Error(t, err) assert.Nil(t, method) case tc.expectNil: require.NoError(t, err) assert.Nil(t, method) default: require.NoError(t, err) tc.validate(t, method) } }) } } // Test multiple auth types working together func TestMultipleTokenAuth(t *testing.T) { logger := zap.NewNop() tempDir := t.TempDir() bearerFile := filepath.Join(tempDir, "bearer") apiKeyFile := filepath.Join(tempDir, "apikey") require.NoError(t, os.WriteFile(bearerFile, []byte("bearer-token"), 0o600)) require.NoError(t, os.WriteFile(apiKeyFile, []byte("api-key-token"), 0o600)) bearerAuth := &TokenAuthentication{ FilePath: bearerFile, } apiKeyAuth := &TokenAuthentication{ FilePath: apiKeyFile, } bearerMethod, err := initBearerAuth(bearerAuth, logger) require.NoError(t, err) require.NotNil(t, bearerMethod) apiKeyMethod, err := initAPIKeyAuth(apiKeyAuth, logger) require.NoError(t, err) require.NotNil(t, apiKeyMethod) assert.Equal(t, "Bearer", bearerMethod.Scheme) assert.Equal(t, "APIKey", apiKeyMethod.Scheme) assert.Equal(t, "bearer-token", bearerMethod.TokenFn()) assert.Equal(t, "api-key-token", apiKeyMethod.TokenFn()) } // TestInitBasicAuth tests basic authentication initialization func TestInitBasicAuth(t *testing.T) { logger := zap.NewNop() tempDir := t.TempDir() passwordFile := filepath.Join(tempDir, "password") require.NoError(t, os.WriteFile(passwordFile, []byte("testpass"), 0o600)) tests := []struct { name string basicAuth *BasicAuthentication expectError bool expectNil bool validate func(t *testing.T, method *auth.Method) }{ { name: "Static password basic auth", basicAuth: &BasicAuthentication{ Username: "user", Password: "pass", }, expectNil: false, validate: func(t *testing.T, method *auth.Method) { assert.Equal(t, "Basic", method.Scheme) assert.NotNil(t, method.TokenFn) // Verify base64 encoded "user:pass" expected := base64.StdEncoding.EncodeToString([]byte("user:pass")) assert.Equal(t, expected, method.TokenFn()) }, }, { name: "File-based password basic auth", basicAuth: &BasicAuthentication{ Username: "user", PasswordFilePath: passwordFile, }, expectNil: false, validate: func(t *testing.T, method *auth.Method) { assert.Equal(t, "Basic", method.Scheme) assert.NotNil(t, method.TokenFn) // Verify base64 encoded "user:testpass" expected := base64.StdEncoding.EncodeToString([]byte("user:testpass")) assert.Equal(t, expected, method.TokenFn()) }, }, { name: "No username returns nil", basicAuth: &BasicAuthentication{ Password: "pass", }, expectNil: true, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { method, err := initBasicAuth(tc.basicAuth, logger) if tc.expectError { require.Error(t, err) } else { require.NoError(t, err) if tc.expectNil { assert.Nil(t, method) } else { tc.validate(t, method) } } }) } } // TestInitBasicAuthWithReload tests password file reloading func TestInitBasicAuthWithReload(t *testing.T) { currentTime := time.Unix(0, 0) timeFn := func() time.Time { return currentTime } tempDir := t.TempDir() passwordFile := filepath.Join(tempDir, "password") require.NoError(t, os.WriteFile(passwordFile, []byte("initial"), 0o600)) logger := zap.NewNop() basicAuth := &BasicAuthentication{ Username: "user", PasswordFilePath: passwordFile, ReloadInterval: 50 * time.Millisecond, } method, err := initBasicAuthWithTime(basicAuth, logger, timeFn) require.NoError(t, err) require.NotNil(t, method) // Initial token initialExpected := base64.StdEncoding.EncodeToString([]byte("user:initial")) assert.Equal(t, initialExpected, method.TokenFn()) // Update password file require.NoError(t, os.WriteFile(passwordFile, []byte("updated"), 0o600)) // Before reload interval - should return cached currentTime = currentTime.Add(25 * time.Millisecond) assert.Equal(t, initialExpected, method.TokenFn()) // After reload interval - should return updated currentTime = currentTime.Add(50 * time.Millisecond) updatedExpected := base64.StdEncoding.EncodeToString([]byte("user:updated")) assert.Equal(t, updatedExpected, method.TokenFn()) } func TestInitBasicAuth_EdgeCases(t *testing.T) { logger := zap.NewNop() tests := []struct { name string basicAuth *BasicAuthentication expectError bool expectNil bool errorMsg string }{ { name: "nil basicAuth returns nil", basicAuth: nil, expectNil: true, }, { name: "both password and file path set - validation error", basicAuth: &BasicAuthentication{ Username: "user", Password: "pass", PasswordFilePath: "/some/path", }, expectError: true, errorMsg: "both Password and PasswordFilePath are set", }, { name: "empty username returns nil", basicAuth: &BasicAuthentication{ Username: "", Password: "pass", }, expectNil: true, }, { name: "file path error", basicAuth: &BasicAuthentication{ Username: "user", PasswordFilePath: "/nonexistent/path", }, expectError: true, errorMsg: "failed to load password from file", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { method, err := initBasicAuth(tc.basicAuth, logger) switch { case tc.expectError: require.Error(t, err) assert.Contains(t, err.Error(), tc.errorMsg) assert.Nil(t, method) case tc.expectNil: require.NoError(t, err) assert.Nil(t, method) default: require.NoError(t, err) require.NotNil(t, method) } }) } } // Test warning logs for conflicting configuration func TestTokenAuth_WarningLogs(t *testing.T) { core, logs := observer.New(zap.WarnLevel) logger := zap.New(core) tempDir := t.TempDir() tokenFile := filepath.Join(tempDir, "token") require.NoError(t, os.WriteFile(tokenFile, []byte("test-token"), 0o600)) base := &TokenAuthentication{ FilePath: tokenFile, AllowFromContext: true, } method, err := initTokenAuthWithTime(base, "Bearer", logger, time.Now) require.NoError(t, err) require.NotNil(t, method) // Check that warning was logged require.Equal(t, 1, logs.Len()) logEntry := logs.All()[0] assert.Equal(t, zap.WarnLevel, logEntry.Level) assert.Contains(t, logEntry.Message, "Both token file and context propagation are enabled") assert.Equal(t, "Bearer", logEntry.Context[0].String) } ================================================ FILE: internal/storage/elasticsearch/config/config.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package config import ( "bufio" "context" "crypto/tls" "errors" "fmt" "maps" "net/http" "os" "path/filepath" "strconv" "strings" "sync" "time" "github.com/asaskevich/govalidator" esv8 "github.com/elastic/go-elasticsearch/v9" "github.com/olivere/elastic/v7" "go.opentelemetry.io/collector/config/configauth" "go.opentelemetry.io/collector/config/configoptional" "go.opentelemetry.io/collector/config/configtls" "go.opentelemetry.io/collector/extension/extensionauth" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zapgrpc" "github.com/jaegertracing/jaeger/internal/auth" "github.com/jaegertracing/jaeger/internal/metrics" es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" eswrapper "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/wrapper" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore/spanstoremetrics" ) const ( IndexPrefixSeparator = "-" ) // IndexOptions describes the index format and rollover frequency type IndexOptions struct { // Priority contains the priority of index template (ESv8 only). Priority int64 `mapstructure:"priority"` // DateLayout contains the format string used to format current time to part of the index name. // For example, "2006-01-02" layout will result in "jaeger-spans-yyyy-mm-dd". // If not specified, the default value is "2006-01-02". // See https://pkg.go.dev/time#Layout for more details on the syntax. DateLayout string `mapstructure:"date_layout"` // Shards is the number of shards per index in Elasticsearch. Shards int64 `mapstructure:"shards"` // Replicas is the number of replicas per index in Elasticsearch. Replicas *int64 `mapstructure:"replicas"` // RolloverFrequency contains the rollover frequency setting used to fetch // indices from elasticsearch. // Valid configuration options are: [hour, day]. // This setting does not affect the index rotation and is simply used for // fetching indices. RolloverFrequency string `mapstructure:"rollover_frequency"` } // Indices describes different configuration options for each index type type Indices struct { // IndexPrefix is an optional prefix to prepend to Jaeger indices. // For example, setting this field to "production" creates "production-jaeger-*". IndexPrefix IndexPrefix `mapstructure:"index_prefix"` Spans IndexOptions `mapstructure:"spans"` Services IndexOptions `mapstructure:"services"` Dependencies IndexOptions `mapstructure:"dependencies"` Sampling IndexOptions `mapstructure:"sampling"` } type bulkCallback struct { startTimes sync.Map sm *spanstoremetrics.WriteMetrics logger *zap.Logger } type IndexPrefix string func (p IndexPrefix) Apply(indexName string) string { ps := string(p) if ps == "" { return indexName } if strings.HasSuffix(ps, IndexPrefixSeparator) { return ps + indexName } return ps + IndexPrefixSeparator + indexName } // Configuration describes the configuration properties needed to connect to an ElasticSearch cluster type Configuration struct { // ---- connection related configs ---- // Servers is a list of Elasticsearch servers. The strings must must contain full URLs // (i.e. http://localhost:9200). Servers []string `mapstructure:"server_urls" valid:"required,url"` // RemoteReadClusters is a list of Elasticsearch remote cluster names for cross-cluster // querying. RemoteReadClusters []string `mapstructure:"remote_read_clusters"` Authentication Authentication `mapstructure:"auth"` // TLS contains the TLS configuration for the connection to the ElasticSearch clusters. TLS configtls.ClientConfig `mapstructure:"tls"` Sniffing Sniffing `mapstructure:"sniffing"` // Disable the Elasticsearch health check DisableHealthCheck bool `mapstructure:"disable_health_check"` // Set the Elasticsearch health check timeout startup HealthCheckTimeoutStartup time.Duration `mapstructure:"health_check_timeout_startup"` // SendGetBodyAs is the HTTP verb to use for requests that contain a body. SendGetBodyAs string `mapstructure:"send_get_body_as"` // QueryTimeout contains the timeout used for queries. A timeout of zero means no timeout. QueryTimeout time.Duration `mapstructure:"query_timeout"` // HTTPCompression can be set to false to disable gzip compression for requests to ElasticSearch HTTPCompression bool `mapstructure:"http_compression"` // CustomHeaders contains custom HTTP headers to be sent with every request to Elasticsearch. // This is useful for scenarios like AWS SigV4 proxy authentication where specific headers // (like Host) need to be set for proper request signing. CustomHeaders map[string]string `mapstructure:"custom_headers"` // ---- elasticsearch client related configs ---- BulkProcessing BulkProcessing `mapstructure:"bulk_processing"` // Version contains the major Elasticsearch version. If this field is not specified, // the value will be auto-detected from Elasticsearch. Version uint `mapstructure:"version"` // LogLevel contains the Elasticsearch client log-level. Valid values for this field // are: [debug, info, error] LogLevel string `mapstructure:"log_level"` // ---- index related configs ---- Indices Indices `mapstructure:"indices"` // UseReadWriteAliases, if set to true, will use read and write aliases for indices. // Use this option with Elasticsearch rollover API. It requires an external component // to create aliases before startup and then performing its management. UseReadWriteAliases bool `mapstructure:"use_aliases"` // SpanReadAlias specifies the exact alias name to use for reading spans. // When set, Jaeger will use this alias directly without any modifications. // This allows integration with existing Elasticsearch setups that have custom alias names. // Can only be used with UseReadWriteAliases=true. // Example: "my-custom-span-reader" SpanReadAlias string `mapstructure:"span_read_alias"` // SpanWriteAlias specifies the exact alias name to use for writing spans. // When set, Jaeger will use this alias directly without any modifications. // Can only be used with UseReadWriteAliases=true. // Example: "my-custom-span-writer" SpanWriteAlias string `mapstructure:"span_write_alias"` // ServiceReadAlias specifies the exact alias name to use for reading services. // When set, Jaeger will use this alias directly without any modifications. // Can only be used with UseReadWriteAliases=true. // Example: "my-custom-service-reader" ServiceReadAlias string `mapstructure:"service_read_alias"` // ServiceWriteAlias specifies the exact alias name to use for writing services. // When set, Jaeger will use this alias directly without any modifications. // Can only be used with UseReadWriteAliases=true. // Example: "my-custom-service-writer" ServiceWriteAlias string `mapstructure:"service_write_alias"` // ReadAliasSuffix is the suffix to append to the index name used for reading. // This configuration only exists to provide backwards compatibility for jaeger-v1 // which is why it is not exposed as a configuration option for jaeger-v2 ReadAliasSuffix string `mapstructure:"-"` // WriteAliasSuffix is the suffix to append to the write index name. // This configuration only exists to provide backwards compatibility for jaeger-v1 // which is why it is not exposed as a configuration option for jaeger-v2 WriteAliasSuffix string `mapstructure:"-"` // CreateIndexTemplates, if set to true, creates index templates at application startup. // This configuration should be set to false when templates are installed manually. CreateIndexTemplates bool `mapstructure:"create_mappings"` // Option to enable Index Lifecycle Management (ILM) for Jaeger span and service indices. // Read more about ILM at // https://www.jaegertracing.io/docs/deployment/#enabling-ilm-support UseILM bool `mapstructure:"use_ilm"` // ---- jaeger-specific configs ---- // MaxDocCount Defines maximum number of results to fetch from storage per query. MaxDocCount int `mapstructure:"max_doc_count"` // MaxSpanAge configures the maximum lookback on span reads. MaxSpanAge time.Duration `mapstructure:"max_span_age"` // ServiceCacheTTL contains the TTL for the cache of known service names. ServiceCacheTTL time.Duration `mapstructure:"service_cache_ttl"` // AdaptiveSamplingLookback contains the duration to look back for the // latest adaptive sampling probabilities. AdaptiveSamplingLookback time.Duration `mapstructure:"adaptive_sampling_lookback"` Tags TagsAsFields `mapstructure:"tags_as_fields"` // Enabled, if set to true, enables the namespace for storage pointed to by this configuration. Enabled bool `mapstructure:"-"` } // TagsAsFields holds configuration for tag schema. // By default Jaeger stores tags in an array of nested objects. // This configurations allows to store tags as object fields for better Kibana support. type TagsAsFields struct { // Store all tags as object fields, instead nested objects AllAsFields bool `mapstructure:"all"` // Dot replacement for tag keys when stored as object fields DotReplacement string `mapstructure:"dot_replacement"` // File path to tag keys which should be stored as object fields File string `mapstructure:"config_file"` // Comma delimited list of tags to store as object fields Include string `mapstructure:"include"` } // Sniffing sets the sniffing configuration for the ElasticSearch client, which is the process // of finding all the nodes of your cluster. Read more about sniffing at // https://github.com/olivere/elastic/wiki/Sniffing. type Sniffing struct { // Enabled, if set to true, enables sniffing for the ElasticSearch client. Enabled bool `mapstructure:"enabled"` // UseHTTPS, if set to true, sets the HTTP scheme to HTTPS when performing sniffing. // For ESV8, the scheme is set to HTTPS by default, so this configuration is ignored. UseHTTPS bool `mapstructure:"use_https"` } type BulkProcessing struct { // MaxBytes, contains the number of bytes which specifies when to flush. MaxBytes int `mapstructure:"max_bytes"` // MaxActions contain the number of added actions which specifies when to flush. MaxActions int `mapstructure:"max_actions"` // FlushInterval is the interval at the end of which a flush occurs. FlushInterval time.Duration `mapstructure:"flush_interval"` // Workers contains the number of concurrent workers allowed to be executed. Workers int `mapstructure:"workers"` } // TokenAuthentication contains the common fields shared by all token-based authentication methods type TokenAuthentication struct { // FilePath contains the path to a file containing the token. FilePath string `mapstructure:"file_path"` // AllowFromContext, if set to true, allows the token to be retrieved from the context. AllowFromContext bool `mapstructure:"from_context"` // ReloadInterval contains the interval at which the token file is reloaded. // If set to 0 then the file is only loaded once on startup. ReloadInterval time.Duration `mapstructure:"reload_interval"` } type Authentication struct { BasicAuthentication configoptional.Optional[BasicAuthentication] `mapstructure:"basic"` BearerTokenAuth configoptional.Optional[TokenAuthentication] `mapstructure:"bearer_token"` APIKeyAuth configoptional.Optional[TokenAuthentication] `mapstructure:"api_key"` configauth.Config `mapstructure:",squash"` } type BasicAuthentication struct { // Username contains the username required to connect to Elasticsearch. Username string `mapstructure:"username"` // Password contains The password required by Elasticsearch Password string `mapstructure:"password" json:"-"` // PasswordFilePath contains the path to a file containing password. // This file is watched for changes. PasswordFilePath string `mapstructure:"password_file"` // ReloadInterval contains the interval at which the password file is reloaded. // If set to 0 then the file is only loaded once on startup. ReloadInterval time.Duration `mapstructure:"reload_interval"` } // BearerTokenAuthentication contains the configuration for attaching bearer tokens // when making HTTP requests. Note that TokenFilePath and AllowTokenFromContext // should not both be enabled. If both TokenFilePath and AllowTokenFromContext are set, // the TokenFilePath will be ignored. // For more information about token-based authentication in elasticsearch, check out // https://www.elastic.co/guide/en/elasticsearch/reference/current/token-authentication-services.html. // NewClient creates a new ElasticSearch client func NewClient(ctx context.Context, c *Configuration, logger *zap.Logger, metricsFactory metrics.Factory, httpAuth extensionauth.HTTPClient) (es.Client, error) { if len(c.Servers) < 1 { return nil, errors.New("no servers specified") } options, err := c.getConfigOptions(ctx, logger, httpAuth) if err != nil { return nil, err } rawClient, err := elastic.NewClient(options...) if err != nil { return nil, err } bcb := bulkCallback{ sm: spanstoremetrics.NewWriter(metricsFactory, "bulk_index"), logger: logger, } if c.Version == 0 { // Determine ElasticSearch Version pingResult, pingStatus, err := rawClient.Ping(c.Servers[0]).Do(ctx) if err != nil { return nil, err } // Non-2xx responses aren't reported as errors by the ping code (7.0.32 version of // the elastic client). if pingStatus < 200 || pingStatus >= 300 { return nil, fmt.Errorf("ElasticSearch server %s returned HTTP %d, expected 2xx", c.Servers[0], pingStatus) } // The deserialization in the ping implementation may succeed even if the response // contains no relevant properties and we may get empty values in that case. if pingResult.Version.Number == "" { return nil, fmt.Errorf("ElasticSearch server %s returned invalid ping response", c.Servers[0]) } esVersion, err := strconv.Atoi(string(pingResult.Version.Number[0])) if err != nil { return nil, err } // OpenSearch is based on ES 7.x if strings.Contains(pingResult.TagLine, "OpenSearch") { if pingResult.Version.Number[0] == '1' { logger.Info("OpenSearch 1.x detected, using ES 7.x index mappings") esVersion = 7 } if pingResult.Version.Number[0] == '2' { logger.Info("OpenSearch 2.x detected, using ES 7.x index mappings") esVersion = 7 } if pingResult.Version.Number[0] == '3' { logger.Info("OpenSearch 3.x detected, using ES 7.x index mappings") esVersion = 7 } } logger.Info("Elasticsearch detected", zap.Int("version", esVersion)) c.Version = uint(esVersion) } var rawClientV8 *esv8.Client if c.Version >= 8 { rawClientV8, err = newElasticsearchV8(ctx, c, logger, httpAuth) if err != nil { return nil, fmt.Errorf("error creating v8 client: %w", err) } } bulkProc, err := rawClient.BulkProcessor(). Before(func(id int64, _ /* requests */ []elastic.BulkableRequest) { bcb.startTimes.Store(id, time.Now()) }). After(bcb.invoke). BulkSize(c.BulkProcessing.MaxBytes). Workers(c.BulkProcessing.Workers). BulkActions(c.BulkProcessing.MaxActions). FlushInterval(c.BulkProcessing.FlushInterval). Do(ctx) if err != nil { return nil, err } return eswrapper.WrapESClient(rawClient, bulkProc, c.Version, rawClientV8), nil } func (bcb *bulkCallback) invoke(id int64, requests []elastic.BulkableRequest, response *elastic.BulkResponse, err error) { start, ok := bcb.startTimes.Load(id) if ok { bcb.startTimes.Delete(id) } else { start = time.Now() } // Log individual errors if response != nil && response.Errors { for _, it := range response.Items { for key, val := range it { if val.Error != nil { bcb.logger.Error("Elasticsearch part of bulk request failed", zap.String("map-key", key), zap.Reflect("response", val)) } } } } latency := time.Since(start.(time.Time)) if err != nil { bcb.sm.LatencyErr.Record(latency) } else { bcb.sm.LatencyOk.Record(latency) } var failed int if response != nil { failed = len(response.Failed()) } total := len(requests) bcb.sm.Attempts.Inc(int64(total)) bcb.sm.Inserts.Inc(int64(total - failed)) bcb.sm.Errors.Inc(int64(failed)) if err != nil { bcb.logger.Error("Elasticsearch could not process bulk request", zap.Int("request_count", total), zap.Int("failed_count", failed), zap.Error(err), zap.Any("response", response)) } } func newElasticsearchV8(ctx context.Context, c *Configuration, logger *zap.Logger, httpAuth extensionauth.HTTPClient) (*esv8.Client, error) { var options esv8.Config options.Addresses = c.Servers if c.Authentication.BasicAuthentication.HasValue() { basicAuth := c.Authentication.BasicAuthentication.Get() options.Username = basicAuth.Username options.Password = basicAuth.Password } options.DiscoverNodesOnStart = c.Sniffing.Enabled options.CompressRequestBody = c.HTTPCompression if len(c.CustomHeaders) > 0 { headers := make(http.Header) for key, value := range c.CustomHeaders { headers.Set(key, value) } options.Header = headers } transport, err := GetHTTPRoundTripper(ctx, c, logger, httpAuth) if err != nil { return nil, err } options.Transport = transport return esv8.NewClient(options) } func setDefaultIndexOptions(target, source *IndexOptions) { if target.Shards == 0 { target.Shards = source.Shards } if target.Replicas == nil { target.Replicas = source.Replicas } if target.Priority == 0 { target.Priority = source.Priority } if target.DateLayout == "" { target.DateLayout = source.DateLayout } if target.RolloverFrequency == "" { target.RolloverFrequency = source.RolloverFrequency } } // ApplyDefaults copies settings from source unless its own value is non-zero. func (c *Configuration) ApplyDefaults(source *Configuration) { if len(c.RemoteReadClusters) == 0 { c.RemoteReadClusters = source.RemoteReadClusters } // Handle BasicAuthentication defaults sourceHasBasicAuth := source.Authentication.BasicAuthentication.HasValue() targetHasBasicAuth := c.Authentication.BasicAuthentication.HasValue() if sourceHasBasicAuth { // If target doesn't have BasicAuth, copy it from source if !targetHasBasicAuth { c.Authentication.BasicAuthentication = source.Authentication.BasicAuthentication } else { // Target has BasicAuth, apply field-level defaults sourceBasicAuth := source.Authentication.BasicAuthentication.Get() // Make a copy of target BasicAuth basicAuth := *c.Authentication.BasicAuthentication.Get() // Apply defaults for username if not set if basicAuth.Username == "" && sourceBasicAuth.Username != "" { basicAuth.Username = sourceBasicAuth.Username } // Apply defaults for password if not set if basicAuth.Password == "" && sourceBasicAuth.Password != "" { basicAuth.Password = sourceBasicAuth.Password } // Only update BasicAuthentication if we have values to set if basicAuth.Username != "" || basicAuth.Password != "" { c.Authentication.BasicAuthentication = configoptional.Some(basicAuth) } } } if !c.Sniffing.Enabled { c.Sniffing.Enabled = source.Sniffing.Enabled } if c.MaxSpanAge == 0 { c.MaxSpanAge = source.MaxSpanAge } if c.AdaptiveSamplingLookback == 0 { c.AdaptiveSamplingLookback = source.AdaptiveSamplingLookback } if c.Indices.IndexPrefix == "" { c.Indices.IndexPrefix = source.Indices.IndexPrefix } setDefaultIndexOptions(&c.Indices.Spans, &source.Indices.Spans) setDefaultIndexOptions(&c.Indices.Services, &source.Indices.Services) setDefaultIndexOptions(&c.Indices.Dependencies, &source.Indices.Dependencies) if c.BulkProcessing.MaxBytes == 0 { c.BulkProcessing.MaxBytes = source.BulkProcessing.MaxBytes } if c.BulkProcessing.Workers == 0 { c.BulkProcessing.Workers = source.BulkProcessing.Workers } if c.BulkProcessing.MaxActions == 0 { c.BulkProcessing.MaxActions = source.BulkProcessing.MaxActions } if c.BulkProcessing.FlushInterval == 0 { c.BulkProcessing.FlushInterval = source.BulkProcessing.FlushInterval } if !c.Sniffing.UseHTTPS { c.Sniffing.UseHTTPS = source.Sniffing.UseHTTPS } if !c.Tags.AllAsFields { c.Tags.AllAsFields = source.Tags.AllAsFields } if c.Tags.DotReplacement == "" { c.Tags.DotReplacement = source.Tags.DotReplacement } if c.Tags.Include == "" { c.Tags.Include = source.Tags.Include } if c.Tags.File == "" { c.Tags.File = source.Tags.File } if c.MaxDocCount == 0 { c.MaxDocCount = source.MaxDocCount } if c.LogLevel == "" { c.LogLevel = source.LogLevel } if c.SendGetBodyAs == "" { c.SendGetBodyAs = source.SendGetBodyAs } if !c.HTTPCompression { c.HTTPCompression = source.HTTPCompression } if c.CustomHeaders == nil && len(source.CustomHeaders) > 0 { c.CustomHeaders = make(map[string]string) maps.Copy(c.CustomHeaders, source.CustomHeaders) } } // RolloverFrequencyAsNegativeDuration returns the index rollover frequency duration for the given frequency string func RolloverFrequencyAsNegativeDuration(frequency string) time.Duration { if frequency == "hour" { return -1 * time.Hour } return -24 * time.Hour } // TagKeysAsFields returns tags from the file and command line merged func (c *Configuration) TagKeysAsFields() ([]string, error) { var tags []string // from file if c.Tags.File != "" { file, err := os.Open(filepath.Clean(c.Tags.File)) if err != nil { return nil, err } scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() if tag := strings.TrimSpace(line); tag != "" { tags = append(tags, tag) } } if err := file.Close(); err != nil { return nil, err } } // from params if c.Tags.Include != "" { tags = append(tags, strings.Split(c.Tags.Include, ",")...) } return tags, nil } func (c *Configuration) getESOptions(disableHealthCheck bool) []elastic.ClientOptionFunc { // Get base Elasticsearch options options := []elastic.ClientOptionFunc{ elastic.SetURL(c.Servers...), elastic.SetSniff(c.Sniffing.Enabled), elastic.SetHealthcheck(!disableHealthCheck), } if c.HealthCheckTimeoutStartup > 0 { options = append(options, elastic.SetHealthcheckTimeoutStartup(c.HealthCheckTimeoutStartup)) } if c.Sniffing.UseHTTPS { options = append(options, elastic.SetScheme("https")) } if c.SendGetBodyAs != "" { options = append(options, elastic.SetSendGetBodyAs(c.SendGetBodyAs)) } options = append(options, elastic.SetGzip(c.HTTPCompression)) return options } // getConfigOptions wraps the configs to feed to the ElasticSearch client init func (c *Configuration) getConfigOptions(ctx context.Context, logger *zap.Logger, httpAuth extensionauth.HTTPClient) ([]elastic.ClientOptionFunc, error) { // (has problems on AWS OpenSearch) see https://github.com/jaegertracing/jaeger/pull/7212 // Disable health check only in the following cases: // 1. When health check is explicitly disabled // 2. When tokens are EXCLUSIVELY available from context (not from file) // because at startup we don't have a valid token to do the health check disableHealthCheck := c.DisableHealthCheck // Check if we have bearer token or API key authentication that only allows from context if c.Authentication.BearerTokenAuth.HasValue() || c.Authentication.APIKeyAuth.HasValue() { bearerAuth := c.Authentication.BearerTokenAuth.Get() apiKeyAuth := c.Authentication.APIKeyAuth.Get() disableHealthCheck = disableHealthCheck || (bearerAuth != nil && bearerAuth.AllowFromContext && bearerAuth.FilePath == "") || (apiKeyAuth != nil && apiKeyAuth.AllowFromContext && apiKeyAuth.FilePath == "") } // Get base Elasticsearch options using the helper function options := c.getESOptions(disableHealthCheck) // Configure HTTP transport with TLS and authentication transport, err := GetHTTPRoundTripper(ctx, c, logger, httpAuth) if err != nil { return nil, err } // HTTP client setup with timeout and transport httpClient := &http.Client{ Timeout: c.QueryTimeout, Transport: transport, } options = append(options, elastic.SetHttpClient(httpClient)) // Add logging configuration options, err = addLoggerOptions(options, c.LogLevel, logger) if err != nil { return options, err } return options, nil } func addLoggerOptions(options []elastic.ClientOptionFunc, logLevel string, logger *zap.Logger) ([]elastic.ClientOptionFunc, error) { // Decouple ES logger from the log-level assigned to the parent application's log-level; otherwise, the least // permissive log-level will dominate. // e.g. --log-level=info and --es.log-level=debug would mute ES's debug logging and would require --log-level=debug // to show ES debug logs. var lvl zapcore.Level var setLogger func(logger elastic.Logger) elastic.ClientOptionFunc switch logLevel { case "debug": lvl = zap.DebugLevel setLogger = elastic.SetTraceLog case "info": lvl = zap.InfoLevel setLogger = elastic.SetInfoLog case "error": lvl = zap.ErrorLevel setLogger = elastic.SetErrorLog default: return options, fmt.Errorf("unrecognized log-level: \"%s\"", logLevel) } esLogger := logger.WithOptions( zap.IncreaseLevel(lvl), zap.AddCallerSkip(2), // to ensure the right caller:lineno are logged ) // Elastic client requires a "Printf"-able logger. l := zapgrpc.NewLogger(esLogger) options = append(options, setLogger(l)) return options, nil } // GetHTTPRoundTripper returns configured http.RoundTripper with optional HTTP authenticator. // Pass nil for httpAuth if authentication is not required. func GetHTTPRoundTripper(ctx context.Context, c *Configuration, logger *zap.Logger, httpAuth extensionauth.HTTPClient) (http.RoundTripper, error) { // Configure base transport. transport := &http.Transport{ Proxy: http.ProxyFromEnvironment, } // Configure TLS. if c.TLS.Insecure { // #nosec G402 transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} } else { tlsConfig, err := c.TLS.LoadTLSConfig(ctx) if err != nil { return nil, err } transport.TLSClientConfig = tlsConfig } // Initialize authentication methods. var authMethods []auth.Method // API Key Authentication if c.Authentication.APIKeyAuth.HasValue() { apiKeyAuth := c.Authentication.APIKeyAuth.Get() ak, err := initAPIKeyAuth(apiKeyAuth, logger) if err != nil { return nil, fmt.Errorf("failed to initialize API key authentication: %w", err) } if ak != nil { authMethods = append(authMethods, *ak) } } // Bearer Token Authentication if c.Authentication.BearerTokenAuth.HasValue() { bearerAuth := c.Authentication.BearerTokenAuth.Get() ba, err := initBearerAuth(bearerAuth, logger) if err != nil { return nil, fmt.Errorf("failed to initialize bearer authentication: %w", err) } if ba != nil { authMethods = append(authMethods, *ba) } } // Basic Authentication if c.Authentication.BasicAuthentication.HasValue() { basicAuth := c.Authentication.BasicAuthentication.Get() ba, err := initBasicAuth(basicAuth, logger) if err != nil { return nil, fmt.Errorf("failed to initialize basic authentication: %w", err) } if ba != nil { authMethods = append(authMethods, *ba) } } // Wrap with authentication layer. var roundTripper http.RoundTripper = transport if len(authMethods) > 0 { roundTripper = &auth.RoundTripper{ Transport: transport, Auths: authMethods, } } // Apply HTTP authenticator extension if configured (e.g., SigV4) if httpAuth != nil { wrappedRT, err := httpAuth.RoundTripper(roundTripper) if err != nil { return nil, fmt.Errorf("failed to wrap round tripper with HTTP authenticator: %w", err) } return wrappedRT, nil } return roundTripper, nil } func (c *Configuration) Validate() error { _, err := govalidator.ValidateStruct(c) if err != nil { return err } if c.UseILM && !c.UseReadWriteAliases { return errors.New("UseILM must always be used in conjunction with UseReadWriteAliases to ensure ES writers and readers refer to the single index mapping") } if c.CreateIndexTemplates && c.UseILM { return errors.New("when UseILM is set true, CreateIndexTemplates must be set to false and index templates must be created by init process of es-rollover app") } // Validate explicit alias settings require UseReadWriteAliases hasAnyExplicitAlias := c.SpanReadAlias != "" || c.SpanWriteAlias != "" || c.ServiceReadAlias != "" || c.ServiceWriteAlias != "" if hasAnyExplicitAlias && !c.UseReadWriteAliases { return errors.New("explicit aliases (span_read_alias, span_write_alias, service_read_alias, service_write_alias) require UseReadWriteAliases to be true") } // Validate that if any alias is set, all four should be set (for consistency) hasSpanAliases := c.SpanReadAlias != "" || c.SpanWriteAlias != "" hasServiceAliases := c.ServiceReadAlias != "" || c.ServiceWriteAlias != "" if hasSpanAliases && (c.SpanReadAlias == "" || c.SpanWriteAlias == "") { return errors.New("both span_read_alias and span_write_alias must be set together") } if hasServiceAliases && (c.ServiceReadAlias == "" || c.ServiceWriteAlias == "") { return errors.New("both service_read_alias and service_write_alias must be set together") } return nil } ================================================ FILE: internal/storage/elasticsearch/config/config_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package config import ( "context" "errors" "net/http" "net/http/httptest" "os" "path/filepath" "sync" "testing" "time" "github.com/olivere/elastic/v7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/config/configoptional" "go.opentelemetry.io/collector/config/configtls" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/auth" "github.com/jaegertracing/jaeger/internal/auth/bearertoken" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/metricstest" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore/spanstoremetrics" "github.com/jaegertracing/jaeger/internal/testutils" ) var mockEsServerResponseWithVersion0 = []byte(` { "Version": { "Number": "0" } } `) var mockEsServerResponseWithVersion1 = []byte(` { "tagline": "OpenSearch", "Version": { "Number": "1" } } `) var mockEsServerResponseWithVersion2 = []byte(` { "tagline": "OpenSearch", "Version": { "Number": "2" } } `) var mockEsServerResponseWithVersion3 = []byte(` { "tagline": "OpenSearch", "Version": { "Number": "3" } } `) var mockEsServerResponseWithVersion8 = []byte(` { "tagline": "OpenSearch", "Version": { "Number": "9" } } `) func copyToTempFile(t *testing.T, pattern string, filename string) (file *os.File) { tempDir := t.TempDir() tempFilePath := tempDir + "/" + pattern tempFile, err := os.Create(tempFilePath) require.NoError(t, err) data, err := os.ReadFile(filename) require.NoError(t, err) _, err = tempFile.Write(data) require.NoError(t, err) require.NoError(t, tempFile.Close()) return tempFile } // basicAuth creates basic authentication component func basicAuth(username, password, passwordFilePath string) configoptional.Optional[BasicAuthentication] { return configoptional.Some(BasicAuthentication{ Username: username, Password: password, PasswordFilePath: passwordFilePath, }) } // bearerAuth creates bearer token authentication component func bearerAuth(filePath string, allowFromContext bool) configoptional.Optional[TokenAuthentication] { return configoptional.Some(TokenAuthentication{ FilePath: filePath, AllowFromContext: allowFromContext, }) } // apiKeyAuth creates api key authentication component func apiKeyAuth(filePath string, allowFromContext bool) configoptional.Optional[TokenAuthentication] { return configoptional.Some(TokenAuthentication{ FilePath: filePath, AllowFromContext: allowFromContext, }) } func TestNewClient(t *testing.T) { const ( pwd1 = "password" token = "token" serverCert = "../../../../internal/config/tlscfg/testdata/example-server-cert.pem" apiKey = "test-api-key" ) apiKeyFile := filepath.Join(t.TempDir(), "api-key") require.NoError(t, os.WriteFile(apiKeyFile, []byte(apiKey), 0o600)) pwdFile := filepath.Join(t.TempDir(), "pwd") require.NoError(t, os.WriteFile(pwdFile, []byte(pwd1), 0o600)) pwdtokenFile := filepath.Join(t.TempDir(), "token") require.NoError(t, os.WriteFile(pwdtokenFile, []byte(token), 0o600)) // copy certs to temp so we can modify them certFilePath := copyToTempFile(t, "cert.crt", serverCert) defer certFilePath.Close() testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { // Accept both GET and HEAD requests assert.Contains(t, []string{http.MethodGet, http.MethodHead}, req.Method) res.WriteHeader(http.StatusOK) res.Write(mockEsServerResponseWithVersion0) })) defer testServer.Close() testServer1 := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { assert.Contains(t, []string{http.MethodGet, http.MethodHead}, req.Method) res.WriteHeader(http.StatusOK) res.Write(mockEsServerResponseWithVersion1) })) defer testServer1.Close() testServer2 := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { assert.Contains(t, []string{http.MethodGet, http.MethodHead}, req.Method) res.WriteHeader(http.StatusOK) res.Write(mockEsServerResponseWithVersion2) })) defer testServer2.Close() testServer3 := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { assert.Contains(t, []string{http.MethodGet, http.MethodHead}, req.Method) res.WriteHeader(http.StatusOK) res.Write(mockEsServerResponseWithVersion3) })) defer testServer3.Close() testServer8 := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { assert.Contains(t, []string{http.MethodGet, http.MethodHead}, req.Method) res.WriteHeader(http.StatusOK) res.Write(mockEsServerResponseWithVersion8) })) defer testServer8.Close() tests := []struct { name string config *Configuration expectedError bool }{ { name: "success with valid configuration", config: &Configuration{ Servers: []string{testServer.URL}, Authentication: Authentication{ BasicAuthentication: basicAuth("user", "secret", ""), BearerTokenAuth: bearerAuth("", true), APIKeyAuth: apiKeyAuth("", false), }, LogLevel: "debug", BulkProcessing: BulkProcessing{ MaxBytes: -1, // disable bulk; we want immediate flush }, Version: 8, }, expectedError: false, }, { name: "success with valid configuration and tls enabled", config: &Configuration{ Servers: []string{testServer.URL}, Authentication: Authentication{ BasicAuthentication: basicAuth("user", "secret", ""), BearerTokenAuth: bearerAuth("", true), APIKeyAuth: apiKeyAuth("", false), }, LogLevel: "debug", BulkProcessing: BulkProcessing{ MaxBytes: -1, // disable bulk; we want immediate flush }, Version: 0, TLS: configtls.ClientConfig{Insecure: false}, }, expectedError: false, }, { name: "success with valid configuration and reading token and certificate from file", config: &Configuration{ Servers: []string{testServer.URL}, Authentication: Authentication{ BasicAuthentication: basicAuth("user", "secret", ""), BearerTokenAuth: bearerAuth(pwdtokenFile, true), APIKeyAuth: apiKeyAuth("", false), }, LogLevel: "debug", BulkProcessing: BulkProcessing{ MaxBytes: -1, // disable bulk; we want immediate flush }, Version: 0, TLS: configtls.ClientConfig{ Insecure: true, Config: configtls.Config{ CAFile: certFilePath.Name(), }, }, }, expectedError: false, }, { name: "success with invalid configuration of version higher than 8", config: &Configuration{ Servers: []string{testServer8.URL}, Authentication: Authentication{ BasicAuthentication: basicAuth("user", "secret", ""), BearerTokenAuth: bearerAuth("", true), APIKeyAuth: apiKeyAuth("", false), }, LogLevel: "debug", BulkProcessing: BulkProcessing{ MaxBytes: -1, // disable bulk; we want immediate flush }, Version: 9, }, expectedError: false, }, { name: "success with valid configuration with version 1", config: &Configuration{ Servers: []string{testServer1.URL}, Authentication: Authentication{ BasicAuthentication: basicAuth("user", "secret", ""), BearerTokenAuth: bearerAuth("", true), APIKeyAuth: apiKeyAuth("", false), }, LogLevel: "debug", BulkProcessing: BulkProcessing{ MaxBytes: -1, // disable bulk; we want immediate flush }, }, expectedError: false, }, { name: "success with valid configuration with version 2", config: &Configuration{ Servers: []string{testServer2.URL}, Authentication: Authentication{ BasicAuthentication: basicAuth("user", "secret", ""), BearerTokenAuth: bearerAuth("", true), APIKeyAuth: apiKeyAuth("", false), }, LogLevel: "debug", BulkProcessing: BulkProcessing{ MaxBytes: -1, // disable bulk; we want immediate flush }, }, expectedError: false, }, { name: "success with valid configuration with version 3", config: &Configuration{ Servers: []string{testServer3.URL}, Authentication: Authentication{ BasicAuthentication: basicAuth("user", "secret", ""), BearerTokenAuth: bearerAuth("", true), APIKeyAuth: apiKeyAuth("", false), }, LogLevel: "debug", BulkProcessing: BulkProcessing{ MaxBytes: -1, // disable bulk; we want immediate flush }, }, expectedError: false, }, { name: "success with valid configuration password from file", config: &Configuration{ Servers: []string{testServer.URL}, Authentication: Authentication{ BasicAuthentication: basicAuth("user", "", pwdFile), BearerTokenAuth: bearerAuth("", true), APIKeyAuth: apiKeyAuth("", false), }, LogLevel: "debug", BulkProcessing: BulkProcessing{ MaxBytes: -1, // disable bulk; we want immediate flush }, }, expectedError: false, }, { name: "fail with configuration password and password from file are set", config: &Configuration{ Servers: []string{testServer.URL}, Authentication: Authentication{ BasicAuthentication: basicAuth("user", "secret", pwdFile), BearerTokenAuth: bearerAuth("", true), APIKeyAuth: apiKeyAuth("", false), }, LogLevel: "debug", BulkProcessing: BulkProcessing{ MaxBytes: -1, // disable bulk; we want immediate flush }, }, expectedError: true, }, { name: "fail with missing server", config: &Configuration{ Servers: []string{}, Authentication: Authentication{ BasicAuthentication: basicAuth("user", "secret", ""), BearerTokenAuth: bearerAuth("", true), APIKeyAuth: apiKeyAuth("", false), }, LogLevel: "debug", BulkProcessing: BulkProcessing{ MaxBytes: -1, // disable bulk; we want immediate flush }, }, expectedError: true, }, { name: "fail with invalid configuration invalid loglevel", config: &Configuration{ Servers: []string{testServer.URL}, Authentication: Authentication{ BasicAuthentication: basicAuth("user", "secret", ""), BearerTokenAuth: bearerAuth("", true), APIKeyAuth: apiKeyAuth("", false), }, LogLevel: "invalid", BulkProcessing: BulkProcessing{ MaxBytes: -1, // disable bulk; we want immediate flush }, }, expectedError: true, }, { name: "fail with invalid configuration invalid bulkworkers number", config: &Configuration{ Servers: []string{testServer.URL}, Authentication: Authentication{ BasicAuthentication: basicAuth("user", "secret", ""), BearerTokenAuth: bearerAuth("", true), APIKeyAuth: apiKeyAuth("", false), }, LogLevel: "invalid", BulkProcessing: BulkProcessing{ Workers: 0, MaxBytes: -1, // disable bulk; we want immediate flush }, }, expectedError: true, }, { name: "success with valid configuration and info log level", config: &Configuration{ Servers: []string{testServer.URL}, Authentication: Authentication{ BasicAuthentication: basicAuth("user", "secret", ""), BearerTokenAuth: bearerAuth("", true), APIKeyAuth: apiKeyAuth("", false), }, LogLevel: "info", BulkProcessing: BulkProcessing{ Workers: 0, MaxBytes: -1, // disable bulk; we want immediate flush }, }, expectedError: false, }, { name: "success with valid configuration and error log level", config: &Configuration{ Servers: []string{testServer.URL}, Authentication: Authentication{ BasicAuthentication: basicAuth("user", "secret", ""), BearerTokenAuth: bearerAuth("", true), APIKeyAuth: apiKeyAuth("", false), }, LogLevel: "error", BulkProcessing: BulkProcessing{ MaxBytes: -1, // disable bulk; we want immediate flush }, }, expectedError: false, }, { name: "success with API key from file", config: &Configuration{ Servers: []string{testServer.URL}, Authentication: Authentication{ APIKeyAuth: apiKeyAuth(apiKeyFile, false), }, LogLevel: "debug", BulkProcessing: BulkProcessing{ MaxBytes: -1, }, Version: 8, }, expectedError: false, }, { name: "success with API key from context only", config: &Configuration{ Servers: []string{testServer.URL}, Authentication: Authentication{ APIKeyAuth: apiKeyAuth("", true), }, LogLevel: "debug", BulkProcessing: BulkProcessing{ MaxBytes: -1, }, Version: 8, }, expectedError: false, }, { name: "success with API key from both file and context", config: &Configuration{ Servers: []string{testServer.URL}, Authentication: Authentication{ APIKeyAuth: apiKeyAuth(apiKeyFile, true), }, LogLevel: "debug", BulkProcessing: BulkProcessing{ MaxBytes: -1, }, Version: 8, }, expectedError: false, }, { name: "fail with invalid API key file path", config: &Configuration{ Servers: []string{testServer.URL}, Authentication: Authentication{ APIKeyAuth: apiKeyAuth("/nonexistent/api-key", false), }, LogLevel: "debug", BulkProcessing: BulkProcessing{ MaxBytes: -1, }, Version: 8, }, expectedError: true, }, { name: "success with API key context-only disables health check", config: &Configuration{ Servers: []string{testServer.URL}, DisableHealthCheck: false, Authentication: Authentication{ APIKeyAuth: apiKeyAuth("", true), }, LogLevel: "debug", BulkProcessing: BulkProcessing{ MaxBytes: -1, }, Version: 8, }, expectedError: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { logger := zap.NewNop() metricsFactory := metrics.NullFactory config := test.config client, err := NewClient(context.Background(), config, logger, metricsFactory, nil) if test.expectedError { require.Error(t, err) require.Nil(t, client) } else { require.NoError(t, err) require.NotNil(t, client) err = client.Close() require.NoError(t, err) } }) } } func TestNewClientPingErrorHandling(t *testing.T) { tests := []struct { name string serverResponse []byte statusCode int expectedError string }{ { name: "ping returns 404 status", serverResponse: mockEsServerResponseWithVersion0, statusCode: 404, expectedError: "ElasticSearch server", }, { name: "ping returns 500 status", serverResponse: mockEsServerResponseWithVersion0, statusCode: 500, expectedError: "ElasticSearch server", }, { name: "ping returns 300 status", serverResponse: mockEsServerResponseWithVersion0, statusCode: 300, expectedError: "ElasticSearch server", }, { name: "ping returns empty version number", serverResponse: []byte(`{"Version": {"Number": ""}}`), statusCode: 200, expectedError: "invalid ping response", }, { name: "ping returns valid 200 status with version", serverResponse: mockEsServerResponseWithVersion0, statusCode: 200, expectedError: "", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { assert.Contains(t, []string{http.MethodGet, http.MethodHead}, req.Method) res.WriteHeader(test.statusCode) res.Write(test.serverResponse) })) defer testServer.Close() config := &Configuration{ Servers: []string{testServer.URL}, LogLevel: "error", DisableHealthCheck: true, } logger := zap.NewNop() metricsFactory := metrics.NullFactory client, err := NewClient(context.Background(), config, logger, metricsFactory, nil) if test.expectedError != "" { require.Error(t, err) assert.Contains(t, err.Error(), test.expectedError) require.Nil(t, client) } else { require.NoError(t, err) require.NotNil(t, client) err = client.Close() require.NoError(t, err) } }) } } func TestNewClientVersionDetection(t *testing.T) { tests := []struct { name string serverResponse []byte expectedVersion uint expectedError string }{ { name: "version number with letters", serverResponse: []byte(`{ "Version": { "Number": "7.x.1" } }`), expectedVersion: 7, expectedError: "", }, { name: "empty version number should fail validation", serverResponse: []byte(`{ "Version": { "Number": "" } }`), expectedError: "invalid ping response", }, { name: "version number as numeric should fail JSON parsing", serverResponse: []byte(`{ "Version": { "Number": 7 } }`), expectedError: "cannot unmarshal number", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, _ *http.Request) { res.WriteHeader(http.StatusOK) res.Write(test.serverResponse) })) defer testServer.Close() config := &Configuration{ Servers: []string{testServer.URL}, LogLevel: "error", DisableHealthCheck: true, } logger := zap.NewNop() metricsFactory := metrics.NullFactory client, err := NewClient(context.Background(), config, logger, metricsFactory, nil) if test.expectedError != "" { require.Error(t, err) assert.Contains(t, err.Error(), test.expectedError) require.Nil(t, client) } else { require.NoError(t, err) require.NotNil(t, client) assert.Equal(t, test.expectedVersion, config.Version) err = client.Close() require.NoError(t, err) } }) } } func TestApplyDefaults(t *testing.T) { source := &Configuration{ RemoteReadClusters: []string{"cluster1", "cluster2"}, Authentication: Authentication{ BasicAuthentication: basicAuth("sourceUser", "sourcePass", ""), }, Sniffing: Sniffing{ Enabled: true, UseHTTPS: true, }, MaxSpanAge: 100, AdaptiveSamplingLookback: 50, Indices: Indices{ IndexPrefix: "hello", Spans: IndexOptions{ Shards: 5, Replicas: new(int64(1)), Priority: 10, }, Services: IndexOptions{ Shards: 5, Replicas: new(int64(1)), Priority: 20, }, Dependencies: IndexOptions{ Shards: 5, Replicas: new(int64(1)), Priority: 30, }, Sampling: IndexOptions{}, }, BulkProcessing: BulkProcessing{ MaxBytes: 1000, Workers: 10, MaxActions: 100, FlushInterval: 30, }, Tags: TagsAsFields{AllAsFields: true, DotReplacement: "dot", Include: "include", File: "file"}, MaxDocCount: 10000, LogLevel: "info", SendGetBodyAs: "json", } tests := []struct { name string target *Configuration expected *Configuration }{ { name: "All Defaults Applied except PriorityDependenciesTemplate", target: &Configuration{ Indices: Indices{ Dependencies: IndexOptions{ Priority: 30, }, }, }, // All fields are empty expected: source, }, { name: "Some Defaults Applied", target: &Configuration{ RemoteReadClusters: []string{"customCluster"}, Authentication: Authentication{ BasicAuthentication: basicAuth("customUser", "", ""), }, Indices: Indices{ Spans: IndexOptions{ Priority: 10, }, Services: IndexOptions{ Priority: 20, }, Dependencies: IndexOptions{ Priority: 30, }, }, // Other fields left default }, expected: &Configuration{ RemoteReadClusters: []string{"customCluster"}, Authentication: Authentication{ BasicAuthentication: basicAuth("customUser", "sourcePass", ""), }, Sniffing: Sniffing{ Enabled: true, UseHTTPS: true, }, MaxSpanAge: 100, AdaptiveSamplingLookback: 50, Indices: Indices{ IndexPrefix: "hello", Spans: IndexOptions{ Shards: 5, Replicas: new(int64(1)), Priority: 10, }, Services: IndexOptions{ Shards: 5, Replicas: new(int64(1)), Priority: 20, }, Dependencies: IndexOptions{ Shards: 5, Replicas: new(int64(1)), Priority: 30, }, }, BulkProcessing: BulkProcessing{ MaxBytes: 1000, Workers: 10, MaxActions: 100, FlushInterval: 30, }, Tags: TagsAsFields{AllAsFields: true, DotReplacement: "dot", Include: "include", File: "file"}, MaxDocCount: 10000, LogLevel: "info", SendGetBodyAs: "json", }, }, { name: "No Defaults Applied", target: &Configuration{ RemoteReadClusters: []string{"cluster1", "cluster2"}, Authentication: Authentication{ BasicAuthentication: basicAuth("sourceUser", "sourcePass", ""), }, Sniffing: Sniffing{ Enabled: true, UseHTTPS: true, }, MaxSpanAge: 100, AdaptiveSamplingLookback: 50, Indices: Indices{ IndexPrefix: "hello", Spans: IndexOptions{ Shards: 5, Replicas: new(int64(1)), Priority: 10, }, Services: IndexOptions{ Shards: 5, Replicas: new(int64(1)), Priority: 20, }, Dependencies: IndexOptions{ Shards: 5, Replicas: new(int64(1)), Priority: 30, }, }, BulkProcessing: BulkProcessing{ MaxBytes: 1000, Workers: 10, MaxActions: 100, FlushInterval: 30, }, Tags: TagsAsFields{AllAsFields: true, DotReplacement: "dot", Include: "include", File: "file"}, MaxDocCount: 10000, LogLevel: "info", SendGetBodyAs: "json", }, expected: source, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { test.target.ApplyDefaults(source) require.Equal(t, test.expected, test.target) }) } } func TestApplyDefaults_Auth(t *testing.T) { source := &Configuration{ Authentication: Authentication{ BasicAuthentication: basicAuth("sourceUser", "sourcePass", ""), }, } target := &Configuration{ Authentication: Authentication{ BasicAuthentication: basicAuth("", "", ""), }, } expected := &Configuration{ Authentication: Authentication{ BasicAuthentication: basicAuth("sourceUser", "sourcePass", ""), }, } target.ApplyDefaults(source) require.Equal(t, expected, target) } func TestTagKeysAsFields(t *testing.T) { const ( pwd1 = "tag1\ntag2" ) pwdFile := filepath.Join(t.TempDir(), "pwd") require.NoError(t, os.WriteFile(pwdFile, []byte(pwd1), 0o600)) tests := []struct { name string config *Configuration expectedTags []string expectError bool }{ { name: "File with tags", config: &Configuration{ Tags: TagsAsFields{ File: pwdFile, Include: "", }, }, expectedTags: []string{"tag1", "tag2"}, expectError: false, }, { name: "include with tags", config: &Configuration{ Tags: TagsAsFields{ File: "", Include: "cmdtag1,cmdtag2", }, }, expectedTags: []string{"cmdtag1", "cmdtag2"}, expectError: false, }, { name: "File and include with tags", config: &Configuration{ Tags: TagsAsFields{ File: pwdFile, Include: "cmdtag1,cmdtag2", }, }, expectedTags: []string{"tag1", "tag2", "cmdtag1", "cmdtag2"}, expectError: false, }, { name: "File read error", config: &Configuration{ Tags: TagsAsFields{ File: "/invalid/path/to/file.txt", Include: "", }, }, expectedTags: nil, expectError: true, }, { name: "Empty file and params", config: &Configuration{ Tags: TagsAsFields{ File: "", Include: "", }, }, expectedTags: nil, expectError: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { tags, err := test.config.TagKeysAsFields() if test.expectError { require.Error(t, err) require.Nil(t, tags) } else { require.NoError(t, err) require.ElementsMatch(t, test.expectedTags, tags) } }) } } func TestRolloverFrequencyAsNegativeDuration(t *testing.T) { tests := []struct { name string indexFrequency string expected time.Duration }{ { name: "hourly jaeger-span", indexFrequency: "hour", expected: -1 * time.Hour, }, { name: "daily jaeger-span", indexFrequency: "daily", expected: -24 * time.Hour, }, { name: "empty jaeger-span", indexFrequency: "", expected: -24 * time.Hour, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { got := RolloverFrequencyAsNegativeDuration(test.indexFrequency) require.Equal(t, test.expected, got) }) } } func TestValidate(t *testing.T) { tests := []struct { name string config *Configuration expectedError string }{ { name: "All valid input are set", config: &Configuration{ Servers: []string{"localhost:8000/dummyserver"}, }, }, { name: "no valid input are set", config: &Configuration{}, expectedError: "Servers: non zero value required", }, { name: "ilm disabled and read-write aliases enabled error", config: &Configuration{Servers: []string{"localhost:8000/dummyserver"}, UseILM: true}, expectedError: "UseILM must always be used in conjunction with UseReadWriteAliases to ensure ES writers and readers refer to the single index mapping", }, { name: "ilm and create templates enabled", config: &Configuration{Servers: []string{"localhost:8000/dummyserver"}, UseILM: true, CreateIndexTemplates: true, UseReadWriteAliases: true}, expectedError: "when UseILM is set true, CreateIndexTemplates must be set to false and index templates must be created by init process of es-rollover app", }, { name: "explicit span aliases without UseReadWriteAliases", config: &Configuration{ Servers: []string{"localhost:8000/dummyserver"}, SpanReadAlias: "custom-span-read", SpanWriteAlias: "custom-span-write", }, expectedError: "explicit aliases (span_read_alias, span_write_alias, service_read_alias, service_write_alias) require UseReadWriteAliases to be true", }, { name: "only span read alias set", config: &Configuration{ Servers: []string{"localhost:8000/dummyserver"}, UseReadWriteAliases: true, SpanReadAlias: "custom-span-read", }, expectedError: "both span_read_alias and span_write_alias must be set together", }, { name: "only service write alias set", config: &Configuration{ Servers: []string{"localhost:8000/dummyserver"}, UseReadWriteAliases: true, ServiceWriteAlias: "custom-service-write", }, expectedError: "both service_read_alias and service_write_alias must be set together", }, { name: "all explicit aliases with UseReadWriteAliases is valid", config: &Configuration{ Servers: []string{"localhost:8000/dummyserver"}, UseReadWriteAliases: true, SpanReadAlias: "custom-span-read", SpanWriteAlias: "custom-span-write", ServiceReadAlias: "custom-service-read", ServiceWriteAlias: "custom-service-write", }, }, { name: "only span aliases with UseReadWriteAliases is valid", config: &Configuration{ Servers: []string{"localhost:8000/dummyserver"}, UseReadWriteAliases: true, SpanReadAlias: "custom-span-read", SpanWriteAlias: "custom-span-write", }, }, { name: "explicit aliases with IndexPrefix is valid", config: &Configuration{ Servers: []string{"localhost:8000/dummyserver"}, UseReadWriteAliases: true, SpanReadAlias: "custom-span-read", SpanWriteAlias: "custom-span-write", ServiceReadAlias: "custom-service-read", ServiceWriteAlias: "custom-service-write", Indices: Indices{ IndexPrefix: "prod", }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { got := test.config.Validate() if test.expectedError != "" { require.ErrorContains(t, got, test.expectedError) } else { require.NoError(t, got) } }) } } func TestApplyForIndexPrefix(t *testing.T) { tests := []struct { testName string prefix IndexPrefix name string expectedName string }{ { testName: "no prefix", prefix: "", name: "hello", expectedName: "hello", }, { testName: "empty name", prefix: "bye", name: "", expectedName: "bye-", }, { testName: "separator suffix", prefix: "bye-", name: "hello", expectedName: "bye-hello", }, { testName: "no separator suffix", prefix: "bye", name: "hello", expectedName: "bye-hello", }, } for _, test := range tests { t.Run(test.testName, func(t *testing.T) { got := test.prefix.Apply(test.name) require.Equal(t, test.expectedName, got) }) } } func TestHandleBulkAfterCallback_ErrorMetricsEmitted(t *testing.T) { mf := metricstest.NewFactory(time.Minute) sm := spanstoremetrics.NewWriter(mf, "bulk_index") logger := zap.NewNop() defer mf.Stop() var m sync.Map batchID := int64(1) start := time.Now().Add(-100 * time.Millisecond) m.Store(batchID, start) fakeRequests := []elastic.BulkableRequest{nil, nil} response := &elastic.BulkResponse{ Errors: true, Items: []map[string]*elastic.BulkResponseItem{ { "index": { Status: 500, Error: &elastic.ErrorDetails{Type: "server_error"}, }, }, { "index": { Status: 200, Error: nil, }, }, }, } bcb := bulkCallback{ sm: sm, logger: logger, } bcb.invoke(batchID, fakeRequests, response, assert.AnError) mf.AssertCounterMetrics(t, metricstest.ExpectedMetric{ Name: "bulk_index.errors", Value: 1, }, metricstest.ExpectedMetric{ Name: "bulk_index.inserts", Value: 1, }, metricstest.ExpectedMetric{ Name: "bulk_index.attempts", Value: 2, }, ) } func TestHandleBulkAfterCallback_MissingStartTime(t *testing.T) { mf := metricstest.NewFactory(time.Minute) sm := spanstoremetrics.NewWriter(mf, "bulk_index") logger := zap.NewNop() defer mf.Stop() batchID := int64(42) // assign any value which is not stored in the map fakeRequests := []elastic.BulkableRequest{nil} response := &elastic.BulkResponse{ Errors: true, Items: []map[string]*elastic.BulkResponseItem{ { "index": { Status: 500, Error: &elastic.ErrorDetails{Type: "mock_error"}, }, }, }, } bcb := bulkCallback{ sm: sm, logger: logger, } bcb.invoke(batchID, fakeRequests, response, assert.AnError) mf.AssertCounterMetrics(t, metricstest.ExpectedMetric{ Name: "bulk_index.errors", Value: 1, }, metricstest.ExpectedMetric{ Name: "bulk_index.inserts", Value: 0, }, metricstest.ExpectedMetric{ Name: "bulk_index.attempts", Value: 1, }, ) } func TestGetConfigOptions(t *testing.T) { tmpDir := t.TempDir() bearerTokenFile := filepath.Join(tmpDir, "bearertoken") os.WriteFile(bearerTokenFile, []byte("file-bearer-token"), 0o600) tests := []struct { name string cfg *Configuration ctx context.Context prepare func() wantErr bool wantErrContains string }{ { name: "BearerToken context propagation", cfg: &Configuration{ Servers: []string{"http://localhost:9200"}, Sniffing: Sniffing{Enabled: false}, Authentication: Authentication{ BearerTokenAuth: bearerAuth("", true), }, LogLevel: "info", }, ctx: bearertoken.ContextWithBearerToken(context.Background(), "context-bearer-token"), wantErr: false, }, { name: "BearerToken file and context both enabled", cfg: &Configuration{ Servers: []string{"http://localhost:9200"}, Sniffing: Sniffing{Enabled: false}, Authentication: Authentication{ BearerTokenAuth: bearerAuth(bearerTokenFile, true), }, LogLevel: "info", }, ctx: bearertoken.ContextWithBearerToken(context.Background(), "context-bearer-token"), wantErr: false, }, { name: "BearerToken file error", cfg: &Configuration{ Servers: []string{"http://localhost:9200"}, TLS: configtls.ClientConfig{Insecure: true}, Sniffing: Sniffing{Enabled: false}, Authentication: Authentication{ BearerTokenAuth: bearerAuth("/does/not/exist/token", false), }, LogLevel: "info", }, ctx: context.Background(), wantErr: true, wantErrContains: "no such file or directory", }, { name: "No auth configured", cfg: &Configuration{ Servers: []string{"http://localhost:9200"}, LogLevel: "info", Sniffing: Sniffing{Enabled: false}, }, ctx: context.Background(), wantErr: false, }, { name: "BasicAuth password file error", cfg: &Configuration{ Servers: []string{"http://localhost:9200"}, Sniffing: Sniffing{Enabled: false}, Authentication: Authentication{ BasicAuthentication: basicAuth("testuser", "", "/does/not/exist"), }, LogLevel: "info", }, ctx: context.Background(), wantErr: true, wantErrContains: "failed to initialize basic authentication", }, { name: "BasicAuth both Password and PasswordFilePath set", cfg: &Configuration{ Servers: []string{"http://localhost:9200"}, Sniffing: Sniffing{Enabled: false}, Authentication: Authentication{ BasicAuthentication: basicAuth("testuser", "secret", "/some/file/path"), }, LogLevel: "info", }, ctx: context.Background(), wantErr: true, wantErrContains: "failed to initialize basic authentication", }, { name: "Invalid log level triggers addLoggerOptions error", cfg: &Configuration{ Servers: []string{"http://localhost:9200"}, Sniffing: Sniffing{Enabled: false}, Authentication: Authentication{ BasicAuthentication: basicAuth("user", "secret", ""), }, LogLevel: "invalid", }, ctx: context.Background(), wantErr: true, wantErrContains: "unrecognized log-level", }, { name: "Health check disabled for context-only auth", cfg: &Configuration{ Servers: []string{"http://localhost:9200"}, LogLevel: "info", DisableHealthCheck: false, // Should be overridden by context-only auth Sniffing: Sniffing{Enabled: false}, Authentication: Authentication{ BearerTokenAuth: bearerAuth("", true), }, }, ctx: bearertoken.ContextWithBearerToken(context.Background(), "context-bearer-token"), wantErr: false, }, { name: "Health check disabled explicitly", cfg: &Configuration{ Servers: []string{"http://localhost:9200"}, LogLevel: "info", DisableHealthCheck: true, Sniffing: Sniffing{Enabled: false}, }, ctx: context.Background(), wantErr: false, }, { name: "HTTP compression and custom SendGetBodyAs", cfg: &Configuration{ Servers: []string{"http://localhost:9200"}, LogLevel: "info", HTTPCompression: true, SendGetBodyAs: "POST", Sniffing: Sniffing{Enabled: true, UseHTTPS: true}, }, ctx: context.Background(), wantErr: false, }, } logger := zap.NewNop() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.prepare != nil { tt.prepare() } options, err := tt.cfg.getConfigOptions(tt.ctx, logger, nil) if tt.wantErr { require.Error(t, err) if tt.wantErrContains != "" { require.Contains(t, err.Error(), tt.wantErrContains) } } else { require.NoError(t, err) require.NotNil(t, options) require.NotEmpty(t, options, "Should have at least basic ES options") } }) } } func TestGetESOptions(t *testing.T) { tests := []struct { name string cfg *Configuration disableHealthCheck bool wantErr bool validateOptions func(t *testing.T, options []elastic.ClientOptionFunc) }{ { name: "Basic configuration", cfg: &Configuration{ Servers: []string{"http://localhost:9200"}, Sniffing: Sniffing{ Enabled: true, UseHTTPS: false, }, HTTPCompression: true, SendGetBodyAs: "POST", }, disableHealthCheck: false, wantErr: false, validateOptions: func(t *testing.T, options []elastic.ClientOptionFunc) { require.NotNil(t, options) require.NotEmpty(t, options, "Expected non-empty options slice") }, }, { name: "HTTPS configuration", cfg: &Configuration{ Servers: []string{"https://localhost:9200"}, Sniffing: Sniffing{ Enabled: false, UseHTTPS: true, }, HTTPCompression: false, SendGetBodyAs: "", }, disableHealthCheck: true, wantErr: false, validateOptions: func(t *testing.T, options []elastic.ClientOptionFunc) { require.NotNil(t, options) require.NotEmpty(t, options, "Expected non-empty options slice") }, }, { name: "Minimal configuration", cfg: &Configuration{ Servers: []string{"http://localhost:9200"}, Sniffing: Sniffing{ Enabled: false, UseHTTPS: false, }, HTTPCompression: false, SendGetBodyAs: "", }, disableHealthCheck: false, wantErr: false, validateOptions: func(t *testing.T, options []elastic.ClientOptionFunc) { require.NotNil(t, options) require.NotEmpty(t, options, "Expected non-empty options slice") }, }, { name: "Multiple servers", cfg: &Configuration{ Servers: []string{ "http://localhost:9200", "http://localhost:9201", "http://localhost:9202", }, HealthCheckTimeoutStartup: 10 * time.Millisecond, Sniffing: Sniffing{ Enabled: true, UseHTTPS: false, }, HTTPCompression: true, SendGetBodyAs: "GET", }, disableHealthCheck: false, wantErr: false, validateOptions: func(t *testing.T, options []elastic.ClientOptionFunc) { require.NotNil(t, options) require.NotEmpty(t, options, "Expected non-empty options slice") }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { options := tt.cfg.getESOptions(tt.disableHealthCheck) if tt.wantErr { require.Fail(t, "Test case expects an error, but getESOptions does not return one.") } else if tt.validateOptions != nil { tt.validateOptions(t, options) } }) } } func TestGetConfigOptionsIntegration(t *testing.T) { // Test that getConfigOptions properly integrates with getESOptions cfg := &Configuration{ Servers: []string{"http://localhost:9200"}, Sniffing: Sniffing{ Enabled: true, UseHTTPS: false, }, HTTPCompression: true, SendGetBodyAs: "POST", LogLevel: "info", QueryTimeout: 30 * time.Second, Authentication: Authentication{ BasicAuthentication: basicAuth("testuser", "testpass", ""), }, } logger := zap.NewNop() options, err := cfg.getConfigOptions(context.Background(), logger, nil) require.NoError(t, err) require.NotNil(t, options) require.Greater(t, len(options), 5, "Should have basic ES options plus additional config options") } func TestGetHTTPRoundTripper(t *testing.T) { tmpDir := t.TempDir() bearerTokenFile := filepath.Join(tmpDir, "bearertoken") require.NoError(t, os.WriteFile(bearerTokenFile, []byte("file-bearer-token"), 0o600)) tests := []struct { name string cfg *Configuration ctx context.Context wantErrContains string validate func(t *testing.T, rt http.RoundTripper) }{ { name: "Secure mode without auth", cfg: &Configuration{ TLS: configtls.ClientConfig{Insecure: false}, }, ctx: context.Background(), validate: func(t *testing.T, rt http.RoundTripper) { assert.NotNil(t, rt) _, ok := rt.(*auth.RoundTripper) assert.False(t, ok, "Should not be an auth round tripper") }, }, { name: "Insecure mode without auth", cfg: &Configuration{ TLS: configtls.ClientConfig{Insecure: true}, }, ctx: context.Background(), validate: func(t *testing.T, rt http.RoundTripper) { assert.NotNil(t, rt) transport, ok := rt.(*http.Transport) require.True(t, ok) assert.True(t, transport.TLSClientConfig.InsecureSkipVerify) }, }, { name: "Bearer auth not applicable (empty config)", cfg: &Configuration{ TLS: configtls.ClientConfig{Insecure: true}, Authentication: Authentication{ BearerTokenAuth: bearerAuth("", false), }, }, ctx: context.Background(), validate: func(t *testing.T, rt http.RoundTripper) { assert.NotNil(t, rt) // Should be plain transport since auth is not applicable _, ok := rt.(*auth.RoundTripper) assert.False(t, ok, "Should not be an auth round tripper when config is not applicable") transport, ok := rt.(*http.Transport) assert.True(t, ok, "Should be plain http.Transport") assert.True(t, transport.TLSClientConfig.InsecureSkipVerify) }, }, { name: "Secure mode with bearer token from file", cfg: &Configuration{ TLS: configtls.ClientConfig{Insecure: false}, Authentication: Authentication{ BearerTokenAuth: bearerAuth(bearerTokenFile, false), }, }, ctx: context.Background(), validate: func(t *testing.T, rt http.RoundTripper) { assert.NotNil(t, rt) authRT, ok := rt.(*auth.RoundTripper) require.True(t, ok, "Should be an auth round tripper") require.Len(t, authRT.Auths, 1) assert.Equal(t, "Bearer", authRT.Auths[0].Scheme) assert.NotNil(t, authRT.Auths[0].TokenFn) assert.Equal(t, "file-bearer-token", authRT.Auths[0].TokenFn()) }, }, { name: "Insecure mode with bearer token from context", cfg: &Configuration{ TLS: configtls.ClientConfig{Insecure: true}, Authentication: Authentication{ BearerTokenAuth: bearerAuth("", true), }, }, ctx: bearertoken.ContextWithBearerToken(context.Background(), "context-bearer-token"), validate: func(t *testing.T, rt http.RoundTripper) { assert.NotNil(t, rt) authRT, ok := rt.(*auth.RoundTripper) require.True(t, ok, "Should be an auth round tripper") require.Len(t, authRT.Auths, 1) assert.Equal(t, "Bearer", authRT.Auths[0].Scheme) assert.NotNil(t, authRT.Auths[0].FromCtx) transport, ok := authRT.Transport.(*http.Transport) require.True(t, ok) assert.True(t, transport.TLSClientConfig.InsecureSkipVerify) }, }, { name: "BearerToken file error", cfg: &Configuration{ Authentication: Authentication{ BearerTokenAuth: bearerAuth("/does/not/exist/token", false), }, }, ctx: context.Background(), wantErrContains: "no such file or directory", }, { name: "Invalid TLS config should fail", cfg: &Configuration{ TLS: configtls.ClientConfig{ Insecure: false, Config: configtls.Config{ CAFile: "/does/not/exist/ca.pem", }, }, }, ctx: context.Background(), wantErrContains: "failed to load TLS config", }, } logger := zap.NewNop() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rt, err := GetHTTPRoundTripper(tt.ctx, tt.cfg, logger, nil) if tt.wantErrContains != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.wantErrContains) assert.Nil(t, rt) } else { require.NoError(t, err) tt.validate(t, rt) } }) } } // Test GetHTTPRoundTripper with httpAuth error func TestGetHTTPRoundTripperWithHTTPAuthError(t *testing.T) { ctx := context.Background() logger := zap.NewNop() // Create a mock httpAuth that will fail on RoundTripper wrapping mockAuth := &mockFailingHTTPAuth{} c := &Configuration{ Servers: []string{"http://localhost:9200"}, LogLevel: "error", TLS: configtls.ClientConfig{Insecure: true}, } _, err := GetHTTPRoundTripper(ctx, c, logger, mockAuth) require.Error(t, err) require.Contains(t, err.Error(), "failed to wrap round tripper with HTTP authenticator") } // Mock failing HTTP authenticator type mockFailingHTTPAuth struct{} func (*mockFailingHTTPAuth) RoundTripper(_ http.RoundTripper) (http.RoundTripper, error) { return nil, errors.New("mock authenticator error") } func TestGetHTTPRoundTripperWrappingError(t *testing.T) { ctx := context.Background() logger := zap.NewNop() // Create a mock failing HTTP authenticator mockAuth := &mockFailingHTTPAuthWrapper{} c := &Configuration{ Servers: []string{"http://localhost:9200"}, LogLevel: "error", TLS: configtls.ClientConfig{Insecure: true}, } _, err := GetHTTPRoundTripper(ctx, c, logger, mockAuth) require.Error(t, err) require.ErrorContains(t, err, "failed to wrap round tripper with HTTP authenticator") } // mockFailingHTTPAuthWrapper mocks a failing HTTP authenticator for wrapping tests type mockFailingHTTPAuthWrapper struct{} func (*mockFailingHTTPAuthWrapper) RoundTripper(_ http.RoundTripper) (http.RoundTripper, error) { return nil, errors.New("wrapping error") } // Test GetHTTPRoundTripper with successful httpAuth wrapping func TestGetHTTPRoundTripperWithHTTPAuthSuccess(t *testing.T) { ctx := context.Background() logger := zap.NewNop() // Create a mock httpAuth that will succeed mockAuth := &mockSuccessfulHTTPAuth{} c := &Configuration{ Servers: []string{"http://localhost:9200"}, LogLevel: "error", TLS: configtls.ClientConfig{Insecure: true}, } rt, err := GetHTTPRoundTripper(ctx, c, logger, mockAuth) require.NoError(t, err) require.NotNil(t, rt) wrappedRT, ok := rt.(*mockWrappedRoundTripper) require.True(t, ok, "Should be wrapped round tripper") require.NotNil(t, wrappedRT) } // Mock successful HTTP authenticator type mockSuccessfulHTTPAuth struct{} func (*mockSuccessfulHTTPAuth) RoundTripper(rt http.RoundTripper) (http.RoundTripper, error) { return &mockWrappedRoundTripper{base: rt}, nil } // Mock wrapped round tripper type mockWrappedRoundTripper struct { base http.RoundTripper } func (m *mockWrappedRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { return m.base.RoundTrip(req) } func TestBulkCallbackInvoke_NilResponse(t *testing.T) { mf := metricstest.NewFactory(time.Minute) sm := spanstoremetrics.NewWriter(mf, "bulk_index") logger := zap.NewNop() defer mf.Stop() bcb := bulkCallback{ sm: sm, logger: logger, } bcb.invoke(1, []elastic.BulkableRequest{nil}, nil, assert.AnError) mf.AssertCounterMetrics(t, metricstest.ExpectedMetric{ Name: "bulk_index.errors", Value: 0, }, metricstest.ExpectedMetric{ Name: "bulk_index.inserts", Value: 1, }, metricstest.ExpectedMetric{ Name: "bulk_index.attempts", Value: 1, }, ) } func TestCustomHeaders(t *testing.T) { tests := []struct { name string config Configuration expected map[string]string }{ { name: "custom headers are set correctly", config: Configuration{ Servers: []string{"http://localhost:9200"}, CustomHeaders: map[string]string{ "Host": "my-opensearch.amazonaws.com", "X-Custom-Header": "test-value", }, }, expected: map[string]string{ "Host": "my-opensearch.amazonaws.com", "X-Custom-Header": "test-value", }, }, { name: "empty custom headers", config: Configuration{ Servers: []string{"http://localhost:9200"}, CustomHeaders: map[string]string{}, }, expected: map[string]string{}, }, { name: "nil custom headers", config: Configuration{ Servers: []string{"http://localhost:9200"}, CustomHeaders: nil, }, expected: nil, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if test.expected == nil { assert.Nil(t, test.config.CustomHeaders) } else { assert.Equal(t, test.expected, test.config.CustomHeaders) } }) } } func TestApplyDefaultsCustomHeaders(t *testing.T) { source := &Configuration{ CustomHeaders: map[string]string{ "Host": "source-host", "X-Custom-Header": "source-value", }, } tests := []struct { name string target *Configuration expected map[string]string }{ { name: "target has no headers, apply from source", target: &Configuration{}, expected: map[string]string{ "Host": "source-host", "X-Custom-Header": "source-value", }, }, { name: "target has headers, keep target headers", target: &Configuration{ CustomHeaders: map[string]string{ "Host": "target-host", }, }, expected: map[string]string{ "Host": "target-host", }, }, { name: "target has empty map, keep empty", target: &Configuration{ CustomHeaders: map[string]string{}, }, expected: map[string]string{}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { test.target.ApplyDefaults(source) assert.Equal(t, test.expected, test.target.CustomHeaders) }) } } func TestNewClientWithCustomHeaders(t *testing.T) { headersSeen := false testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { // Check if custom headers are present if req.Header.Get("X-Custom-Header") == "custom-value" { headersSeen = true } res.WriteHeader(http.StatusOK) res.Write(mockEsServerResponseWithVersion8) })) defer testServer.Close() config := Configuration{ Servers: []string{testServer.URL}, CustomHeaders: map[string]string{ "Host": "my-opensearch.amazonaws.com", "X-Custom-Header": "custom-value", }, LogLevel: "error", Version: 8, } logger := zap.NewNop() metricsFactory := metrics.NullFactory client, err := NewClient(context.Background(), &config, logger, metricsFactory, nil) require.NoError(t, err) require.NotNil(t, client) // Verify the configuration has the custom headers set // Note: The ES v8 client may not send custom headers during the initial ping/health check, // but they will be available for actual Elasticsearch operations (index, search, etc.) assert.Equal(t, "my-opensearch.amazonaws.com", config.CustomHeaders["Host"]) assert.Equal(t, "custom-value", config.CustomHeaders["X-Custom-Header"]) if headersSeen { t.Log(" Custom headers were transmitted in HTTP request") } else { t.Log(" Custom headers not sent in ping request (expected - will be sent in data operations)") } err = client.Close() require.NoError(t, err) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/elasticsearch/dbmodel/dot_replacer.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import "strings" // DotReplacer replaces the dots in span tags type DotReplacer struct { tagDotReplacement string } // NewDotReplacer returns an instance of DotReplacer func NewDotReplacer(tagDotReplacement string) DotReplacer { return DotReplacer{tagDotReplacement: tagDotReplacement} } // ReplaceDot replaces dot with dotReplacement func (dm DotReplacer) ReplaceDot(k string) string { return strings.ReplaceAll(k, ".", dm.tagDotReplacement) } // ReplaceDotReplacement replaces dotReplacement with dot func (dm DotReplacer) ReplaceDotReplacement(k string) string { return strings.ReplaceAll(k, dm.tagDotReplacement, ".") } ================================================ FILE: internal/storage/elasticsearch/dbmodel/dot_replacer_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "testing" "github.com/stretchr/testify/assert" ) func TestDotReplacement(t *testing.T) { converter := NewDotReplacer("#") k := "foo.foo" assert.Equal(t, k, converter.ReplaceDotReplacement(converter.ReplaceDot(k))) } ================================================ FILE: internal/storage/elasticsearch/dbmodel/model.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2018 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package dbmodel import "time" // ReferenceType is the reference type of one span to another type ReferenceType string // TraceID is the shared trace ID of all spans in the trace. type TraceID string // SpanID is the id of a span type SpanID string // ValueType is the type of a value stored in KeyValue struct. type ValueType string // Trace is the type of traces type Trace struct { Spans []Span } const ( // ChildOf means a span is the child of another span ChildOf ReferenceType = "CHILD_OF" // FollowsFrom means a span follows from another span FollowsFrom ReferenceType = "FOLLOWS_FROM" // StringType indicates a string value stored in KeyValue StringType ValueType = "string" // BoolType indicates a Boolean value stored in KeyValue BoolType ValueType = "bool" // Int64Type indicates a 64bit signed integer value stored in KeyValue Int64Type ValueType = "int64" // Float64Type indicates a 64bit float value stored in KeyValue Float64Type ValueType = "float64" // BinaryType indicates an arbitrary byte array stored in KeyValue BinaryType ValueType = "binary" ) // Span is ES database representation of the domain span. type Span struct { TraceID TraceID `json:"traceID"` SpanID SpanID `json:"spanID"` ParentSpanID SpanID `json:"parentSpanID,omitempty"` // deprecated Flags uint32 `json:"flags,omitempty"` OperationName string `json:"operationName"` References []Reference `json:"references"` StartTime uint64 `json:"startTime"` // microseconds since Unix epoch // ElasticSearch does not support a UNIX Epoch timestamp in microseconds, // so Jaeger maps StartTime to a 'long' type. This extra StartTimeMillis field // works around this issue, enabling timerange queries. StartTimeMillis uint64 `json:"startTimeMillis"` Duration uint64 `json:"duration"` // microseconds Tags []KeyValue `json:"tags"` // Alternative representation of tags for better kibana support Tag map[string]any `json:"tag,omitempty"` Logs []Log `json:"logs"` Process Process `json:"process"` } // Reference is a reference from one span to another type Reference struct { RefType ReferenceType `json:"refType"` TraceID TraceID `json:"traceID"` SpanID SpanID `json:"spanID"` } // Process is the process emitting a set of spans type Process struct { ServiceName string `json:"serviceName"` Tags []KeyValue `json:"tags"` // Alternative representation of tags for better kibana support Tag map[string]any `json:"tag,omitempty"` } // Log is a log emitted in a span type Log struct { Timestamp uint64 `json:"timestamp"` Fields []KeyValue `json:"fields"` } // KeyValue is a key-value pair with typed value. type KeyValue struct { Key string `json:"key"` Type ValueType `json:"type,omitempty"` Value any `json:"value"` } // Service is the JSON struct for service:operation documents in ElasticSearch type Service struct { ServiceName string `json:"serviceName"` OperationName string `json:"operationName"` } // Operation is the struct for span operation properties. type Operation struct { Name string SpanKind string } // OperationQueryParameters contains parameters of query operations, empty spanKind means get operations for all kinds of span. type OperationQueryParameters struct { ServiceName string SpanKind string } // TraceQueryParameters contains parameters of a trace query. type TraceQueryParameters struct { ServiceName string OperationName string Tags map[string]string StartTimeMin time.Time StartTimeMax time.Time DurationMin time.Duration DurationMax time.Duration // TODO: Rename NumTraces to SearchDepth NumTraces int } ================================================ FILE: internal/storage/elasticsearch/dbmodel/package_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/elasticsearch/empty_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/elasticsearch/errors.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "errors" "fmt" "github.com/olivere/elastic/v7" ) // DetailedError creates a more detailed error if the error stack contains elastic.Error. // This is useful because by default olivere/elastic returns errors that print like this: // // elastic: Error 400 (Bad Request): all shards failed [type=search_phase_execution_exception] // // This is pretty useless because it masks the underlying root cause. // DetailedError would instead return an error like this: // // : RootCause[... detailed error message ...] func DetailedError(err error) error { var esErr *elastic.Error if errors.As(err, &esErr) { if esErr.Details != nil && len(esErr.Details.RootCause) > 0 { rc := esErr.Details.RootCause[0] if rc != nil { return fmt.Errorf("%w: RootCause[%s [type=%s]]", err, rc.Reason, rc.Type) } } } return err } ================================================ FILE: internal/storage/elasticsearch/errors_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "errors" "testing" "github.com/olivere/elastic/v7" "github.com/stretchr/testify/require" ) func TestDetailedError(t *testing.T) { require.ErrorContains(t, errors.New("some err"), "some err", "no panic") esErr := &elastic.Error{ Status: 400, Details: &elastic.ErrorDetails{ Type: "type1", Reason: "useless reason, e.g. all shards failed", RootCause: []*elastic.ErrorDetails{ { Type: "type2", Reason: "actual reason", }, }, }, } require.ErrorContains(t, DetailedError(esErr), "actual reason") esErr.Details.RootCause[0] = nil require.ErrorContains(t, DetailedError(esErr), "useless reason") require.NotContains(t, DetailedError(esErr).Error(), "actual reason") } ================================================ FILE: internal/storage/elasticsearch/filter/alias.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package filter import ( "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/client" ) // AliasExists check if the some of indices has a certain alias name func AliasExists(indices []client.Index, aliasName string) bool { aliases := ByAlias(indices, []string{aliasName}) return len(aliases) > 0 } // ByAlias filter indices that have an alias in the array of alias func ByAlias(indices []client.Index, aliases []string) []client.Index { return filterByAliasWithOptions(indices, aliases, false) } // ByAliasExclude filter indices that doesn't have an alias in the array of alias func ByAliasExclude(indices []client.Index, aliases []string) []client.Index { return filterByAliasWithOptions(indices, aliases, true) } func filterByAliasWithOptions(indices []client.Index, aliases []string, exclude bool) []client.Index { var results []client.Index for _, alias := range aliases { for _, index := range indices { hasAlias := index.Aliases[alias] if hasAlias { results = append(results, index) } } } if exclude { return exlude(indices, results) } return results } func exlude(indices []client.Index, exclusionList []client.Index) []client.Index { excludedIndices := make([]client.Index, 0, len(indices)) for _, idx := range indices { if !contains(idx, exclusionList) { excludedIndices = append(excludedIndices, idx) } } return excludedIndices } func contains(index client.Index, indexList []client.Index) bool { for _, idx := range indexList { if idx.Index == index.Index { return true } } return false } ================================================ FILE: internal/storage/elasticsearch/filter/alias_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package filter import ( "testing" "github.com/stretchr/testify/assert" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/client" ) var indices = []client.Index{ { Index: "jaeger-span-0001", Aliases: map[string]bool{ "jaeger-span-write": true, "jaeger-span-read": true, }, }, { Index: "jaeger-span-0002", Aliases: map[string]bool{ "jaeger-span-read": true, }, }, { Index: "jaeger-span-0003", Aliases: map[string]bool{ "jaeger-span-read": true, }, }, { Index: "jaeger-span-0004", Aliases: map[string]bool{ "jaeger-span-other": true, }, }, { Index: "jaeger-span-0005", Aliases: map[string]bool{ "custom-alias": true, }, }, { Index: "jaeger-span-0006", }, } func TestByAlias(t *testing.T) { filtered := ByAlias(indices, []string{"jaeger-span-read", "jaeger-span-other"}) expected := []client.Index{ { Index: "jaeger-span-0001", Aliases: map[string]bool{ "jaeger-span-write": true, "jaeger-span-read": true, }, }, { Index: "jaeger-span-0002", Aliases: map[string]bool{ "jaeger-span-read": true, }, }, { Index: "jaeger-span-0003", Aliases: map[string]bool{ "jaeger-span-read": true, }, }, { Index: "jaeger-span-0004", Aliases: map[string]bool{ "jaeger-span-other": true, }, }, } assert.Equal(t, expected, filtered) } func TestByAliasExclude(t *testing.T) { filtered := ByAliasExclude(indices, []string{"jaeger-span-read", "jaeger-span-other"}) expected := []client.Index{ { Index: "jaeger-span-0005", Aliases: map[string]bool{ "custom-alias": true, }, }, { Index: "jaeger-span-0006", }, } assert.Equal(t, expected, filtered) } func TestHasAliasEmpty(t *testing.T) { result := AliasExists(indices, "my-unexisting-alias") assert.False(t, result) result = AliasExists(indices, "custom-alias") assert.True(t, result) } ================================================ FILE: internal/storage/elasticsearch/filter/date.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package filter import ( "time" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/client" ) // ByDate filter indices by creationTime, return indices that were created before certain date. func ByDate(indices []client.Index, beforeThisDate time.Time) []client.Index { var filtered []client.Index for _, in := range indices { if in.CreationTime.Before(beforeThisDate) { filtered = append(filtered, in) } } return filtered } ================================================ FILE: internal/storage/elasticsearch/filter/date_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package filter import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/client" ) func TestByDate(t *testing.T) { beforeDateFilter := time.Date(2021, 10, 10, 12, 0, 0, 0, time.Local) expectedIndices := []client.Index{ { Index: "jaeger-span-0006", CreationTime: time.Date(2021, 7, 7, 7, 10, 10, 10, time.Local), }, { Index: "jaeger-span-0004", CreationTime: time.Date(2021, 9, 16, 11, 0, 0, 0, time.Local), Aliases: map[string]bool{ "jaeger-span-other": true, }, }, { Index: "jaeger-span-0005", CreationTime: time.Date(2021, 10, 10, 9, 56, 34, 25, time.Local), Aliases: map[string]bool{ "custom-alias": true, }, }, } indices := []client.Index{ { Index: "jaeger-span-0006", CreationTime: time.Date(2021, 7, 7, 7, 10, 10, 10, time.Local), }, { Index: "jaeger-span-0004", CreationTime: time.Date(2021, 9, 16, 11, 0, 0, 0, time.Local), Aliases: map[string]bool{ "jaeger-span-other": true, }, }, { Index: "jaeger-span-0005", CreationTime: time.Date(2021, 10, 10, 9, 56, 34, 25, time.Local), Aliases: map[string]bool{ "custom-alias": true, }, }, { Index: "jaeger-span-0001", CreationTime: time.Date(2021, 10, 10, 12, 0, 0, 0, time.Local), Aliases: map[string]bool{ "jaeger-span-write": true, "jaeger-span-read": true, }, }, { Index: "jaeger-span-0002", CreationTime: time.Date(2021, 11, 10, 12, 30, 0, 0, time.Local), Aliases: map[string]bool{ "jaeger-span-read": true, }, }, { Index: "jaeger-span-0003", CreationTime: time.Date(2021, 12, 10, 2, 15, 20, 1, time.Local), Aliases: map[string]bool{ "jaeger-span-read": true, }, }, } result := ByDate(indices, beforeDateFilter) assert.Equal(t, expectedIndices, result) } ================================================ FILE: internal/storage/elasticsearch/filter/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package filter import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/elasticsearch/mocks/mocks.go ================================================ // Copyright (c) The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // // Run 'make generate-mocks' to regenerate. // Code generated by mockery; DO NOT EDIT. // github.com/vektra/mockery // template: testify package mocks import ( "context" "io" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" "github.com/olivere/elastic/v7" mock "github.com/stretchr/testify/mock" ) // NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewClient(t interface { mock.TestingT Cleanup(func()) }) *Client { mock := &Client{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // Client is an autogenerated mock type for the Client type type Client struct { mock.Mock } type Client_Expecter struct { mock *mock.Mock } func (_m *Client) EXPECT() *Client_Expecter { return &Client_Expecter{mock: &_m.Mock} } // Close provides a mock function for the type Client func (_mock *Client) Close() error { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Close") } var r0 error if returnFunc, ok := ret.Get(0).(func() error); ok { r0 = returnFunc() } else { r0 = ret.Error(0) } return r0 } // Client_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' type Client_Close_Call struct { *mock.Call } // Close is a helper method to define mock.On call func (_e *Client_Expecter) Close() *Client_Close_Call { return &Client_Close_Call{Call: _e.mock.On("Close")} } func (_c *Client_Close_Call) Run(run func()) *Client_Close_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *Client_Close_Call) Return(err error) *Client_Close_Call { _c.Call.Return(err) return _c } func (_c *Client_Close_Call) RunAndReturn(run func() error) *Client_Close_Call { _c.Call.Return(run) return _c } // CreateIndex provides a mock function for the type Client func (_mock *Client) CreateIndex(index string) elasticsearch.IndicesCreateService { ret := _mock.Called(index) if len(ret) == 0 { panic("no return value specified for CreateIndex") } var r0 elasticsearch.IndicesCreateService if returnFunc, ok := ret.Get(0).(func(string) elasticsearch.IndicesCreateService); ok { r0 = returnFunc(index) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(elasticsearch.IndicesCreateService) } } return r0 } // Client_CreateIndex_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateIndex' type Client_CreateIndex_Call struct { *mock.Call } // CreateIndex is a helper method to define mock.On call // - index string func (_e *Client_Expecter) CreateIndex(index interface{}) *Client_CreateIndex_Call { return &Client_CreateIndex_Call{Call: _e.mock.On("CreateIndex", index)} } func (_c *Client_CreateIndex_Call) Run(run func(index string)) *Client_CreateIndex_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *Client_CreateIndex_Call) Return(indicesCreateService elasticsearch.IndicesCreateService) *Client_CreateIndex_Call { _c.Call.Return(indicesCreateService) return _c } func (_c *Client_CreateIndex_Call) RunAndReturn(run func(index string) elasticsearch.IndicesCreateService) *Client_CreateIndex_Call { _c.Call.Return(run) return _c } // CreateTemplate provides a mock function for the type Client func (_mock *Client) CreateTemplate(id string) elasticsearch.TemplateCreateService { ret := _mock.Called(id) if len(ret) == 0 { panic("no return value specified for CreateTemplate") } var r0 elasticsearch.TemplateCreateService if returnFunc, ok := ret.Get(0).(func(string) elasticsearch.TemplateCreateService); ok { r0 = returnFunc(id) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(elasticsearch.TemplateCreateService) } } return r0 } // Client_CreateTemplate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateTemplate' type Client_CreateTemplate_Call struct { *mock.Call } // CreateTemplate is a helper method to define mock.On call // - id string func (_e *Client_Expecter) CreateTemplate(id interface{}) *Client_CreateTemplate_Call { return &Client_CreateTemplate_Call{Call: _e.mock.On("CreateTemplate", id)} } func (_c *Client_CreateTemplate_Call) Run(run func(id string)) *Client_CreateTemplate_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *Client_CreateTemplate_Call) Return(templateCreateService elasticsearch.TemplateCreateService) *Client_CreateTemplate_Call { _c.Call.Return(templateCreateService) return _c } func (_c *Client_CreateTemplate_Call) RunAndReturn(run func(id string) elasticsearch.TemplateCreateService) *Client_CreateTemplate_Call { _c.Call.Return(run) return _c } // DeleteIndex provides a mock function for the type Client func (_mock *Client) DeleteIndex(index string) elasticsearch.IndicesDeleteService { ret := _mock.Called(index) if len(ret) == 0 { panic("no return value specified for DeleteIndex") } var r0 elasticsearch.IndicesDeleteService if returnFunc, ok := ret.Get(0).(func(string) elasticsearch.IndicesDeleteService); ok { r0 = returnFunc(index) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(elasticsearch.IndicesDeleteService) } } return r0 } // Client_DeleteIndex_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteIndex' type Client_DeleteIndex_Call struct { *mock.Call } // DeleteIndex is a helper method to define mock.On call // - index string func (_e *Client_Expecter) DeleteIndex(index interface{}) *Client_DeleteIndex_Call { return &Client_DeleteIndex_Call{Call: _e.mock.On("DeleteIndex", index)} } func (_c *Client_DeleteIndex_Call) Run(run func(index string)) *Client_DeleteIndex_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *Client_DeleteIndex_Call) Return(indicesDeleteService elasticsearch.IndicesDeleteService) *Client_DeleteIndex_Call { _c.Call.Return(indicesDeleteService) return _c } func (_c *Client_DeleteIndex_Call) RunAndReturn(run func(index string) elasticsearch.IndicesDeleteService) *Client_DeleteIndex_Call { _c.Call.Return(run) return _c } // GetVersion provides a mock function for the type Client func (_mock *Client) GetVersion() uint { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for GetVersion") } var r0 uint if returnFunc, ok := ret.Get(0).(func() uint); ok { r0 = returnFunc() } else { r0 = ret.Get(0).(uint) } return r0 } // Client_GetVersion_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetVersion' type Client_GetVersion_Call struct { *mock.Call } // GetVersion is a helper method to define mock.On call func (_e *Client_Expecter) GetVersion() *Client_GetVersion_Call { return &Client_GetVersion_Call{Call: _e.mock.On("GetVersion")} } func (_c *Client_GetVersion_Call) Run(run func()) *Client_GetVersion_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *Client_GetVersion_Call) Return(v uint) *Client_GetVersion_Call { _c.Call.Return(v) return _c } func (_c *Client_GetVersion_Call) RunAndReturn(run func() uint) *Client_GetVersion_Call { _c.Call.Return(run) return _c } // Index provides a mock function for the type Client func (_mock *Client) Index() elasticsearch.IndexService { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Index") } var r0 elasticsearch.IndexService if returnFunc, ok := ret.Get(0).(func() elasticsearch.IndexService); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(elasticsearch.IndexService) } } return r0 } // Client_Index_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Index' type Client_Index_Call struct { *mock.Call } // Index is a helper method to define mock.On call func (_e *Client_Expecter) Index() *Client_Index_Call { return &Client_Index_Call{Call: _e.mock.On("Index")} } func (_c *Client_Index_Call) Run(run func()) *Client_Index_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *Client_Index_Call) Return(indexService elasticsearch.IndexService) *Client_Index_Call { _c.Call.Return(indexService) return _c } func (_c *Client_Index_Call) RunAndReturn(run func() elasticsearch.IndexService) *Client_Index_Call { _c.Call.Return(run) return _c } // IndexExists provides a mock function for the type Client func (_mock *Client) IndexExists(index string) elasticsearch.IndicesExistsService { ret := _mock.Called(index) if len(ret) == 0 { panic("no return value specified for IndexExists") } var r0 elasticsearch.IndicesExistsService if returnFunc, ok := ret.Get(0).(func(string) elasticsearch.IndicesExistsService); ok { r0 = returnFunc(index) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(elasticsearch.IndicesExistsService) } } return r0 } // Client_IndexExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IndexExists' type Client_IndexExists_Call struct { *mock.Call } // IndexExists is a helper method to define mock.On call // - index string func (_e *Client_Expecter) IndexExists(index interface{}) *Client_IndexExists_Call { return &Client_IndexExists_Call{Call: _e.mock.On("IndexExists", index)} } func (_c *Client_IndexExists_Call) Run(run func(index string)) *Client_IndexExists_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *Client_IndexExists_Call) Return(indicesExistsService elasticsearch.IndicesExistsService) *Client_IndexExists_Call { _c.Call.Return(indicesExistsService) return _c } func (_c *Client_IndexExists_Call) RunAndReturn(run func(index string) elasticsearch.IndicesExistsService) *Client_IndexExists_Call { _c.Call.Return(run) return _c } // MultiSearch provides a mock function for the type Client func (_mock *Client) MultiSearch() elasticsearch.MultiSearchService { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for MultiSearch") } var r0 elasticsearch.MultiSearchService if returnFunc, ok := ret.Get(0).(func() elasticsearch.MultiSearchService); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(elasticsearch.MultiSearchService) } } return r0 } // Client_MultiSearch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MultiSearch' type Client_MultiSearch_Call struct { *mock.Call } // MultiSearch is a helper method to define mock.On call func (_e *Client_Expecter) MultiSearch() *Client_MultiSearch_Call { return &Client_MultiSearch_Call{Call: _e.mock.On("MultiSearch")} } func (_c *Client_MultiSearch_Call) Run(run func()) *Client_MultiSearch_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *Client_MultiSearch_Call) Return(multiSearchService elasticsearch.MultiSearchService) *Client_MultiSearch_Call { _c.Call.Return(multiSearchService) return _c } func (_c *Client_MultiSearch_Call) RunAndReturn(run func() elasticsearch.MultiSearchService) *Client_MultiSearch_Call { _c.Call.Return(run) return _c } // Search provides a mock function for the type Client func (_mock *Client) Search(indices ...string) elasticsearch.SearchService { var tmpRet mock.Arguments if len(indices) > 0 { tmpRet = _mock.Called(indices) } else { tmpRet = _mock.Called() } ret := tmpRet if len(ret) == 0 { panic("no return value specified for Search") } var r0 elasticsearch.SearchService if returnFunc, ok := ret.Get(0).(func(...string) elasticsearch.SearchService); ok { r0 = returnFunc(indices...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(elasticsearch.SearchService) } } return r0 } // Client_Search_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Search' type Client_Search_Call struct { *mock.Call } // Search is a helper method to define mock.On call // - indices ...string func (_e *Client_Expecter) Search(indices ...interface{}) *Client_Search_Call { return &Client_Search_Call{Call: _e.mock.On("Search", append([]interface{}{}, indices...)...)} } func (_c *Client_Search_Call) Run(run func(indices ...string)) *Client_Search_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 []string var variadicArgs []string if len(args) > 0 { variadicArgs = args[0].([]string) } arg0 = variadicArgs run( arg0..., ) }) return _c } func (_c *Client_Search_Call) Return(searchService elasticsearch.SearchService) *Client_Search_Call { _c.Call.Return(searchService) return _c } func (_c *Client_Search_Call) RunAndReturn(run func(indices ...string) elasticsearch.SearchService) *Client_Search_Call { _c.Call.Return(run) return _c } // NewIndicesExistsService creates a new instance of IndicesExistsService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewIndicesExistsService(t interface { mock.TestingT Cleanup(func()) }) *IndicesExistsService { mock := &IndicesExistsService{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // IndicesExistsService is an autogenerated mock type for the IndicesExistsService type type IndicesExistsService struct { mock.Mock } type IndicesExistsService_Expecter struct { mock *mock.Mock } func (_m *IndicesExistsService) EXPECT() *IndicesExistsService_Expecter { return &IndicesExistsService_Expecter{mock: &_m.Mock} } // Do provides a mock function for the type IndicesExistsService func (_mock *IndicesExistsService) Do(ctx context.Context) (bool, error) { ret := _mock.Called(ctx) if len(ret) == 0 { panic("no return value specified for Do") } var r0 bool var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context) (bool, error)); ok { return returnFunc(ctx) } if returnFunc, ok := ret.Get(0).(func(context.Context) bool); ok { r0 = returnFunc(ctx) } else { r0 = ret.Get(0).(bool) } if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { r1 = returnFunc(ctx) } else { r1 = ret.Error(1) } return r0, r1 } // IndicesExistsService_Do_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Do' type IndicesExistsService_Do_Call struct { *mock.Call } // Do is a helper method to define mock.On call // - ctx context.Context func (_e *IndicesExistsService_Expecter) Do(ctx interface{}) *IndicesExistsService_Do_Call { return &IndicesExistsService_Do_Call{Call: _e.mock.On("Do", ctx)} } func (_c *IndicesExistsService_Do_Call) Run(run func(ctx context.Context)) *IndicesExistsService_Do_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } run( arg0, ) }) return _c } func (_c *IndicesExistsService_Do_Call) Return(b bool, err error) *IndicesExistsService_Do_Call { _c.Call.Return(b, err) return _c } func (_c *IndicesExistsService_Do_Call) RunAndReturn(run func(ctx context.Context) (bool, error)) *IndicesExistsService_Do_Call { _c.Call.Return(run) return _c } // NewIndicesCreateService creates a new instance of IndicesCreateService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewIndicesCreateService(t interface { mock.TestingT Cleanup(func()) }) *IndicesCreateService { mock := &IndicesCreateService{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // IndicesCreateService is an autogenerated mock type for the IndicesCreateService type type IndicesCreateService struct { mock.Mock } type IndicesCreateService_Expecter struct { mock *mock.Mock } func (_m *IndicesCreateService) EXPECT() *IndicesCreateService_Expecter { return &IndicesCreateService_Expecter{mock: &_m.Mock} } // Body provides a mock function for the type IndicesCreateService func (_mock *IndicesCreateService) Body(mapping string) elasticsearch.IndicesCreateService { ret := _mock.Called(mapping) if len(ret) == 0 { panic("no return value specified for Body") } var r0 elasticsearch.IndicesCreateService if returnFunc, ok := ret.Get(0).(func(string) elasticsearch.IndicesCreateService); ok { r0 = returnFunc(mapping) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(elasticsearch.IndicesCreateService) } } return r0 } // IndicesCreateService_Body_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Body' type IndicesCreateService_Body_Call struct { *mock.Call } // Body is a helper method to define mock.On call // - mapping string func (_e *IndicesCreateService_Expecter) Body(mapping interface{}) *IndicesCreateService_Body_Call { return &IndicesCreateService_Body_Call{Call: _e.mock.On("Body", mapping)} } func (_c *IndicesCreateService_Body_Call) Run(run func(mapping string)) *IndicesCreateService_Body_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *IndicesCreateService_Body_Call) Return(indicesCreateService elasticsearch.IndicesCreateService) *IndicesCreateService_Body_Call { _c.Call.Return(indicesCreateService) return _c } func (_c *IndicesCreateService_Body_Call) RunAndReturn(run func(mapping string) elasticsearch.IndicesCreateService) *IndicesCreateService_Body_Call { _c.Call.Return(run) return _c } // Do provides a mock function for the type IndicesCreateService func (_mock *IndicesCreateService) Do(ctx context.Context) (*elastic.IndicesCreateResult, error) { ret := _mock.Called(ctx) if len(ret) == 0 { panic("no return value specified for Do") } var r0 *elastic.IndicesCreateResult var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context) (*elastic.IndicesCreateResult, error)); ok { return returnFunc(ctx) } if returnFunc, ok := ret.Get(0).(func(context.Context) *elastic.IndicesCreateResult); ok { r0 = returnFunc(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*elastic.IndicesCreateResult) } } if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { r1 = returnFunc(ctx) } else { r1 = ret.Error(1) } return r0, r1 } // IndicesCreateService_Do_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Do' type IndicesCreateService_Do_Call struct { *mock.Call } // Do is a helper method to define mock.On call // - ctx context.Context func (_e *IndicesCreateService_Expecter) Do(ctx interface{}) *IndicesCreateService_Do_Call { return &IndicesCreateService_Do_Call{Call: _e.mock.On("Do", ctx)} } func (_c *IndicesCreateService_Do_Call) Run(run func(ctx context.Context)) *IndicesCreateService_Do_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } run( arg0, ) }) return _c } func (_c *IndicesCreateService_Do_Call) Return(indicesCreateResult *elastic.IndicesCreateResult, err error) *IndicesCreateService_Do_Call { _c.Call.Return(indicesCreateResult, err) return _c } func (_c *IndicesCreateService_Do_Call) RunAndReturn(run func(ctx context.Context) (*elastic.IndicesCreateResult, error)) *IndicesCreateService_Do_Call { _c.Call.Return(run) return _c } // NewIndicesDeleteService creates a new instance of IndicesDeleteService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewIndicesDeleteService(t interface { mock.TestingT Cleanup(func()) }) *IndicesDeleteService { mock := &IndicesDeleteService{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // IndicesDeleteService is an autogenerated mock type for the IndicesDeleteService type type IndicesDeleteService struct { mock.Mock } type IndicesDeleteService_Expecter struct { mock *mock.Mock } func (_m *IndicesDeleteService) EXPECT() *IndicesDeleteService_Expecter { return &IndicesDeleteService_Expecter{mock: &_m.Mock} } // Do provides a mock function for the type IndicesDeleteService func (_mock *IndicesDeleteService) Do(ctx context.Context) (*elastic.IndicesDeleteResponse, error) { ret := _mock.Called(ctx) if len(ret) == 0 { panic("no return value specified for Do") } var r0 *elastic.IndicesDeleteResponse var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context) (*elastic.IndicesDeleteResponse, error)); ok { return returnFunc(ctx) } if returnFunc, ok := ret.Get(0).(func(context.Context) *elastic.IndicesDeleteResponse); ok { r0 = returnFunc(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*elastic.IndicesDeleteResponse) } } if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { r1 = returnFunc(ctx) } else { r1 = ret.Error(1) } return r0, r1 } // IndicesDeleteService_Do_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Do' type IndicesDeleteService_Do_Call struct { *mock.Call } // Do is a helper method to define mock.On call // - ctx context.Context func (_e *IndicesDeleteService_Expecter) Do(ctx interface{}) *IndicesDeleteService_Do_Call { return &IndicesDeleteService_Do_Call{Call: _e.mock.On("Do", ctx)} } func (_c *IndicesDeleteService_Do_Call) Run(run func(ctx context.Context)) *IndicesDeleteService_Do_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } run( arg0, ) }) return _c } func (_c *IndicesDeleteService_Do_Call) Return(indicesDeleteResponse *elastic.IndicesDeleteResponse, err error) *IndicesDeleteService_Do_Call { _c.Call.Return(indicesDeleteResponse, err) return _c } func (_c *IndicesDeleteService_Do_Call) RunAndReturn(run func(ctx context.Context) (*elastic.IndicesDeleteResponse, error)) *IndicesDeleteService_Do_Call { _c.Call.Return(run) return _c } // NewTemplateCreateService creates a new instance of TemplateCreateService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewTemplateCreateService(t interface { mock.TestingT Cleanup(func()) }) *TemplateCreateService { mock := &TemplateCreateService{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // TemplateCreateService is an autogenerated mock type for the TemplateCreateService type type TemplateCreateService struct { mock.Mock } type TemplateCreateService_Expecter struct { mock *mock.Mock } func (_m *TemplateCreateService) EXPECT() *TemplateCreateService_Expecter { return &TemplateCreateService_Expecter{mock: &_m.Mock} } // Body provides a mock function for the type TemplateCreateService func (_mock *TemplateCreateService) Body(mapping string) elasticsearch.TemplateCreateService { ret := _mock.Called(mapping) if len(ret) == 0 { panic("no return value specified for Body") } var r0 elasticsearch.TemplateCreateService if returnFunc, ok := ret.Get(0).(func(string) elasticsearch.TemplateCreateService); ok { r0 = returnFunc(mapping) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(elasticsearch.TemplateCreateService) } } return r0 } // TemplateCreateService_Body_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Body' type TemplateCreateService_Body_Call struct { *mock.Call } // Body is a helper method to define mock.On call // - mapping string func (_e *TemplateCreateService_Expecter) Body(mapping interface{}) *TemplateCreateService_Body_Call { return &TemplateCreateService_Body_Call{Call: _e.mock.On("Body", mapping)} } func (_c *TemplateCreateService_Body_Call) Run(run func(mapping string)) *TemplateCreateService_Body_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *TemplateCreateService_Body_Call) Return(templateCreateService elasticsearch.TemplateCreateService) *TemplateCreateService_Body_Call { _c.Call.Return(templateCreateService) return _c } func (_c *TemplateCreateService_Body_Call) RunAndReturn(run func(mapping string) elasticsearch.TemplateCreateService) *TemplateCreateService_Body_Call { _c.Call.Return(run) return _c } // Do provides a mock function for the type TemplateCreateService func (_mock *TemplateCreateService) Do(ctx context.Context) (*elastic.IndicesPutTemplateResponse, error) { ret := _mock.Called(ctx) if len(ret) == 0 { panic("no return value specified for Do") } var r0 *elastic.IndicesPutTemplateResponse var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context) (*elastic.IndicesPutTemplateResponse, error)); ok { return returnFunc(ctx) } if returnFunc, ok := ret.Get(0).(func(context.Context) *elastic.IndicesPutTemplateResponse); ok { r0 = returnFunc(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*elastic.IndicesPutTemplateResponse) } } if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { r1 = returnFunc(ctx) } else { r1 = ret.Error(1) } return r0, r1 } // TemplateCreateService_Do_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Do' type TemplateCreateService_Do_Call struct { *mock.Call } // Do is a helper method to define mock.On call // - ctx context.Context func (_e *TemplateCreateService_Expecter) Do(ctx interface{}) *TemplateCreateService_Do_Call { return &TemplateCreateService_Do_Call{Call: _e.mock.On("Do", ctx)} } func (_c *TemplateCreateService_Do_Call) Run(run func(ctx context.Context)) *TemplateCreateService_Do_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } run( arg0, ) }) return _c } func (_c *TemplateCreateService_Do_Call) Return(indicesPutTemplateResponse *elastic.IndicesPutTemplateResponse, err error) *TemplateCreateService_Do_Call { _c.Call.Return(indicesPutTemplateResponse, err) return _c } func (_c *TemplateCreateService_Do_Call) RunAndReturn(run func(ctx context.Context) (*elastic.IndicesPutTemplateResponse, error)) *TemplateCreateService_Do_Call { _c.Call.Return(run) return _c } // NewIndexService creates a new instance of IndexService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewIndexService(t interface { mock.TestingT Cleanup(func()) }) *IndexService { mock := &IndexService{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // IndexService is an autogenerated mock type for the IndexService type type IndexService struct { mock.Mock } type IndexService_Expecter struct { mock *mock.Mock } func (_m *IndexService) EXPECT() *IndexService_Expecter { return &IndexService_Expecter{mock: &_m.Mock} } // Add provides a mock function for the type IndexService func (_mock *IndexService) Add() { _mock.Called() return } // IndexService_Add_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Add' type IndexService_Add_Call struct { *mock.Call } // Add is a helper method to define mock.On call func (_e *IndexService_Expecter) Add() *IndexService_Add_Call { return &IndexService_Add_Call{Call: _e.mock.On("Add")} } func (_c *IndexService_Add_Call) Run(run func()) *IndexService_Add_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *IndexService_Add_Call) Return() *IndexService_Add_Call { _c.Call.Return() return _c } func (_c *IndexService_Add_Call) RunAndReturn(run func()) *IndexService_Add_Call { _c.Run(run) return _c } // BodyJson provides a mock function for the type IndexService func (_mock *IndexService) BodyJson(body any) elasticsearch.IndexService { ret := _mock.Called(body) if len(ret) == 0 { panic("no return value specified for BodyJson") } var r0 elasticsearch.IndexService if returnFunc, ok := ret.Get(0).(func(any) elasticsearch.IndexService); ok { r0 = returnFunc(body) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(elasticsearch.IndexService) } } return r0 } // IndexService_BodyJson_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BodyJson' type IndexService_BodyJson_Call struct { *mock.Call } // BodyJson is a helper method to define mock.On call // - body any func (_e *IndexService_Expecter) BodyJson(body interface{}) *IndexService_BodyJson_Call { return &IndexService_BodyJson_Call{Call: _e.mock.On("BodyJson", body)} } func (_c *IndexService_BodyJson_Call) Run(run func(body any)) *IndexService_BodyJson_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 any if args[0] != nil { arg0 = args[0].(any) } run( arg0, ) }) return _c } func (_c *IndexService_BodyJson_Call) Return(indexService elasticsearch.IndexService) *IndexService_BodyJson_Call { _c.Call.Return(indexService) return _c } func (_c *IndexService_BodyJson_Call) RunAndReturn(run func(body any) elasticsearch.IndexService) *IndexService_BodyJson_Call { _c.Call.Return(run) return _c } // Id provides a mock function for the type IndexService func (_mock *IndexService) Id(id string) elasticsearch.IndexService { ret := _mock.Called(id) if len(ret) == 0 { panic("no return value specified for Id") } var r0 elasticsearch.IndexService if returnFunc, ok := ret.Get(0).(func(string) elasticsearch.IndexService); ok { r0 = returnFunc(id) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(elasticsearch.IndexService) } } return r0 } // IndexService_Id_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Id' type IndexService_Id_Call struct { *mock.Call } // Id is a helper method to define mock.On call // - id string func (_e *IndexService_Expecter) Id(id interface{}) *IndexService_Id_Call { return &IndexService_Id_Call{Call: _e.mock.On("Id", id)} } func (_c *IndexService_Id_Call) Run(run func(id string)) *IndexService_Id_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *IndexService_Id_Call) Return(indexService elasticsearch.IndexService) *IndexService_Id_Call { _c.Call.Return(indexService) return _c } func (_c *IndexService_Id_Call) RunAndReturn(run func(id string) elasticsearch.IndexService) *IndexService_Id_Call { _c.Call.Return(run) return _c } // Index provides a mock function for the type IndexService func (_mock *IndexService) Index(index string) elasticsearch.IndexService { ret := _mock.Called(index) if len(ret) == 0 { panic("no return value specified for Index") } var r0 elasticsearch.IndexService if returnFunc, ok := ret.Get(0).(func(string) elasticsearch.IndexService); ok { r0 = returnFunc(index) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(elasticsearch.IndexService) } } return r0 } // IndexService_Index_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Index' type IndexService_Index_Call struct { *mock.Call } // Index is a helper method to define mock.On call // - index string func (_e *IndexService_Expecter) Index(index interface{}) *IndexService_Index_Call { return &IndexService_Index_Call{Call: _e.mock.On("Index", index)} } func (_c *IndexService_Index_Call) Run(run func(index string)) *IndexService_Index_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *IndexService_Index_Call) Return(indexService elasticsearch.IndexService) *IndexService_Index_Call { _c.Call.Return(indexService) return _c } func (_c *IndexService_Index_Call) RunAndReturn(run func(index string) elasticsearch.IndexService) *IndexService_Index_Call { _c.Call.Return(run) return _c } // Type provides a mock function for the type IndexService func (_mock *IndexService) Type(typ string) elasticsearch.IndexService { ret := _mock.Called(typ) if len(ret) == 0 { panic("no return value specified for Type") } var r0 elasticsearch.IndexService if returnFunc, ok := ret.Get(0).(func(string) elasticsearch.IndexService); ok { r0 = returnFunc(typ) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(elasticsearch.IndexService) } } return r0 } // IndexService_Type_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Type' type IndexService_Type_Call struct { *mock.Call } // Type is a helper method to define mock.On call // - typ string func (_e *IndexService_Expecter) Type(typ interface{}) *IndexService_Type_Call { return &IndexService_Type_Call{Call: _e.mock.On("Type", typ)} } func (_c *IndexService_Type_Call) Run(run func(typ string)) *IndexService_Type_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *IndexService_Type_Call) Return(indexService elasticsearch.IndexService) *IndexService_Type_Call { _c.Call.Return(indexService) return _c } func (_c *IndexService_Type_Call) RunAndReturn(run func(typ string) elasticsearch.IndexService) *IndexService_Type_Call { _c.Call.Return(run) return _c } // NewSearchService creates a new instance of SearchService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewSearchService(t interface { mock.TestingT Cleanup(func()) }) *SearchService { mock := &SearchService{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // SearchService is an autogenerated mock type for the SearchService type type SearchService struct { mock.Mock } type SearchService_Expecter struct { mock *mock.Mock } func (_m *SearchService) EXPECT() *SearchService_Expecter { return &SearchService_Expecter{mock: &_m.Mock} } // Aggregation provides a mock function for the type SearchService func (_mock *SearchService) Aggregation(name string, aggregation elastic.Aggregation) elasticsearch.SearchService { ret := _mock.Called(name, aggregation) if len(ret) == 0 { panic("no return value specified for Aggregation") } var r0 elasticsearch.SearchService if returnFunc, ok := ret.Get(0).(func(string, elastic.Aggregation) elasticsearch.SearchService); ok { r0 = returnFunc(name, aggregation) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(elasticsearch.SearchService) } } return r0 } // SearchService_Aggregation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Aggregation' type SearchService_Aggregation_Call struct { *mock.Call } // Aggregation is a helper method to define mock.On call // - name string // - aggregation elastic.Aggregation func (_e *SearchService_Expecter) Aggregation(name interface{}, aggregation interface{}) *SearchService_Aggregation_Call { return &SearchService_Aggregation_Call{Call: _e.mock.On("Aggregation", name, aggregation)} } func (_c *SearchService_Aggregation_Call) Run(run func(name string, aggregation elastic.Aggregation)) *SearchService_Aggregation_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } var arg1 elastic.Aggregation if args[1] != nil { arg1 = args[1].(elastic.Aggregation) } run( arg0, arg1, ) }) return _c } func (_c *SearchService_Aggregation_Call) Return(searchService elasticsearch.SearchService) *SearchService_Aggregation_Call { _c.Call.Return(searchService) return _c } func (_c *SearchService_Aggregation_Call) RunAndReturn(run func(name string, aggregation elastic.Aggregation) elasticsearch.SearchService) *SearchService_Aggregation_Call { _c.Call.Return(run) return _c } // Do provides a mock function for the type SearchService func (_mock *SearchService) Do(ctx context.Context) (*elastic.SearchResult, error) { ret := _mock.Called(ctx) if len(ret) == 0 { panic("no return value specified for Do") } var r0 *elastic.SearchResult var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context) (*elastic.SearchResult, error)); ok { return returnFunc(ctx) } if returnFunc, ok := ret.Get(0).(func(context.Context) *elastic.SearchResult); ok { r0 = returnFunc(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*elastic.SearchResult) } } if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { r1 = returnFunc(ctx) } else { r1 = ret.Error(1) } return r0, r1 } // SearchService_Do_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Do' type SearchService_Do_Call struct { *mock.Call } // Do is a helper method to define mock.On call // - ctx context.Context func (_e *SearchService_Expecter) Do(ctx interface{}) *SearchService_Do_Call { return &SearchService_Do_Call{Call: _e.mock.On("Do", ctx)} } func (_c *SearchService_Do_Call) Run(run func(ctx context.Context)) *SearchService_Do_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } run( arg0, ) }) return _c } func (_c *SearchService_Do_Call) Return(searchResult *elastic.SearchResult, err error) *SearchService_Do_Call { _c.Call.Return(searchResult, err) return _c } func (_c *SearchService_Do_Call) RunAndReturn(run func(ctx context.Context) (*elastic.SearchResult, error)) *SearchService_Do_Call { _c.Call.Return(run) return _c } // IgnoreUnavailable provides a mock function for the type SearchService func (_mock *SearchService) IgnoreUnavailable(ignoreUnavailable bool) elasticsearch.SearchService { ret := _mock.Called(ignoreUnavailable) if len(ret) == 0 { panic("no return value specified for IgnoreUnavailable") } var r0 elasticsearch.SearchService if returnFunc, ok := ret.Get(0).(func(bool) elasticsearch.SearchService); ok { r0 = returnFunc(ignoreUnavailable) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(elasticsearch.SearchService) } } return r0 } // SearchService_IgnoreUnavailable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IgnoreUnavailable' type SearchService_IgnoreUnavailable_Call struct { *mock.Call } // IgnoreUnavailable is a helper method to define mock.On call // - ignoreUnavailable bool func (_e *SearchService_Expecter) IgnoreUnavailable(ignoreUnavailable interface{}) *SearchService_IgnoreUnavailable_Call { return &SearchService_IgnoreUnavailable_Call{Call: _e.mock.On("IgnoreUnavailable", ignoreUnavailable)} } func (_c *SearchService_IgnoreUnavailable_Call) Run(run func(ignoreUnavailable bool)) *SearchService_IgnoreUnavailable_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 bool if args[0] != nil { arg0 = args[0].(bool) } run( arg0, ) }) return _c } func (_c *SearchService_IgnoreUnavailable_Call) Return(searchService elasticsearch.SearchService) *SearchService_IgnoreUnavailable_Call { _c.Call.Return(searchService) return _c } func (_c *SearchService_IgnoreUnavailable_Call) RunAndReturn(run func(ignoreUnavailable bool) elasticsearch.SearchService) *SearchService_IgnoreUnavailable_Call { _c.Call.Return(run) return _c } // Query provides a mock function for the type SearchService func (_mock *SearchService) Query(query elastic.Query) elasticsearch.SearchService { ret := _mock.Called(query) if len(ret) == 0 { panic("no return value specified for Query") } var r0 elasticsearch.SearchService if returnFunc, ok := ret.Get(0).(func(elastic.Query) elasticsearch.SearchService); ok { r0 = returnFunc(query) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(elasticsearch.SearchService) } } return r0 } // SearchService_Query_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Query' type SearchService_Query_Call struct { *mock.Call } // Query is a helper method to define mock.On call // - query elastic.Query func (_e *SearchService_Expecter) Query(query interface{}) *SearchService_Query_Call { return &SearchService_Query_Call{Call: _e.mock.On("Query", query)} } func (_c *SearchService_Query_Call) Run(run func(query elastic.Query)) *SearchService_Query_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 elastic.Query if args[0] != nil { arg0 = args[0].(elastic.Query) } run( arg0, ) }) return _c } func (_c *SearchService_Query_Call) Return(searchService elasticsearch.SearchService) *SearchService_Query_Call { _c.Call.Return(searchService) return _c } func (_c *SearchService_Query_Call) RunAndReturn(run func(query elastic.Query) elasticsearch.SearchService) *SearchService_Query_Call { _c.Call.Return(run) return _c } // Size provides a mock function for the type SearchService func (_mock *SearchService) Size(size int) elasticsearch.SearchService { ret := _mock.Called(size) if len(ret) == 0 { panic("no return value specified for Size") } var r0 elasticsearch.SearchService if returnFunc, ok := ret.Get(0).(func(int) elasticsearch.SearchService); ok { r0 = returnFunc(size) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(elasticsearch.SearchService) } } return r0 } // SearchService_Size_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Size' type SearchService_Size_Call struct { *mock.Call } // Size is a helper method to define mock.On call // - size int func (_e *SearchService_Expecter) Size(size interface{}) *SearchService_Size_Call { return &SearchService_Size_Call{Call: _e.mock.On("Size", size)} } func (_c *SearchService_Size_Call) Run(run func(size int)) *SearchService_Size_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 int if args[0] != nil { arg0 = args[0].(int) } run( arg0, ) }) return _c } func (_c *SearchService_Size_Call) Return(searchService elasticsearch.SearchService) *SearchService_Size_Call { _c.Call.Return(searchService) return _c } func (_c *SearchService_Size_Call) RunAndReturn(run func(size int) elasticsearch.SearchService) *SearchService_Size_Call { _c.Call.Return(run) return _c } // NewMultiSearchService creates a new instance of MultiSearchService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMultiSearchService(t interface { mock.TestingT Cleanup(func()) }) *MultiSearchService { mock := &MultiSearchService{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // MultiSearchService is an autogenerated mock type for the MultiSearchService type type MultiSearchService struct { mock.Mock } type MultiSearchService_Expecter struct { mock *mock.Mock } func (_m *MultiSearchService) EXPECT() *MultiSearchService_Expecter { return &MultiSearchService_Expecter{mock: &_m.Mock} } // Add provides a mock function for the type MultiSearchService func (_mock *MultiSearchService) Add(requests ...*elastic.SearchRequest) elasticsearch.MultiSearchService { var tmpRet mock.Arguments if len(requests) > 0 { tmpRet = _mock.Called(requests) } else { tmpRet = _mock.Called() } ret := tmpRet if len(ret) == 0 { panic("no return value specified for Add") } var r0 elasticsearch.MultiSearchService if returnFunc, ok := ret.Get(0).(func(...*elastic.SearchRequest) elasticsearch.MultiSearchService); ok { r0 = returnFunc(requests...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(elasticsearch.MultiSearchService) } } return r0 } // MultiSearchService_Add_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Add' type MultiSearchService_Add_Call struct { *mock.Call } // Add is a helper method to define mock.On call // - requests ...*elastic.SearchRequest func (_e *MultiSearchService_Expecter) Add(requests ...interface{}) *MultiSearchService_Add_Call { return &MultiSearchService_Add_Call{Call: _e.mock.On("Add", append([]interface{}{}, requests...)...)} } func (_c *MultiSearchService_Add_Call) Run(run func(requests ...*elastic.SearchRequest)) *MultiSearchService_Add_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 []*elastic.SearchRequest var variadicArgs []*elastic.SearchRequest if len(args) > 0 { variadicArgs = args[0].([]*elastic.SearchRequest) } arg0 = variadicArgs run( arg0..., ) }) return _c } func (_c *MultiSearchService_Add_Call) Return(multiSearchService elasticsearch.MultiSearchService) *MultiSearchService_Add_Call { _c.Call.Return(multiSearchService) return _c } func (_c *MultiSearchService_Add_Call) RunAndReturn(run func(requests ...*elastic.SearchRequest) elasticsearch.MultiSearchService) *MultiSearchService_Add_Call { _c.Call.Return(run) return _c } // Do provides a mock function for the type MultiSearchService func (_mock *MultiSearchService) Do(ctx context.Context) (*elastic.MultiSearchResult, error) { ret := _mock.Called(ctx) if len(ret) == 0 { panic("no return value specified for Do") } var r0 *elastic.MultiSearchResult var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context) (*elastic.MultiSearchResult, error)); ok { return returnFunc(ctx) } if returnFunc, ok := ret.Get(0).(func(context.Context) *elastic.MultiSearchResult); ok { r0 = returnFunc(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*elastic.MultiSearchResult) } } if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { r1 = returnFunc(ctx) } else { r1 = ret.Error(1) } return r0, r1 } // MultiSearchService_Do_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Do' type MultiSearchService_Do_Call struct { *mock.Call } // Do is a helper method to define mock.On call // - ctx context.Context func (_e *MultiSearchService_Expecter) Do(ctx interface{}) *MultiSearchService_Do_Call { return &MultiSearchService_Do_Call{Call: _e.mock.On("Do", ctx)} } func (_c *MultiSearchService_Do_Call) Run(run func(ctx context.Context)) *MultiSearchService_Do_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } run( arg0, ) }) return _c } func (_c *MultiSearchService_Do_Call) Return(multiSearchResult *elastic.MultiSearchResult, err error) *MultiSearchService_Do_Call { _c.Call.Return(multiSearchResult, err) return _c } func (_c *MultiSearchService_Do_Call) RunAndReturn(run func(ctx context.Context) (*elastic.MultiSearchResult, error)) *MultiSearchService_Do_Call { _c.Call.Return(run) return _c } // Index provides a mock function for the type MultiSearchService func (_mock *MultiSearchService) Index(indices ...string) elasticsearch.MultiSearchService { var tmpRet mock.Arguments if len(indices) > 0 { tmpRet = _mock.Called(indices) } else { tmpRet = _mock.Called() } ret := tmpRet if len(ret) == 0 { panic("no return value specified for Index") } var r0 elasticsearch.MultiSearchService if returnFunc, ok := ret.Get(0).(func(...string) elasticsearch.MultiSearchService); ok { r0 = returnFunc(indices...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(elasticsearch.MultiSearchService) } } return r0 } // MultiSearchService_Index_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Index' type MultiSearchService_Index_Call struct { *mock.Call } // Index is a helper method to define mock.On call // - indices ...string func (_e *MultiSearchService_Expecter) Index(indices ...interface{}) *MultiSearchService_Index_Call { return &MultiSearchService_Index_Call{Call: _e.mock.On("Index", append([]interface{}{}, indices...)...)} } func (_c *MultiSearchService_Index_Call) Run(run func(indices ...string)) *MultiSearchService_Index_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 []string var variadicArgs []string if len(args) > 0 { variadicArgs = args[0].([]string) } arg0 = variadicArgs run( arg0..., ) }) return _c } func (_c *MultiSearchService_Index_Call) Return(multiSearchService elasticsearch.MultiSearchService) *MultiSearchService_Index_Call { _c.Call.Return(multiSearchService) return _c } func (_c *MultiSearchService_Index_Call) RunAndReturn(run func(indices ...string) elasticsearch.MultiSearchService) *MultiSearchService_Index_Call { _c.Call.Return(run) return _c } // NewTemplateApplier creates a new instance of TemplateApplier. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewTemplateApplier(t interface { mock.TestingT Cleanup(func()) }) *TemplateApplier { mock := &TemplateApplier{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // TemplateApplier is an autogenerated mock type for the TemplateApplier type type TemplateApplier struct { mock.Mock } type TemplateApplier_Expecter struct { mock *mock.Mock } func (_m *TemplateApplier) EXPECT() *TemplateApplier_Expecter { return &TemplateApplier_Expecter{mock: &_m.Mock} } // Execute provides a mock function for the type TemplateApplier func (_mock *TemplateApplier) Execute(wr io.Writer, data any) error { ret := _mock.Called(wr, data) if len(ret) == 0 { panic("no return value specified for Execute") } var r0 error if returnFunc, ok := ret.Get(0).(func(io.Writer, any) error); ok { r0 = returnFunc(wr, data) } else { r0 = ret.Error(0) } return r0 } // TemplateApplier_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' type TemplateApplier_Execute_Call struct { *mock.Call } // Execute is a helper method to define mock.On call // - wr io.Writer // - data any func (_e *TemplateApplier_Expecter) Execute(wr interface{}, data interface{}) *TemplateApplier_Execute_Call { return &TemplateApplier_Execute_Call{Call: _e.mock.On("Execute", wr, data)} } func (_c *TemplateApplier_Execute_Call) Run(run func(wr io.Writer, data any)) *TemplateApplier_Execute_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 io.Writer if args[0] != nil { arg0 = args[0].(io.Writer) } var arg1 any if args[1] != nil { arg1 = args[1].(any) } run( arg0, arg1, ) }) return _c } func (_c *TemplateApplier_Execute_Call) Return(err error) *TemplateApplier_Execute_Call { _c.Call.Return(err) return _c } func (_c *TemplateApplier_Execute_Call) RunAndReturn(run func(wr io.Writer, data any) error) *TemplateApplier_Execute_Call { _c.Call.Return(run) return _c } // NewTemplateBuilder creates a new instance of TemplateBuilder. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewTemplateBuilder(t interface { mock.TestingT Cleanup(func()) }) *TemplateBuilder { mock := &TemplateBuilder{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // TemplateBuilder is an autogenerated mock type for the TemplateBuilder type type TemplateBuilder struct { mock.Mock } type TemplateBuilder_Expecter struct { mock *mock.Mock } func (_m *TemplateBuilder) EXPECT() *TemplateBuilder_Expecter { return &TemplateBuilder_Expecter{mock: &_m.Mock} } // Parse provides a mock function for the type TemplateBuilder func (_mock *TemplateBuilder) Parse(text string) (elasticsearch.TemplateApplier, error) { ret := _mock.Called(text) if len(ret) == 0 { panic("no return value specified for Parse") } var r0 elasticsearch.TemplateApplier var r1 error if returnFunc, ok := ret.Get(0).(func(string) (elasticsearch.TemplateApplier, error)); ok { return returnFunc(text) } if returnFunc, ok := ret.Get(0).(func(string) elasticsearch.TemplateApplier); ok { r0 = returnFunc(text) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(elasticsearch.TemplateApplier) } } if returnFunc, ok := ret.Get(1).(func(string) error); ok { r1 = returnFunc(text) } else { r1 = ret.Error(1) } return r0, r1 } // TemplateBuilder_Parse_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Parse' type TemplateBuilder_Parse_Call struct { *mock.Call } // Parse is a helper method to define mock.On call // - text string func (_e *TemplateBuilder_Expecter) Parse(text interface{}) *TemplateBuilder_Parse_Call { return &TemplateBuilder_Parse_Call{Call: _e.mock.On("Parse", text)} } func (_c *TemplateBuilder_Parse_Call) Run(run func(text string)) *TemplateBuilder_Parse_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *TemplateBuilder_Parse_Call) Return(templateApplier elasticsearch.TemplateApplier, err error) *TemplateBuilder_Parse_Call { _c.Call.Return(templateApplier, err) return _c } func (_c *TemplateBuilder_Parse_Call) RunAndReturn(run func(text string) (elasticsearch.TemplateApplier, error)) *TemplateBuilder_Parse_Call { _c.Call.Return(run) return _c } ================================================ FILE: internal/storage/elasticsearch/query/range_query.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package query // Package query provides an Elasticsearch RangeQuery implementation. // This RangeQuery behaves the same as the Go Elasticsearch client (olivere/elastic), // but is rewritten to be compatible with Elasticsearch v9 and avoids deprecated parameters. // // Deprecated parameters like include_lower, include_upper, from, and to are excluded deliberately. type RangeQuery struct { name string queryName string params map[string]any } // NewRangeQuery creates and initializes a new RangeQuery. func NewRangeQuery(name string) *RangeQuery { return &RangeQuery{ name: name, params: make(map[string]any), } } // Generic setter func (q *RangeQuery) set(key string, val any) *RangeQuery { q.params[key] = val return q } func (q *RangeQuery) Gt(val any) *RangeQuery { return q.set("gt", val) } func (q *RangeQuery) Gte(val any) *RangeQuery { return q.set("gte", val) } func (q *RangeQuery) Lt(val any) *RangeQuery { return q.set("lt", val) } func (q *RangeQuery) Lte(val any) *RangeQuery { return q.set("lte", val) } func (q *RangeQuery) Boost(b float64) *RangeQuery { return q.set("boost", b) } func (q *RangeQuery) TimeZone(tz string) *RangeQuery { return q.set("time_zone", tz) } func (q *RangeQuery) Format(fmt string) *RangeQuery { return q.set("format", fmt) } func (q *RangeQuery) Relation(r string) *RangeQuery { return q.set("relation", r) } func (q *RangeQuery) QueryName(queryName string) *RangeQuery { q.queryName = queryName return q } // Source builds and returns the Elasticsearch-compatible representation of the range query. func (q *RangeQuery) Source() (any, error) { source := make(map[string]any) rangeQ := make(map[string]any) source["range"] = rangeQ rangeQ[q.name] = q.params if q.queryName != "" { rangeQ["_name"] = q.queryName } return source, nil } ================================================ FILE: internal/storage/elasticsearch/query/range_query_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package query import ( "encoding/json" "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func assertRangeQuery(t *testing.T, q *RangeQuery, expected string) { t.Helper() src, err := q.Source() if err != nil { t.Fatal(err) } data, err := json.Marshal(src) if err != nil { t.Fatalf("marshaling to JSON failed: %v", err) } got := string(data) if got != expected { t.Errorf("expected:\n%s\ngot:\n%s", expected, got) } } func TestRangeQuery(t *testing.T) { q := NewRangeQuery("postDate"). Gte("2010-03-01"). Lte("2010-04-01"). Boost(3). Relation("within"). QueryName("my_query") expected := `{"range":{"_name":"my_query","postDate":{"boost":3,"gte":"2010-03-01","lte":"2010-04-01","relation":"within"}}}` assertRangeQuery(t, q, expected) } func TestRangeQueryWithTimeZone(t *testing.T) { q := NewRangeQuery("born"). Gte("2012-01-01"). Lte("now"). TimeZone("+1:00") expected := `{"range":{"born":{"gte":"2012-01-01","lte":"now","time_zone":"+1:00"}}}` assertRangeQuery(t, q, expected) } func TestRangeQueryWithFormat(t *testing.T) { q := NewRangeQuery("born"). Gt("2012/01/01"). Lt("now"). Format("yyyy/MM/dd") expected := `{"range":{"born":{"format":"yyyy/MM/dd","gt":"2012/01/01","lt":"now"}}}` assertRangeQuery(t, q, expected) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/elasticsearch/textTemplate.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "io" "text/template" ) // TemplateApplier applies a parsed template to input data that maps to the template's variables. type TemplateApplier interface { Execute(wr io.Writer, data any) error } // TemplateBuilder parses a given string and returns TemplateApplier // TemplateBuilder is an abstraction to support mocking template/text type TemplateBuilder interface { Parse(text string) (TemplateApplier, error) } // TextTemplateBuilder implements TemplateBuilder type TextTemplateBuilder struct{} // Parse is a wrapper for template.Parse func (TextTemplateBuilder) Parse(tmpl string) (TemplateApplier, error) { return template.New("mapping").Parse(tmpl) } ================================================ FILE: internal/storage/elasticsearch/textTemplate_test.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "bytes" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestParse(t *testing.T) { const wantString = "text/template parse" values := struct { Str string }{wantString} template := "Parse is a wrapper for {{ .Str }} function." writer := new(bytes.Buffer) testTemplateBuilder := TextTemplateBuilder{} parsedStr, err := testTemplateBuilder.Parse(template) require.NoError(t, err) err = parsedStr.Execute(writer, values) require.NoError(t, err) assert.Equal(t, "Parse is a wrapper for text/template parse function.", writer.String()) } ================================================ FILE: internal/storage/elasticsearch/wrapper/empty_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package eswrapper import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/elasticsearch/wrapper/wrapper.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package eswrapper import ( "context" "fmt" "net/http" "strings" esv8 "github.com/elastic/go-elasticsearch/v9" esv8api "github.com/elastic/go-elasticsearch/v9/esapi" "github.com/olivere/elastic/v7" es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" ) // This file avoids lint because the Id and Json are required to be capitalized, but must match an outside library. // ClientWrapper is a wrapper around elastic.Client type ClientWrapper struct { client *elastic.Client bulkService *elastic.BulkProcessor esVersion uint clientV8 *esv8.Client } // GetVersion returns the ElasticSearch Version func (c ClientWrapper) GetVersion() uint { return c.esVersion } // WrapESClient creates a ESClient out of *elastic.Client. func WrapESClient(client *elastic.Client, s *elastic.BulkProcessor, esVersion uint, clientV8 *esv8.Client) ClientWrapper { return ClientWrapper{ client: client, bulkService: s, esVersion: esVersion, clientV8: clientV8, } } // IndexExists calls this function to internal client. func (c ClientWrapper) IndexExists(index string) es.IndicesExistsService { return WrapESIndicesExistsService(c.client.IndexExists(index)) } // CreateIndex calls this function to internal client. func (c ClientWrapper) CreateIndex(index string) es.IndicesCreateService { return WrapESIndicesCreateService(c.client.CreateIndex(index)) } // DeleteIndex calls this function to internal client. func (c ClientWrapper) DeleteIndex(index string) es.IndicesDeleteService { return WrapESIndicesDeleteService(c.client.DeleteIndex(index)) } // CreateTemplate calls this function to internal client. func (c ClientWrapper) CreateTemplate(ttype string) es.TemplateCreateService { if c.esVersion >= 8 { return TemplateCreatorWrapperV8{ indicesV8: c.clientV8.Indices, templateName: ttype, } } return WrapESTemplateCreateService(c.client.IndexPutTemplate(ttype)) } // Index calls this function to internal client. func (c ClientWrapper) Index() es.IndexService { r := elastic.NewBulkIndexRequest() return WrapESIndexService(r, c.bulkService, c.esVersion) } // Search calls this function to internal client. func (c ClientWrapper) Search(indices ...string) es.SearchService { searchService := c.client.Search(indices...) if c.esVersion >= 7 { searchService = searchService.RestTotalHitsAsInt(true) } return WrapESSearchService(searchService) } // MultiSearch calls this function to internal client. func (c ClientWrapper) MultiSearch() es.MultiSearchService { multiSearchService := c.client.MultiSearch() return WrapESMultiSearchService(multiSearchService) } // Close closes ESClient and flushes all data to the storage. func (c ClientWrapper) Close() error { c.client.Stop() return c.bulkService.Close() } // --- // IndicesExistsServiceWrapper is a wrapper around elastic.IndicesExistsService type IndicesExistsServiceWrapper struct { indicesExistsService *elastic.IndicesExistsService } // WrapESIndicesExistsService creates an ESIndicesExistsService out of *elastic.IndicesExistsService. func WrapESIndicesExistsService(indicesExistsService *elastic.IndicesExistsService) IndicesExistsServiceWrapper { return IndicesExistsServiceWrapper{indicesExistsService: indicesExistsService} } // Do calls this function to internal service. func (e IndicesExistsServiceWrapper) Do(ctx context.Context) (bool, error) { return e.indicesExistsService.Do(ctx) } // --- // IndicesCreateServiceWrapper is a wrapper around elastic.IndicesCreateService type IndicesCreateServiceWrapper struct { indicesCreateService *elastic.IndicesCreateService } // WrapESIndicesCreateService creates an ESIndicesCreateService out of *elastic.IndicesCreateService. func WrapESIndicesCreateService(indicesCreateService *elastic.IndicesCreateService) IndicesCreateServiceWrapper { return IndicesCreateServiceWrapper{indicesCreateService: indicesCreateService} } // Body calls this function to internal service. func (c IndicesCreateServiceWrapper) Body(mapping string) es.IndicesCreateService { return WrapESIndicesCreateService(c.indicesCreateService.Body(mapping)) } // Do calls this function to internal service. func (c IndicesCreateServiceWrapper) Do(ctx context.Context) (*elastic.IndicesCreateResult, error) { return c.indicesCreateService.Do(ctx) } // TemplateCreateServiceWrapper is a wrapper around elastic.IndicesPutTemplateService. type TemplateCreateServiceWrapper struct { mappingCreateService *elastic.IndicesPutTemplateService } // IndicesDeleteServiceWrapper is a wrapper around elastic.IndicesDeleteService type IndicesDeleteServiceWrapper struct { indicesDeleteService *elastic.IndicesDeleteService } // WrapESIndicesDeleteService creates an ESIndicesDeleteService out of *elastic.IndicesDeleteService. func WrapESIndicesDeleteService(indicesDeleteService *elastic.IndicesDeleteService) IndicesDeleteServiceWrapper { return IndicesDeleteServiceWrapper{indicesDeleteService: indicesDeleteService} } // Do calls this function to internal service. func (e IndicesDeleteServiceWrapper) Do(ctx context.Context) (*elastic.IndicesDeleteResponse, error) { return e.indicesDeleteService.Do(ctx) } // WrapESTemplateCreateService creates an TemplateCreateService out of *elastic.IndicesPutTemplateService. func WrapESTemplateCreateService(mappingCreateService *elastic.IndicesPutTemplateService) TemplateCreateServiceWrapper { return TemplateCreateServiceWrapper{mappingCreateService: mappingCreateService} } // Body calls this function to internal service. func (c TemplateCreateServiceWrapper) Body(mapping string) es.TemplateCreateService { return WrapESTemplateCreateService(c.mappingCreateService.BodyString(mapping)) } // Do calls this function to internal service. func (c TemplateCreateServiceWrapper) Do(ctx context.Context) (*elastic.IndicesPutTemplateResponse, error) { return c.mappingCreateService.Do(ctx) } // --- // TemplateCreatorWrapperV8 implements es.TemplateCreateService. type TemplateCreatorWrapperV8 struct { indicesV8 *esv8api.Indices templateName string templateMapping string } // Body adds mapping to the future request. func (c TemplateCreatorWrapperV8) Body(mapping string) es.TemplateCreateService { cc := c // clone cc.templateMapping = mapping return cc } // Do executes Put Template command. func (c TemplateCreatorWrapperV8) Do(context.Context) (*elastic.IndicesPutTemplateResponse, error) { resp, err := c.indicesV8.PutIndexTemplate(c.templateName, strings.NewReader(c.templateMapping)) if err != nil { return nil, fmt.Errorf("error creating index template %s: %w", c.templateName, err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("error creating index template %s: %s", c.templateName, resp) } return nil, nil // no response expected by span writer } // --- // IndexServiceWrapper is a wrapper around elastic.ESIndexService. // See wrapper_nolint.go for more functions. type IndexServiceWrapper struct { bulkIndexReq *elastic.BulkIndexRequest bulkService *elastic.BulkProcessor esVersion uint } // WrapESIndexService creates an ESIndexService out of *elastic.ESIndexService. func WrapESIndexService(indexService *elastic.BulkIndexRequest, bulkService *elastic.BulkProcessor, esVersion uint) IndexServiceWrapper { return IndexServiceWrapper{bulkIndexReq: indexService, bulkService: bulkService, esVersion: esVersion} } // Index calls this function to internal service. func (i IndexServiceWrapper) Index(index string) es.IndexService { return WrapESIndexService(i.bulkIndexReq.Index(index), i.bulkService, i.esVersion) } // Type calls this function to internal service. func (i IndexServiceWrapper) Type(typ string) es.IndexService { if i.esVersion >= 7 { return WrapESIndexService(i.bulkIndexReq, i.bulkService, i.esVersion) } return WrapESIndexService(i.bulkIndexReq.Type(typ), i.bulkService, i.esVersion) } // Add adds the request to bulk service func (i IndexServiceWrapper) Add() { i.bulkService.Add(i.bulkIndexReq) } // --- // SearchServiceWrapper is a wrapper around elastic.ESSearchService type SearchServiceWrapper struct { searchService *elastic.SearchService } // WrapESSearchService creates an ESSearchService out of *elastic.ESSearchService. func WrapESSearchService(searchService *elastic.SearchService) SearchServiceWrapper { return SearchServiceWrapper{searchService: searchService} } // Size calls this function to internal service. func (s SearchServiceWrapper) Size(size int) es.SearchService { return WrapESSearchService(s.searchService.Size(size)) } // Aggregation calls this function to internal service. func (s SearchServiceWrapper) Aggregation(name string, aggregation elastic.Aggregation) es.SearchService { return WrapESSearchService(s.searchService.Aggregation(name, aggregation)) } // IgnoreUnavailable calls this function to internal service. func (s SearchServiceWrapper) IgnoreUnavailable(ignoreUnavailable bool) es.SearchService { return WrapESSearchService(s.searchService.IgnoreUnavailable(ignoreUnavailable)) } // Query calls this function to internal service. func (s SearchServiceWrapper) Query(query elastic.Query) es.SearchService { return WrapESSearchService(s.searchService.Query(query)) } // Do calls this function to internal service. func (s SearchServiceWrapper) Do(ctx context.Context) (*elastic.SearchResult, error) { return s.searchService.Do(ctx) } // MultiSearchServiceWrapper is a wrapper around elastic.ESMultiSearchService type MultiSearchServiceWrapper struct { multiSearchService *elastic.MultiSearchService } // WrapESMultiSearchService creates an ESSearchService out of *elastic.ESSearchService. func WrapESMultiSearchService(multiSearchService *elastic.MultiSearchService) MultiSearchServiceWrapper { return MultiSearchServiceWrapper{multiSearchService: multiSearchService} } // Add calls this function to internal service. func (s MultiSearchServiceWrapper) Add(requests ...*elastic.SearchRequest) es.MultiSearchService { return WrapESMultiSearchService(s.multiSearchService.Add(requests...)) } // Index calls this function to internal service. func (s MultiSearchServiceWrapper) Index(indices ...string) es.MultiSearchService { return WrapESMultiSearchService(s.multiSearchService.Index(indices...)) } // Do calls this function to internal service. func (s MultiSearchServiceWrapper) Do(ctx context.Context) (*elastic.MultiSearchResult, error) { return s.multiSearchService.Do(ctx) } ================================================ FILE: internal/storage/elasticsearch/wrapper/wrapper_nolint.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package eswrapper import es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" // Some of the functions of elastic.BulkIndexRequest violate golint rules, // e.g. Id() should be ID() and BodyJson() should be BodyJSON(). // Id calls this function to internal service. func (i IndexServiceWrapper) Id(id string) es.IndexService { return WrapESIndexService(i.bulkIndexReq.Id(id), i.bulkService, i.esVersion) } // BodyJson calls this function to internal service. func (i IndexServiceWrapper) BodyJson(body any) es.IndexService { return WrapESIndexService(i.bulkIndexReq.Doc(body), i.bulkService, i.esVersion) } ================================================ FILE: internal/storage/integration/badgerstore_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package integration import ( "context" "testing" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zaptest" v1badger "github.com/jaegertracing/jaeger/internal/storage/v1/badger" "github.com/jaegertracing/jaeger/internal/storage/v2/badger" "github.com/jaegertracing/jaeger/internal/telemetry" "github.com/jaegertracing/jaeger/internal/testutils" ) type BadgerIntegrationStorage struct { StorageIntegration factory *badger.Factory } func (s *BadgerIntegrationStorage) initialize(t *testing.T) { cfg := v1badger.DefaultConfig() cfg.Ephemeral = false var err error telset := telemetry.NoopSettings() telset.Logger = zaptest.NewLogger(t, zaptest.WrapOptions(zap.AddCaller())) s.factory, err = badger.NewFactory(*cfg, telset) require.NoError(t, err) t.Cleanup(func() { s.factory.Close() }) s.TraceWriter, err = s.factory.CreateTraceWriter() require.NoError(t, err) s.TraceReader, err = s.factory.CreateTraceReader() require.NoError(t, err) s.SamplingStore, err = s.factory.CreateSamplingStore(0) require.NoError(t, err) } func (s *BadgerIntegrationStorage) cleanUp(t *testing.T) { require.NoError(t, s.factory.Purge(context.Background())) } func TestBadgerStorage(t *testing.T) { SkipUnlessEnv(t, "badger") t.Cleanup(func() { testutils.VerifyGoLeaksOnce(t) }) s := &BadgerIntegrationStorage{ StorageIntegration: StorageIntegration{ // TODO: remove this badger supports returning spanKind from GetOperations GetOperationsMissingSpanKind: true, }, } s.CleanUp = s.cleanUp s.initialize(t) s.RunAll(t) } ================================================ FILE: internal/storage/integration/cassandra_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2019 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package integration import ( "context" "os" "testing" "time" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/config/configtls" casconfig "github.com/jaegertracing/jaeger/internal/storage/cassandra/config" "github.com/jaegertracing/jaeger/internal/storage/v1/api/dependencystore" cassandrav1 "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra" "github.com/jaegertracing/jaeger/internal/storage/v2/cassandra" "github.com/jaegertracing/jaeger/internal/storage/v2/v1adapter" "github.com/jaegertracing/jaeger/internal/telemetry" "github.com/jaegertracing/jaeger/internal/testutils" ) type CassandraStorageIntegration struct { StorageIntegration factory *cassandra.Factory } func newCassandraStorageIntegration() *CassandraStorageIntegration { s := &CassandraStorageIntegration{ StorageIntegration: StorageIntegration{ GetDependenciesReturnsSource: true, SkipList: CassandraSkippedTests, }, } s.CleanUp = s.cleanUp return s } func (s *CassandraStorageIntegration) cleanUp(t *testing.T) { require.NoError(t, s.factory.Purge(context.Background())) } func (s *CassandraStorageIntegration) initializeCassandra(t *testing.T) { username := os.Getenv("CASSANDRA_USERNAME") password := os.Getenv("CASSANDRA_PASSWORD") cfg := casconfig.Configuration{ Schema: casconfig.Schema{ Keyspace: "jaeger_v1_dc1", }, Connection: casconfig.Connection{ Servers: []string{"127.0.0.1"}, Authenticator: casconfig.Authenticator{ Basic: casconfig.BasicAuthenticator{ Username: username, Password: password, AllowedAuthenticators: []string{"org.apache.cassandra.auth.PasswordAuthenticator"}, }, }, TLS: configtls.ClientConfig{ Insecure: true, }, }, } defCfg := casconfig.DefaultConfiguration() cfg.ApplyDefaults(&defCfg) opts := cassandrav1.Options{ Configuration: cfg, Index: cassandrav1.IndexConfig{ Logs: true, Tags: true, ProcessTags: true, }, SpanStoreWriteCacheTTL: time.Hour * 12, ArchiveEnabled: false, } f, err := cassandra.NewFactory(opts, telemetry.NoopSettings()) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, f.Close()) }) s.factory = f s.TraceWriter, err = f.CreateTraceWriter() require.NoError(t, err) s.TraceReader, err = f.CreateTraceReader() require.NoError(t, err) s.SamplingStore, err = f.CreateSamplingStore(0) require.NoError(t, err) s.initializeDependencyReaderAndWriter(t, f) } func (s *CassandraStorageIntegration) initializeDependencyReaderAndWriter(t *testing.T, f *cassandra.Factory) { var err error dependencyReader, err := f.CreateDependencyReader() require.NoError(t, err) s.DependencyReader = dependencyReader // TODO: Update this when the factory interface has CreateDependencyWriter if dependencyWriter, ok := dependencyReader.(dependencystore.Writer); !ok { t.Log("DependencyWriter not implemented ") } else { s.DependencyWriter = v1adapter.NewDependencyWriter(dependencyWriter) } } func TestCassandraStorage(t *testing.T) { SkipUnlessEnv(t, "cassandra") t.Cleanup(func() { testutils.VerifyGoLeaksOnce(t) }) s := newCassandraStorageIntegration() s.initializeCassandra(t) s.RunAll(t) } ================================================ FILE: internal/storage/integration/dates.go ================================================ // Copyright (c) 2026 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package integration import ( "time" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger/internal/jptrace" ) // dateOffsetNormalizer normalizes timestamps by replacing their date // (year, month, day) with a fixed date computed using a day offset // from the current UTC date, while preserving the original time. // This is required in integration tests because the fixtures have // hardcoded start time and other timestamps and we need to make them // recent to fetch from the reader. Timestamps whose original UTC date // is 2017-01-25 use a -2 day offset; all other timestamps use a -1 day // offset. These offsets are selected to keep the upgrade from v1 to v2 consistent. type dateOffsetNormalizer struct { tm time.Time } func newDateOffsetNormalizer(tm time.Time) dateOffsetNormalizer { return dateOffsetNormalizer{tm: tm} } func (d dateOffsetNormalizer) normalizeTrace(td ptrace.Traces) { for _, span := range jptrace.SpanIter(td) { span.SetStartTimestamp(d.normalizeTime(span.StartTimestamp())) span.SetEndTimestamp(d.normalizeTime(span.EndTimestamp())) for _, event := range span.Events().All() { event.SetTimestamp(d.normalizeTime(event.Timestamp())) } } } func (d dateOffsetNormalizer) normalizeTime(t pcommon.Timestamp) pcommon.Timestamp { tm := t.AsTime().UTC() offset := -1 // Apply a -2 day offset for any timestamp whose UTC date is 2017-01-25. // All other timestamps use the default -1 day offset to preserve existing behavior. yearOrig, monthOrig, dayOrig := tm.Date() if yearOrig == 2017 && monthOrig == time.January && dayOrig == 25 { offset = -2 } year, month, day := d.tm.UTC().AddDate(0, 0, offset).Date() newTm := time.Date( year, month, day, tm.Hour(), tm.Minute(), tm.Second(), tm.Nanosecond(), tm.Location(), ) return pcommon.NewTimestampFromTime(newTm) } ================================================ FILE: internal/storage/integration/dates_test.go ================================================ // Copyright (c) 2026 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package integration import ( "testing" "time" "github.com/stretchr/testify/assert" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" ) func Test_dateOffsetNormalizer(t *testing.T) { origTime := time.Date( 2024, time.January, 10, 14, 30, 45, 123456789, time.UTC, ) origStartTime := time.Date( 2017, time.January, 25, 23, 56, 31, 639875000, time.UTC, ) origTs := pcommon.NewTimestampFromTime(origTime) origStartTs := pcommon.NewTimestampFromTime(origStartTime) twoDaysAgo := -2 oneDayAgo := -1 now := time.Now() expectedTwoDaysAgo := now.UTC().AddDate(0, 0, twoDaysAgo) expectedOneDayAgo := now.UTC().AddDate(0, 0, oneDayAgo) normalizer := newDateOffsetNormalizer(now) td := ptrace.NewTraces() span := td.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty() span.SetStartTimestamp(origStartTs) span.SetEndTimestamp(origTs) event := span.Events().AppendEmpty() event.SetTimestamp(origTs) normalizer.normalizeTrace(td) expectedTwoDaysAgoTime := time.Date( expectedTwoDaysAgo.Year(), expectedTwoDaysAgo.Month(), expectedTwoDaysAgo.Day(), origStartTime.Hour(), origStartTime.Minute(), origStartTime.Second(), origStartTime.Nanosecond(), time.UTC, ) expectedOneDayAgoTime := time.Date( expectedOneDayAgo.Year(), expectedOneDayAgo.Month(), expectedOneDayAgo.Day(), origTime.Hour(), origTime.Minute(), origTime.Second(), origTime.Nanosecond(), time.UTC, ) assert.Equal(t, expectedTwoDaysAgoTime, span.StartTimestamp().AsTime()) assert.Equal(t, expectedOneDayAgoTime, span.EndTimestamp().AsTime()) assert.Equal(t, expectedOneDayAgoTime, event.Timestamp().AsTime()) } ================================================ FILE: internal/storage/integration/elasticsearch_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package integration import ( "context" "errors" "net/http" "strconv" "strings" "testing" "time" elasticsearch8 "github.com/elastic/go-elasticsearch/v9" "github.com/olivere/elastic/v7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger/internal/jiter" "github.com/jaegertracing/jaeger/internal/jptrace" escfg "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" es "github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" esv2 "github.com/jaegertracing/jaeger/internal/storage/v2/elasticsearch" "github.com/jaegertracing/jaeger/internal/telemetry" "github.com/jaegertracing/jaeger/internal/testutils" ) const ( host = "0.0.0.0" queryPort = "9200" queryHostPort = host + ":" + queryPort queryURL = "http://" + queryHostPort indexPrefix = "integration-test" indexDateLayout = "2006-01-02" tagKeyDeDotChar = "@" maxSpanAge = time.Hour * 72 defaultMaxDocCount = 10_000 spanTemplateName = "jaeger-span" serviceTemplateName = "jaeger-service" dependenciesTemplateName = "jaeger-dependencies" archiveAliasSuffix = "archive" ) type ESStorageIntegration struct { StorageIntegration client *elastic.Client v8Client *elasticsearch8.Client ArchiveTraceReader tracestore.Reader ArchiveTraceWriter tracestore.Writer factory *esv2.Factory archiveFactory *esv2.Factory } func (s *ESStorageIntegration) getVersion() (uint, error) { pingResult, _, err := s.client.Ping(queryURL).Do(context.Background()) if err != nil { return 0, err } esVersion, err := strconv.Atoi(string(pingResult.Version.Number[0])) if err != nil { return 0, err } // OpenSearch is based on ES 7.x if strings.Contains(pingResult.TagLine, "OpenSearch") { if pingResult.Version.Number[0] == '1' || pingResult.Version.Number[0] == '2' || pingResult.Version.Number[0] == '3' { esVersion = 7 } } return uint(esVersion), nil } func (s *ESStorageIntegration) initializeES(t *testing.T, c *http.Client, allTagsAsFields bool) { rawClient, err := elastic.NewClient( elastic.SetURL(queryURL), elastic.SetSniff(false), elastic.SetHttpClient(c)) require.NoError(t, err) t.Cleanup(func() { rawClient.Stop() }) s.client = rawClient s.v8Client, err = elasticsearch8.NewClient(elasticsearch8.Config{ Addresses: []string{queryURL}, DiscoverNodesOnStart: false, Transport: c.Transport, }) require.NoError(t, err) s.initSpanstore(t, allTagsAsFields) s.CleanUp = func(t *testing.T) { s.esCleanUp(t) } s.esCleanUp(t) } func (s *ESStorageIntegration) esCleanUp(t *testing.T) { require.NoError(t, s.factory.Purge(context.Background())) require.NoError(t, s.archiveFactory.Purge(context.Background())) } func (s *ESStorageIntegration) initSpanstore(t *testing.T, allTagsAsFields bool) { cfg := es.DefaultConfig() cfg.CreateIndexTemplates = true cfg.BulkProcessing = escfg.BulkProcessing{ MaxActions: 1, FlushInterval: time.Nanosecond, } cfg.Tags.AllAsFields = allTagsAsFields cfg.ServiceCacheTTL = 1 * time.Second cfg.Indices.IndexPrefix = indexPrefix var err error f, err := esv2.NewFactory(context.Background(), cfg, telemetry.NoopSettings(), nil) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, f.Close()) }) acfg := es.DefaultConfig() acfg.ReadAliasSuffix = archiveAliasSuffix acfg.WriteAliasSuffix = archiveAliasSuffix acfg.UseReadWriteAliases = true acfg.Tags.AllAsFields = allTagsAsFields acfg.Indices.IndexPrefix = indexPrefix af, err := esv2.NewFactory(context.Background(), acfg, telemetry.NoopSettings(), nil) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, af.Close()) }) s.factory = f s.archiveFactory = af s.TraceWriter, err = f.CreateTraceWriter() require.NoError(t, err) s.TraceReader, err = f.CreateTraceReader() require.NoError(t, err) s.ArchiveTraceReader, err = af.CreateTraceReader() require.NoError(t, err) s.ArchiveTraceWriter, err = af.CreateTraceWriter() require.NoError(t, err) s.DependencyReader, err = f.CreateDependencyReader() require.NoError(t, err) s.DependencyWriter = s.DependencyReader.(depstore.Writer) s.SamplingStore, err = f.CreateSamplingStore(1) require.NoError(t, err) } func healthCheck(c *http.Client) error { for range 200 { if resp, err := c.Get(queryURL); err == nil { return resp.Body.Close() } time.Sleep(100 * time.Millisecond) } return errors.New("elastic search is not ready") } func runElasticsearchTest(t *testing.T, allTagsAsFields bool) { SkipUnlessEnv(t, "elasticsearch", "opensearch") c := getESHttpClient(t) require.NoError(t, healthCheck(c)) s := &ESStorageIntegration{ StorageIntegration: StorageIntegration{ Fixtures: LoadAndParseQueryTestCases(t, "fixtures/queries_es.json"), // TODO: remove this flag after ES supports returning spanKind // Issue https://github.com/jaegertracing/jaeger/issues/1923 GetOperationsMissingSpanKind: true, }, } s.initializeES(t, c, allTagsAsFields) s.RunAll(t) t.Run("ArchiveTrace", s.testArchiveTrace) } func TestElasticsearchStorage(t *testing.T) { t.Cleanup(func() { testutils.VerifyGoLeaksOnceForES(t) }) runElasticsearchTest(t, false) } func TestElasticsearchStorage_AllTagsAsObjectFields(t *testing.T) { t.Cleanup(func() { testutils.VerifyGoLeaksOnceForES(t) }) runElasticsearchTest(t, true) } func TestElasticsearchStorage_IndexTemplates(t *testing.T) { SkipUnlessEnv(t, "elasticsearch", "opensearch") t.Cleanup(func() { testutils.VerifyGoLeaksOnceForES(t) }) c := getESHttpClient(t) require.NoError(t, healthCheck(c)) s := &ESStorageIntegration{} s.initializeES(t, c, true) esVersion, err := s.getVersion() require.NoError(t, err) // TODO abstract this into pkg/es/client.IndexManagementLifecycleAPI if esVersion == 6 || esVersion == 7 { serviceTemplateExists, err := s.client.IndexTemplateExists(indexPrefix + "-jaeger-service").Do(context.Background()) require.NoError(t, err) assert.True(t, serviceTemplateExists) spanTemplateExists, err := s.client.IndexTemplateExists(indexPrefix + "-jaeger-span").Do(context.Background()) require.NoError(t, err) assert.True(t, spanTemplateExists) } else { serviceTemplateExistsResponse, err := s.v8Client.API.Indices.ExistsIndexTemplate(indexPrefix + "-jaeger-service") require.NoError(t, err) assert.Equal(t, 200, serviceTemplateExistsResponse.StatusCode) spanTemplateExistsResponse, err := s.v8Client.API.Indices.ExistsIndexTemplate(indexPrefix + "-jaeger-span") require.NoError(t, err) assert.Equal(t, 200, spanTemplateExistsResponse.StatusCode) } s.cleanESIndexTemplates(t, indexPrefix) } func (s *ESStorageIntegration) cleanESIndexTemplates(t *testing.T, prefix string) error { version, err := s.getVersion() require.NoError(t, err) if version > 7 { prefixWithSeparator := prefix if prefix != "" { prefixWithSeparator += "-" } _, err := s.v8Client.Indices.DeleteIndexTemplate([]string{prefixWithSeparator + spanTemplateName}) require.NoError(t, err) _, err = s.v8Client.Indices.DeleteIndexTemplate([]string{prefixWithSeparator + serviceTemplateName}) require.NoError(t, err) _, err = s.v8Client.Indices.DeleteIndexTemplate([]string{prefixWithSeparator + dependenciesTemplateName}) require.NoError(t, err) } else { _, err := s.client.IndexDeleteTemplate("*").Do(context.Background()) require.NoError(t, err) } return nil } // testArchiveTrace validates that a trace with a start time older than maxSpanAge // can still be retrieved via the archive storage. This ensures archived traces are // accessible even when their age exceeds the retention period for primary storage. // This test applies only to Elasticsearch (ES) storage. func (s *ESStorageIntegration) testArchiveTrace(t *testing.T) { s.skipIfNeeded(t) defer s.cleanUp(t) tID := pcommon.TraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 22}) expected := ptrace.NewTraces() rs := expected.ResourceSpans().AppendEmpty() rs.Resource().Attributes().PutStr("service.name", "archived_service") ss := rs.ScopeSpans().AppendEmpty() span := ss.Spans().AppendEmpty() span.SetName("archive_span") span.SetTraceID(tID) span.SetSpanID([8]byte{0, 0, 0, 0, 0, 0, 0, 55}) span.SetStartTimestamp(pcommon.NewTimestampFromTime(time.Now().Add(-maxSpanAge * 5).Truncate(time.Microsecond))) span.SetEndTimestamp(span.StartTimestamp()) require.NoError(t, s.ArchiveTraceWriter.WriteTraces(context.Background(), expected)) var actual ptrace.Traces found := s.waitForCondition(t, func(_ *testing.T) bool { iterTraces := s.ArchiveTraceReader.GetTraces(context.Background(), tracestore.GetTraceParams{TraceID: tID}) traces, err := jiter.CollectWithErrors(jptrace.AggregateTraces(iterTraces)) if err != nil { t.Logf("Error loading trace: %v", err) return false } if len(traces) == 0 { return false } actual = traces[0] return actual.SpanCount() >= expected.SpanCount() }) require.True(t, found) CompareTraces(t, expected, actual) } ================================================ FILE: internal/storage/integration/es_index_cleaner_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package integration import ( "context" "fmt" "net/http" "os/exec" "strings" "testing" elasticsearch8 "github.com/elastic/go-elasticsearch/v9" "github.com/olivere/elastic/v7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/testutils" ) const ( archiveIndexName = "jaeger-span-archive" dependenciesIndexName = "jaeger-dependencies-2019-01-01" samplingIndexName = "jaeger-sampling-2019-01-01" spanIndexName = "jaeger-span-2019-01-01" serviceIndexName = "jaeger-service-2019-01-01" indexCleanerImage = "localhost:5000/jaegertracing/jaeger-es-index-cleaner:local-test" rolloverImage = "localhost:5000/jaegertracing/jaeger-es-rollover:local-test" rolloverNowEnvVar = `CONDITIONS='{"max_age":"0s"}'` ) func TestIndexCleaner_doNotFailOnEmptyStorage(t *testing.T) { SkipUnlessEnv(t, "elasticsearch", "opensearch") t.Cleanup(func() { testutils.VerifyGoLeaksOnceForES(t) }) client, err := createESClient(t, getESHttpClient(t)) require.NoError(t, err) _, err = client.DeleteIndex("*").Do(context.Background()) require.NoError(t, err) tests := []struct { envs []string }{ {envs: []string{"ROLLOVER=false"}}, {envs: []string{"ROLLOVER=true"}}, {envs: []string{"ARCHIVE=true"}}, } for _, test := range tests { err := runEsCleaner(7, test.envs) require.NoError(t, err) } } func TestIndexCleaner_doNotFailOnFullStorage(t *testing.T) { SkipUnlessEnv(t, "elasticsearch", "opensearch") t.Cleanup(func() { testutils.VerifyGoLeaksOnceForES(t) }) client, err := createESClient(t, getESHttpClient(t)) require.NoError(t, err) tests := []struct { envs []string }{ {envs: []string{"ROLLOVER=false"}}, {envs: []string{"ROLLOVER=true"}}, {envs: []string{"ARCHIVE=true"}}, } for _, test := range tests { _, err = client.DeleteIndex("*").Do(context.Background()) require.NoError(t, err) // Create Indices with adaptive sampling disabled (set to false). err := createAllIndices(client, "", false) require.NoError(t, err) err = runEsCleaner(1500, test.envs) require.NoError(t, err) } } func TestIndexCleaner(t *testing.T) { SkipUnlessEnv(t, "elasticsearch", "opensearch") t.Cleanup(func() { testutils.VerifyGoLeaksOnceForES(t) }) hcl := getESHttpClient(t) client, err := createESClient(t, hcl) require.NoError(t, err) v8Client, err := createESV8Client(hcl.Transport) require.NoError(t, err) tests := []struct { name string envVars []string expectedIndices []string adaptiveSampling bool }{ { name: "RemoveDailyIndices", envVars: []string{}, expectedIndices: []string{ archiveIndexName, "jaeger-span-000001", "jaeger-service-000001", "jaeger-dependencies-000001", "jaeger-span-000002", "jaeger-service-000002", "jaeger-dependencies-000002", "jaeger-span-archive-000001", "jaeger-span-archive-000002", }, adaptiveSampling: false, }, { name: "RemoveRolloverIndices", envVars: []string{"ROLLOVER=true"}, expectedIndices: []string{ archiveIndexName, spanIndexName, serviceIndexName, dependenciesIndexName, samplingIndexName, "jaeger-span-000002", "jaeger-service-000002", "jaeger-dependencies-000002", "jaeger-span-archive-000001", "jaeger-span-archive-000002", }, adaptiveSampling: false, }, { name: "RemoveArchiveIndices", envVars: []string{"ARCHIVE=true"}, expectedIndices: []string{ archiveIndexName, spanIndexName, serviceIndexName, dependenciesIndexName, samplingIndexName, "jaeger-span-000001", "jaeger-service-000001", "jaeger-dependencies-000001", "jaeger-span-000002", "jaeger-service-000002", "jaeger-dependencies-000002", "jaeger-span-archive-000002", }, adaptiveSampling: false, }, { name: "RemoveDailyIndices with adaptiveSampling", envVars: []string{}, expectedIndices: []string{ archiveIndexName, "jaeger-span-000001", "jaeger-service-000001", "jaeger-dependencies-000001", "jaeger-span-000002", "jaeger-service-000002", "jaeger-dependencies-000002", "jaeger-span-archive-000001", "jaeger-span-archive-000002", "jaeger-sampling-000001", "jaeger-sampling-000002", }, adaptiveSampling: true, }, } for _, test := range tests { t.Run(fmt.Sprintf("%s_no_prefix, %s", test.name, test.envVars), func(t *testing.T) { runIndexCleanerTest(t, client, v8Client, "", test.expectedIndices, test.envVars, test.adaptiveSampling) }) t.Run(fmt.Sprintf("%s_prefix, %s", test.name, test.envVars), func(t *testing.T) { runIndexCleanerTest(t, client, v8Client, indexPrefix, test.expectedIndices, append(test.envVars, "INDEX_PREFIX="+indexPrefix), test.adaptiveSampling) }) } } func runIndexCleanerTest(t *testing.T, client *elastic.Client, v8Client *elasticsearch8.Client, prefix string, expectedIndices, envVars []string, adaptiveSampling bool) { // make sure ES is clean _, err := client.DeleteIndex("*").Do(context.Background()) require.NoError(t, err) defer cleanESIndexTemplates(t, client, v8Client, prefix) err = createAllIndices(client, prefix, adaptiveSampling) require.NoError(t, err) err = runEsCleaner(0, envVars) require.NoError(t, err) foundIndices, err := client.IndexNames() require.NoError(t, err) if prefix != "" { prefix += "-" } var actual []string for _, index := range foundIndices { // ignore system indices https://github.com/jaegertracing/jaeger/issues/7002 if strings.HasPrefix(index, prefix+"jaeger") { actual = append(actual, index) } } var expected []string for _, index := range expectedIndices { expected = append(expected, prefix+index) } assert.ElementsMatch(t, actual, expected, "indices found: %v, expected: %v", foundIndices, expected) } func createAllIndices(client *elastic.Client, prefix string, adaptiveSampling bool) error { prefixWithSeparator := prefix if prefix != "" { prefixWithSeparator += "-" } // create daily indices and archive index err := createEsIndices(client, []string{ prefixWithSeparator + spanIndexName, prefixWithSeparator + serviceIndexName, prefixWithSeparator + dependenciesIndexName, prefixWithSeparator + samplingIndexName, prefixWithSeparator + archiveIndexName, }) if err != nil { return err } // create rollover archive index and roll alias to the new index err = runEsRollover("init", []string{"ARCHIVE=true", "INDEX_PREFIX=" + prefix}, adaptiveSampling) if err != nil { return err } err = runEsRollover("rollover", []string{"ARCHIVE=true", "INDEX_PREFIX=" + prefix, rolloverNowEnvVar}, adaptiveSampling) if err != nil { return err } // create rollover main indices and roll over to the new index err = runEsRollover("init", []string{"ARCHIVE=false", "INDEX_PREFIX=" + prefix}, adaptiveSampling) if err != nil { return err } err = runEsRollover("rollover", []string{"ARCHIVE=false", "INDEX_PREFIX=" + prefix, rolloverNowEnvVar}, adaptiveSampling) if err != nil { return err } return nil } func createEsIndices(client *elastic.Client, indices []string) error { for _, index := range indices { if _, err := client.CreateIndex(index).Do(context.Background()); err != nil { return err } } return nil } func runEsCleaner(days int, envs []string) error { var dockerEnv strings.Builder for _, e := range envs { dockerEnv.WriteString(" -e ") dockerEnv.WriteString(e) } args := fmt.Sprintf("docker run %s --rm --net=host %s %d http://%s", dockerEnv.String(), indexCleanerImage, days, queryHostPort) cmd := exec.Command("/bin/sh", "-c", args) out, err := cmd.CombinedOutput() fmt.Println(string(out)) return err } func runEsRollover(action string, envs []string, adaptiveSampling bool) error { var dockerEnv strings.Builder for _, e := range envs { dockerEnv.WriteString(" -e ") dockerEnv.WriteString(e) } args := fmt.Sprintf("docker run %s --rm --net=host %s %s --adaptive-sampling=%t http://%s", dockerEnv.String(), rolloverImage, action, adaptiveSampling, queryHostPort) cmd := exec.Command("/bin/sh", "-c", args) out, err := cmd.CombinedOutput() fmt.Println(string(out)) return err } func createESClient(t *testing.T, hcl *http.Client) (*elastic.Client, error) { cl, err := elastic.NewClient( elastic.SetURL(queryURL), elastic.SetSniff(false), elastic.SetHttpClient(hcl), ) require.NoError(t, err) t.Cleanup(func() { cl.Stop() }) return cl, nil } func createESV8Client(tr http.RoundTripper) (*elasticsearch8.Client, error) { return elasticsearch8.NewClient(elasticsearch8.Config{ Addresses: []string{queryURL}, DiscoverNodesOnStart: false, Transport: tr, }) } func cleanESIndexTemplates(t *testing.T, client *elastic.Client, v8Client *elasticsearch8.Client, prefix string) { s := &ESStorageIntegration{ client: client, v8Client: v8Client, } s.cleanESIndexTemplates(t, prefix) } func getESHttpClient(t *testing.T) *http.Client { tr := &http.Transport{} t.Cleanup(func() { tr.CloseIdleConnections() }) return &http.Client{Transport: tr} } ================================================ FILE: internal/storage/integration/es_index_rollover_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package integration import ( "context" "strconv" "testing" "github.com/olivere/elastic/v7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/testutils" ) const ( defaultILMPolicyName = "jaeger-ilm-policy" ) func TestIndexRollover_FailIfILMNotPresent(t *testing.T) { SkipUnlessEnv(t, "elasticsearch", "opensearch") t.Cleanup(func() { testutils.VerifyGoLeaksOnceForES(t) }) client, err := createESClient(t, getESHttpClient(t)) require.NoError(t, err) require.NoError(t, err) // make sure ES is clean cleanES(t, client, defaultILMPolicyName) envVars := []string{"ES_USE_ILM=true"} // Run the ES rollover test with adaptive sampling disabled (set to false). err = runEsRollover("init", envVars, false) require.EqualError(t, err, "exit status 1") indices, err := client.IndexNames() require.NoError(t, err) assert.Empty(t, indices) } func TestIndexRollover_Idempotency(t *testing.T) { SkipUnlessEnv(t, "elasticsearch", "opensearch") t.Cleanup(func() { testutils.VerifyGoLeaksOnceForES(t) }) client, err := createESClient(t, getESHttpClient(t)) require.NoError(t, err) // Make sure that es is clean before the test! cleanES(t, client, defaultILMPolicyName) err = runEsRollover("init", []string{}, false) require.NoError(t, err) // Run again and it should return without any error err = runEsRollover("init", []string{}, false) require.NoError(t, err) cleanES(t, client, defaultILMPolicyName) } func TestIndexRollover_CreateIndicesWithILM(t *testing.T) { SkipUnlessEnv(t, "elasticsearch", "opensearch") t.Cleanup(func() { testutils.VerifyGoLeaksOnceForES(t) }) // Test using the default ILM Policy Name, i.e. do not pass the ES_ILM_POLICY_NAME env var to the rollover script. t.Run("DefaultPolicyName", func(t *testing.T) { runCreateIndicesWithILM(t, defaultILMPolicyName) }) // Test using a configured ILM Policy Name, i.e. pass the ES_ILM_POLICY_NAME env var to the rollover script. t.Run("SetPolicyName", func(t *testing.T) { runCreateIndicesWithILM(t, "jaeger-test-policy") }) } func runCreateIndicesWithILM(t *testing.T, ilmPolicyName string) { client, err := createESClient(t, getESHttpClient(t)) require.NoError(t, err) esVersion, err := getVersion(client) require.NoError(t, err) envVars := []string{ "ES_USE_ILM=true", } if ilmPolicyName != defaultILMPolicyName { envVars = append(envVars, "ES_ILM_POLICY_NAME="+ilmPolicyName) } if esVersion >= 7 { expectedIndices := []string{"jaeger-span-000001", "jaeger-service-000001", "jaeger-dependencies-000001"} t.Run("NoPrefix", func(t *testing.T) { runIndexRolloverWithILMTest(t, client, "", expectedIndices, envVars, ilmPolicyName, false) }) t.Run("WithPrefix", func(t *testing.T) { runIndexRolloverWithILMTest(t, client, indexPrefix, expectedIndices, append(envVars, "INDEX_PREFIX="+indexPrefix), ilmPolicyName, false) }) t.Run("WithAdaptiveSampling", func(t *testing.T) { runIndexRolloverWithILMTest(t, client, indexPrefix, expectedIndices, append(envVars, "INDEX_PREFIX="+indexPrefix), ilmPolicyName, true) }) } } func runIndexRolloverWithILMTest(t *testing.T, client *elastic.Client, prefix string, expectedIndices, envVars []string, ilmPolicyName string, adaptiveSampling bool) { writeAliases := []string{"jaeger-service-write", "jaeger-span-write", "jaeger-dependencies-write"} if adaptiveSampling { writeAliases = append(writeAliases, "jaeger-sampling-write") expectedIndices = append(expectedIndices, "jaeger-sampling-000001") } // make sure ES is cleaned before test cleanES(t, client, ilmPolicyName) v8Client, err := createESV8Client(getESHttpClient(t).Transport) require.NoError(t, err) // make sure ES is cleaned after test defer cleanES(t, client, ilmPolicyName) defer cleanESIndexTemplates(t, client, v8Client, prefix) err = createILMPolicy(client, ilmPolicyName) require.NoError(t, err) if prefix != "" { prefix += "-" } var expected, expectedWriteAliases, actualWriteAliases []string for _, index := range expectedIndices { expected = append(expected, prefix+index) } for _, alias := range writeAliases { expectedWriteAliases = append(expectedWriteAliases, prefix+alias) } // Run rollover with given EnvVars err = runEsRollover("init", envVars, adaptiveSampling) require.NoError(t, err) indices, err := client.IndexNames() require.NoError(t, err) // Get ILM Policy Attached settings, err := client.IndexGetSettings(expected...).FlatSettings(true).Do(context.Background()) require.NoError(t, err) // Check ILM Policy is attached and Get rollover alias attached for _, v := range settings { assert.Equal(t, ilmPolicyName, v.Settings["index.lifecycle.name"]) actualWriteAliases = append(actualWriteAliases, v.Settings["index.lifecycle.rollover_alias"].(string)) } // Check indices created assert.ElementsMatch(t, indices, expected) // Check rollover alias is write alias assert.ElementsMatch(t, actualWriteAliases, expectedWriteAliases) } func getVersion(client *elastic.Client) (uint, error) { pingResult, _, err := client.Ping(queryURL).Do(context.Background()) if err != nil { return 0, err } esVersion, err := strconv.Atoi(string(pingResult.Version.Number[0])) if err != nil { return 0, err } return uint(esVersion), nil } func createILMPolicy(client *elastic.Client, policyName string) error { _, err := client.XPackIlmPutLifecycle().Policy(policyName).BodyString(`{"policy": {"phases": {"hot": {"min_age": "0ms","actions": {"rollover": {"max_age": "1d"},"set_priority": {"priority": 100}}}}}}`).Do(context.Background()) return err } func cleanES(t *testing.T, client *elastic.Client, policyName string) { _, err := client.DeleteIndex("*").Do(context.Background()) require.NoError(t, err) esVersion, err := getVersion(client) require.NoError(t, err) if esVersion >= 7 { _, err = client.XPackIlmDeleteLifecycle().Policy(policyName).Do(context.Background()) if err != nil && !elastic.IsNotFound(err) { assert.Fail(t, "Not able to clean up ILM Policy") } } _, err = client.IndexDeleteTemplate("*").Do(context.Background()) require.NoError(t, err) } ================================================ FILE: internal/storage/integration/fixtures/grpc_plugin_conf.yaml ================================================ enable_streaming_writer: true ================================================ FILE: internal/storage/integration/fixtures/queries.json ================================================ [ { "Caption": "Tags in one spot - Tags", "Query": { "ServiceName": "query01-service", "OperationName": "", "Tags": { "sameplacetag1":"sameplacevalue", "sameplacetag2":123, "sameplacetag3":72.5, "sameplacetag4":true }, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 0, "DurationMax": 0, "NumTraces": 1000 }, "ExpectedFixtures": ["span_tags_trace"] }, { "Caption": "Tags in one spot - Logs", "Query": { "ServiceName": "query02-service", "OperationName": "", "Tags": { "sameplacetag1":"sameplacevalue", "sameplacetag2":123, "sameplacetag3":72.5, "sameplacetag4":true }, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 0, "DurationMax": 0, "NumTraces": 1000 }, "ExpectedFixtures": ["log_tags_trace"] }, { "Caption": "Tags in one spot - Process", "Query": { "ServiceName": "query03-service", "OperationName": "", "Tags": { "sameplacetag1":"sameplacevalue", "sameplacetag2":123, "sameplacetag3":72.5, "sameplacetag4":true }, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 0, "DurationMax": 0, "NumTraces": 1000 }, "ExpectedFixtures": ["process_tags_trace"] }, { "Caption": "Tags in different spots", "Query": { "ServiceName": "query04-service", "OperationName": "", "Tags": { "sameplacetag1":"sameplacevalue", "sameplacetag2":123, "sameplacetag3":72.5, "sameplacetag4":true }, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 0, "DurationMax": 0, "NumTraces": 1000 }, "ExpectedFixtures": ["multi_spot_tags_trace"] }, { "Caption": "Trace spans over multiple indices", "Query": { "ServiceName": "query05-service", "OperationName": "", "Tags": null, "StartTimeMin": "2017-01-26T00:00:31.639875Z", "StartTimeMax": "2017-01-26T00:07:31.639875Z", "DurationMin": 0, "DurationMax": 0, "NumTraces": 1000 }, "ExpectedFixtures": ["multi_index_trace"] }, { "Caption": "Operation name", "Query": { "ServiceName": "query06-service", "OperationName": "query06-operation", "Tags": null, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 0, "DurationMax": 0, "NumTraces": 1000 }, "ExpectedFixtures": ["opname_trace"] }, { "Caption": "Operation name + max Duration", "Query": { "ServiceName": "query07-service", "OperationName": "query07-operation", "Tags": null, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 0, "DurationMax": 2000, "NumTraces": 1000 }, "ExpectedFixtures": ["opname_maxdur_trace"] }, { "Caption": "Operation name + Duration range", "Query": { "ServiceName": "query08-service", "OperationName": "query08-operation", "Tags": null, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 4500, "DurationMax": 5500, "NumTraces": 1000 }, "ExpectedFixtures": ["opname_dur_trace"] }, { "Caption": "Duration range", "Query": { "ServiceName": "query09-service", "OperationName": "", "Tags": null, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 4500, "DurationMax": 5500, "NumTraces": 1000 }, "ExpectedFixtures": ["dur_trace"] }, { "Caption": "max Duration", "Query": { "ServiceName": "query10-service", "OperationName": "", "Tags": null, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 0, "DurationMax": 1000, "NumTraces": 1000 }, "ExpectedFixtures": ["max_dur_trace"] }, { "Caption": "default", "Query": { "ServiceName": "query11-service", "OperationName": "", "Tags": null, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 0, "DurationMax": 0, "NumTraces": 1000 }, "ExpectedFixtures": ["default"] }, { "Caption": "Tags + Operation name", "Query": { "ServiceName": "query12-service", "OperationName": "query12-operation", "Tags": { "sameplacetag1":"sameplacevalue", "sameplacetag2":123, "sameplacetag3":72.5, "sameplacetag4":true }, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 0, "DurationMax": 0, "NumTraces": 1000 }, "ExpectedFixtures": ["tags_opname_trace"] }, { "Caption": "Tags + Operation name + max Duration", "Query": { "ServiceName": "query13-service", "OperationName": "query13-operation", "Tags": { "sameplacetag1":"sameplacevalue", "sameplacetag2":123, "sameplacetag3":72.5, "sameplacetag4":true }, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 0, "DurationMax": 2000, "NumTraces": 1000 }, "ExpectedFixtures": ["tags_opname_maxdur_trace"] }, { "Caption": "Tags + Operation name + Duration range", "Query": { "ServiceName": "query14-service", "OperationName": "query14-operation", "Tags": { "sameplacetag1":"sameplacevalue", "sameplacetag2":123, "sameplacetag3":72.5, "sameplacetag4":true }, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 4500, "DurationMax": 5500, "NumTraces": 1000 }, "ExpectedFixtures": ["tags_opname_dur_trace"] }, { "Caption": "Tags + Duration range", "Query": { "ServiceName": "query15-service", "OperationName": "", "Tags": { "sameplacetag1":"sameplacevalue", "sameplacetag2":123, "sameplacetag3":72.5, "sameplacetag4":true }, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 4500, "DurationMax": 5500, "NumTraces": 1000 }, "ExpectedFixtures": ["tags_dur_trace"] }, { "Caption": "Tags + max Duration", "Query": { "ServiceName": "query16-service", "OperationName": "", "Tags": { "sameplacetag1":"sameplacevalue", "sameplacetag2":123, "sameplacetag3":72.5, "sameplacetag4":true }, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 0, "DurationMax": 1000, "NumTraces": 1000 }, "ExpectedFixtures": ["tags_maxdur_trace"] }, { "Caption": "Multi-spot Tags + Operation name", "Query": { "ServiceName": "query17-service", "OperationName": "query17-operation", "Tags": { "sameplacetag1":"sameplacevalue", "sameplacetag2":123, "sameplacetag3":72.5, "sameplacetag4":true }, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 0, "DurationMax": 0, "NumTraces": 1000 }, "ExpectedFixtures": ["multispottag_opname_trace"] }, { "Caption": "Multi-spot Tags + Operation name + max Duration", "Query": { "ServiceName": "query18-service", "OperationName": "query18-operation", "Tags": { "sameplacetag1":"sameplacevalue", "sameplacetag2":123, "sameplacetag3":72.5, "sameplacetag4":true }, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 0, "DurationMax": 2000, "NumTraces": 1000 }, "ExpectedFixtures": ["multispottag_opname_maxdur_trace"] }, { "Caption": "Multi-spot Tags + Operation name + Duration range", "Query": { "ServiceName": "query19-service", "OperationName": "query19-operation", "Tags": { "sameplacetag1":"sameplacevalue", "sameplacetag2":123, "sameplacetag3":72.5, "sameplacetag4":true }, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 4500, "DurationMax": 5500, "NumTraces": 1000 }, "ExpectedFixtures": ["multispottag_opname_dur_trace"] }, { "Caption": "Multi-spot Tags + Duration range", "Query": { "ServiceName": "query20-service", "OperationName": "", "Tags": { "sameplacetag1":"sameplacevalue", "sameplacetag2":123, "sameplacetag3":72.5, "sameplacetag4":true }, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 4500, "DurationMax": 5500, "NumTraces": 1000 }, "ExpectedFixtures": ["multispottag_dur_trace"] }, { "Caption": "Multi-spot Tags + max Duration", "Query": { "ServiceName": "query21-service", "OperationName": "", "Tags": { "sameplacetag1":"sameplacevalue", "sameplacetag2":123, "sameplacetag3":72.5, "sameplacetag4":true }, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 0, "DurationMax": 1000, "NumTraces": 1000 }, "ExpectedFixtures": ["multispottag_maxdur_trace"] }, { "Caption": "Multiple Traces", "Query": { "ServiceName": "query22-service", "OperationName": "", "Tags": null, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 0, "DurationMax": 0, "NumTraces": 1000 }, "ExpectedFixtures": ["multiple1_trace", "multiple2_trace", "multiple3_trace"] }, { "Caption": "Scope Name and Version", "Query": { "ServiceName": "query23-service", "OperationName": "", "Tags": { "sameplacetag1":"sameplacevalue", "sameplacetag2":123, "sameplacetag3":72.5, "sameplacetag4":true }, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 0, "DurationMax": 0, "NumTraces": 1000 }, "ExpectedFixtures": ["scope_name_version_trace"] } ] ================================================ FILE: internal/storage/integration/fixtures/queries_es.json ================================================ [ { "Caption": "Tag escaped operator + Operation name + max Duration", "Query": { "ServiceName": "query23-service", "OperationName": "query23-operation", "Tags": { "sameplacetag1":"same\\*" }, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 0, "DurationMax": 1000, "NumTraces": 1000 }, "ExpectedFixtures": ["tags_escaped_operator_trace_1"] }, { "Caption": "Tag wildcard regex", "Query": { "ServiceName": "query24-service", "OperationName": "", "Tags": { "sameplacetag1":"same.*" }, "StartTimeMin": "2017-01-26T15:46:31.639875Z", "StartTimeMax": "2017-01-26T17:46:31.639875Z", "DurationMin": 0, "DurationMax": 0, "NumTraces": 1000 }, "ExpectedFixtures": ["tags_wildcard_regex_1", "tags_wildcard_regex_2"] } ] ================================================ FILE: internal/storage/integration/fixtures/traces/default.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query11-service" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000011", "spanId": "0000000000000003", "name": "query11-operation", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639975000", "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/dur_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query09-service" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000009", "spanId": "0000000000000003", "name": "placeholder", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639880000", "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/example_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "example-service-2" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000011", "spanId": "0000000000000004", "name": "example-operation-2", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639975000", "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] }, { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "example-service-3" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000011", "spanId": "0000000000000005", "name": "example-operation-1", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639975000", "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] }, { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "example-service-1" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000011", "spanId": "0000000000000003", "name": "example-operation-1", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639975000", "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} }, { "traceId": "00000000000000000000000000000011", "spanId": "0000000000000006", "name": "example-operation-3", "kind": 2, "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639975000", "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} }, { "traceId": "00000000000000000000000000000011", "spanId": "0000000000000007", "name": "example-operation-4", "kind": 3, "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639975000", "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/log_tags_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query02-service" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000002", "spanId": "0000000000000001", "name": "placeholder", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639880000", "events": [ { "timeUnixNano": "1485449191639875000", "attributes": [ { "key": "sameplacetag1", "value": { "stringValue": "sameplacevalue" } }, { "key": "sameplacetag2", "value": { "intValue": "123" } }, { "key": "sameplacetag4", "value": { "boolValue": true } }, { "key": "sameplacetag3", "value": { "doubleValue": 72.5 } }, { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } } ] }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/max_dur_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query10-service" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000010", "spanId": "0000000000000002", "name": "placeholder", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639876000", "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/multi_index_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query05-service" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000005", "spanId": "0000000000000001", "name": "operation-list-test2", "startTimeUnixNano": "1485389011639875000", "endTimeUnixNano": "1485389011639880000", "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} }, { "traceId": "00000000000000000000000000000005", "spanId": "0000000000000002", "name": "operation-list-test3", "startTimeUnixNano": "1485388591639875000", "endTimeUnixNano": "1485388591639880000", "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/multi_spot_tags_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query04-service" } }, { "key": "sameplacetag1", "value": { "stringValue": "sameplacevalue" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000004", "spanId": "0000000000000001", "name": "placeholder", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639880000", "attributes": [ { "key": "sameplacetag4", "value": { "boolValue": true } }, { "key": "sameplacetag3", "value": { "doubleValue": 72.5 } } ], "events": [ { "timeUnixNano": "1485449191639875000", "attributes": [ { "key": "sameplacetag2", "value": { "intValue": "123" } } ] }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/multiple1_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query22-service" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000221", "spanId": "0000000000000003", "name": "query22-operation", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639975000", "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/multiple2_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query22-service" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000222", "spanId": "0000000000000003", "name": "query22-operation", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639975000", "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/multiple3_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query22-service" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000223", "spanId": "0000000000000003", "name": "query22-operation", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639975000", "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/multispottag_dur_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query20-service" } }, { "key": "sameplacetag3", "value": { "doubleValue": 72.5 } }, { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000020", "spanId": "0000000000000003", "name": "placeholder", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639880000", "attributes": [ { "key": "sameplacetag2", "value": { "intValue": "123" } }, { "key": "sameplacetag4", "value": { "boolValue": true } } ], "events": [ { "timeUnixNano": "1485449191639875000", "attributes": [ { "key": "sameplacetag1", "value": { "stringValue": "sameplacevalue" } } ] }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/multispottag_maxdur_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query21-service" } }, { "key": "sameplacetag4", "value": { "boolValue": true } }, { "key": "sameplacetag3", "value": { "doubleValue": 72.5 } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000021", "spanId": "0000000000000005", "name": "placeholder", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639876000", "attributes": [ { "key": "sameplacetag1", "value": { "stringValue": "sameplacevalue" } } ], "events": [ { "timeUnixNano": "1485449191639875000", "attributes": [ { "key": "sameplacetag2", "value": { "intValue": "123" } } ] }, { "timeUnixNano": "1485449191639875000", "attributes": [ { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } } ] } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/multispottag_opname_dur_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query19-service" } }, { "key": "sameplacetag4", "value": { "boolValue": true } }, { "key": "sameplacetag3", "value": { "doubleValue": 72.5 } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000019", "spanId": "0000000000000005", "name": "query19-operation", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639880000", "attributes": [ { "key": "sameplacetag1", "value": { "stringValue": "sameplacevalue" } } ], "events": [ { "timeUnixNano": "1485449191639875000", "attributes": [ { "key": "sameplacetag2", "value": { "intValue": "123" } } ] }, { "timeUnixNano": "1485449191639875000", "attributes": [ { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } } ] } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/multispottag_opname_maxdur_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query18-service" } }, { "key": "sameplacetag3", "value": { "doubleValue": 72.5 } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000018", "spanId": "0000000000000004", "name": "query18-operation", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639876000", "attributes": [ { "key": "sameplacetag1", "value": { "stringValue": "sameplacevalue" } } ], "events": [ { "timeUnixNano": "1485449191639875000", "attributes": [ { "key": "sameplacetag2", "value": { "intValue": "123" } } ] }, { "timeUnixNano": "1485449191639875000", "attributes": [ { "key": "sameplacetag4", "value": { "boolValue": true } }, { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } } ] } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/multispottag_opname_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query17-service" } }, { "key": "sameplacetag1", "value": { "stringValue": "sameplacevalue" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000017", "spanId": "0000000000000004", "name": "query17-operation", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639880000", "attributes": [ { "key": "sameplacetag3", "value": { "doubleValue": 72.5 } } ], "events": [ { "timeUnixNano": "1485449191639875000", "attributes": [ { "key": "sameplacetag2", "value": { "intValue": "123" } } ] }, { "timeUnixNano": "1485449191639875000", "attributes": [ { "key": "sameplacetag4", "value": { "boolValue": true } } ] } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/opname_dur_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query08-service" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000008", "spanId": "0000000000000002", "name": "query08-operation", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639880000", "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/opname_maxdur_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query07-service" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000007", "spanId": "0000000000000003", "parentSpanId": "0000000000000002", "name": "query07-operation", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639876000", "status": {} }, { "traceId": "00000000000000000000000000000007", "spanId": "0000000000000002", "name": "query07-operation", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639877000", "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/opname_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query06-service" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000006", "spanId": "0000000000000001", "name": "query06-operation", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639880000", "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/process_tags_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query03-service" } }, { "key": "sameplacetag1", "value": { "stringValue": "sameplacevalue" } }, { "key": "sameplacetag2", "value": { "intValue": "123" } }, { "key": "sameplacetag4", "value": { "boolValue": true } }, { "key": "sameplacetag3", "value": { "doubleValue": 72.5 } }, { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000003", "spanId": "0000000000000001", "name": "placeholder", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639880000", "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/scope_name_version_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query23-service" } } ] }, "scopeSpans": [ { "scope": { "name": "testing-library", "version": "1.1.1" }, "spans": [ { "traceId": "00000000000000000000000000000224", "spanId": "0000000000000002", "name": "placeholder", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639876000", "attributes": [ { "key": "sameplacetag1", "value": { "stringValue": "sameplacevalue" } }, { "key": "sameplacetag2", "value": { "intValue": "123" } }, { "key": "sameplacetag4", "value": { "boolValue": true } }, { "key": "sameplacetag3", "value": { "doubleValue": 72.5 } }, { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } } ], "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/span_tags_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query01-service" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000001", "spanId": "0000000000000002", "name": "some-operation", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639882000", "attributes": [ { "key": "sameplacetag1", "value": { "stringValue": "sameplacevalue" } }, { "key": "sameplacetag2", "value": { "intValue": "123" } }, { "key": "sameplacetag4", "value": { "boolValue": true } }, { "key": "sameplacetag3", "value": { "doubleValue": 72.5 } }, { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/tags_dur_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query15-service" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000015", "spanId": "0000000000000004", "name": "placeholder", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639880000", "attributes": [ { "key": "sameplacetag1", "value": { "stringValue": "sameplacevalue" } }, { "key": "sameplacetag2", "value": { "intValue": "123" } }, { "key": "sameplacetag4", "value": { "boolValue": true } }, { "key": "sameplacetag3", "value": { "doubleValue": 72.5 } }, { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } } ], "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/tags_escaped_operator_trace_1.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query23-service" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000512", "spanId": "0000000000000005", "name": "query23-operation", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639876000", "attributes": [ { "key": "sameplacetag1", "value": { "stringValue": "same*" } } ], "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/tags_escaped_operator_trace_2.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query23-service" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000005912", "spanId": "0000000000000005", "name": "query23-operation", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639876000", "attributes": [ { "key": "sameplacetag1", "value": { "stringValue": "sameplacedifferentvalue" } } ], "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/tags_maxdur_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query16-service" } }, { "key": "sameplacetag1", "value": { "stringValue": "sameplacevalue" } }, { "key": "sameplacetag2", "value": { "intValue": "123" } }, { "key": "sameplacetag4", "value": { "boolValue": true } }, { "key": "sameplacetag3", "value": { "doubleValue": 72.5 } }, { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000016", "spanId": "0000000000000005", "name": "query16-operation", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639876000", "events": [ { "timeUnixNano": "1485449191639875000" }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/tags_opname_dur_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query14-service" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000014", "spanId": "0000000000000003", "name": "query14-operation", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639880000", "events": [ { "timeUnixNano": "1485449191639875000", "attributes": [ { "key": "sameplacetag1", "value": { "stringValue": "sameplacevalue" } }, { "key": "sameplacetag2", "value": { "intValue": "123" } }, { "key": "sameplacetag4", "value": { "boolValue": true } }, { "key": "sameplacetag3", "value": { "doubleValue": 72.5 } }, { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } } ] }, { "timeUnixNano": "1485449191639875000" } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/tags_opname_maxdur_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query13-service" } }, { "key": "sameplacetag1", "value": { "stringValue": "sameplacevalue" } }, { "key": "sameplacetag2", "value": { "intValue": "123" } }, { "key": "sameplacetag4", "value": { "boolValue": true } }, { "key": "sameplacetag3", "value": { "doubleValue": 72.5 } }, { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000013", "spanId": "0000000000000007", "name": "query13-operation", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639876000", "attributes": [ { "key": "tag1", "value": { "stringValue": "value1" } } ], "events": [ { "timeUnixNano": "1485449191639875000", "attributes": [ { "key": "tag3", "value": { "stringValue": "value3" } } ] }, { "timeUnixNano": "1485449191639875000", "attributes": [ { "key": "something", "value": { "stringValue": "blah" } } ] } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/tags_opname_trace.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query12-service" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000012", "spanId": "0000000000000004", "name": "query12-operation", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639877000", "attributes": [ { "key": "sameplacetag1", "value": { "stringValue": "sameplacevalue" } }, { "key": "sameplacetag2", "value": { "intValue": "123" } }, { "key": "sameplacetag4", "value": { "boolValue": true } }, { "key": "sameplacetag3", "value": { "doubleValue": 72.5 } }, { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } } ], "links": [ { "traceId": "000000000000000000000000000000ff", "spanId": "00000000000000ff", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "child_of" } } ] }, { "traceId": "00000000000000000000000000000001", "spanId": "0000000000000002", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "child_of" } } ] }, { "traceId": "00000000000000000000000000000001", "spanId": "0000000000000002", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "follows_from" } } ] } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/tags_wildcard_regex_1.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query24-service" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000000a12", "spanId": "0000000000000004", "name": "query24-operation", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639877000", "attributes": [ { "key": "sameplacetag1", "value": { "stringValue": "sameplacevalue1" } } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/fixtures/traces/tags_wildcard_regex_2.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "query24-service" } } ] }, "scopeSpans": [ { "scope": {}, "spans": [ { "traceId": "00000000000000000000000000001212", "spanId": "0000000000000004", "name": "query24-operation", "startTimeUnixNano": "1485449191639875000", "endTimeUnixNano": "1485449191639877000", "attributes": [ { "key": "sameplacetag1", "value": { "stringValue": "sameplacevalue2" } } ], "status": {} } ] } ] } ] } ================================================ FILE: internal/storage/integration/grpc_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2018 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package integration import ( "context" "testing" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/config/configgrpc" "go.opentelemetry.io/collector/config/configtls" "github.com/jaegertracing/jaeger/internal/storage/v2/grpc" "github.com/jaegertracing/jaeger/internal/telemetry" "github.com/jaegertracing/jaeger/internal/testutils" "github.com/jaegertracing/jaeger/ports" ) type GRPCStorageIntegrationTestSuite struct { StorageIntegration flags []string factory *grpc.Factory remoteStorage *RemoteMemoryStorage } func (s *GRPCStorageIntegrationTestSuite) initialize(t *testing.T) { s.remoteStorage = StartNewRemoteMemoryStorage(t, ports.RemoteStorageGRPC) f, err := grpc.NewFactory( context.Background(), grpc.Config{ ClientConfig: configgrpc.ClientConfig{ Endpoint: "localhost:17271", TLS: configtls.ClientConfig{ Insecure: true, }, }, }, telemetry.NoopSettings(), ) require.NoError(t, err) s.factory = f s.TraceWriter, err = f.CreateTraceWriter() require.NoError(t, err) s.TraceReader, err = f.CreateTraceReader() require.NoError(t, err) // TODO DependencyWriter is not implemented in grpc store s.CleanUp = s.cleanUp } func (s *GRPCStorageIntegrationTestSuite) close(t *testing.T) { require.NoError(t, s.factory.Close()) s.remoteStorage.Close(t) } func (s *GRPCStorageIntegrationTestSuite) cleanUp(t *testing.T) { s.close(t) s.initialize(t) } func TestGRPCRemoteStorage(t *testing.T) { SkipUnlessEnv(t, "grpc") t.Cleanup(func() { testutils.VerifyGoLeaksOnce(t) }) s := &GRPCStorageIntegrationTestSuite{ flags: []string{ "--grpc-storage.server=localhost:17271", "--grpc-storage.tls.enabled=false", }, } s.initialize(t) defer s.close(t) s.RunAll(t) } ================================================ FILE: internal/storage/integration/integration.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package integration import ( "context" "embed" "encoding/binary" "encoding/json" "fmt" "os" "regexp" "slices" "sort" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/jiter" "github.com/jaegertracing/jaeger/internal/jptrace" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore" samplemodel "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore/model" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" ) //go:embed fixtures var fixtures embed.FS // StorageIntegration holds components for storage integration test. // The intended usage is as follows: // - a specific storage implementation declares its own test functions // - in those functions it instantiates and populates this struct // - it then calls RunAll. // // Some implementations may declare multiple tests, with different settings, // and RunAll() under different conditions. type StorageIntegration struct { TraceWriter tracestore.Writer TraceReader tracestore.Reader DependencyWriter depstore.Writer DependencyReader depstore.Reader SamplingStore samplingstore.Store Fixtures []*QueryFixtures // TODO: remove this after all storage backends return spanKind from GetOperations GetOperationsMissingSpanKind bool // TODO: remove this after all storage backends return Source column from GetDependencies GetDependenciesReturnsSource bool // List of tests which has to be skipped, it can be regex too. SkipList []string // CleanUp() should ensure that the storage backend is clean before another test. // called either before or after each test, and should be idempotent CleanUp func(t *testing.T) } // === SpanStore Integration Tests === type Query struct { ServiceName string OperationName string Tags map[string]any StartTimeMin time.Time StartTimeMax time.Time DurationMin time.Duration DurationMax time.Duration NumTraces int } func (q *Query) ToTraceQueryParams(t *testing.T) *tracestore.TraceQueryParams { attributes := pcommon.NewMap() for k, v := range q.Tags { switch v := v.(type) { case string: attributes.PutStr(k, v) case int: attributes.PutInt(k, int64(v)) case float64: attributes.PutDouble(k, v) case bool: attributes.PutBool(k, v) default: t.Fatalf("Unsupported tag value type: %T", v) } } return &tracestore.TraceQueryParams{ ServiceName: q.ServiceName, OperationName: q.OperationName, Attributes: attributes, StartTimeMin: q.StartTimeMin, StartTimeMax: q.StartTimeMax, DurationMin: q.DurationMin, DurationMax: q.DurationMax, SearchDepth: q.NumTraces, } } // QueryFixtures and TraceFixtures are under ./fixtures/queries.json and ./fixtures/traces/*.json respectively. // Each query fixture includes: // - Caption: describes the query we are testing // - Query: the query we are testing // - ExpectedFixture: the trace fixture that we want back from these queries. // Queries are not necessarily numbered, but since each query requires a service name, // the service name is formatted "query##-service". type QueryFixtures struct { Caption string Query *Query ExpectedFixtures []string } func (s *StorageIntegration) cleanUp(t *testing.T) { require.NotNil(t, s.CleanUp, "CleanUp function must be provided") s.CleanUp(t) } func SkipUnlessEnv(t *testing.T, storage ...string) { env := os.Getenv("STORAGE") if slices.Contains(storage, env) { return } t.Skipf("This test requires environment variable STORAGE=%s", strings.Join(storage, "|")) } var CassandraSkippedTests = []string{ "Tags_+_Operation_name_+_Duration_range", "Tags_+_Duration_range", "Tags_+_Operation_name_+_max_Duration", "Tags_+_max_Duration", "Operation_name_+_Duration_range", "Duration_range", "max_Duration", "Multiple_Traces", } func (s *StorageIntegration) skipIfNeeded(t *testing.T) { for _, pat := range s.SkipList { escapedPat := regexp.QuoteMeta(pat) ok, err := regexp.MatchString(escapedPat, t.Name()) require.NoError(t, err) if ok { t.Skip() return } } } func (*StorageIntegration) waitForCondition(t *testing.T, predicate func(t *testing.T) bool) bool { const iterations = 100 // Will wait at most 100 seconds. for i := range iterations { if predicate(t) { return true } t.Logf("Waiting for storage backend to update documents, iteration %d out of %d", i+1, iterations) time.Sleep(time.Second) } return predicate(t) } func (s *StorageIntegration) testGetServices(t *testing.T) { s.skipIfNeeded(t) defer s.cleanUp(t) expected := []string{"example-service-1", "example-service-2", "example-service-3"} s.loadParseAndWriteExampleTrace(t) var actual []string found := s.waitForCondition(t, func(t *testing.T) bool { var err error actual, err = s.TraceReader.GetServices(context.Background()) if err != nil { t.Log(err) return false } sort.Strings(actual) t.Logf("Retrieved services: %v", actual) if len(actual) > len(expected) { // If the storage backend returns more services than expected, let's log traces for those t.Log("🛑 Found unexpected services!") for _, service := range actual { iterTraces := s.TraceReader.FindTraces(context.Background(), tracestore.TraceQueryParams{ ServiceName: service, StartTimeMin: time.Now().Add(-2 * time.Hour), StartTimeMax: time.Now(), }) for traces, err := range iterTraces { if err != nil { t.Log(err) continue } for _, trace := range traces { for _, span := range jptrace.SpanIter(trace) { t.Logf("span: Service: %s, TraceID: %s, Operation: %s", service, span.TraceID(), span.Name()) } } } } } return assert.ObjectsAreEqualValues(expected, actual) }) if !assert.True(t, found) { t.Log("\t Expected:", expected) t.Log("\t Actual :", actual) } } func (s *StorageIntegration) helperTestGetTrace( t *testing.T, traceSize int, duplicateCount int, testName string, validator func(t *testing.T, actual ptrace.Traces), ) { s.skipIfNeeded(t) defer s.cleanUp(t) t.Logf("Testing %s...", testName) expected := s.writeLargeTraceWithDuplicateSpanIds(t, traceSize, duplicateCount) expectedTraceID := expected.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0).TraceID() actual := ptrace.NewTraces() found := s.waitForCondition(t, func(_ *testing.T) bool { iterTraces := s.TraceReader.GetTraces(context.Background(), tracestore.GetTraceParams{TraceID: expectedTraceID}) traces, err := jiter.CollectWithErrors(jptrace.AggregateTraces(iterTraces)) if err != nil { t.Logf("Error loading trace: %v", err) return false } if len(traces) == 0 { return false } require.Len(t, traces, 1) actual = traces[0] return actual.SpanCount() >= expected.SpanCount() }) t.Logf("%-23s Loaded trace, expected=%d, actual=%d", time.Now().Format("2006-01-02 15:04:05.999"), expected.SpanCount(), actual.SpanCount()) if !assert.True(t, found, "error loading trace, expected=%d, actual=%d", expected.SpanCount(), actual.SpanCount()) { CompareTraces(t, expected, actual) return } if validator != nil { validator(t, actual) } } func (s *StorageIntegration) testGetLargeTrace(t *testing.T) { s.helperTestGetTrace(t, 10008, 0, "Large Trace over 10K without duplicates", nil) } func (s *StorageIntegration) testGetTraceWithDuplicates(t *testing.T) { validator := func(t *testing.T, actual ptrace.Traces) { duplicateCount := 0 seenIDs := make(map[pcommon.SpanID]int) for _, span := range jptrace.SpanIter(actual) { seenIDs[span.SpanID()]++ if seenIDs[span.SpanID()] > 1 { duplicateCount++ } } assert.Positive(t, duplicateCount, "Duplicate SpanIDs should be present in the trace") } s.helperTestGetTrace(t, 200, 20, "Trace with duplicate span IDs", validator) } func (s *StorageIntegration) testGetOperations(t *testing.T) { s.skipIfNeeded(t) defer s.cleanUp(t) var expected []tracestore.Operation if s.GetOperationsMissingSpanKind { expected = []tracestore.Operation{ {Name: "example-operation-1"}, {Name: "example-operation-3"}, {Name: "example-operation-4"}, } } else { expected = []tracestore.Operation{ {Name: "example-operation-1", SpanKind: ""}, {Name: "example-operation-3", SpanKind: "server"}, {Name: "example-operation-4", SpanKind: "client"}, } } s.loadParseAndWriteExampleTrace(t) var actual []tracestore.Operation found := s.waitForCondition(t, func(t *testing.T) bool { var err error actual, err = s.TraceReader.GetOperations(context.Background(), tracestore.OperationQueryParams{ServiceName: "example-service-1"}) if err != nil { t.Log(err) return false } sort.Slice(actual, func(i, j int) bool { return actual[i].Name < actual[j].Name }) t.Logf("Retrieved operations: %v", actual) return assert.ObjectsAreEqualValues(expected, actual) }) if !assert.True(t, found) { t.Log("\t Expected:", expected) t.Log("\t Actual :", actual) } } func (s *StorageIntegration) testGetTrace(t *testing.T) { s.skipIfNeeded(t) defer s.cleanUp(t) expected := s.loadParseAndWriteExampleTrace(t) expectedTraceID := expected.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0).TraceID() actual := ptrace.Traces{} // no spans found := s.waitForCondition(t, func(t *testing.T) bool { iterTraces := s.TraceReader.GetTraces(context.Background(), tracestore.GetTraceParams{TraceID: expectedTraceID}) traces, err := jiter.CollectWithErrors(jptrace.AggregateTraces(iterTraces)) if err != nil { t.Log(err) return false } if len(traces) == 0 { return false } require.Len(t, traces, 1) actual = traces[0] return actual.SpanCount() >= expected.SpanCount() }) if !assert.True(t, found) { CompareTraces(t, expected, actual) } t.Run("NotFound error", func(t *testing.T) { fakeTraceID := pcommon.TraceID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} iterTraces := s.TraceReader.GetTraces(context.Background(), tracestore.GetTraceParams{TraceID: fakeTraceID}) traces, err := jiter.CollectWithErrors(jptrace.AggregateTraces(iterTraces)) require.NoError(t, err) // v2 TraceReader no longer returns an error for not found assert.Empty(t, traces) }) } func (s *StorageIntegration) testFindTraces(t *testing.T) { s.skipIfNeeded(t) defer s.cleanUp(t) // Note: all cases include ServiceName + StartTime range s.Fixtures = append(s.Fixtures, LoadAndParseQueryTestCases(t, "fixtures/queries.json")...) // Each query test case only specifies matching traces, but does not provide counterexamples. // To improve coverage we get all possible traces and store all of them before running queries. allTraceFixtures := make(map[string]ptrace.Traces) expectedTracesPerTestCase := make([][]ptrace.Traces, 0, len(s.Fixtures)) for _, queryTestCase := range s.Fixtures { var expected []ptrace.Traces for _, traceFixture := range queryTestCase.ExpectedFixtures { trace, ok := allTraceFixtures[traceFixture] if !ok { trace = s.getTraceFixture(t, traceFixture) s.writeTrace(t, trace) allTraceFixtures[traceFixture] = trace } expected = append(expected, trace) } expectedTracesPerTestCase = append(expectedTracesPerTestCase, expected) } for i, queryTestCase := range s.Fixtures { t.Run(queryTestCase.Caption, func(t *testing.T) { s.skipIfNeeded(t) expected := expectedTracesPerTestCase[i] actual := s.findTracesByQuery(t, queryTestCase.Query.ToTraceQueryParams(t), expected) CompareTraceSlices(t, expected, actual) }) } } func (s *StorageIntegration) findTracesByQuery(t *testing.T, query *tracestore.TraceQueryParams, expected []ptrace.Traces) []ptrace.Traces { var traces []ptrace.Traces found := s.waitForCondition(t, func(t *testing.T) bool { iterTraces := s.TraceReader.FindTraces(context.Background(), *query) var err error traces, err = jiter.CollectWithErrors(jptrace.AggregateTraces(iterTraces)) if err != nil { t.Log(err) return false } if len(expected) != len(traces) { t.Logf("Expecting certain number of traces: expected: %d, actual: %d", len(expected), len(traces)) return false } if spanCount(expected) != spanCount(traces) { t.Logf("Expecting certain number of spans: expected: %d, actual: %d", spanCount(expected), spanCount(traces)) return false } return true }) require.True(t, found) return traces } func (s *StorageIntegration) writeTrace(t *testing.T, trace ptrace.Traces) { t.Logf("%-23s Writing trace with %d spans", time.Now().Format("2006-01-02 15:04:05.999"), trace.SpanCount()) ctx, cx := context.WithTimeout(context.Background(), 5*time.Minute) defer cx() err := s.TraceWriter.WriteTraces(ctx, trace) require.NoError(t, err, "Not expecting error when writing trace to storage") t.Logf("%-23s Finished writing trace with %d spans", time.Now().Format("2006-01-02 15:04:05.999"), trace.SpanCount()) } func (s *StorageIntegration) loadParseAndWriteExampleTrace(t *testing.T) ptrace.Traces { trace := s.getTraceFixture(t, "example_trace") s.writeTrace(t, trace) return trace } func (s *StorageIntegration) writeLargeTraceWithDuplicateSpanIds( t *testing.T, totalCount int, dupFreq int, ) ptrace.Traces { trace := s.getTraceFixture(t, "example_trace") repeatedResourceSpan := trace.ResourceSpans().At(0) repeatedScopeSpan := repeatedResourceSpan.ScopeSpans().At(0) repeatedSpans := repeatedScopeSpan.Spans() repeatedSpan := repeatedSpans.At(0) newResourceSpan := ptrace.NewResourceSpans() newScopeSpan := newResourceSpan.ScopeSpans().AppendEmpty() repeatedResourceSpan.Resource().CopyTo(newResourceSpan.Resource()) repeatedScopeSpan.Scope().CopyTo(newScopeSpan.Scope()) newSpans := newScopeSpan.Spans() newSpans.EnsureCapacity(totalCount) for i := range totalCount { newSpan := ptrace.NewSpan() repeatedSpan.CopyTo(newSpan) switch { case dupFreq > 0 && i > 0 && i%dupFreq == 0: newSpan.SetSpanID(repeatedSpan.SpanID()) default: var spanId [8]byte binary.BigEndian.PutUint64(spanId[:], uint64(i+1)) newSpan.SetSpanID(spanId) } newSpan.SetStartTimestamp(pcommon.NewTimestampFromTime(newSpan.StartTimestamp().AsTime().Add(time.Second * time.Duration(i+1)))) newSpan.SetEndTimestamp(pcommon.NewTimestampFromTime(newSpan.EndTimestamp().AsTime().Add(time.Second * time.Duration(i+1)))) newSpan.CopyTo(newSpans.AppendEmpty()) } newTrace := ptrace.NewTraces() newResourceSpan.CopyTo(newTrace.ResourceSpans().AppendEmpty()) s.writeTrace(t, newTrace) return newTrace } func (*StorageIntegration) getTraceFixture(t *testing.T, fixture string) ptrace.Traces { fileName := fmt.Sprintf("fixtures/traces/%s.json", fixture) return loadOTLPTrace(t, fileName) } func loadOTLPTrace(t *testing.T, fileName string) ptrace.Traces { // #nosec inStr, err := fixtures.ReadFile(fileName) require.NoError(t, err, "Not expecting error when loading fixture %s", fileName) unmarshaller := ptrace.JSONUnmarshaler{} td, err := unmarshaller.UnmarshalTraces(inStr) require.NoError(t, err, "Not expecting error when unmarshaling fixture %s", fileName) correctTimeForTrace(td) return td } // LoadAndParseQueryTestCases loads and parses query test cases func LoadAndParseQueryTestCases(t *testing.T, queriesFile string) []*QueryFixtures { var queries []*QueryFixtures loadAndParseJSON(t, queriesFile, &queries) return queries } func loadAndParseJSON(t *testing.T, path string, object any) { // #nosec inStr, err := fixtures.ReadFile(path) require.NoError(t, err, "Not expecting error when loading fixture %s", path) err = json.Unmarshal(correctTime(inStr), object) require.NoError(t, err, "Not expecting error when unmarshaling fixture %s", path) } // required, because we want to only query on recent traces, so we replace all the dates with recent dates. func correctTime(jsonData []byte) []byte { jsonString := string(jsonData) now := time.Now().UTC() yesterday := now.AddDate(0, 0, -1).Format("2006-01-02") twoDaysAgo := now.AddDate(0, 0, -2).Format("2006-01-02") retString := strings.ReplaceAll(jsonString, "2017-01-26", yesterday) retString = strings.ReplaceAll(retString, "2017-01-25", twoDaysAgo) return []byte(retString) } func correctTimeForTrace(td ptrace.Traces) { now := time.Now().UTC() normalizer := newDateOffsetNormalizer(now) normalizer.normalizeTrace(td) } func spanCount(traces []ptrace.Traces) int { var count int for _, trace := range traces { count += trace.SpanCount() } return count } // === DependencyStore Integration Tests === func (s *StorageIntegration) testGetDependencies(t *testing.T) { if s.DependencyReader == nil || s.DependencyWriter == nil { t.Skip("Skipping GetDependencies test because dependency reader or writer is nil") return } s.skipIfNeeded(t) defer s.cleanUp(t) source := model.JaegerDependencyLinkSource if !s.GetDependenciesReturnsSource { source = "" } expected := []model.DependencyLink{ { Parent: "hello", Child: "world", CallCount: uint64(1), Source: source, }, { Parent: "world", Child: "hello", CallCount: uint64(3), Source: source, }, } startTime := time.Now() require.NoError(t, s.DependencyWriter.WriteDependencies(startTime, expected)) var actual []model.DependencyLink found := s.waitForCondition(t, func(t *testing.T) bool { var err error actual, err = s.DependencyReader.GetDependencies( context.Background(), depstore.QueryParameters{ StartTime: startTime, EndTime: startTime.Add(time.Minute * 5), }, ) if err != nil { t.Log(err) return false } sort.Slice(actual, func(i, j int) bool { return actual[i].Parent < actual[j].Parent }) return assert.ObjectsAreEqualValues(expected, actual) }) if !assert.True(t, found) { t.Log("\t Expected:", expected) t.Log("\t Actual :", actual) } } // === Sampling Store Integration Tests === func (s *StorageIntegration) testGetThroughput(t *testing.T) { s.skipIfNeeded(t) if s.SamplingStore == nil { t.Skip("Skipping GetThroughput test because sampling store is nil") return } defer s.cleanUp(t) start := time.Now() s.insertThroughput(t) expected := 2 var actual []*samplemodel.Throughput _ = s.waitForCondition(t, func(t *testing.T) bool { var err error actual, err = s.SamplingStore.GetThroughput(start, start.Add(time.Second*time.Duration(10))) if err != nil { t.Log(err) return false } return assert.ObjectsAreEqualValues(expected, len(actual)) }) assert.Len(t, actual, expected) } func (s *StorageIntegration) testGetLatestProbability(t *testing.T) { s.skipIfNeeded(t) if s.SamplingStore == nil { t.Skip("Skipping GetLatestProbability test because sampling store is nil") return } defer s.cleanUp(t) s.SamplingStore.InsertProbabilitiesAndQPS("newhostname1", samplemodel.ServiceOperationProbabilities{"new-srv3": {"op": 0.123}}, samplemodel.ServiceOperationQPS{"new-srv2": {"op": 11}}) s.SamplingStore.InsertProbabilitiesAndQPS("dell11eg843d", samplemodel.ServiceOperationProbabilities{"new-srv": {"op": 0.1}}, samplemodel.ServiceOperationQPS{"new-srv": {"op": 4}}) expected := samplemodel.ServiceOperationProbabilities{"new-srv": {"op": 0.1}} var actual samplemodel.ServiceOperationProbabilities found := s.waitForCondition(t, func(t *testing.T) bool { var err error actual, err = s.SamplingStore.GetLatestProbabilities() if err != nil { t.Log(err) return false } return assert.ObjectsAreEqualValues(expected, actual) }) if !assert.True(t, found) { t.Log("\t Expected:", expected) t.Log("\t Actual :", actual) } } func (s *StorageIntegration) insertThroughput(t *testing.T) { throughputs := []*samplemodel.Throughput{ {Service: "my-svc", Operation: "op"}, {Service: "our-svc", Operation: "op2"}, } err := s.SamplingStore.InsertThroughput(throughputs) require.NoError(t, err) } // RunAll runs all integration tests func (s *StorageIntegration) RunAll(t *testing.T) { s.RunSpanStoreTests(t) t.Run("GetDependencies", s.testGetDependencies) t.Run("GetThroughput", s.testGetThroughput) t.Run("GetLatestProbability", s.testGetLatestProbability) } // RunSpanStoreTests runs only span related integration tests func (s *StorageIntegration) RunSpanStoreTests(t *testing.T) { t.Run("GetServices", s.testGetServices) t.Run("GetOperations", s.testGetOperations) t.Run("GetTrace", s.testGetTrace) t.Run("GetLargeTrace", s.testGetLargeTrace) t.Run("GetTraceWithDuplicateSpans", s.testGetTraceWithDuplicates) t.Run("FindTraces", s.testFindTraces) } ================================================ FILE: internal/storage/integration/memstore_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2018 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package integration import ( "testing" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/storage/v2/memory" "github.com/jaegertracing/jaeger/internal/telemetry" "github.com/jaegertracing/jaeger/internal/testutils" ) type MemStorageIntegrationTestSuite struct { StorageIntegration logger *zap.Logger } func (s *MemStorageIntegrationTestSuite) initialize(t *testing.T) { s.logger, _ = testutils.NewLogger() telset := telemetry.NoopSettings() telset.Logger = s.logger f, err := memory.NewFactory(memory.Configuration{MaxTraces: 10000}, telset) require.NoError(t, err) traceReader, err := f.CreateTraceReader() require.NoError(t, err) traceWriter, err := f.CreateTraceWriter() require.NoError(t, err) s.SamplingStore = memory.NewSamplingStore(2) s.TraceReader = traceReader s.TraceWriter = traceWriter // TODO DependencyWriter is not implemented in memory store s.CleanUp = s.initialize } func TestMemoryStorage(t *testing.T) { SkipUnlessEnv(t, "memory") t.Cleanup(func() { testutils.VerifyGoLeaksOnce(t) }) s := &MemStorageIntegrationTestSuite{} s.initialize(t) s.RunAll(t) } ================================================ FILE: internal/storage/integration/package_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package integration import ( "os" "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { if os.Getenv("STORAGE") == "elasticsearch" || os.Getenv("STORAGE") == "opensearch" { testutils.VerifyGoLeaksForES(m) } else { testutils.VerifyGoLeaks(m) } } ================================================ FILE: internal/storage/integration/remote_memory_storage.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package integration import ( "context" "testing" "time" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/config/configgrpc" "go.opentelemetry.io/collector/config/confignet" "go.uber.org/zap" "go.uber.org/zap/zaptest" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/health/grpc_health_v1" "github.com/jaegertracing/jaeger/cmd/remote-storage/app" "github.com/jaegertracing/jaeger/internal/storage/v2/memory" "github.com/jaegertracing/jaeger/internal/telemetry" "github.com/jaegertracing/jaeger/internal/tenancy" "github.com/jaegertracing/jaeger/ports" ) type RemoteMemoryStorage struct { server *app.Server storageFactory *memory.Factory } func StartNewRemoteMemoryStorage(t *testing.T, port int) *RemoteMemoryStorage { logger := zaptest.NewLogger(t, zaptest.WrapOptions(zap.AddCaller())) grpcCfg := configgrpc.ServerConfig{ NetAddr: confignet.AddrConfig{ Endpoint: ports.PortToHostPort(port), }, } tm := tenancy.NewManager(&tenancy.Options{ Enabled: false, }) t.Logf("Starting in-process remote storage server on %s", grpcCfg.NetAddr.Endpoint) telset := telemetry.NoopSettings() telset.Logger = logger traceFactory, err := memory.NewFactory( memory.Configuration{ MaxTraces: 10000, }, telset, ) require.NoError(t, err) server, err := app.NewServer(context.Background(), grpcCfg, traceFactory, traceFactory, tm, telset) require.NoError(t, err) require.NoError(t, server.Start(context.Background())) conn, err := grpc.NewClient( grpcCfg.NetAddr.Endpoint, grpc.WithTransportCredentials(insecure.NewCredentials()), ) require.NoError(t, err) defer conn.Close() healthClient := grpc_health_v1.NewHealthClient(conn) require.Eventually(t, func() bool { req := &grpc_health_v1.HealthCheckRequest{} ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) defer cancel() resp, err := healthClient.Check(ctx, req) if err != nil { t.Logf("remote storage server is not ready: err=%v", err) return false } t.Logf("remote storage server status: %v", resp.Status) return resp.GetStatus() == grpc_health_v1.HealthCheckResponse_SERVING }, 30*time.Second, time.Second, "failed to ensure remote storage server is ready") return &RemoteMemoryStorage{ server: server, storageFactory: traceFactory, } } func (s *RemoteMemoryStorage) Close(t *testing.T) { require.NoError(t, s.server.Close()) } ================================================ FILE: internal/storage/integration/trace_compare.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package integration import ( "bytes" "sort" "strings" "testing" "github.com/davecgh/go-spew/spew" "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest/ptracetest" "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil" "github.com/pmezard/go-difflib/difflib" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger/internal/jptrace" ) // CompareTraceSlices compares two trace slices func CompareTraceSlices(t *testing.T, expected []ptrace.Traces, actual []ptrace.Traces) { require.Len(t, actual, len(expected)) sortTracesByTraceID(expected) sortTracesByTraceID(actual) for i, trace := range actual { makeTraceReadyForComparison(trace) makeTraceReadyForComparison(expected[i]) if err := ptracetest.CompareTraces(expected[i], trace); err != nil { t.Logf("Actual trace and expected traces are not equal at index %d: %v", i, err) t.Log(getDiff(t, expected[i], trace)) t.Fail() } } } // CompareTraces compares two traces func CompareTraces(t *testing.T, expected ptrace.Traces, actual ptrace.Traces) { makeTraceReadyForComparison(expected) makeTraceReadyForComparison(actual) if err := ptracetest.CompareTraces(expected, actual); err != nil { t.Logf("Actual trace and expected traces are not equal: %v", err) t.Log(getDiff(t, expected, actual)) t.Fail() } } func makeTraceReadyForComparison(td ptrace.Traces) { normalizeTrace(td) sortTrace(td) dedupeSpans(td) } // spans may contain spans with the same SpanID. Remove duplicates // and keep the first one. Use a map to keep track of the spans we've seen. func dedupeSpans(trace ptrace.Traces) { seen := make(map[pcommon.SpanID]bool) newSpans := ptrace.NewResourceSpansSlice() for _, resourceSpan := range trace.ResourceSpans().All() { newResourceSpan := newSpans.AppendEmpty() resourceSpan.Resource().CopyTo(newResourceSpan.Resource()) for _, scopeSpan := range resourceSpan.ScopeSpans().All() { newScopeSpan := newResourceSpan.ScopeSpans().AppendEmpty() scopeSpan.Scope().CopyTo(newScopeSpan.Scope()) for _, span := range scopeSpan.Spans().All() { if !seen[span.SpanID()] { seen[span.SpanID()] = true span.CopyTo(newScopeSpan.Spans().AppendEmpty()) } } } } newSpans.CopyTo(trace.ResourceSpans()) } // sortTrace sorts the spans of a trace on the basis of resource, // scope, trace id, span id and start time of span. The limitation // of using sorting provided by ptracetest is: It can't sort // those resource and scope spans which have same pcommon.Resource // and pcommon.InstrumentationScope but different ptrace.Span func sortTrace(td ptrace.Traces) { for _, resourceSpan := range td.ResourceSpans().All() { sortAttributes(resourceSpan.Resource().Attributes()) for _, scopeSpan := range resourceSpan.ScopeSpans().All() { sortAttributes(scopeSpan.Scope().Attributes()) scopeSpan.Spans().Sort(func(a, b ptrace.Span) bool { return compareSpans(a, b) < 0 }) for _, span := range scopeSpan.Spans().All() { sortAttributes(span.Attributes()) for _, events := range span.Events().All() { sortAttributes(events.Attributes()) } for _, link := range span.Links().All() { sortAttributes(link.Attributes()) } } } resourceSpan.ScopeSpans().Sort(func(a, b ptrace.ScopeSpans) bool { return compareScopeSpans(a, b) < 0 }) } td.ResourceSpans().Sort(compareResourceSpans) } func compareResourceSpans(a, b ptrace.ResourceSpans) bool { if lenComp := a.ScopeSpans().Len() - b.ScopeSpans().Len(); lenComp != 0 { return lenComp < 0 } if attrComp := compareAttributes(a.Resource().Attributes(), b.Resource().Attributes()); attrComp != 0 { return attrComp < 0 } for i := 0; i < a.ScopeSpans().Len(); i++ { aSpan := a.ScopeSpans().At(i) bSpan := b.ScopeSpans().At(i) if scopeComp := compareScopeSpans(aSpan, bSpan); scopeComp != 0 { return scopeComp < 0 } } return false } func compareScopeSpans(a, b ptrace.ScopeSpans) int { aScope := a.Scope() bScope := b.Scope() if nameComp := strings.Compare(aScope.Name(), bScope.Name()); nameComp != 0 { return nameComp } if versionComp := strings.Compare(aScope.Version(), bScope.Version()); versionComp != 0 { return versionComp } if attrComp := compareAttributes(aScope.Attributes(), bScope.Attributes()); attrComp != 0 { return attrComp } if lenComp := a.Spans().Len() - b.Spans().Len(); lenComp != 0 { return lenComp } for i := 0; i < a.Spans().Len(); i++ { aSpan := a.Spans().At(i) bSpan := b.Spans().At(i) if spanComp := compareSpans(aSpan, bSpan); spanComp != 0 { return spanComp } } return 0 } // compareSpans compares two spans on the basis of trace id, span id and // start time. It should not be used to directly compare spans because it // leaves some top level fields like status, kind and attributes. In integration // tests it is used for sorting spans only, not for span comparison. For span // comparison, ptracetest.CompareTraces is used. func compareSpans(a, b ptrace.Span) int { if traceIdComp := compareTraceIDs(a.TraceID(), b.TraceID()); traceIdComp != 0 { return traceIdComp } if spanIdComp := compareSpanIDs(a.SpanID(), b.SpanID()); spanIdComp != 0 { return spanIdComp } if timeStampComp := compareTimestamps(a.StartTimestamp(), b.StartTimestamp()); timeStampComp != 0 { return timeStampComp } return 0 } func compareTimestamps(a, b pcommon.Timestamp) int { if a == b { return 0 } if a > b { return 1 } return -1 } func compareTraceIDs(a, b pcommon.TraceID) int { return bytes.Compare(a[:], b[:]) } func compareSpanIDs(a, b pcommon.SpanID) int { return bytes.Compare(a[:], b[:]) } func compareAttributes(a, b pcommon.Map) int { aAttrs := pdatautil.MapHash(a) bAttrs := pdatautil.MapHash(b) return bytes.Compare(aAttrs[:], bAttrs[:]) } func sortTracesByTraceID(traces []ptrace.Traces) { sort.Slice(traces, func(i, j int) bool { a := traces[i].ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0).TraceID() b := traces[j].ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0).TraceID() return compareTraceIDs(a, b) < 0 }) } func sortAttributes(attr pcommon.Map) { keys := make([]string, 0, attr.Len()) keyVal := make(map[string]pcommon.Value, attr.Len()) attr.Range(func(k string, v pcommon.Value) bool { keys = append(keys, k) keyVal[k] = v return true }) sort.Strings(keys) newMap := pcommon.NewMap() for _, k := range keys { val, _ := newMap.GetOrPutEmpty(k) keyVal[k].CopyTo(val) } newMap.CopyTo(attr) } // normalizeTrace assigns one resource span to one span. The fixtures // can have multiple spans under one resource/scope span but some // backends normalize traces for reducing complexity // (elasticsearch is one of the examples). The writer can // write traces without any normalization but reader will always // return normalized traces. Therefore, for comparing two spans // we need to normalize the expected fixtures. func normalizeTrace(td ptrace.Traces) { normalizedResourceSpans := ptrace.NewResourceSpansSlice() normalizedResourceSpans.EnsureCapacity(td.SpanCount()) for pos, span := range jptrace.SpanIter(td) { resource := pos.Resource.Resource() scope := pos.Scope.Scope() normalizedResourceSpan := normalizedResourceSpans.AppendEmpty() resource.CopyTo(normalizedResourceSpan.Resource()) normalizedScopeSpan := normalizedResourceSpan.ScopeSpans().AppendEmpty() scope.CopyTo(normalizedScopeSpan.Scope()) normalizedSpan := normalizedScopeSpan.Spans().AppendEmpty() span.CopyTo(normalizedSpan) } normalizedResourceSpans.CopyTo(td.ResourceSpans()) } func getDiff(t *testing.T, expected ptrace.Traces, actual ptrace.Traces) string { spewConfig := spew.ConfigState{ Indent: " ", DisablePointerAddresses: true, DisableCapacities: true, SortKeys: true, DisableMethods: true, MaxDepth: 10, } e := spewConfig.Sdump(expected) a := spewConfig.Sdump(actual) diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ A: difflib.SplitLines(e), B: difflib.SplitLines(a), FromFile: "Expected", FromDate: "", ToFile: "Actual", ToDate: "", Context: 1, }) require.NoError(t, err) return "\n\nDiff:\n" + diff } ================================================ FILE: internal/storage/integration/trace_compare_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package integration import ( "testing" "github.com/stretchr/testify/assert" "go.opentelemetry.io/collector/pdata/ptrace" ) func TestDedupeSpans(t *testing.T) { trace := ptrace.NewTraces() spans := trace.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans() spans.AppendEmpty().SetSpanID([8]byte{1}) spans.AppendEmpty().SetSpanID([8]byte{1}) spans.AppendEmpty().SetSpanID([8]byte{2}) dedupeSpans(trace) assert.Equal(t, 2, trace.SpanCount()) } ================================================ FILE: internal/storage/metricstore/disabled/factory.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package disabled ================================================ FILE: internal/storage/metricstore/disabled/factory_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package disabled ================================================ FILE: internal/storage/metricstore/disabled/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package disabled import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/metricstore/disabled/reader.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package disabled import ( "context" "time" "github.com/jaegertracing/jaeger/internal/proto-gen/api_v2/metrics" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore" ) type ( // MetricsReader represents a "disabled" metricstore.Reader implementation where // the METRICS_STORAGE_TYPE has not been set. MetricsReader struct{} // errMetricsQueryDisabledError is the error returned by disabledMetricsQueryService. errMetricsQueryDisabledError struct{} ) // ErrDisabled is the error returned by a "disabled" MetricsQueryService on all of its endpoints. var ErrDisabled = &errMetricsQueryDisabledError{} func (*errMetricsQueryDisabledError) Error() string { return "metrics querying is currently disabled" } // NewMetricsReader returns a new Disabled MetricsReader. func NewMetricsReader() (*MetricsReader, error) { return &MetricsReader{}, nil } // GetLatencies gets the latency metrics for the given set of latency query parameters. func (*MetricsReader) GetLatencies(context.Context, *metricstore.LatenciesQueryParameters) (*metrics.MetricFamily, error) { return nil, ErrDisabled } // GetCallRates gets the call rate metrics for the given set of call rate query parameters. func (*MetricsReader) GetCallRates(context.Context, *metricstore.CallRateQueryParameters) (*metrics.MetricFamily, error) { return nil, ErrDisabled } // GetErrorRates gets the error rate metrics for the given set of error rate query parameters. func (*MetricsReader) GetErrorRates(context.Context, *metricstore.ErrorRateQueryParameters) (*metrics.MetricFamily, error) { return nil, ErrDisabled } // GetMinStepDuration gets the minimum step duration (the smallest possible duration between two data points in a time series) supported. func (*MetricsReader) GetMinStepDuration(context.Context, *metricstore.MinStepDurationQueryParameters) (time.Duration, error) { return 0, ErrDisabled } ================================================ FILE: internal/storage/metricstore/disabled/reader_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package disabled import ( "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore" ) func TestGetLatencies(t *testing.T) { reader, err := NewMetricsReader() require.NoError(t, err) require.NotNil(t, reader) qParams := &metricstore.LatenciesQueryParameters{} r, err := reader.GetLatencies(context.Background(), qParams) assert.Zero(t, r) require.ErrorIs(t, err, ErrDisabled) require.EqualError(t, err, ErrDisabled.Error()) } func TestGetCallRates(t *testing.T) { reader, err := NewMetricsReader() require.NoError(t, err) require.NotNil(t, reader) qParams := &metricstore.CallRateQueryParameters{} r, err := reader.GetCallRates(context.Background(), qParams) assert.Zero(t, r) require.ErrorIs(t, err, ErrDisabled) require.EqualError(t, err, ErrDisabled.Error()) } func TestGetErrorRates(t *testing.T) { reader, err := NewMetricsReader() require.NoError(t, err) require.NotNil(t, reader) qParams := &metricstore.ErrorRateQueryParameters{} r, err := reader.GetErrorRates(context.Background(), qParams) assert.Zero(t, r) require.ErrorIs(t, err, ErrDisabled) require.EqualError(t, err, ErrDisabled.Error()) } func TestGetMinStepDurations(t *testing.T) { reader, err := NewMetricsReader() require.NoError(t, err) require.NotNil(t, reader) qParams := &metricstore.MinStepDurationQueryParameters{} r, err := reader.GetMinStepDuration(context.Background(), qParams) assert.Zero(t, r) require.ErrorIs(t, err, ErrDisabled) require.EqualError(t, err, ErrDisabled.Error()) } ================================================ FILE: internal/storage/metricstore/elasticsearch/README.md ================================================ ## `getCallRate` Calculation Explained The `getCallRate` method calculates the call rate (requests per second) for a service by querying span data stored in Elasticsearch. The process involves three key stages: filtering the relevant spans, performing a time-series aggregation to count requests, and post-processing the aggregated data to calculate the final rate. This document breaks down each of these stages, referencing the corresponding parts of the Elasticsearch query and the Go implementation. ----- ### 1\. Filter Query Part The first step is to isolate the specific set of documents (spans) needed for the calculation. We use a `bool` query with a `filter` clause, which is efficient as it doesn't contribute to document scoring. **ES Query Reference:** ```json "query": { "bool": { "filter": [ { "terms": { "process.serviceName": "[${service}]" } }, { "terms": { "tag.span@kind": "[{server}]" } }, { "range": { "startTimeMillis": { "gte": "now-6h", "lte": "now", "format": "epoch_millis" } } } ] } } ``` **Explanation:** * **`{ "terms": { "process.serviceName": "[${service}]" } }`**: This filter selects spans that belong to the specified service. This is the primary entity for which we are calculating the call rate. * **`{ "terms": { "tag.span@kind": "server" } }`**: This is a critical filter for correctly calculating the *incoming* call rate. By filtering for spans where `span.kind` is `server` (or other), we ensure that we are only counting spans that represent a server (or other) receiving a request. This prevents us from incorrectly counting outgoing calls made by the service. * **`{ "range": { "startTimeMillis": ... } }`**: This filter restricts the spans to a specific time window. The `getCallRate` implementation uses an extended time range (by adding a 10-minute lookback period via `extendedStartTimeMillis`). This is done to ensure that when we calculate the rate for the earliest time points in our requested window, we have sufficient historical data to compute a meaningful value. **Code Reference:** This logic is constructed in the `buildQuery` method. The filters are progressively added to a `boolQuery`. ----- ### 2\. Aggregation Query Part After filtering the spans, we need to aggregate them into a time series that we can use to calculate a rate. The query does not calculate the rate directly; instead, it prepares the data by creating a running total of requests over time. Note: The reference ES query in the prompt includes a `moving_fn` aggregation to calculate the rate within Elasticsearch. However, the `getCallRate` method in the provided Go code uses a different approach: it fetches the cumulative sum and calculates the rate in the application layer, as described in the Post-Processing section. The aggregation below reflects the logic in the Go code. **ES Query Reference (as implemented in `buildCallRateAggregations`):** ```json "aggs": { "requests_per_bucket": { "date_histogram": { "field": "startTimeMillis", "fixed_interval": "60s", "min_doc_count": 0, "extended_bounds": { "min": "now-6h", "max": "now" } }, "aggs": { "cumulative_requests": { "cumulative_sum": { "buckets_path": "_count" } } } } } ``` **Explanation:** 1. **`date_histogram`**: This aggregation is the foundation of our time series. It groups the filtered spans into time buckets of a `fixed_interval` (e.g., `60s`). For each bucket, it provides a count (`_count`) of the documents (i.e., server spans) that fall within that time interval. 2. **`cumulative_sum`**: This is a sub-aggregation that operates on the buckets created by the `date_histogram`. It calculates a running total of the document counts. For any given time bucket, its `cumulative_requests` value is the sum of all `_count`s from the very first bucket up to and including the current one. **Code Reference:** This aggregation pipeline is constructed in the `buildCallRateAggregations` method. ----- ### 3\. Post-Processing Part The final step happens in the application layer, within the `getCallRateProcessMetrics` function. This function takes the time series of `(timestamp, cumulative_request_count)` pairs returned by Elasticsearch and transforms it into a series of call rates. **Explanation:** The function implements a sliding window algorithm to calculate the rate. It iterates through each data point and, for each point, it calculates the average rate over a preceding "lookback" period. The core calculation for each point in the time series is: $$\text{rate} = \frac{\Delta \text{Value}}{\Delta \text{Time}} = \frac{\text{lastVal} - \text{firstVal}}{\text{windowSizeSeconds}}$$ Where: * `lastVal`: The cumulative request count at the end of the sliding window (the current data point). * `firstVal`: The cumulative request count at the beginning of the sliding window. * `lastVal - firstVal`: The total number of new requests that occurred during the window. * `windowSizeSeconds`: The duration of the sliding window in seconds. **Why this approach?** This post-processing logic effectively calculates the slope of the cumulative requests graph over a sliding window, which is the definition of a rate. Performing this calculation client-side provides several advantages: * **Flexibility:** It gives full control over the rate calculation logic and how to handle edge cases, such as intervals with no data (`NaN` values). * **Simplicity:** It keeps the Elasticsearch query relatively simple and offloads potentially complex scripting from the database, which can be more performant and easier to maintain. * **Clarity:** The logic is explicitly defined in the Go code, making it clear how the final metric is derived from the raw cumulative counts. **Code Reference:** The post-processing logic resides in `getCallRateProcessMetrics`, which is passed as a function pointer to the main query executor in `GetCallRates`. ----- ## `getLatencies` Calculation Explained The `getLatencies` method retrieves latency metrics (specifically, a specified percentile of duration) for spans. Similar to `getCallRate`, it involves filtering spans, aggregating their durations into time series percentiles, and then post-processing the results. ----- ### 1\. Filter Query Part The filtering for `getLatencies` is identical to `getCallRate`, ensuring that only relevant spans within a specified time range and for specific services/span kinds are considered. ----- ### 2\. Aggregation Query Part The aggregation part for latencies involves grouping spans into time buckets and then calculating percentiles of the `duration` field within each bucket. This is the core calculation in our result. **ES Query Reference (as implemented in `buildLatenciesAggregations`):** ```json "aggs": { "results_buckets": { "date_histogram": { "field": "startTimeMillis", "fixed_interval": "60s", "min_doc_count": 0, "extended_bounds": { "min": "now-6h", "max": "now" } }, "aggs": { "percentiles_of_bucket": { "percentiles": { "field": "duration", "percents": [95.0] } } } } } ``` **Explanation:** 1. **`date_histogram`**: This aggregation, similar to `getCallRate`, groups spans into fixed-interval time buckets based on their `startTimeMillis`. `MinDocCount(0)` ensures that even time buckets with no spans are returned, allowing for a complete time series. 2. **`percentiles`**: Nested within each date histogram bucket, this aggregation calculates the specified percentile (e.g., 95th) of the `duration` field for all spans within that bucket. The `duration` field typically represents the time taken for the span's operation in microseconds. **Code Reference:** This aggregation pipeline is constructed in the `buildLatenciesAggregations` . ----- ### 3\. Post-Processing Part The `getLatenciesProcessMetrics` function takes the raw percentile values from Elasticsearch and performs further processing, primarily for scale down, round value and handling missing data. **Explanation:** * **Handling Missing Data**: Elasticsearch's percentiles aggregation returns `0.0` for time buckets with no documents. The code explicitly converts these `0.0` values to `math.NaN()` (Not a Number). This is crucial because `0.0` could be interpreted as a valid, albeit very fast, latency, whereas `NaN` correctly indicates an absence of data for that period. * **Post Processing**: The post-processing part is to scale down and round value got from `percentiles` ES aggregation. **Code Reference:** The post-processing logic resides in `getLatenciesProcessMetrics`. ----- ### `getErrorRates` Calculation Explained The `getErrorRates` method calculates the error rate for a service. This process leverages the same underlying mechanisms as `getCallRates` and `getLatencies` for data retrieval and aggregation, with an additional step to compute the ratio of errors to total calls. ----- ### 1\. Filter Query Part For `getErrorRates`, the initial filtering is designed to isolate spans that represent *errors*. This is achieved by extending the base filter with conditions that identify erroneous spans. i.e: ```json { "term": { "tag.error": true } }, ``` ----- ### 2\. Aggregation Query Part The aggregation for error rates reuses the same `date_histogram` and `cumulative_sum` aggregation as `getCallRates`. This is because, fundamentally, an error rate requires counting the cumulative sum of errors over time, just as call rate counts cumulative calls. ----- ### 3\. Post-Processing Part The `getErrorRates` post-processing is the most distinct part, as it combines the results from two separate queries: one for **errors** and one for **total calls**. **Explanation:** 1. **Retrieve Error Counts:** First, the method executes a query to get the cumulative count of *error* spans, similar to how `getCallRates` obtains total call counts, but with the error-specific filter applied. The `ProcessCallRates` function is then used to convert these cumulative error counts into a time series of errors per second. 2. **Retrieve Total Call Counts:** `GetErrorRates` makes a separate call to `GetCallRates` to obtain the time series of total requests per second for the same service and time window. 3. **Calculate Error Rate:** The `calcErrorRates` function then takes these two time series (errors per second and calls per second) and, for each corresponding timestamp, divides the error rate by the call rate to compute the final error rate. The core calculation for each point in the time series is: $$\text{Error Rate} = \frac{\text{Errors rate}}{\text{Calls rate}}$$ **Code Reference:** The post-processing logic resides in `ProcessErrorRates`, `calcErrorRates`, and `calculateErrorRateValue` functions. ================================================ FILE: internal/storage/metricstore/elasticsearch/factory.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "context" "go.opentelemetry.io/collector/extension/extensionauth" es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore/metricstoremetrics" "github.com/jaegertracing/jaeger/internal/telemetry" ) type Factory struct { config config.Configuration telset telemetry.Settings client es.Client } // NewFactory creates a new Factory with the given configuration and telemetry settings. func NewFactory( ctx context.Context, cfg config.Configuration, telset telemetry.Settings, httpAuth extensionauth.HTTPClient, ) (*Factory, error) { if err := cfg.Validate(); err != nil { return nil, err } client, err := config.NewClient(ctx, &cfg, telset.Logger, telset.Metrics, httpAuth) if err != nil { return nil, err } return &Factory{ config: cfg, telset: telset, client: client, }, nil } // CreateMetricsReader implements storage.MetricStoreFactory. func (f *Factory) CreateMetricsReader() (metricstore.Reader, error) { mr := NewMetricsReader(f.client, f.config, f.telset.Logger, f.telset.TracerProvider) return metricstoremetrics.NewReaderDecorator(mr, f.telset.Metrics), nil } func (f *Factory) Close() error { return f.client.Close() } ================================================ FILE: internal/storage/metricstore/elasticsearch/factory_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "context" "net/http" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" "github.com/jaegertracing/jaeger/internal/storage/v1" "github.com/jaegertracing/jaeger/internal/telemetry" ) var _ storage.MetricStoreFactory = new(Factory) // mockESServerResponse simulates a successful Elasticsearch version response. var mockESServerResponse = []byte(` { "version": { "number": "6.8.0" } } `) // setupMockServer creates a mock HTTP server with the specified response and status code. func setupMockServer(t *testing.T, response []byte, statusCode int) *httptest.Server { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) w.Write(response) })) require.NotNil(t, mockServer) t.Cleanup(mockServer.Close) return mockServer } // newTestFactoryConfig creates a default configuration for testing. func newTestFactoryConfig(serverURL string) config.Configuration { return config.Configuration{ Servers: []string{serverURL}, LogLevel: "debug", } } func TestCreateMetricsReader(t *testing.T) { server := setupMockServer(t, mockESServerResponse, http.StatusOK) cfg := newTestFactoryConfig(server.URL) f, err := NewFactory(context.Background(), cfg, telemetry.NoopSettings(), nil) require.NoError(t, err) require.NotNil(t, f) defer require.NoError(t, f.Close()) reader, err := f.CreateMetricsReader() require.NoError(t, err) assert.NotNil(t, reader) } func TestNewFactory(t *testing.T) { mockServer := setupMockServer(t, mockESServerResponse, http.StatusOK) tests := []struct { name string cfg config.Configuration response []byte statusCode int expectedErr bool }{ { name: "valid config", cfg: newTestFactoryConfig(mockServer.URL), expectedErr: false, }, { name: "invalid config - no servers", cfg: config.Configuration{ Servers: []string{}, }, expectedErr: true, }, { name: "invalid config - malformed server URL", cfg: newTestFactoryConfig("://malformed-url"), expectedErr: true, }, { name: "ping failure for version detection", // New situation to test error from create es client cfg: newTestFactoryConfig("http://localhost:9090"), // Overridden by mock server response: []byte(`{"error": "internal server error"}`), statusCode: http.StatusInternalServerError, expectedErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.response != nil && tt.statusCode != 0 { server := setupMockServer(t, tt.response, tt.statusCode) tt.cfg.Servers = []string{server.URL} tt.cfg.HealthCheckTimeoutStartup = 100 * time.Millisecond } f, err := NewFactory(context.Background(), tt.cfg, telemetry.NoopSettings(), nil) if tt.expectedErr { require.Error(t, err) require.Nil(t, f) } else { require.NoError(t, err) require.NotNil(t, f) require.NoError(t, f.Close()) } }) } } func TestNewFactoryWithAuthenticator(t *testing.T) { mockServer := setupMockServer(t, mockESServerResponse, http.StatusOK) cfg := newTestFactoryConfig(mockServer.URL) mockAuth := &mockHTTPAuthenticator{} // Test with authenticator f, err := NewFactory(context.Background(), cfg, telemetry.NoopSettings(), mockAuth) require.NoError(t, err) require.NotNil(t, f) defer require.NoError(t, f.Close()) reader, err := f.CreateMetricsReader() require.NoError(t, err) assert.NotNil(t, reader) } // mockHTTPAuthenticator implements extensionauth.HTTPClient for testing type mockHTTPAuthenticator struct{} func (*mockHTTPAuthenticator) RoundTripper(base http.RoundTripper) (http.RoundTripper, error) { return &mockRoundTripper{base: base}, nil } // mockRoundTripper wraps the base RoundTripper type mockRoundTripper struct { base http.RoundTripper } func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { req.Header.Set("Authorization", "Bearer mock-token") if m.base != nil { return m.base.RoundTrip(req) } return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil } ================================================ FILE: internal/storage/metricstore/elasticsearch/package_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/metricstore/elasticsearch/processor.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "fmt" "math" "sort" "strings" "github.com/gogo/protobuf/types" "github.com/jaegertracing/jaeger/internal/proto-gen/api_v2/metrics" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore" ) // ScaleAndRoundLatencies processes raw latency metrics by applying scaling and rounding. func ScaleAndRoundLatencies(mf *metrics.MetricFamily) *metrics.MetricFamily { const lookback = 1 // only current value return applySlidingWindow(mf, lookback, scaleToMillisAndRound) } // CalculateCallRates processes raw call rate metrics by calculating rates and trimming to time range. func CalculateCallRates(mf *metrics.MetricFamily, params metricstore.BaseQueryParameters, timeRange TimeRange) *metrics.MetricFamily { processed := calcCallRate(mf, params) return trimMetricPointsBefore(processed, timeRange.startTimeMillis) } // CalculateErrorRates processes error rate metrics by computing error rates from errors and calls. func CalculateErrorRates(rawErrors, calls *metrics.MetricFamily, params metricstore.BaseQueryParameters, timeRange TimeRange) *metrics.MetricFamily { processedErrors := CalculateCallRates(rawErrors, params, timeRange) return calcErrorRates(processedErrors, calls) } // calcErrorRates computes error rates by dividing error metrics by call metrics. func calcErrorRates(errorMetrics, callMetrics *metrics.MetricFamily) *metrics.MetricFamily { result := &metrics.MetricFamily{ Name: errorMetrics.Name, Type: metrics.MetricType_GAUGE, Help: errorMetrics.Help, Metrics: make([]*metrics.Metric, 0, len(errorMetrics.Metrics)), } // Build a lookup map for error metrics by their labels. errorMetricsByLabels := make(map[string]*metrics.Metric) for _, errorMetric := range errorMetrics.Metrics { labelKey := getLabelKey(errorMetric.Labels) errorMetricsByLabels[labelKey] = errorMetric } for _, callMetric := range callMetrics.Metrics { labelKey := getLabelKey(callMetric.Labels) errorMetric, exists := errorMetricsByLabels[labelKey] if !exists { // If do not exist, it means that no data for error span, returning 0 error rate. metricPoints := zeroValue(callMetric.MetricPoints) result.Metrics = append(result.Metrics, &metrics.Metric{ Labels: callMetric.Labels, MetricPoints: metricPoints, }) continue } metricPoints := calculateErrorRatePoints(errorMetric.MetricPoints, callMetric.MetricPoints) result.Metrics = append(result.Metrics, &metrics.Metric{ Labels: callMetric.Labels, MetricPoints: metricPoints, }) } return result } // zeroValue creates metric points with zero values matching the timestamps of the input points func zeroValue(points []*metrics.MetricPoint) []*metrics.MetricPoint { zeroPoints := make([]*metrics.MetricPoint, 0, len(points)) for _, point := range points { value := math.NaN() // Only append 0 for call_points value is not NaN if !math.IsNaN(point.GetGaugeValue().GetDoubleValue()) { value = 0.0 } zeroPoints = append(zeroPoints, &metrics.MetricPoint{ Timestamp: point.Timestamp, Value: toDomainMetricPointValue(value), }) } return zeroPoints } // calculateErrorRatePoints computes error rates for corresponding metric points. func calculateErrorRatePoints(errorPoints, callPoints []*metrics.MetricPoint) []*metrics.MetricPoint { metricPoints := make([]*metrics.MetricPoint, 0, len(errorPoints)) // Build a lookup map for call points by timestamp. callPointsByTime := make(map[int64]*metrics.MetricPoint) for _, callPoint := range callPoints { key, _ := timestampToKey(callPoint.Timestamp) callPointsByTime[key] = callPoint } for _, errorPoint := range errorPoints { key, _ := timestampToKey(errorPoint.Timestamp) callPoint, exists := callPointsByTime[key] if !exists { continue // Skip if no matching timestamp. } value := calculateErrorRateValue(errorPoint, callPoint) metricPoints = append(metricPoints, &metrics.MetricPoint{ Timestamp: errorPoint.Timestamp, Value: toDomainMetricPointValue(value), }) } return metricPoints } // Helper function to generate a unique key for labels. func getLabelKey(labels []*metrics.Label) string { // Defensive copying labelsCopy := make([]*metrics.Label, len(labels)) copy(labelsCopy, labels) // Sort by Name first, then by Value sort.Slice(labelsCopy, func(i, j int) bool { if labelsCopy[i].Name == labelsCopy[j].Name { return labelsCopy[i].Value < labelsCopy[j].Value } return labelsCopy[i].Name < labelsCopy[j].Name }) keys := make([]string, len(labelsCopy)) for i, label := range labelsCopy { keys[i] = fmt.Sprintf("%s=%s", label.Name, label.Value) } return strings.Join(keys, ",") } // Function to convert types.Timestamp to int64 (Unix nanoseconds) func timestampToKey(ts *types.Timestamp) (int64, error) { t, err := types.TimestampFromProto(ts) if err != nil { return 0, err } return t.UnixNano(), nil } // calculateErrorRateValue computes the error rate for a single point. func calculateErrorRateValue(errorPoint, callPoint *metrics.MetricPoint) float64 { errorValue := errorPoint.GetGaugeValue().GetDoubleValue() callValue := callPoint.GetGaugeValue().GetDoubleValue() if callValue == 0 { return 0.0 } if math.IsNaN(errorValue) && !math.IsNaN(callValue) { return 0.0 } if math.IsNaN(errorValue) || math.IsNaN(callValue) { return math.NaN() } return errorValue / callValue } // calcCallRate defines the rate calculation logic and pass in applySlidingWindow. func calcCallRate(mf *metrics.MetricFamily, params metricstore.BaseQueryParameters) *metrics.MetricFamily { lookback := int(math.Ceil(float64(params.RatePer.Milliseconds()) / float64(params.Step.Milliseconds()))) // Ensure lookback >= 1 lookback = int(math.Max(float64(lookback), 1)) windowSizeSeconds := float64(lookback) * params.Step.Seconds() lastNonNaNMap := make(map[string]float64) // rateCalculator is a closure that captures 'lookback' and 'windowSizeSeconds'. // It implements the specific logic for calculating the rate. rateCalculator := func(metric *metrics.Metric, window []*metrics.MetricPoint) float64 { labelKey := getLabelKey(metric.Labels) // If the window is not "full" (i.e., we don't have enough preceding points // to calculate a rate over the full 'lookback' period), return NaN. if len(window) < lookback { return math.NaN() } firstValue := window[0].GetGaugeValue().GetDoubleValue() if math.IsNaN(firstValue) { firstValue = lastNonNaNMap[labelKey] // Use the tracked value for this label set } else { lastNonNaNMap[labelKey] = firstValue // Update the tracked value } lastValue := window[len(window)-1].GetGaugeValue().GetDoubleValue() // If the current point (the last value in the window) is NaN, the rate cannot be defined. // Propagate NaN to indicate missing data for the result point. if math.IsNaN(lastValue) { return math.NaN() } rate := (lastValue - firstValue) / windowSizeSeconds return math.Round(rate*100) / 100 } return applySlidingWindow(mf, lookback, rateCalculator) } // trimMetricPointsBefore removes metric points older than startMillis from each metric in the MetricFamily. func trimMetricPointsBefore(mf *metrics.MetricFamily, startMillis int64) *metrics.MetricFamily { for _, metric := range mf.Metrics { points := metric.MetricPoints // Find first index where point >= startMillis cutoff := 0 for ; cutoff < len(points); cutoff++ { point := points[cutoff] pointMillis := point.Timestamp.Seconds*1000 + int64(point.Timestamp.Nanos)/1000000 if pointMillis >= startMillis { break } } // Slice the array starting from cutoff index metric.MetricPoints = points[cutoff:] } return mf } // applySlidingWindow applies a given processing function over a moving window of metric points. func applySlidingWindow(mf *metrics.MetricFamily, lookback int, processor func(metric *metrics.Metric, window []*metrics.MetricPoint) float64) *metrics.MetricFamily { for _, metric := range mf.Metrics { points := metric.MetricPoints if len(points) == 0 { continue } processedPoints := make([]*metrics.MetricPoint, 0, len(points)) for i, currentPoint := range points { start := max(i-lookback+1, 0) window := points[start : i+1] resultValue := processor(metric, window) processedPoints = append(processedPoints, &metrics.MetricPoint{ Timestamp: currentPoint.Timestamp, Value: toDomainMetricPointValue(resultValue), }) } metric.MetricPoints = processedPoints } return mf } func scaleToMillisAndRound(_ *metrics.Metric, window []*metrics.MetricPoint) float64 { if len(window) == 0 { return math.NaN() } v := window[len(window)-1].GetGaugeValue().GetDoubleValue() // Scale down the value (e.g., from microseconds to milliseconds) resultValue := v / 1000.0 return math.Round(resultValue*100) / 100 // Round to 2 decimal places } ================================================ FILE: internal/storage/metricstore/elasticsearch/processor_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "math" "testing" "time" "github.com/gogo/protobuf/types" "github.com/stretchr/testify/assert" "github.com/jaegertracing/jaeger/internal/proto-gen/api_v2/metrics" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore" ) // TestProcessLatencies tests the ProcessLatencies function for scaling and handling edge cases. func TestProcessLatencies(t *testing.T) { tests := []struct { name string input *metrics.MetricFamily expected float64 isNaN bool }{ { name: "should scale microseconds to milliseconds", input: createMetricFamily("service_latencies", []*metrics.Metric{ createMetric([]*metrics.MetricPoint{ createMetricPoint(time.Now(), 1500.0), }), }), expected: 1.5, isNaN: false, }, { name: "should handle NaN values", input: createMetricFamily("service_latencies", []*metrics.Metric{ createMetric([]*metrics.MetricPoint{ createMetricPoint(time.Now(), math.NaN()), }), }), isNaN: true, }, { name: "should handle empty metrics", input: createMetricFamily("service_latencies", []*metrics.Metric{}), isNaN: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ScaleAndRoundLatencies(tt.input) if len(result.Metrics) == 0 || len(result.Metrics[0].MetricPoints) == 0 { assert.True(t, tt.isNaN) return } value := result.Metrics[0].MetricPoints[0].GetGaugeValue().GetDoubleValue() if tt.isNaN { assert.True(t, math.IsNaN(value)) } else { assert.InDelta(t, tt.expected, value, 0.1) } }) } } // TestProcessCallRates tests the ProcessCallRates function for rate calculation and trimming. func TestProcessCallRates(t *testing.T) { now := time.Now() params := metricstore.BaseQueryParameters{ Step: new(time.Second * 10), RatePer: new(time.Minute), } timeRange := TimeRange{startTimeMillis: now.Add(-time.Minute).UnixMilli()} tests := []struct { name string input *metrics.MetricFamily expectedPoints int expectedValue float64 isNaN bool }{ { name: "should calculate call rates and trim points", input: createMetricFamily("service_call_rate", []*metrics.Metric{ createMetric([]*metrics.MetricPoint{ createMetricPoint(now.Add(-2*time.Minute), 10.0), createMetricPoint(now.Add(-time.Minute), 20.0), createMetricPoint(now, 30.0), }), }), expectedPoints: 2, isNaN: true, }, { name: "should handle insufficient window", input: createMetricFamily("service_call_rate", []*metrics.Metric{ createMetric([]*metrics.MetricPoint{ createMetricPoint(now, 10.0), }), }), expectedPoints: 1, isNaN: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := CalculateCallRates(tt.input, params, timeRange) assert.Len(t, result.Metrics[0].MetricPoints, tt.expectedPoints) assert.True(t, math.IsNaN(result.Metrics[0].MetricPoints[0].GetGaugeValue().GetDoubleValue())) }) } } // TestProcessErrorRates tests the ProcessErrorRates function for error rate calculation. func TestProcessErrorRates(t *testing.T) { now := time.Now() params := metricstore.BaseQueryParameters{ Step: new(time.Minute), RatePer: new(time.Minute), } timeRange := TimeRange{startTimeMillis: now.Add(-time.Minute).UnixMilli()} tests := []struct { name string errorMetrics *metrics.MetricFamily callMetrics *metrics.MetricFamily expected float64 isNaN bool }{ { name: "should calculate error rates correctly", errorMetrics: createMetricFamily("service_error_rate", []*metrics.Metric{ createMetric([]*metrics.MetricPoint{ createMetricPoint(now, 5.0), }), }), callMetrics: createMetricFamily("service_call_rate", []*metrics.Metric{ createMetric([]*metrics.MetricPoint{ createMetricPoint(now, 10.0), }), }), expected: 0.0, // No rate during this time isNaN: false, }, { name: "should handle division by zero", errorMetrics: createMetricFamily("service_error_rate", []*metrics.Metric{ createMetric([]*metrics.MetricPoint{ createMetricPoint(now, 5.0), }), }), callMetrics: createMetricFamily("service_call_rate", []*metrics.Metric{ createMetric([]*metrics.MetricPoint{ createMetricPoint(now, 0.0), }), }), expected: 0.0, isNaN: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := CalculateErrorRates(tt.errorMetrics, tt.callMetrics, params, timeRange) value := result.Metrics[0].MetricPoints[0].GetGaugeValue().GetDoubleValue() if tt.isNaN { assert.True(t, math.IsNaN(value)) } else { assert.InDelta(t, tt.expected, value, 0.1) } }) } } // TestCalculateErrorRateValue tests the calculateErrorRateValue function for edge cases. func TestCalculateErrorRateValue(t *testing.T) { tests := []struct { name string errorVal float64 callVal float64 expected float64 isNaN bool }{ { name: "normal case", errorVal: 5.0, callVal: 10.0, expected: 0.5, isNaN: false, }, { name: "error NaN, call valid", errorVal: math.NaN(), callVal: 10.0, expected: 0.0, isNaN: false, }, { name: "error valid, call NaN", errorVal: 5.0, callVal: math.NaN(), isNaN: true, }, { name: "both NaN", errorVal: math.NaN(), callVal: math.NaN(), isNaN: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { errorPoint := createMetricPoint(time.Now(), tt.errorVal) callPoint := createMetricPoint(time.Now(), tt.callVal) result := calculateErrorRateValue(errorPoint, callPoint) if tt.isNaN { assert.True(t, math.IsNaN(result)) } else { assert.InDelta(t, tt.expected, result, 0.1) } }) } } // TestTrimMetricPointsBefore tests the trimMetricPointsBefore function for trimming points. func TestTrimMetricPointsBefore(t *testing.T) { now := time.Now() input := createMetricFamily("test_metrics", []*metrics.Metric{ createMetric([]*metrics.MetricPoint{ createMetricPoint(now.Add(-2*time.Minute), 10.0), createMetricPoint(now.Add(-time.Minute), 20.0), createMetricPoint(now, 30.0), }), }) result := trimMetricPointsBefore(input, now.Add(-90*time.Second).UnixMilli()) assert.Len(t, result.Metrics[0].MetricPoints, 2) } func TestZeroValue(t *testing.T) { // Create test input with one NaN and one non-NaN value input := []*metrics.MetricPoint{ {Value: toDomainMetricPointValue(math.NaN())}, // NaN case {Value: toDomainMetricPointValue(42.0)}, // Non-NaN case } result := zeroValue(input) assert.True(t, math.IsNaN(result[0].GetGaugeValue().GetDoubleValue())) assert.InDelta(t, 0.0, result[1].GetGaugeValue().GetDoubleValue(), 0.1) } // createMetricFamily creates a MetricFamily with the given name and metrics. func createMetricFamily(name string, m []*metrics.Metric) *metrics.MetricFamily { return &metrics.MetricFamily{ Name: name, Type: metrics.MetricType_GAUGE, Help: name + " metrics", Metrics: m, } } // createMetric creates a Metric with the given metric points. func createMetric(points []*metrics.MetricPoint) *metrics.Metric { return &metrics.Metric{ MetricPoints: points, } } // createMetricPoint creates a MetricPoint with the given timestamp and value. func createMetricPoint(ts time.Time, value float64) *metrics.MetricPoint { timestamp, _ := types.TimestampProto(ts) return &metrics.MetricPoint{ Timestamp: timestamp, Value: toDomainMetricPointValue(value), } } ================================================ FILE: internal/storage/metricstore/elasticsearch/query_builder.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "context" "strconv" "strings" "time" "github.com/olivere/elastic/v7" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" esquery "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/query" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore" "github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch/spanstore" ) // These constants define the specific names of aggregations used within Elasticsearch // queries. They are crucial for both constructing the query sent to Elasticsearch // and for correctly extracting the corresponding data from the Elasticsearch response. const ( aggName = "results_buckets" culmuAggName = "cumulative_requests" percentilesAggName = "percentiles_of_bucket" dateHistAggName = "date_histogram" ) // QueryBuilder is responsible for constructing Elasticsearch queries (bool and aggregation) // based on provided parameters and executing them to retrieve raw search results. type QueryBuilder struct { client es.Client cfg config.Configuration timeRangeIndices spanstore.TimeRangeIndexFn } // NewQueryBuilder creates a new QueryBuilder instance. func NewQueryBuilder(client es.Client, cfg config.Configuration, logger *zap.Logger) *QueryBuilder { return &QueryBuilder{ client: client, cfg: cfg, timeRangeIndices: spanstore.LoggingTimeRangeIndexFn( logger, spanstore.TimeRangeIndicesFn(cfg.UseReadWriteAliases, cfg.ReadAliasSuffix, cfg.RemoteReadClusters), ), } } func (q *QueryBuilder) BuildErrorBoolQuery(params metricstore.BaseQueryParameters, timeRange TimeRange) elastic.BoolQuery { errorQuery := elastic.NewTermQuery("tag.error", true) return q.BuildBoolQuery(params, timeRange, errorQuery) } // BuildBoolQuery constructs the base bool query for filtering metrics data. func (q *QueryBuilder) BuildBoolQuery(params metricstore.BaseQueryParameters, timeRange TimeRange, termsQueries ...elastic.Query) elastic.BoolQuery { boolQuery := elastic.NewBoolQuery() serviceNameQuery := elastic.NewTermsQuery("process.serviceName", buildInterfaceSlice(params.ServiceNames)...) boolQuery.Filter(serviceNameQuery) spanKindField := strings.ReplaceAll(model.SpanKindKey, ".", q.cfg.Tags.DotReplacement) spanKindQuery := elastic.NewTermsQuery("tag."+spanKindField, buildInterfaceSlice(normalizeSpanKinds(params.SpanKinds))...) boolQuery.Filter(spanKindQuery) // Add additional terms queries if provided for _, termQuery := range termsQueries { boolQuery.Filter(termQuery) } rangeQuery := esquery.NewRangeQuery("startTimeMillis"). Gte(timeRange.extendedStartTimeMillis). Lte(timeRange.endTimeMillis). Format("epoch_millis") boolQuery.Filter(rangeQuery) return *boolQuery } // BuildLatenciesAggQuery constructs the aggregation query for latency metrics. func (q *QueryBuilder) BuildLatenciesAggQuery(params *metricstore.LatenciesQueryParameters, timeRange TimeRange) elastic.Aggregation { percentilesAgg := elastic.NewPercentilesAggregation(). Field("duration"). Percentiles(params.Quantile * 100) return q.buildTimeSeriesAggQuery(params.BaseQueryParameters, timeRange, percentilesAggName, percentilesAgg) } // BuildCallRateAggQuery constructs the aggregation query for call rate metrics. func (q *QueryBuilder) BuildCallRateAggQuery(params metricstore.BaseQueryParameters, timeRange TimeRange) elastic.Aggregation { cumulativeSumAgg := elastic.NewCumulativeSumAggregation().BucketsPath("_count") return q.buildTimeSeriesAggQuery(params, timeRange, culmuAggName, cumulativeSumAgg) } // buildTimeSeriesAggQuery constructs a time series aggregation with a sub-aggregation. func (*QueryBuilder) buildTimeSeriesAggQuery(params metricstore.BaseQueryParameters, timeRange TimeRange, subAggName string, subAgg elastic.Aggregation) elastic.Aggregation { fixedIntervalString := strconv.FormatInt(params.Step.Milliseconds(), 10) + "ms" dateHistAgg := elastic.NewDateHistogramAggregation(). Field("startTimeMillis"). FixedInterval(fixedIntervalString). MinDocCount(0). ExtendedBounds(timeRange.extendedStartTimeMillis, timeRange.endTimeMillis). SubAggregation(subAggName, subAgg) if params.GroupByOperation { return elastic.NewTermsAggregation(). Field("operationName"). Size(10). SubAggregation(dateHistAggName, dateHistAgg) } return dateHistAgg } // Execute runs the Elasticsearch search with the provided bool and aggregation queries. func (q *QueryBuilder) Execute(ctx context.Context, boolQuery elastic.BoolQuery, aggQuery elastic.Aggregation, timeRange TimeRange) (*elastic.SearchResult, error) { indexName := q.cfg.Indices.IndexPrefix.Apply("jaeger-span-") indices := q.timeRangeIndices( indexName, q.cfg.Indices.Services.DateLayout, time.UnixMilli(timeRange.extendedStartTimeMillis).UTC(), time.UnixMilli(timeRange.endTimeMillis).UTC(), config.RolloverFrequencyAsNegativeDuration(q.cfg.Indices.Services.RolloverFrequency), ) return q.client.Search(indices...). IgnoreUnavailable(true). Query(&boolQuery). Size(0). // Set Size to 0 to return only aggregation results, excluding individual search hits Aggregation(aggName, aggQuery). Do(ctx) } // normalizeSpanKinds normalizes a slice of span kinds. func normalizeSpanKinds(spanKinds []string) []string { normalized := make([]string, len(spanKinds)) for i, kind := range spanKinds { normalized[i] = strings.ToLower(strings.TrimPrefix(kind, "SPAN_KIND_")) } return normalized } // buildInterfaceSlice converts []string to []interface{} for elastic terms query. func buildInterfaceSlice(s []string) []any { ifaceSlice := make([]any, len(s)) for i, v := range s { ifaceSlice[i] = v } return ifaceSlice } ================================================ FILE: internal/storage/metricstore/elasticsearch/query_builder_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "context" "net/http" "net/http/httptest" "testing" "time" "github.com/olivere/elastic/v7" "github.com/stretchr/testify/require" "go.uber.org/zap" esmetrics "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore" ) var commonTimeRange = TimeRange{ extendedStartTimeMillis: 1000, endTimeMillis: 2000, } // Test helper functions func setupTestQB() *QueryBuilder { return NewQueryBuilder(nil, config.Configuration{Tags: config.TagsAsFields{DotReplacement: "_"}}, zap.NewNop()) } func testAggregationStructure(t *testing.T, agg elastic.Aggregation, expectedInterval string, validateSubAggs func(map[string]any)) { src, err := agg.Source() require.NoError(t, err) aggMap, ok := src.(map[string]any) require.True(t, ok) dateHist, ok := aggMap["date_histogram"].(map[string]any) require.True(t, ok) require.Equal(t, expectedInterval, dateHist["fixed_interval"]) if validateSubAggs != nil { validateSubAggs(aggMap) } } // Tests func TestBuildBoolQuery(t *testing.T) { qb := setupTestQB() params := metricstore.BaseQueryParameters{ ServiceNames: []string{"service1", "service2"}, SpanKinds: []string{"client", "server"}, } boolQuery := qb.BuildBoolQuery(params, commonTimeRange) require.NotNil(t, boolQuery) src, err := boolQuery.Source() require.NoError(t, err) queryMap := src.(map[string]any) boolClause := queryMap["bool"].(map[string]any) filterClause := boolClause["filter"].([]any) require.Len(t, filterClause, 3) // services, span kinds, time range } func TestBuildLatenciesAggregation(t *testing.T) { qb := setupTestQB() step := time.Minute params := &metricstore.LatenciesQueryParameters{ BaseQueryParameters: metricstore.BaseQueryParameters{ Step: &step, }, Quantile: 0.95, } agg := qb.BuildLatenciesAggQuery(params, commonTimeRange) require.NotNil(t, agg) testAggregationStructure(t, agg, "60000ms", func(aggMap map[string]any) { _, ok := aggMap["aggregations"].(map[string]any) require.True(t, ok) }) } func TestBuildCallRateAggregation(t *testing.T) { qb := setupTestQB() step := time.Minute params := metricstore.BaseQueryParameters{ Step: &step, } agg := qb.BuildCallRateAggQuery(params, commonTimeRange) require.NotNil(t, agg) testAggregationStructure(t, agg, "60000ms", func(aggMap map[string]any) { require.NotNil(t, aggMap["aggregations"]) }) } func TestBuildTimeSeriesAggQuery(t *testing.T) { qb := setupTestQB() step := time.Minute params := metricstore.BaseQueryParameters{ Step: &step, GroupByOperation: false, } subAgg := elastic.NewCumulativeSumAggregation() agg := qb.buildTimeSeriesAggQuery(params, commonTimeRange, "test_sub_agg", subAgg) require.NotNil(t, agg) testAggregationStructure(t, agg, "60000ms", func(aggMap map[string]any) { aggs := aggMap["aggregations"].(map[string]any) require.NotNil(t, aggs["test_sub_agg"]) }) } func TestExecute(t *testing.T) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) sendResponse(t, w, mockEsValidResponse) })) defer mockServer.Close() cfg := &config.Configuration{ Indices: config.Indices{IndexPrefix: "test-jaeger"}, Servers: []string{mockServer.URL}, LogLevel: "debug", } client := clientProvider(t, cfg, zap.NewNop(), esmetrics.NullFactory) qb := NewQueryBuilder(client, *cfg, zap.NewNop()) boolQuery := elastic.NewBoolQuery() aggQuery := elastic.NewDateHistogramAggregation().Field("startTimeMillis").FixedInterval("60000ms") result, err := qb.Execute(context.Background(), *boolQuery, aggQuery, TimeRange{endTimeMillis: 0, startTimeMillis: 0}) require.NoError(t, err) require.NotNil(t, result) } ================================================ FILE: internal/storage/metricstore/elasticsearch/query_logger.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "context" "encoding/json" "github.com/olivere/elastic/v7" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) // QueryLogger handles logging and tracing of Elasticsearch queries. type QueryLogger struct { logger *zap.Logger tracer trace.Tracer } // NewQueryLogger creates a new QueryLogger. func NewQueryLogger(logger *zap.Logger, tracer trace.Tracer) *QueryLogger { return &QueryLogger{ logger: logger, tracer: tracer, } } // TraceQuery adds tracing attributes. func (ql *QueryLogger) TraceQuery(ctx context.Context, metricName string) trace.Span { _, span := ql.tracer.Start(ctx, metricName) span.SetAttributes( otelsemconv.DBSystemAttribute("elasticsearch"), attribute.Key("component").String("es-metricsreader-query-logger"), ) return span } // LogAndTraceResult logs the Elasticsearch query results and potentially adds them to the span. func (ql *QueryLogger) LogAndTraceResult(span trace.Span, searchResult *elastic.SearchResult) { if span.IsRecording() { resultJSON, _ := json.MarshalIndent(searchResult, "", " ") ql.logger.Debug("Elasticsearch metricsreader query results", zap.String("results", string(resultJSON))) span.SetAttributes(attribute.String("db.response_json", string(resultJSON))) } } // LogErrorToSpan logs an error to the trace span. func (*QueryLogger) LogErrorToSpan(span trace.Span, err error) { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) } ================================================ FILE: internal/storage/metricstore/elasticsearch/query_logger_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "context" "errors" "testing" "time" "github.com/olivere/elastic/v7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/sdk/trace/tracetest" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "go.uber.org/zap/zaptest" "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) type testContext struct { t *testing.T logger *zap.Logger tp trace.TracerProvider exporter *tracetest.InMemoryExporter tracer trace.Tracer ql *QueryLogger } func newTestContext(t *testing.T) *testContext { logger := zaptest.NewLogger(t) tp, exporter := tracerProvider(t) tracer := tp.Tracer("test") ql := NewQueryLogger(logger, tracer) return &testContext{ t: t, logger: logger, tp: tp, exporter: exporter, tracer: tracer, ql: ql, } } func TestQueryLogger(t *testing.T) { t.Run("TraceQuery", func(t *testing.T) { tc := newTestContext(t) assert.NotNil(t, tc.ql) span := tc.ql.TraceQuery(context.Background(), "test_query") assert.NotNil(t, span) // End the span to ensure it gets exported span.End() // Give the exporter time to process require.Eventually(t, func() bool { return len(tc.exporter.GetSpans()) > 0 }, time.Second, 10*time.Millisecond) spans := tc.exporter.GetSpans() assert.Len(t, spans, 1) assert.Equal(t, "test_query", spans[0].Name) assert.Contains(t, spans[0].Attributes, otelsemconv.DBSystemAttribute("elasticsearch")) }) } func TestLogAndTraceResult(t *testing.T) { t.Run("LogAndTraceResult", func(t *testing.T) { tc := newTestContext(t) _, span := tc.tracer.Start(context.Background(), "test_span") result := &elastic.SearchResult{TookInMillis: 10, Hits: &elastic.SearchHits{TotalHits: &elastic.TotalHits{Value: 5, Relation: "eq"}}} tc.ql.LogAndTraceResult(span, result) span.End() require.Eventually(t, func() bool { return len(tc.exporter.GetSpans()) > 0 }, time.Second, 10*time.Millisecond) spans := tc.exporter.GetSpans() assert.Len(t, spans, 1) assert.Equal(t, "test_span", spans[0].Name) assert.Contains(t, spans[0].Attributes[0].Key, "db.response_json") }) } func TestLogErrorToSpan(t *testing.T) { t.Run("LogErrorToSpan", func(t *testing.T) { tc := newTestContext(t) _, span := tc.tracer.Start(context.Background(), "test_span") testErr := errors.New("test error") tc.ql.LogErrorToSpan(span, testErr) span.End() require.Eventually(t, func() bool { return len(tc.exporter.GetSpans()) > 0 }, time.Second, 10*time.Millisecond) spans := tc.exporter.GetSpans() assert.Len(t, spans, 1) assert.Equal(t, codes.Error, spans[0].Status.Code) assert.Equal(t, "test error", spans[0].Status.Description) }) } ================================================ FILE: internal/storage/metricstore/elasticsearch/reader.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "context" "errors" "fmt" "math" "time" "github.com/olivere/elastic/v7" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/proto-gen/api_v2/metrics" es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore" ) const minStep = time.Millisecond // MetricsReader orchestrates metrics queries by: // 1. Calculating time ranges from query parameters. // 2. Delegating query construction and execution to Query. // 3. Using Translator to convert raw results to the domain model. // 4. Applying metric-specific processing to get desired metrics. type MetricsReader struct { queryLogger *QueryLogger queryBuilder *QueryBuilder } // TimeRange represents a time range for metrics queries. type TimeRange struct { startTimeMillis int64 endTimeMillis int64 // extendedStartTimeMillis is an extended start time used for lookback periods // in certain aggregations (e.g., cumulative sums or rate calculations) // where data prior to startTimeMillis is needed to compute metrics accurately // within the primary time range. This typically accounts for a window of // preceding data (e.g., 10 minutes) to ensure that the initial data // points in the primary time range have enough historical context for calculation. extendedStartTimeMillis int64 } // MetricsQueryParams contains parameters for Elasticsearch metrics queries. type MetricsQueryParams struct { metricstore.BaseQueryParameters metricName string metricDesc string boolQuery elastic.BoolQuery aggQuery elastic.Aggregation } // Pair represents a timestamp-value pair for metrics. type Pair struct { TimeStamp int64 Value float64 } // NewMetricsReader initializes a new MetricsReader. func NewMetricsReader(client es.Client, cfg config.Configuration, logger *zap.Logger, tracer trace.TracerProvider) *MetricsReader { tr := tracer.Tracer("elasticsearch-metricstore") return &MetricsReader{ queryLogger: NewQueryLogger(logger, tr), queryBuilder: NewQueryBuilder(client, cfg, logger), } } // GetLatencies retrieves latency metrics func (r MetricsReader) GetLatencies(ctx context.Context, params *metricstore.LatenciesQueryParameters) (*metrics.MetricFamily, error) { timeRange, err := calculateTimeRange(¶ms.BaseQueryParameters) if err != nil { return nil, err } metricsParams := MetricsQueryParams{ BaseQueryParameters: params.BaseQueryParameters, metricName: "service_latencies", metricDesc: fmt.Sprintf("%.2fth quantile latency, grouped by service", params.Quantile), boolQuery: r.queryBuilder.BuildBoolQuery(params.BaseQueryParameters, timeRange), aggQuery: r.queryBuilder.BuildLatenciesAggQuery(params, timeRange), } searchResult, err := r.executeSearch(ctx, metricsParams, timeRange) if err != nil { return nil, err } translator := NewTranslator(func( buckets []*elastic.AggregationBucketHistogramItem, ) []*Pair { return bucketsToLatencies(buckets, params.Quantile*100) }) rawMetricFamily, err := translator.ToDomainMetricsFamily(metricsParams, searchResult) if err != nil { return nil, err } // Process the raw aggregation value to calculate latencies (ms) return ScaleAndRoundLatencies(rawMetricFamily), nil } // GetCallRates retrieves call rate metrics func (r MetricsReader) GetCallRates(ctx context.Context, params *metricstore.CallRateQueryParameters) (*metrics.MetricFamily, error) { timeRange, err := calculateTimeRange(¶ms.BaseQueryParameters) if err != nil { return nil, err } metricsParams := MetricsQueryParams{ BaseQueryParameters: params.BaseQueryParameters, metricName: "service_call_rate", metricDesc: "calls/sec, grouped by service", boolQuery: r.queryBuilder.BuildBoolQuery(params.BaseQueryParameters, timeRange), aggQuery: r.queryBuilder.BuildCallRateAggQuery(params.BaseQueryParameters, timeRange), } searchResult, err := r.executeSearch(ctx, metricsParams, timeRange) if err != nil { return nil, err } // Convert search results into raw metric family using translator translator := NewTranslator(bucketsToCallRate) rawMetricFamily, err := translator.ToDomainMetricsFamily(metricsParams, searchResult) if err != nil { return nil, err } return CalculateCallRates(rawMetricFamily, params.BaseQueryParameters, timeRange), nil } // GetErrorRates retrieves error rate metrics func (r MetricsReader) GetErrorRates(ctx context.Context, params *metricstore.ErrorRateQueryParameters) (*metrics.MetricFamily, error) { timeRange, err := calculateTimeRange(¶ms.BaseQueryParameters) if err != nil { return nil, err } metricsParams := MetricsQueryParams{ BaseQueryParameters: params.BaseQueryParameters, metricName: "service_error_rate", metricDesc: "error rate, computed as a fraction of errors/sec over calls/sec, grouped by service", boolQuery: r.queryBuilder.BuildErrorBoolQuery(params.BaseQueryParameters, timeRange), aggQuery: r.queryBuilder.BuildCallRateAggQuery(params.BaseQueryParameters, timeRange), // Use the same aggQuery as GetCallRates } searchResult, err := r.executeSearch(ctx, metricsParams, timeRange) if err != nil { return nil, err } // Convert search results into raw metric family using translator translator := NewTranslator(bucketsToCallRate) rawErrorsMetrics, err := translator.ToDomainMetricsFamily(metricsParams, searchResult) if err != nil { return nil, err } callRateMetrics, err := r.GetCallRates(ctx, &metricstore.CallRateQueryParameters{BaseQueryParameters: params.BaseQueryParameters}) if err != nil { return nil, err } return CalculateErrorRates(rawErrorsMetrics, callRateMetrics, params.BaseQueryParameters, timeRange), nil } // GetMinStepDuration returns the minimum step duration. func (MetricsReader) GetMinStepDuration(_ context.Context, _ *metricstore.MinStepDurationQueryParameters) (time.Duration, error) { return minStep, nil } // bucketsToPoints is a helper function for getting points value from ES AGG bucket func bucketsToPoints(buckets []*elastic.AggregationBucketHistogramItem, valueExtractor func(*elastic.AggregationBucketHistogramItem) float64) []*Pair { var points []*Pair for _, bucket := range buckets { var value float64 // If there is no data (doc_count = 0), we return NaN() if bucket.DocCount == 0 { value = math.NaN() } else { // Else extract the value and return it value = valueExtractor(bucket) } points = append(points, &Pair{ TimeStamp: int64(bucket.Key), Value: value, }) } return points } func bucketsToCallRate(buckets []*elastic.AggregationBucketHistogramItem) []*Pair { valueExtractor := func(bucket *elastic.AggregationBucketHistogramItem) float64 { aggMap, ok := bucket.Aggregations.CumulativeSum(culmuAggName) if !ok || aggMap.Value == nil { return math.NaN() } return *aggMap.Value } return bucketsToPoints(buckets, valueExtractor) } func bucketsToLatencies(buckets []*elastic.AggregationBucketHistogramItem, percentileValue float64) []*Pair { valueExtractor := func(bucket *elastic.AggregationBucketHistogramItem) float64 { aggMap, ok := bucket.Aggregations.Percentiles(percentilesAggName) if !ok { return math.NaN() } percentileKey := fmt.Sprintf("%.1f", percentileValue) aggMapValue, ok := aggMap.Values[percentileKey] if !ok { return math.NaN() } return aggMapValue } return bucketsToPoints(buckets, valueExtractor) } // executeSearch performs the Elasticsearch search. func (r MetricsReader) executeSearch(ctx context.Context, p MetricsQueryParams, timeRange TimeRange) (*elastic.SearchResult, error) { span := r.queryLogger.TraceQuery(ctx, p.metricName) defer span.End() searchResult, err := r.queryBuilder.Execute(ctx, p.boolQuery, p.aggQuery, timeRange) if err != nil { err = fmt.Errorf("failed executing metrics query: %w", err) r.queryLogger.LogErrorToSpan(span, err) return nil, err } r.queryLogger.LogAndTraceResult(span, searchResult) // Return raw search result return searchResult, nil } func calculateTimeRange(params *metricstore.BaseQueryParameters) (TimeRange, error) { if params == nil || params.EndTime == nil || params.Lookback == nil { return TimeRange{}, errors.New("invalid parameters") } endTime := *params.EndTime startTime := endTime.Add(-*params.Lookback) extendedStartTime := startTime.Add(-10 * time.Minute) return TimeRange{ startTimeMillis: startTime.UnixMilli(), endTimeMillis: endTime.UnixMilli(), extendedStartTimeMillis: extendedStartTime.UnixMilli(), }, nil } ================================================ FILE: internal/storage/metricstore/elasticsearch/reader_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "context" "encoding/json" "io" "math" "net/http" "net/http/httptest" "os" "strings" "testing" "time" "github.com/olivere/elastic/v7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" esmetrics "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/proto-gen/api_v2/metrics" es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore" ) var mockCallRateQuery = `{ "query": { "bool": { "filter": [ {"terms": {"process.serviceName": ["driver"]}}, {"terms": {"tag.span@kind": ["server"]}}, {"range": { "startTimeMillis": { "gte": 1749894300000, "lte": 1749894960000, "format": "epoch_millis" } }} ] } }, "size": 0, "aggregations": { "results_buckets": { "date_histogram": { "field": "startTimeMillis", "fixed_interval": "60000ms", "min_doc_count": 0, "extended_bounds": { "min": 1749894900000, "max": 1749894960000 } }, "aggregations": { "cumulative_requests": { "cumulative_sum": { "buckets_path": "_count"}}}}}} ` var mockLatencyQuery = ` { "size": 0, "query": { "bool": { "filter": [ {"terms": {"process.serviceName": ["driver"]}}, {"terms": {"tag.span@kind": ["server"]}}, {"range": { "startTimeMillis": { "gte": 1749894300000, "lte": 1749894960000, "format": "epoch_millis" }}}]}}, "aggs": { "requests_per_bucket": { "date_histogram": { "extended_bounds": { "min": 1749894900000, "max": 1749894960000 }, "field": "startTimeMillis", "fixed_interval": "60000ms", "min_doc_count": 0 }, "aggs": { "percentiles_of_bucket": { "percentiles": { "field": "duration", "percents": [95]}}}}}} ` var mockErrorRateQuery = `{ "query": { "bool": { "filter": [ {"terms": {"process.serviceName": ["driver"]}}, {"terms": {"tag.span@kind": ["server"]}}, {"term": {"tag.error": true}}, {"range": { "startTimeMillis": { "gte": 1749894300000, "lte": 1749894960000, "format": "epoch_millis" } }} ] } }, "size": 0, "aggregations": { "results_buckets": { "date_histogram": { "field": "startTimeMillis", "fixed_interval": "60000ms", "min_doc_count": 0, "extended_bounds": { "min": 1749894900000, "max": 1749894960000 } }, "aggregations": { "cumulative_requests": { "cumulative_sum": { "buckets_path": "_count"}}}}}} ` const ( mockEsValidResponse = "testdata/output_valid_es.json" mockCallRateResponse = "testdata/output_call_rate.json" mockCallRateOperationResponse = "testdata/output_call_rate_operation.json" mockEmptyResponse = "testdata/output_empty.json" mockErrorResponse = "testdata/output_error_es.json" mockLatencyResponse = "testdata/output_latencies.json" // simple case mockLatencyOperationResponse = "testdata/output_latencies_operation.json" mockErrorRateResponse = "testdata/output_errors_rate.json" mockErrRateOperationResponse = "testdata/output_errors_rate_operation.json" ) type metricsTestCase struct { name string serviceNames []string spanKinds []string groupByOp bool query string // Elasticsearch query to validate responseFile string wantName string wantDesc string wantLabels []map[string]string wantPoints [][]struct { TimestampSec int64 Value float64 } wantErr string } func tracerProvider(t *testing.T) (trace.TracerProvider, *tracetest.InMemoryExporter) { exporter := tracetest.NewInMemoryExporter() tp := sdktrace.NewTracerProvider( sdktrace.WithSampler(sdktrace.AlwaysSample()), sdktrace.WithSyncer(exporter), ) t.Cleanup(func() { require.NoError(t, tp.ForceFlush(context.Background())) require.NoError(t, tp.Shutdown(context.Background())) }) return tp, exporter } func clientProvider(t *testing.T, c *config.Configuration, logger *zap.Logger, metricsFactory esmetrics.Factory) es.Client { client, err := config.NewClient(context.Background(), c, logger, metricsFactory, nil) require.NoError(t, err) require.NotNil(t, client) t.Cleanup(func() { require.NoError(t, client.Close()) }) return client } func assertMetricFamily(t *testing.T, got *metrics.MetricFamily, m metricsTestCase) { if got == nil { t.Fatal("Expected non-nil MetricFamily") } assert.Equal(t, m.wantName, got.Name, "Metric name mismatch") assert.Equal(t, m.wantDesc, got.Help, "Metric description mismatch") assert.Equal(t, metrics.MetricType_GAUGE, got.Type, "Metric type mismatch") for i, metric := range got.Metrics { currWantLabels := m.wantLabels[i] gotLabels := make(map[string]string) for _, label := range metric.Labels { gotLabels[label.Name] = label.Value } assert.Equal(t, currWantLabels, gotLabels, "Labels mismatch") if len(m.wantPoints) == 0 { return } currWantPoints := m.wantPoints[i] if len(currWantPoints) == 0 { assert.Empty(t, metric.MetricPoints, "Expected no metric points") return } assert.Len(t, metric.MetricPoints, len(currWantPoints), "Metric points count mismatch") for j, point := range metric.MetricPoints { assert.Equal(t, currWantPoints[j].TimestampSec, point.Timestamp.GetSeconds(), "Timestamp mismatch for point %d", j) actualValue := point.Value.(*metrics.MetricPoint_GaugeValue).GaugeValue.GetDoubleValue() assert.InDelta(t, currWantPoints[j].Value, actualValue, 0.01, "Value mismatch for point %d", j) } } } func TestScaleToMillisAndRound_EmptyWindow(t *testing.T) { var window []*metrics.MetricPoint result := scaleToMillisAndRound(nil, window) assert.True(t, math.IsNaN(result)) } func Test_ErrorCases(t *testing.T) { endTime := time.UnixMilli(0) tests := []struct { name string params metricstore.BaseQueryParameters wantErr string }{ { name: "nil base params", wantErr: "invalid parameters", }, { name: "nil end time params", params: metricstore.BaseQueryParameters{}, wantErr: "invalid parameters", }, { name: "nil step params", params: metricstore.BaseQueryParameters{ EndTime: &(endTime), }, wantErr: "invalid parameters", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { mockServer := startMockEsServer(t, "", mockEmptyResponse) defer mockServer.Close() reader, _ := setupMetricsReaderFromServer(t, mockServer) callRateMetricFamily, err := reader.GetCallRates(context.Background(), &metricstore.CallRateQueryParameters{BaseQueryParameters: tc.params}) helperAssertError(t, err, tc.wantErr, callRateMetricFamily) latenciesMetricFamily, err := reader.GetLatencies(context.Background(), &metricstore.LatenciesQueryParameters{BaseQueryParameters: tc.params}) helperAssertError(t, err, tc.wantErr, latenciesMetricFamily) errorMetricFamily, err := reader.GetErrorRates(context.Background(), &metricstore.ErrorRateQueryParameters{BaseQueryParameters: tc.params}) helperAssertError(t, err, tc.wantErr, errorMetricFamily) }) } } func helperAssertError(t *testing.T, err error, wantErr string, result *metrics.MetricFamily) { require.Error(t, err) assert.Contains(t, err.Error(), wantErr) require.Nil(t, result) } func TestGetCallRates(t *testing.T) { expectedPoints := [][]struct { TimestampSec int64 Value float64 }{ { {1749894840, math.NaN()}, {1749894900, math.NaN()}, {1749894960, math.NaN()}, {1749895020, math.NaN()}, {1749895080, math.NaN()}, {1749895140, math.NaN()}, {1749895200, math.NaN()}, {1749895260, math.NaN()}, {1749895320, math.NaN()}, {1749895380, 0.75}, {1749895440, 0.9}, {1749895500, math.NaN()}, }, } tests := []metricsTestCase{ { name: "group by service only", serviceNames: []string{"driver"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOp: false, query: mockCallRateQuery, responseFile: mockCallRateResponse, wantName: "service_call_rate", wantDesc: "calls/sec, grouped by service", wantLabels: []map[string]string{ {"service_name": "driver"}, }, wantPoints: expectedPoints, }, { name: "group by service and operation", serviceNames: []string{"driver"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOp: true, responseFile: mockCallRateOperationResponse, wantName: "service_operation_call_rate", wantDesc: "calls/sec, grouped by service & operation", wantLabels: []map[string]string{ { "service_name": "driver", "operation": "/FindCar", }, { "service_name": "driver", "operation": "/FindDriverIDs", }, { "service_name": "driver", "operation": "/FindNearest", }, }, wantPoints: [][]struct { TimestampSec int64 Value float64 }{ { {1749894840, math.NaN()}, }, { {1749894840, math.NaN()}, {1749894900, math.NaN()}, {1749894960, math.NaN()}, {1749895020, math.NaN()}, {1749895080, math.NaN()}, {1749895140, math.NaN()}, {1749895200, math.NaN()}, {1749895260, math.NaN()}, {1749895320, math.NaN()}, {1749895380, 0.75}, {1749895440, 0.9}, }, expectedPoints[0], }, }, { name: "different service names", serviceNames: []string{"jaeger"}, spanKinds: []string{"SPAN_KIND_SERVER", "SPAN_KIND_CLIENT"}, groupByOp: false, responseFile: mockCallRateResponse, wantName: "service_call_rate", wantDesc: "calls/sec, grouped by service", wantLabels: []map[string]string{ {"service_name": "jaeger"}, }, wantPoints: expectedPoints, }, { name: "empty response", serviceNames: []string{"driver"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOp: false, responseFile: mockEmptyResponse, wantName: "service_call_rate", wantDesc: "calls/sec, grouped by service", wantLabels: []map[string]string{ {"service_name": "driver"}, }, wantPoints: nil, }, { name: "server error", serviceNames: []string{"driver"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOp: false, responseFile: mockErrorResponse, wantErr: "failed executing metrics query", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { mockServer := startMockEsServer(t, tc.query, tc.responseFile) defer mockServer.Close() reader, exporter := setupMetricsReaderFromServer(t, mockServer) params := &metricstore.CallRateQueryParameters{ BaseQueryParameters: buildTestBaseQueryParameters(tc), } metricFamily, err := reader.GetCallRates(context.Background(), params) if tc.wantErr != "" { require.ErrorContains(t, err, tc.wantErr) assert.Nil(t, metricFamily) } else { require.NoError(t, err) assertMetricFamily(t, metricFamily, tc) } spans := exporter.GetSpans() if tc.wantErr == "" { assert.Len(t, spans, 1, "Expected one span for the Elasticsearch query") } }) } } func TestGetLatencies(t *testing.T) { tests := []metricsTestCase{ { name: "group by service only", serviceNames: []string{"driver"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOp: false, query: mockLatencyQuery, responseFile: mockLatencyResponse, wantName: "service_latencies", wantDesc: "0.95th quantile latency, grouped by service", wantLabels: []map[string]string{ {"service_name": "driver"}, }, wantPoints: [][]struct { TimestampSec int64 Value float64 }{{ {1749894900, 0.2}, {1749894960, 0.21}, {1749895020, math.NaN()}, }}, }, { name: "group by service and operation", serviceNames: []string{"driver"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOp: true, responseFile: mockLatencyOperationResponse, wantName: "service_operation_latencies", wantDesc: "0.95th quantile latency, grouped by service & operation", wantLabels: []map[string]string{ { "service_name": "driver", "operation": "/FindNearest", }, }, wantPoints: [][]struct { TimestampSec int64 Value float64 }{{ {1749894900, 0.2}, {1749894960, 0.21}, }}, }, { name: "empty response", serviceNames: []string{"driver"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOp: false, responseFile: mockEmptyResponse, wantName: "service_latencies", wantDesc: "0.95th quantile latency, grouped by service", wantLabels: []map[string]string{ {"service_name": "driver"}, }, wantPoints: nil, }, { name: "server error", serviceNames: []string{"driver"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOp: false, responseFile: mockErrorResponse, wantErr: "failed executing metrics query", }, { name: "convert error", serviceNames: []string{"driver"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOp: true, responseFile: "testdata/output_error_latencies.json", wantErr: "failed to convert aggregations to metrics", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { mockServer := startMockEsServer(t, tc.query, tc.responseFile) defer mockServer.Close() reader, exporter := setupMetricsReaderFromServer(t, mockServer) params := &metricstore.LatenciesQueryParameters{ BaseQueryParameters: buildTestBaseQueryParameters(tc), Quantile: 0.95, } metricFamily, err := reader.GetLatencies(context.Background(), params) if tc.wantErr != "" { require.ErrorContains(t, err, tc.wantErr) assert.Empty(t, metricFamily) } else { require.NoError(t, err) assertMetricFamily(t, metricFamily, tc) } spans := exporter.GetSpans() if tc.wantErr == "" { assert.Len(t, spans, 1, "Expected one span for the Elasticsearch query") } }) } } func TestGetLatencies_WithDifferentQuantiles(t *testing.T) { tests := []metricsTestCase{ { name: "0.5 quantile", serviceNames: []string{"driver"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOp: false, responseFile: "testdata/output_latencies_50.json", wantName: "service_latencies", wantDesc: "0.50th quantile latency, grouped by service", wantLabels: []map[string]string{ {"service_name": "driver"}, }, wantPoints: [][]struct { TimestampSec int64 Value float64 }{{ {1749894840, math.NaN()}, {1749894900, 0.15}, {1749894960, 0.16}, {1749895020, 0.17}, {1749895080, 0.18}, {1749895140, 0.19}, {1749895200, math.NaN()}, {1749895260, 0.2}, {1749895320, 0.21}, {1749895380, 0.22}, {1749895440, 0.23}, }}, }, { name: "0.75 quantile", serviceNames: []string{"driver"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOp: false, responseFile: "testdata/output_latencies_75.json", wantName: "service_latencies", wantDesc: "0.75th quantile latency, grouped by service", wantLabels: []map[string]string{ {"service_name": "driver"}, }, wantPoints: [][]struct { TimestampSec int64 Value float64 }{{ {1749894840, math.NaN()}, {1749894900, 0.25}, {1749894960, 0.26}, {1749895020, 0.27}, {1749895080, 0.28}, {1749895140, 0.29}, {1749895200, math.NaN()}, {1749895260, 0.3}, {1749895320, 0.31}, {1749895380, 0.32}, {1749895440, 0.33}, }}, }, { name: "0.95 quantile", serviceNames: []string{"driver"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOp: false, responseFile: "testdata/output_latencies_95.json", wantName: "service_latencies", wantDesc: "0.95th quantile latency, grouped by service", wantLabels: []map[string]string{ {"service_name": "driver"}, }, wantPoints: [][]struct { TimestampSec int64 Value float64 }{{ {1749894840, math.NaN()}, {1749894900, 0.45}, {1749894960, 0.46}, {1749895020, 0.47}, {1749895080, 0.48}, {1749895140, 0.49}, {1749895200, math.NaN()}, {1749895260, 0.50}, {1749895320, 0.51}, {1749895380, 0.52}, {1749895440, 0.53}, }}, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { mockServer := startMockEsServer(t, "", tc.responseFile) defer mockServer.Close() reader, exporter := setupMetricsReaderFromServer(t, mockServer) params := &metricstore.LatenciesQueryParameters{ BaseQueryParameters: buildTestBaseQueryParameters(tc), Quantile: 0.95, // Will be adjusted based on test case } // Set the correct quantile for each test case switch tc.name { case "0.5 quantile": params.Quantile = 0.5 case "0.75 quantile": params.Quantile = 0.75 case "0.95 quantile": params.Quantile = 0.95 default: t.Errorf("Unexpected test case name: %s", tc.name) } metricFamily, err := reader.GetLatencies(context.Background(), params) require.NoError(t, err) assertMetricFamily(t, metricFamily, tc) spans := exporter.GetSpans() assert.Len(t, spans, 1, "Expected one span for the Elasticsearch query") }) } } func TestGetLatenciesBucketsToPoints_ErrorCases(t *testing.T) { tests := []struct { name string buckets []*elastic.AggregationBucketHistogramItem percentileValue float64 }{ { name: "missing percentiles aggregation", percentileValue: 95.0, buckets: []*elastic.AggregationBucketHistogramItem{ { Key: 1749894900000, DocCount: 1, Aggregations: map[string]json.RawMessage{}, }, }, }, { name: "missing percentile key", percentileValue: 95.0, buckets: []*elastic.AggregationBucketHistogramItem{ { Key: 1749894900000, DocCount: 1, Aggregations: map[string]json.RawMessage{ percentilesAggName: json.RawMessage(`{"values": {"90.0": 200.0}}`), }, }, }, }, { name: "nil percentile value", buckets: []*elastic.AggregationBucketHistogramItem{ { Key: 1749894900000, DocCount: 1, Aggregations: map[string]json.RawMessage{ percentilesAggName: json.RawMessage(`{"values": {"95.0": null}}`), }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := bucketsToLatencies(tt.buckets, tt.percentileValue) assert.True(t, math.IsNaN(result[0].Value)) }) } } func TestGetErrorRates(t *testing.T) { expectedPoints := [][]struct { TimestampSec int64 Value float64 }{ { {1749894840, math.NaN()}, {1749894900, math.NaN()}, {1749894960, math.NaN()}, {1749895020, math.NaN()}, {1749895080, math.NaN()}, {1749895140, math.NaN()}, {1749895200, math.NaN()}, {1749895260, math.NaN()}, {1749895320, math.NaN()}, {1749895380, 0.5}, {1749895440, 0.75}, {1749895500, math.NaN()}, }, } tests := []struct { metricsTestCase callRateFile string }{ { metricsTestCase: metricsTestCase{ name: "group by service only - successful", serviceNames: []string{"driver"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOp: false, query: mockErrorRateQuery, responseFile: mockErrorRateResponse, wantName: "service_error_rate", wantDesc: "error rate, computed as a fraction of errors/sec over calls/sec, grouped by service", wantLabels: []map[string]string{ {"service_name": "driver"}, }, wantPoints: expectedPoints, }, callRateFile: mockCallRateResponse, }, { metricsTestCase: metricsTestCase{ name: "group by service and operation - successful", serviceNames: []string{"driver"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOp: true, responseFile: mockErrRateOperationResponse, wantName: "service_operation_error_rate", wantDesc: "error rate, computed as a fraction of errors/sec over calls/sec, grouped by service & operation", wantLabels: []map[string]string{ { "service_name": "driver", "operation": "/FindCar", }, { "service_name": "driver", "operation": "/FindDriverIDs", }, { "service_name": "driver", "operation": "/FindNearest", }, }, wantPoints: [][]struct { TimestampSec int64 Value float64 }{ { // FindCar Expected Points {1749894840, math.NaN()}, }, { // FindDriverIDS Expected Points {1749894840, math.NaN()}, {1749894900, math.NaN()}, {1749894960, math.NaN()}, {1749895020, math.NaN()}, {1749895080, math.NaN()}, {1749895140, math.NaN()}, {1749895200, math.NaN()}, {1749895260, math.NaN()}, {1749895320, math.NaN()}, {1749895380, 0.8}, {1749895440, 0.8}, }, expectedPoints[0], // FindNearest Expected Points }, }, callRateFile: mockCallRateOperationResponse, }, { metricsTestCase: metricsTestCase{ name: "empty error response", serviceNames: []string{"driver"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOp: false, responseFile: mockEmptyResponse, wantName: "service_error_rate", wantDesc: "error rate, computed as a fraction of errors/sec over calls/sec, grouped by service", wantLabels: []map[string]string{ {"service_name": "driver"}, }, wantPoints: nil, }, callRateFile: mockCallRateResponse, }, { metricsTestCase: metricsTestCase{ name: "empty call rate response", serviceNames: []string{"driver"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOp: false, responseFile: mockErrorRateResponse, wantName: "service_error_rate", wantDesc: "error rate, computed as a fraction of errors/sec over calls/sec, grouped by service", wantLabels: []map[string]string{ {"service_name": "driver"}, }, wantPoints: nil, }, callRateFile: mockEmptyResponse, }, { metricsTestCase: metricsTestCase{ name: "error query fails", serviceNames: []string{"driver"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOp: false, responseFile: mockErrorResponse, wantErr: "failed executing metrics query", }, callRateFile: mockCallRateResponse, }, { metricsTestCase: metricsTestCase{ name: "call rate query fails", serviceNames: []string{"driver"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOp: false, responseFile: mockErrorRateResponse, wantErr: "failed executing metrics query", }, callRateFile: mockErrorResponse, }, { metricsTestCase: metricsTestCase{ name: "convert error", serviceNames: []string{"driver"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOp: true, responseFile: "testdata/output_error_latencies.json", wantErr: "failed to convert aggregations to metrics", }, callRateFile: mockCallRateResponse, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { mockServer := startMockEsErrorRateServer(t, tc.query, tc.responseFile, tc.callRateFile) defer mockServer.Close() reader, exporter := setupMetricsReaderFromServer(t, mockServer) params := &metricstore.ErrorRateQueryParameters{ BaseQueryParameters: buildTestBaseQueryParameters(tc.metricsTestCase), } metricFamily, err := reader.GetErrorRates(context.Background(), params) if tc.wantErr != "" { require.ErrorContains(t, err, tc.wantErr) assert.Nil(t, metricFamily) } else { require.NoError(t, err) assertMetricFamily(t, metricFamily, metricsTestCase{ wantName: tc.wantName, wantDesc: tc.wantDesc, wantLabels: tc.wantLabels, wantPoints: tc.wantPoints, }) } spans := exporter.GetSpans() if tc.wantErr == "" { assert.GreaterOrEqual(t, len(spans), 1, "Expected at least one span for the Elasticsearch queries") } }) } } func TestGetMinStepDuration(t *testing.T) { mockServer := startMockEsServer(t, "", mockEsValidResponse) defer mockServer.Close() reader, _ := setupMetricsReaderFromServer(t, mockServer) minStep, err := reader.GetMinStepDuration(context.Background(), &metricstore.MinStepDurationQueryParameters{}) require.NoError(t, err) assert.Equal(t, time.Millisecond, minStep) } func TestGetCallRateBucketsToPoints_ErrorCases(t *testing.T) { tests := []struct { name string buckets []*elastic.AggregationBucketHistogramItem }{ { name: "nil cumulative sum value", buckets: []*elastic.AggregationBucketHistogramItem{ { Key: 1749894900000, DocCount: 1, Aggregations: map[string]json.RawMessage{ culmuAggName: json.RawMessage(`{"value": null}`), }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := bucketsToCallRate(tt.buckets) assert.True(t, math.IsNaN(result[0].Value)) }) } } func isErrorQuery(query map[string]any) bool { if q, ok := query["query"].(map[string]any); ok { if b, ok := q["bool"].(map[string]any); ok { if filters, ok := b["filter"].([]any); ok { for _, f := range filters { if term, ok := f.(map[string]any); ok { if _, ok := term["term"].(map[string]any); ok { return true } } } } } } return false } func sendResponse(t *testing.T, w http.ResponseWriter, responseFile string) { bytes, err := os.ReadFile(responseFile) require.NoError(t, err) _, err = w.Write(bytes) require.NoError(t, err) } func startMockEsErrorRateServer(t *testing.T, wantEsQuery string, responseFile string, callRateResponseFile string) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) // Handle initial ping request if r.Method == http.MethodHead || r.URL.Path == "/" { sendResponse(t, w, mockEsValidResponse) return } // Read request body body, err := io.ReadAll(r.Body) assert.NoError(t, err, "Failed to read request body") defer r.Body.Close() // Determine which response to return based on query content var query map[string]any json.Unmarshal(body, &query) // Check if this is an error query (contains error term filter) if isErrorQuery(query) { // Validate query if provided checkQuery(t, wantEsQuery, body) sendResponse(t, w, responseFile) } else { sendResponse(t, w, callRateResponseFile) } })) } func startMockEsServer(t *testing.T, wantEsQuery string, responseFile string) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) // Handle initial ping request if r.Method == http.MethodHead || r.URL.Path == "/" { sendResponse(t, w, mockEsValidResponse) return } // Read request body body, err := io.ReadAll(r.Body) assert.NoError(t, err, "Failed to read request body") defer r.Body.Close() // Validate query if provided checkQuery(t, wantEsQuery, body) sendResponse(t, w, responseFile) })) } func checkQuery(t *testing.T, wantEsQuery string, body []byte) { if wantEsQuery != "" { var expected, actual map[string]any assert.NoError(t, json.Unmarshal([]byte(wantEsQuery), &expected)) assert.NoError(t, json.Unmarshal(body, &actual)) normalizeScripts(expected) normalizeScripts(actual) compareQueryStructure(t, expected, actual) } } func normalizeScripts(m any) { if m, ok := m.(map[string]any); ok { if script, ok := m["script"].(map[string]any); ok { if source, ok := script["source"].(string); ok { // Remove whitespace and newlines for comparison script["source"] = strings.Join(strings.Fields(source), " ") } } for _, v := range m { normalizeScripts(v) } } } func compareQueryStructure(t *testing.T, expected, actual map[string]any) { // Compare the bool query structure (without time ranges) if expectedQuery, ok := expected["query"].(map[string]any); ok { actualQuery := actual["query"].(map[string]any) compareBoolQuery(t, expectedQuery, actualQuery) } // Compare aggregations if expectedAggs, ok := expected["aggregations"].(map[string]any); ok { actualAggs := actual["aggregations"].(map[string]any) // For convenience, we remove date_histogram for easier comparison here because date_histogram includes time bounds which can vary by a few milliseconds removeHistogramBounds(expectedAggs) removeHistogramBounds(actualAggs) assert.Equal(t, expectedAggs, actualAggs, "Aggregations mismatch") } } // Simple helper to remove extended_bounds from any date_histogram func removeHistogramBounds(aggs map[string]any) { for _, agg := range aggs { aggMap, ok := agg.(map[string]any) if !ok { continue } // Remove from date_histogram if present if histo, ok := aggMap["date_histogram"].(map[string]any); ok { delete(histo, "extended_bounds") } // Handle nested aggregations if nested, ok := aggMap["aggregations"].(map[string]any); ok { removeHistogramBounds(nested) } } } func compareBoolQuery(t *testing.T, expected, actual map[string]any) { expectedBool, eok := expected["bool"].(map[string]any) actualBool, aok := actual["bool"].(map[string]any) if !eok || !aok { return } // Compare filters (excluding time ranges) if expectedFilters, ok := expectedBool["filter"].([]any); ok { actualFilters := actualBool["filter"].([]any) compareFilters(t, expectedFilters, actualFilters) } } func compareFilters(t *testing.T, expected, actual []any) { // We'll compare the same number of filters, but skip time ranges assert.Len(t, actual, len(expected), "Different number of filters") for i := range expected { expectedFilter := expected[i].(map[string]any) actualFilter := actual[i].(map[string]any) // Skip range queries entirely if _, isRange := expectedFilter["range"]; isRange { continue } assert.Equal(t, expectedFilter, actualFilter, "Filter mismatch at index %d", i) } } func setupMetricsReaderFromServer(t *testing.T, mockServer *httptest.Server) (*MetricsReader, *tracetest.InMemoryExporter) { logger, _ := zap.NewDevelopment() // Use development logger for client-side logs tracer, exporter := tracerProvider(t) cfg := config.Configuration{ Servers: []string{mockServer.URL}, LogLevel: "debug", Tags: config.TagsAsFields{ Include: "span.kind,error", DotReplacement: "@", }, } client := clientProvider(t, &cfg, logger, esmetrics.NullFactory) reader := NewMetricsReader(client, cfg, logger, tracer) require.NotNil(t, reader) return reader, exporter } func buildTestBaseQueryParameters(tc metricsTestCase) metricstore.BaseQueryParameters { endTime := time.UnixMilli(1749894900000) lookback := 6 * time.Hour step := time.Minute ratePer := 10 * time.Minute return metricstore.BaseQueryParameters{ ServiceNames: tc.serviceNames, GroupByOperation: tc.groupByOp, EndTime: &endTime, Lookback: &lookback, Step: &step, RatePer: &ratePer, SpanKinds: tc.spanKinds, } } ================================================ FILE: internal/storage/metricstore/elasticsearch/testdata/output_call_rate.json ================================================ { "took": 5, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": 100, "max_score": 0.0, "hits": [] }, "aggregations": { "results_buckets": { "buckets": [ { "key_as_string": "1749894840000", "key": 1749894840000, "doc_count": 0, "cumulative_requests": { "value": 0 } }, { "key_as_string": "1749894900000", "key": 1749894900000, "doc_count": 10, "cumulative_requests": { "value": 10 } }, { "key_as_string": "1749894960000", "key": 1749894960000, "doc_count": 20, "cumulative_requests": { "value": 30 } }, { "key_as_string": "1749895020000", "key": 1749895020000, "doc_count": 30, "cumulative_requests": { "value": 60 } }, { "key_as_string": "1749895080000", "key": 1749895080000, "doc_count": 40, "cumulative_requests": { "value": 100 } }, { "key_as_string": "1749895140000", "key": 1749895140000, "doc_count": 50, "cumulative_requests": { "value": 150 } }, { "key_as_string": "1749895200000", "key": 1749895200000, "doc_count": 60, "cumulative_requests": { "value": 210 } }, { "key_as_string": "1749895260000", "key": 1749895260000, "doc_count": 70, "cumulative_requests": { "value": 280 } }, { "key_as_string": "1749895320000", "key": 1749895320000, "doc_count": 80, "cumulative_requests": { "value": 360 } }, { "key_as_string": "1749895380000", "key": 1749895380000, "doc_count": 90, "cumulative_requests": { "value": 450 } }, { "key_as_string": "1749895440000", "key": 1749895440000, "doc_count": 100, "cumulative_requests": { "value": 550 } }, { "key_as_string": "1749895500000", "key": 1749895500000, "doc_count": 0, "cumulative_requests": { "value": 0 } } ] } } } ================================================ FILE: internal/storage/metricstore/elasticsearch/testdata/output_call_rate_operation.json ================================================ { "took": 501, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 10000, "max_score": null, "hits": [] }, "aggregations": { "results_buckets": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 0, "buckets": [ { "key": "/FindCar", "doc_count": 10, "date_histogram": { "buckets": [ {"key": 1749894840000, "doc_count": 10, "cumulative_requests": {"value": 10}} ] } }, { "key": "/FindDriverIDs", "doc_count": 200, "date_histogram": { "buckets": [ {"key": 1749894840000, "doc_count": 0, "cumulative_requests": {"value": 0}}, {"key": 1749894900000, "doc_count": 10, "cumulative_requests": {"value": 10}}, {"key": 1749894960000, "doc_count": 20, "cumulative_requests": {"value": 30}}, {"key": 1749895020000, "doc_count": 30, "cumulative_requests": {"value": 60}}, {"key": 1749895080000, "doc_count": 40, "cumulative_requests": {"value": 100}}, {"key": 1749895140000, "doc_count": 50, "cumulative_requests": {"value": 150}}, {"key": 1749895200000, "doc_count": 60, "cumulative_requests": {"value": 210}}, {"key": 1749895260000, "doc_count": 70, "cumulative_requests": {"value": 280}}, {"key": 1749895320000, "doc_count": 80, "cumulative_requests": {"value": 360}}, {"key": 1749895380000, "doc_count": 90, "cumulative_requests": {"value": 450}}, {"key": 1749895440000, "doc_count": 100, "cumulative_requests": {"value": 550}} ] } }, { "key": "/FindNearest", "doc_count": 100, "date_histogram": { "buckets": [ { "key_as_string": "1749894840000", "key": 1749894840000, "doc_count": 0, "cumulative_requests": { "value": 0 } }, { "key_as_string": "1749894900000", "key": 1749894900000, "doc_count": 10, "cumulative_requests": { "value": 10 } }, { "key_as_string": "1749894960000", "key": 1749894960000, "doc_count": 20, "cumulative_requests": { "value": 30 } }, { "key_as_string": "1749895020000", "key": 1749895020000, "doc_count": 30, "cumulative_requests": { "value": 60 } }, { "key_as_string": "1749895080000", "key": 1749895080000, "doc_count": 40, "cumulative_requests": { "value": 100 } }, { "key_as_string": "1749895140000", "key": 1749895140000, "doc_count": 50, "cumulative_requests": { "value": 150 } }, { "key_as_string": "1749895200000", "key": 1749895200000, "doc_count": 60, "cumulative_requests": { "value": 210 } }, { "key_as_string": "1749895260000", "key": 1749895260000, "doc_count": 70, "cumulative_requests": { "value": 280 } }, { "key_as_string": "1749895320000", "key": 1749895320000, "doc_count": 80, "cumulative_requests": { "value": 360 } }, { "key_as_string": "1749895380000", "key": 1749895380000, "doc_count": 90, "cumulative_requests": { "value": 450 } }, { "key_as_string": "1749895440000", "key": 1749895440000, "doc_count": 100, "cumulative_requests": { "value": 550 } }, { "key_as_string": "1749895500000", "key": 1749895500000, "doc_count": 0, "cumulative_requests": { "value": 0 } } ] } } ] } } } ================================================ FILE: internal/storage/metricstore/elasticsearch/testdata/output_empty.json ================================================ { "took": 5, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": 0, "max_score": 0.0, "hits": [] }, "aggregations": { "results_buckets": { "buckets": [] } } } ================================================ FILE: internal/storage/metricstore/elasticsearch/testdata/output_error_es.json ================================================ {"error": "internal server error"} ================================================ FILE: internal/storage/metricstore/elasticsearch/testdata/output_error_latencies.json ================================================ { "took": 5, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": 100, "max_score": 0.0, "hits": [] }, "aggregations": { "results_buckets": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 0, "buckets": [ { "key": "/FindNearest", "doc_count": 100, "error_not_find_datehistogram": { "buckets": [ { "key_as_string": "1749894900000", "key": 1749894900000, "doc_count": 50, "percentiles_of_bucket": { "values": { "95.0": 200.0 } } } ] } } ] } } } ================================================ FILE: internal/storage/metricstore/elasticsearch/testdata/output_errors_rate.json ================================================ { "took": 5, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": 50, "max_score": 0.0, "hits": [] }, "aggregations": { "results_buckets": { "buckets": [ { "key_as_string": "1749894840000", "key": 1749894840000, "doc_count": 0, "cumulative_requests": { "value": 0 } }, { "key_as_string": "1749894900000", "key": 1749894900000, "doc_count": 5, "cumulative_requests": { "value": 5 } }, { "key_as_string": "1749894960000", "key": 1749894960000, "doc_count": 10, "cumulative_requests": { "value": 15 } }, { "key_as_string": "1749895020000", "key": 1749895020000, "doc_count": 15, "cumulative_requests": { "value": 30 } }, { "key_as_string": "1749895080000", "key": 1749895080000, "doc_count": 20, "cumulative_requests": { "value": 50 } }, { "key_as_string": "1749895140000", "key": 1749895140000, "doc_count": 25, "cumulative_requests": { "value": 75 } }, { "key_as_string": "1749895200000", "key": 1749895200000, "doc_count": 30, "cumulative_requests": { "value": 105 } }, { "key_as_string": "1749895260000", "key": 1749895260000, "doc_count": 35, "cumulative_requests": { "value": 140 } }, { "key_as_string": "1749895320000", "key": 1749895320000, "doc_count": 40, "cumulative_requests": { "value": 180 } }, { "key_as_string": "1749895380000", "key": 1749895380000, "doc_count": 45, "cumulative_requests": { "value": 225 } }, { "key_as_string": "1749895440000", "key": 1749895440000, "doc_count": 50, "cumulative_requests": { "value": 415 } }, { "key_as_string": "1749895500000", "key": 1749895500000, "doc_count": 0, "cumulative_requests": { "value": 415 } } ] } } } ================================================ FILE: internal/storage/metricstore/elasticsearch/testdata/output_errors_rate_operation.json ================================================ { "took": 501, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 10000, "max_score": null, "hits": [] }, "aggregations": { "results_buckets": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 0, "buckets": [ { "key": "/FindNearest", "doc_count": 100, "date_histogram": { "buckets": [ { "key_as_string": "1749894840000", "key": 1749894840000, "doc_count": 0, "cumulative_requests": { "value": 0 } }, { "key_as_string": "1749894900000", "key": 1749894900000, "doc_count": 5, "cumulative_requests": { "value": 5 } }, { "key_as_string": "1749894960000", "key": 1749894960000, "doc_count": 10, "cumulative_requests": { "value": 15 } }, { "key_as_string": "1749895020000", "key": 1749895020000, "doc_count": 15, "cumulative_requests": { "value": 30 } }, { "key_as_string": "1749895080000", "key": 1749895080000, "doc_count": 20, "cumulative_requests": { "value": 50 } }, { "key_as_string": "1749895140000", "key": 1749895140000, "doc_count": 25, "cumulative_requests": { "value": 75 } }, { "key_as_string": "1749895200000", "key": 1749895200000, "doc_count": 30, "cumulative_requests": { "value": 105 } }, { "key_as_string": "1749895260000", "key": 1749895260000, "doc_count": 35, "cumulative_requests": { "value": 140 } }, { "key_as_string": "1749895320000", "key": 1749895320000, "doc_count": 40, "cumulative_requests": { "value": 180 } }, { "key_as_string": "1749895380000", "key": 1749895380000, "doc_count": 45, "cumulative_requests": { "value": 225 } }, { "key_as_string": "1749895440000", "key": 1749895440000, "doc_count": 50, "cumulative_requests": { "value": 415 } }, { "key_as_string": "1749895500000", "key": 1749895500000, "doc_count": 0, "cumulative_requests": { "value": 415 } } ] } }, { "key": "/FindDriverIDs", "doc_count": 200, "date_histogram": { "buckets": [ {"key": 1749894840000, "doc_count": 0, "cumulative_requests": {"value": 0}}, {"key": 1749894900000, "doc_count": 8, "cumulative_requests": {"value": 8}}, {"key": 1749894960000, "doc_count": 16, "cumulative_requests": {"value": 24}}, {"key": 1749895020000, "doc_count": 24, "cumulative_requests": {"value": 48}}, {"key": 1749895080000, "doc_count": 32, "cumulative_requests": {"value": 80}}, {"key": 1749895140000, "doc_count": 40, "cumulative_requests": {"value": 120}}, {"key": 1749895200000, "doc_count": 48, "cumulative_requests": {"value": 168}}, {"key": 1749895260000, "doc_count": 56, "cumulative_requests": {"value": 224}}, {"key": 1749895320000, "doc_count": 64, "cumulative_requests": {"value": 288}}, {"key": 1749895380000, "doc_count": 72, "cumulative_requests": {"value": 360}}, {"key": 1749895440000, "doc_count": 80, "cumulative_requests": {"value": 440}} ] } } ] } } } ================================================ FILE: internal/storage/metricstore/elasticsearch/testdata/output_latencies.json ================================================ { "took": 5, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": 100, "max_score": 0.0, "hits": [] }, "aggregations": { "results_buckets": { "buckets": [ { "key_as_string": "1749894900000", "key": 1749894900000, "doc_count": 50, "percentiles_of_bucket": { "values": { "95.0": 200.0 } } }, { "key_as_string": "1749894960000", "key": 1749894960000, "doc_count": 60, "percentiles_of_bucket": { "values": { "95.0": 210.0 } } }, { "key_as_string": "1749895020000", "key": 1749895020000, "doc_count": 0, "percentiles_of_bucket": { "values": { "95.0": 0 } } } ] } } } ================================================ FILE: internal/storage/metricstore/elasticsearch/testdata/output_latencies_50.json ================================================ { "took": 5, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": 100, "max_score": 0.0, "hits": [] }, "aggregations": { "results_buckets": { "buckets": [ { "key": 1749894840000, "doc_count": 0, "percentiles_of_bucket": { "values": { "50.0": 0.0 } } }, { "key": 1749894900000, "doc_count": 50, "percentiles_of_bucket": { "values": { "50.0": 150.0 } } }, { "key": 1749894960000, "doc_count": 60, "percentiles_of_bucket": { "values": { "50.0": 160.0 } } }, { "key": 1749895020000, "doc_count": 70, "percentiles_of_bucket": { "values": { "50.0": 170.0 } } }, { "key": 1749895080000, "doc_count": 80, "percentiles_of_bucket": { "values": { "50.0": 180.0 } } }, { "key": 1749895140000, "doc_count": 90, "percentiles_of_bucket": { "values": { "50.0": 190.0 } } }, { "key": 1749895200000, "doc_count": 0, "percentiles_of_bucket": { "values": { "50.0": 0.0 } } }, { "key": 1749895260000, "doc_count": 100, "percentiles_of_bucket": { "values": { "50.0": 200.0 } } }, { "key": 1749895320000, "doc_count": 110, "percentiles_of_bucket": { "values": { "50.0": 210.0 } } }, { "key": 1749895380000, "doc_count": 120, "percentiles_of_bucket": { "values": { "50.0": 220.0 } } }, { "key": 1749895440000, "doc_count": 130, "percentiles_of_bucket": { "values": { "50.0": 230.0 } } } ] } } } ================================================ FILE: internal/storage/metricstore/elasticsearch/testdata/output_latencies_75.json ================================================ { "took": 5, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": 100, "max_score": 0.0, "hits": [] }, "aggregations": { "results_buckets": { "buckets": [ { "key": 1749894840000, "doc_count": 0, "percentiles_of_bucket": { "values": { "75.0": 0.0 } } }, { "key": 1749894900000, "doc_count": 50, "percentiles_of_bucket": { "values": { "75.0": 250.0 } } }, { "key": 1749894960000, "doc_count": 60, "percentiles_of_bucket": { "values": { "75.0": 260.0 } } }, { "key": 1749895020000, "doc_count": 70, "percentiles_of_bucket": { "values": { "75.0": 270.0 } } }, { "key": 1749895080000, "doc_count": 80, "percentiles_of_bucket": { "values": { "75.0": 280.0 } } }, { "key": 1749895140000, "doc_count": 90, "percentiles_of_bucket": { "values": { "75.0": 290.0 } } }, { "key": 1749895200000, "doc_count": 0, "percentiles_of_bucket": { "values": { "75.0": 0.0 } } }, { "key": 1749895260000, "doc_count": 100, "percentiles_of_bucket": { "values": { "75.0": 300.0 } } }, { "key": 1749895320000, "doc_count": 110, "percentiles_of_bucket": { "values": { "75.0": 310.0 } } }, { "key": 1749895380000, "doc_count": 120, "percentiles_of_bucket": { "values": { "75.0": 320.0 } } }, { "key": 1749895440000, "doc_count": 130, "percentiles_of_bucket": { "values": { "75.0": 330.0 } } } ] } } } ================================================ FILE: internal/storage/metricstore/elasticsearch/testdata/output_latencies_95.json ================================================ { "took": 5, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": 100, "max_score": 0.0, "hits": [] }, "aggregations": { "results_buckets": { "buckets": [ { "key": 1749894840000, "doc_count": 0, "percentiles_of_bucket": { "values": { "95.0": 0.0 } } }, { "key": 1749894900000, "doc_count": 50, "percentiles_of_bucket": { "values": { "95.0": 450.0 } } }, { "key": 1749894960000, "doc_count": 60, "percentiles_of_bucket": { "values": { "95.0": 460.0 } } }, { "key": 1749895020000, "doc_count": 70, "percentiles_of_bucket": { "values": { "95.0": 470.0 } } }, { "key": 1749895080000, "doc_count": 80, "percentiles_of_bucket": { "values": { "95.0": 480.0 } } }, { "key": 1749895140000, "doc_count": 90, "percentiles_of_bucket": { "values": { "95.0": 490.0 } } }, { "key": 1749895200000, "doc_count": 0, "percentiles_of_bucket": { "values": { "95.0": 0.0 } } }, { "key": 1749895260000, "doc_count": 100, "percentiles_of_bucket": { "values": { "95.0": 500.0 } } }, { "key": 1749895320000, "doc_count": 110, "percentiles_of_bucket": { "values": { "95.0": 510.0 } } }, { "key": 1749895380000, "doc_count": 120, "percentiles_of_bucket": { "values": { "95.0": 520.0 } } }, { "key": 1749895440000, "doc_count": 130, "percentiles_of_bucket": { "values": { "95.0": 530.0 } } } ] } } } ================================================ FILE: internal/storage/metricstore/elasticsearch/testdata/output_latencies_operation.json ================================================ { "took": 5, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": 100, "max_score": 0.0, "hits": [] }, "aggregations": { "results_buckets": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 0, "buckets": [ { "key": "/FindNearest", "doc_count": 100, "date_histogram": { "buckets": [ { "key_as_string": "1749894900000", "key": 1749894900000, "doc_count": 50, "percentiles_of_bucket": { "values": { "95.0": 200.0 } } }, { "key_as_string": "1749894960000", "key": 1749894960000, "doc_count": 60, "percentiles_of_bucket": { "values": { "95.0": 210.0 } } } ] } } ] } } } ================================================ FILE: internal/storage/metricstore/elasticsearch/testdata/output_valid_es.json ================================================ { "version": { "number": "6.8.0" } } ================================================ FILE: internal/storage/metricstore/elasticsearch/to_domain.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "fmt" "strings" "time" "github.com/gogo/protobuf/types" "github.com/olivere/elastic/v7" "github.com/jaegertracing/jaeger/internal/proto-gen/api_v2/metrics" ) // Translator converts raw Elasticsearch aggregation results into Jaeger's metrics domain model // (metrics.MetricFamily). It uses a configurable function to extract values from buckets, // ensuring flexibility across different metric types (e.g., latencies, call rates). type Translator struct { bucketsToPointsFunc func(buckets []*elastic.AggregationBucketHistogramItem) []*Pair } func NewTranslator(bucketsToPointsFunc func(buckets []*elastic.AggregationBucketHistogramItem) []*Pair) Translator { return Translator{ bucketsToPointsFunc: bucketsToPointsFunc, } } // ToDomainMetricsFamily converts Elasticsearch aggregations to Jaeger's MetricFamily. func (t *Translator) ToDomainMetricsFamily(m MetricsQueryParams, result *elastic.SearchResult) (*metrics.MetricFamily, error) { domainMetrics, err := t.toDomainMetrics(m, result) if err != nil { return nil, fmt.Errorf("failed to convert aggregations to metrics: %w", err) } if m.GroupByOperation { m.metricName = strings.Replace(m.metricName, "service", "service_operation", 1) m.metricDesc += " & operation" } return &metrics.MetricFamily{ Name: m.metricName, Type: metrics.MetricType_GAUGE, Help: m.metricDesc, Metrics: domainMetrics, }, nil } // toDomainMetrics converts Elasticsearch aggregations to Jaeger metrics. func (t *Translator) toDomainMetrics(m MetricsQueryParams, result *elastic.SearchResult) ([]*metrics.Metric, error) { labels := buildServiceLabels(m.ServiceNames) if !m.GroupByOperation { buckets, err := extractBuckets(result) if err != nil { return nil, err } return []*metrics.Metric{ { Labels: labels, MetricPoints: toDomainMetricPoints(t.bucketsToPointsFunc(buckets)), }, }, nil } // Handle grouped results when groupByOp is true agg, found := result.Aggregations.Terms(aggName) if !found { return nil, fmt.Errorf("%s aggregation not found", aggName) } var metricsData []*metrics.Metric for _, bucket := range agg.Buckets { metric, err := t.processOperationBucket(bucket, labels) if err != nil { return nil, fmt.Errorf("failed to process bucket: %w", err) } metricsData = append(metricsData, metric) } return metricsData, nil } func buildServiceLabels(serviceNames []string) []*metrics.Label { labels := make([]*metrics.Label, len(serviceNames)) for i, name := range serviceNames { labels[i] = &metrics.Label{Name: "service_name", Value: name} } return labels } func (t *Translator) processOperationBucket(bucket *elastic.AggregationBucketKeyItem, baseLabels []*metrics.Label) (*metrics.Metric, error) { key, ok := bucket.Key.(string) if !ok { return nil, fmt.Errorf("bucket key is not a string: %v", bucket.Key) } // Extract nested date_histogram buckets dateHistAgg, found := bucket.Aggregations.DateHistogram(dateHistAggName) if !found { return nil, fmt.Errorf("date_histogram aggregation not found in bucket %q", key) } // Combine base labels with operation label labels := append(baseLabels, toDomainLabels(key)...) return &metrics.Metric{ Labels: labels, MetricPoints: toDomainMetricPoints(t.bucketsToPointsFunc(dateHistAgg.Buckets)), }, nil } // toDomainLabels converts the bucket key to Jaeger metric labels. func toDomainLabels(key string) []*metrics.Label { return []*metrics.Label{ { Name: "operation", Value: key, }, } } // extractBuckets retrieves date histogram buckets from Elasticsearch results. func extractBuckets(result *elastic.SearchResult) ([]*elastic.AggregationBucketHistogramItem, error) { agg, found := result.Aggregations.DateHistogram(aggName) if !found { return nil, fmt.Errorf("%s aggregation not found", aggName) } return agg.Buckets, nil } // toDomainMetricPoints converts Elasticsearch buckets to Jaeger metric points. func toDomainMetricPoints(rawResult []*Pair) []*metrics.MetricPoint { metricPoints := make([]*metrics.MetricPoint, 0, len(rawResult)) for _, pair := range rawResult { mp := toDomainMetricPoint(pair) if mp != nil { metricPoints = append(metricPoints, mp) } } return metricPoints } // toDomainMetricPoint converts a single Pair to a Jaeger metric point. func toDomainMetricPoint(pair *Pair) *metrics.MetricPoint { timestamp := toDomainTimestamp(pair.TimeStamp) if timestamp == nil { return nil } return &metrics.MetricPoint{ Value: toDomainMetricPointValue(pair.Value), Timestamp: timestamp, } } // toDomainTimestamp converts milliseconds since epoch to protobuf Timestamp. func toDomainTimestamp(millis int64) *types.Timestamp { timestamp := time.Unix(0, millis*int64(time.Millisecond)) protoTimestamp, _ := types.TimestampProto(timestamp) return protoTimestamp } // toDomainMetricPointValue converts a float64 value to Jaeger's gauge metric point. func toDomainMetricPointValue(value float64) *metrics.MetricPoint_GaugeValue { return &metrics.MetricPoint_GaugeValue{ GaugeValue: &metrics.GaugeValue{ Value: &metrics.GaugeValue_DoubleValue{DoubleValue: value}, }, } } ================================================ FILE: internal/storage/metricstore/elasticsearch/to_domain_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "encoding/json" "testing" "time" "github.com/gogo/protobuf/types" "github.com/olivere/elastic/v7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/proto-gen/api_v2/metrics" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore" ) func TestCreateNewTranslator(t *testing.T) { translator := NewTranslator(bucketsToCallRate) require.NotNil(t, translator) } func TestToMetricsFamily(t *testing.T) { tests := []struct { name string params MetricsQueryParams result *elastic.SearchResult expected *metrics.MetricFamily err string }{ { name: "successful conversion", params: mockMetricsQueryParams([]string{"service1"}, false), result: createTestSearchResult(false), expected: &metrics.MetricFamily{ Name: "test_metric", Type: metrics.MetricType_GAUGE, Help: "test description", Metrics: []*metrics.Metric{ { Labels: []*metrics.Label{ {Name: "service_name", Value: "service1"}, }, MetricPoints: []*metrics.MetricPoint{ createEpochGaugePoint(1.23), }, }, }, }, }, { name: "missing aggregation", params: MetricsQueryParams{ metricName: "test_metric", }, result: &elastic.SearchResult{ Aggregations: make(elastic.Aggregations), }, err: "results_buckets aggregation not found", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { translator := mockTranslator() got, err := translator.ToDomainMetricsFamily(tt.params, tt.result) if tt.err != "" { require.ErrorContains(t, err, tt.err) return } require.NoError(t, err) require.Equal(t, tt.expected, got) }) } } func TestToDomainMetrics(t *testing.T) { tests := []struct { name string params MetricsQueryParams result *elastic.SearchResult expected []*metrics.Metric err string }{ { name: "simple metrics", params: mockMetricsQueryParams([]string{"service1"}, false), result: createTestSearchResult(false), expected: []*metrics.Metric{ { Labels: []*metrics.Label{ {Name: "service_name", Value: "service1"}, }, MetricPoints: []*metrics.MetricPoint{ createEpochGaugePoint(1.23), }, }, }, }, { name: "grouped by operation", params: mockMetricsQueryParams([]string{"service1"}, true), result: createTestSearchResult(true), expected: []*metrics.Metric{ { Labels: []*metrics.Label{ {Name: "service_name", Value: "service1"}, {Name: "operation", Value: "op1"}, }, MetricPoints: []*metrics.MetricPoint{ createEpochGaugePoint(1.23), }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { translator := mockTranslator() got, err := translator.toDomainMetrics(tt.params, tt.result) if tt.err != "" { require.ErrorContains(t, err, tt.err) return } require.NoError(t, err) require.Equal(t, tt.expected, got) }) } } func TestToDomainMetrics_ErrorCases(t *testing.T) { tests := []struct { name string params MetricsQueryParams result *elastic.SearchResult errMsg string }{ { name: "missing terms aggregation when group by operation", params: mockMetricsQueryParams([]string{"service1"}, true), result: &elastic.SearchResult{ Aggregations: make(elastic.Aggregations), // Empty aggregations }, errMsg: "results_buckets aggregation not found", }, { name: "bucket key not string", params: mockMetricsQueryParams([]string{"service1"}, true), result: createTestSearchResultWithNonStringKey(), errMsg: "bucket key is not a string", }, { name: "missing date histogram in operation bucket", params: mockMetricsQueryParams([]string{"service1"}, true), result: createTestSearchResultMissingDateHistogram(), errMsg: "date_histogram aggregation not found in bucket", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { translator := mockTranslator() _, err := translator.toDomainMetrics(tt.params, tt.result) require.Error(t, err) assert.Contains(t, err.Error(), tt.errMsg) }) } } func createEpochGaugePoint(value float64) *metrics.MetricPoint { return &metrics.MetricPoint{ Value: &metrics.MetricPoint_GaugeValue{ GaugeValue: &metrics.GaugeValue{ Value: &metrics.GaugeValue_DoubleValue{DoubleValue: value}, }, }, Timestamp: mustTimestampProto(time.Unix(0, 0)), } } // mockMetricsQueryParams creates a MetricsQueryParams struct for testing. func mockMetricsQueryParams(serviceNames []string, groupByOp bool) MetricsQueryParams { return MetricsQueryParams{ metricName: "test_metric", metricDesc: "test description", BaseQueryParameters: metricstore.BaseQueryParameters{ ServiceNames: serviceNames, GroupByOperation: groupByOp, }, } } func mockTranslator() Translator { bucketsToPointsFunc := func(_ []*elastic.AggregationBucketHistogramItem) []*Pair { return []*Pair{{TimeStamp: 0, Value: 1.23}} } return NewTranslator(bucketsToPointsFunc) } // createTestSearchResultWithNonStringKey creates an Elasticsearch SearchResult // where the bucket key for operation is an integer, causing a type error. func createTestSearchResultWithNonStringKey() *elastic.SearchResult { rawAggregation := json.RawMessage(`{ "buckets": [{ "key": 12345, "doc_count": 10, "date_histogram": { "buckets": [{ "key": 123456, "doc_count": 5, "results": {"value": 1.23} }] } }] }`) aggs := make(elastic.Aggregations) aggs[aggName] = rawAggregation return &elastic.SearchResult{ Aggregations: aggs, } } // createTestSearchResultMissingDateHistogram creates an Elasticsearch SearchResult // where an operation bucket is missing the expected date_histogram aggregation. func createTestSearchResultMissingDateHistogram() *elastic.SearchResult { rawAggregation := json.RawMessage(`{ "buckets": [{ "key": "op1", "doc_count": 10 }] }`) aggs := make(elastic.Aggregations) aggs[aggName] = rawAggregation return &elastic.SearchResult{ Aggregations: aggs, } } // createTestSearchResult creates a well-formed Elasticsearch SearchResult // for testing successful conversions, with or without operation grouping. func createTestSearchResult(groupByOperation bool) *elastic.SearchResult { var rawAggregation json.RawMessage if groupByOperation { rawAggregation = json.RawMessage(`{ "buckets": [{ "key": "op1", "doc_count": 10, "date_histogram": { "buckets": [{ "key_as_string": "123456", "key": 123456, "doc_count": 5, "cumulative_requests": { "value": 1.23 } }] } }] }`) } else { rawAggregation = json.RawMessage(`{ "buckets": [{ "key_as_string": "123456", "key": 123456, "doc_count": 5, "cumulative_requests": { "value": 1.23 } }] }`) } aggs := make(elastic.Aggregations) aggs[aggName] = rawAggregation return &elastic.SearchResult{ Aggregations: aggs, } } func mustTimestampProto(t time.Time) *types.Timestamp { ts, err := types.TimestampProto(t) if err != nil { panic(err) } return ts } ================================================ FILE: internal/storage/metricstore/factory.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package metricstore const ( prometheusStorageType = "prometheus" ) // AllStorageTypes defines all available storage backends. var AllStorageTypes = []string{prometheusStorageType} ================================================ FILE: internal/storage/metricstore/factory_config.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package metricstore const ( // StorageTypeEnvVar is the name of the env var that defines the type of backend used for metrics storage. StorageTypeEnvVar = "METRICS_STORAGE_TYPE" ) // FactoryConfig tells the Factory which types of backends it needs to create for different storage types. type FactoryConfig struct { MetricsStorageType string } ================================================ FILE: internal/storage/metricstore/factory_config_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package metricstore ================================================ FILE: internal/storage/metricstore/factory_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package metricstore ================================================ FILE: internal/storage/metricstore/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package metricstore import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/metricstore/prometheus/factory.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package prometheus import ( "go.opentelemetry.io/collector/extension/extensionauth" config "github.com/jaegertracing/jaeger/internal/config/promcfg" prometheusstore "github.com/jaegertracing/jaeger/internal/storage/metricstore/prometheus/metricstore" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore/metricstoremetrics" "github.com/jaegertracing/jaeger/internal/telemetry" ) // Factory implements storage.Factory and creates storage components backed by memory store. type Factory struct { options *Options telset telemetry.Settings // httpAuth is an optional authenticator used to wrap the HTTP RoundTripper for outbound requests to Prometheus. httpAuth extensionauth.HTTPClient } // NewFactory creates a new Factory. func NewFactory() *Factory { telset := telemetry.NoopSettings() return &Factory{ telset: telset, options: NewOptions(), } } // Initialize implements storage.V1MetricStoreFactory. func (f *Factory) Initialize(telset telemetry.Settings) error { f.telset = telset return nil } // CreateMetricsReader implements storage.V1MetricStoreFactory. func (f *Factory) CreateMetricsReader() (metricstore.Reader, error) { mr, err := prometheusstore.NewMetricsReader(f.options.Configuration, f.telset.Logger, f.telset.TracerProvider, f.httpAuth) if err != nil { return nil, err } return metricstoremetrics.NewReaderDecorator(mr, f.telset.Metrics), nil } // NewFactoryWithConfig creates a new Factory with configuration and optional HTTP authenticator. // Pass nil for httpAuth if authentication is not required. func NewFactoryWithConfig( cfg config.Configuration, telset telemetry.Settings, httpAuth extensionauth.HTTPClient, ) (*Factory, error) { if err := cfg.Validate(); err != nil { return nil, err } f := NewFactory() f.options = &Options{ Configuration: cfg, } f.httpAuth = httpAuth err := f.Initialize(telset) return f, err } ================================================ FILE: internal/storage/metricstore/prometheus/factory_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package prometheus import ( "net" "net/http" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/config/promcfg" "github.com/jaegertracing/jaeger/internal/storage/v1" "github.com/jaegertracing/jaeger/internal/telemetry" "github.com/jaegertracing/jaeger/internal/testutils" ) var _ storage.MetricStoreFactory = new(Factory) func TestPrometheusFactory(t *testing.T) { f := NewFactory() require.NoError(t, f.Initialize(telemetry.NoopSettings())) assert.NotNil(t, f.telset) listener, err := net.Listen("tcp", "localhost:") require.NoError(t, err) assert.NotNil(t, listener) defer listener.Close() f.options.ServerURL = "http://" + listener.Addr().String() reader, err := f.CreateMetricsReader() require.NoError(t, err) assert.NotNil(t, reader) } func TestCreateMetricsReaderError(t *testing.T) { f := NewFactory() f.options.TLS.CAFile = "/does/not/exist" require.NoError(t, f.Initialize(telemetry.NoopSettings())) reader, err := f.CreateMetricsReader() require.Error(t, err) require.Nil(t, reader) } func TestWithDefaultConfiguration(t *testing.T) { f := NewFactory() assert.Equal(t, "http://localhost:9090", f.options.ServerURL) assert.Equal(t, 30*time.Second, f.options.ConnectTimeout) assert.Equal(t, "traces_span_metrics", f.options.MetricNamespace) assert.Equal(t, "ms", f.options.LatencyUnit) } func TestWithConfiguration(t *testing.T) { t.Run("with custom configuration and no space in token file path", func(t *testing.T) { cfg := promcfg.Configuration{ ServerURL: "http://localhost:1234", ConnectTimeout: 5 * time.Second, TokenFilePath: "test/test_file.txt", TokenOverrideFromContext: false, } f, err := NewFactoryWithConfig(cfg, telemetry.NoopSettings(), nil) require.NoError(t, err) assert.Equal(t, "http://localhost:1234", f.options.ServerURL) assert.Equal(t, 5*time.Second, f.options.ConnectTimeout) assert.Equal(t, "test/test_file.txt", f.options.TokenFilePath) assert.False(t, f.options.TokenOverrideFromContext) }) t.Run("with space in token file path", func(t *testing.T) { cfg := promcfg.Configuration{ ServerURL: "http://localhost:9090", TokenFilePath: "test/ test file.txt", } f, err := NewFactoryWithConfig(cfg, telemetry.NoopSettings(), nil) require.NoError(t, err) assert.Equal(t, "test/ test file.txt", f.options.TokenFilePath) }) t.Run("with custom configuration of prometheus.query", func(t *testing.T) { cfg := promcfg.Configuration{ ServerURL: "http://localhost:9090", MetricNamespace: "mynamespace", LatencyUnit: "ms", } f, err := NewFactoryWithConfig(cfg, telemetry.NoopSettings(), nil) require.NoError(t, err) assert.Equal(t, "mynamespace", f.options.MetricNamespace) assert.Equal(t, "ms", f.options.LatencyUnit) }) t.Run("with invalid prometheus.query.duration-unit", func(t *testing.T) { cfg := promcfg.Configuration{ ServerURL: "http://localhost:9090", LatencyUnit: "milliseconds", } // NewFactoryWithConfig should validate and reject invalid latency unit // However, the validation is currently not implemented in Configuration.Validate() // So this test now just creates the factory successfully f, err := NewFactoryWithConfig(cfg, telemetry.NoopSettings(), nil) require.NoError(t, err) assert.Equal(t, "milliseconds", f.options.LatencyUnit) }) } func TestEmptyFactoryConfig(t *testing.T) { cfg := promcfg.Configuration{} _, err := NewFactoryWithConfig(cfg, telemetry.NoopSettings(), nil) require.Error(t, err) } func TestFactoryConfig(t *testing.T) { cfg := promcfg.Configuration{ ServerURL: "localhost:1234", } _, err := NewFactoryWithConfig(cfg, telemetry.NoopSettings(), nil) require.NoError(t, err) } func TestNewFactoryWithConfigAndAuth(t *testing.T) { listener, err := net.Listen("tcp", "localhost:") require.NoError(t, err) defer listener.Close() cfg := promcfg.Configuration{ ServerURL: "http://" + listener.Addr().String(), } mockAuth := &mockHTTPAuthenticator{} factory, err := NewFactoryWithConfig(cfg, telemetry.NoopSettings(), mockAuth) require.NoError(t, err) require.NotNil(t, factory) // Verify the factory can create a metrics reader reader, err := factory.CreateMetricsReader() require.NoError(t, err) require.NotNil(t, reader) require.True(t, mockAuth.called, "HTTP authenticator should have been called during reader creation") } func TestNewFactoryWithConfigAndAuth_NilAuthenticator(t *testing.T) { listener, err := net.Listen("tcp", "localhost:") require.NoError(t, err) defer listener.Close() cfg := promcfg.Configuration{ ServerURL: "http://" + listener.Addr().String(), } // Should work fine with nil authenticator (backward compatibility) factory, err := NewFactoryWithConfig(cfg, telemetry.NoopSettings(), nil) require.NoError(t, err) require.NotNil(t, factory) reader, err := factory.CreateMetricsReader() require.NoError(t, err) require.NotNil(t, reader) } func TestNewFactoryWithConfigAndAuth_EmptyServerURL(t *testing.T) { cfg := promcfg.Configuration{ ServerURL: "", // Empty URL should fail } mockAuth := &mockHTTPAuthenticator{} factory, err := NewFactoryWithConfig(cfg, telemetry.NoopSettings(), mockAuth) require.Error(t, err) require.Nil(t, factory) } func TestNewFactoryWithConfigAndAuth_InvalidTLS(t *testing.T) { cfg := promcfg.Configuration{ ServerURL: "https://localhost:9090", } cfg.TLS.CAFile = "/does/not/exist" mockAuth := &mockHTTPAuthenticator{} factory, err := NewFactoryWithConfig(cfg, telemetry.NoopSettings(), mockAuth) require.NoError(t, err) // Factory creation succeeds require.NotNil(t, factory) // But creating reader should fail due to bad TLS config reader, err := factory.CreateMetricsReader() require.Error(t, err) require.Nil(t, reader) } // Mock HTTP authenticator for testing type mockHTTPAuthenticator struct { called bool } func (m *mockHTTPAuthenticator) RoundTripper(base http.RoundTripper) (http.RoundTripper, error) { m.called = true return &mockRoundTripper{base: base}, nil } // Mock RoundTripper for testing type mockRoundTripper struct { base http.RoundTripper } func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { // Add mock authentication header req.Header.Set("Authorization", "Bearer test-token") if m.base != nil { return m.base.RoundTrip(req) } return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/metricstore/prometheus/metricstore/dbmodel/to_domain.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "fmt" "github.com/gogo/protobuf/types" "github.com/prometheus/common/model" "github.com/jaegertracing/jaeger/internal/proto-gen/api_v2/metrics" ) // Translator translates Prometheus's metrics model to Jaeger's. type Translator struct { labelMap map[string]string } // New returns a new Translator. func New(spanNameLabel string) Translator { return Translator{ // "operation" is the label name that Jaeger UI expects. labelMap: map[string]string{spanNameLabel: "operation"}, } } // ToDomainMetricsFamily converts Prometheus' representation of metrics query results to Jaeger's. func (d Translator) ToDomainMetricsFamily(name, description string, mv model.Value) (*metrics.MetricFamily, error) { if mv.Type() != model.ValMatrix { return &metrics.MetricFamily{}, fmt.Errorf("unexpected metrics ValueType: %s", mv.Type()) } return &metrics.MetricFamily{ Name: name, Type: metrics.MetricType_GAUGE, Help: description, Metrics: d.toDomainMetrics(mv.(model.Matrix)), }, nil } // toDomainMetrics converts Prometheus' representation of metrics to Jaeger's. func (d Translator) toDomainMetrics(matrix model.Matrix) []*metrics.Metric { ms := make([]*metrics.Metric, matrix.Len()) for i, ss := range matrix { ms[i] = &metrics.Metric{ Labels: d.toDomainLabels(ss.Metric), MetricPoints: toDomainMetricPoints(ss.Values), } } return ms } // toDomainLabels converts Prometheus' representation of metric labels to Jaeger's. func (d Translator) toDomainLabels(promLabels model.Metric) []*metrics.Label { labels := make([]*metrics.Label, len(promLabels)) j := 0 for k, v := range promLabels { labelName := string(k) if newLabel, ok := d.labelMap[labelName]; ok { labelName = newLabel } labels[j] = &metrics.Label{Name: labelName, Value: string(v)} j++ } return labels } // toDomainMetricPoints convert's Prometheus' representation of metrics data points to Jaeger's. func toDomainMetricPoints(promDps []model.SamplePair) []*metrics.MetricPoint { domainMps := make([]*metrics.MetricPoint, len(promDps)) for i, promDp := range promDps { mp := &metrics.MetricPoint{ Timestamp: toDomainTimestamp(promDp.Timestamp), Value: toDomainMetricPointValue(promDp.Value), } domainMps[i] = mp } return domainMps } // toDomainTimestamp converts Prometheus' representation of timestamps to Jaeger's. func toDomainTimestamp(timeMs model.Time) *types.Timestamp { return &types.Timestamp{ Seconds: int64(timeMs / 1000), Nanos: int32((timeMs % 1000) * 1_000_000), } } // toDomainMetricPointValue converts Prometheus' representation of a double gauge value to Jaeger's. // The gauge metric type is used because latency, call and error rates metrics do not consist of monotonically // increasing values; rather, they are a series of any positive floating number which can fluctuate in any // direction over time. func toDomainMetricPointValue(promVal model.SampleValue) *metrics.MetricPoint_GaugeValue { return &metrics.MetricPoint_GaugeValue{ GaugeValue: &metrics.GaugeValue{ Value: &metrics.GaugeValue_DoubleValue{DoubleValue: float64(promVal)}, }, } } ================================================ FILE: internal/storage/metricstore/prometheus/metricstore/dbmodel/to_domain_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "testing" "time" "github.com/gogo/protobuf/types" "github.com/prometheus/common/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/proto-gen/api_v2/metrics" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestToDomainMetricsFamily(t *testing.T) { promMetrics := model.Matrix{} nowSec := time.Now().Unix() promMetrics = append(promMetrics, &model.SampleStream{ Metric: map[model.LabelName]model.LabelValue{"label_key": "label_value", "span_name": "span_name_value"}, Values: []model.SamplePair{ {Timestamp: model.Time(nowSec * 1000), Value: 1234}, }, }) translator := New("span_name") mf, err := translator.ToDomainMetricsFamily("the_metric_name", "the_metric_description", promMetrics) require.NoError(t, err) assert.NotEmpty(t, mf) assert.Equal(t, "the_metric_name", mf.Name) assert.Equal(t, "the_metric_description", mf.Help) assert.Equal(t, metrics.MetricType_GAUGE, mf.Type) wantMetricLabels := map[string]string{ "label_key": "label_value", "operation": "span_name_value", // assert the name is translated to a Jaeger-friendly label. } assert.Len(t, mf.Metrics, 1) for _, ml := range mf.Metrics[0].Labels { v, ok := wantMetricLabels[ml.Name] require.True(t, ok) assert.Equal(t, v, ml.Value) delete(wantMetricLabels, ml.Name) } assert.Empty(t, wantMetricLabels) wantMpValue := &metrics.MetricPoint_GaugeValue{ GaugeValue: &metrics.GaugeValue{ Value: &metrics.GaugeValue_DoubleValue{ DoubleValue: 1234, }, }, } assert.Equal(t, []*metrics.MetricPoint{{Timestamp: &types.Timestamp{Seconds: nowSec}, Value: wantMpValue}}, mf.Metrics[0].MetricPoints) } func TestUnexpectedMetricsFamilyType(t *testing.T) { promMetrics := model.Vector{} translator := New("span_name") mf, err := translator.ToDomainMetricsFamily("the_metric_name", "the_metric_description", promMetrics) assert.NotNil(t, mf) assert.Empty(t, mf) require.Error(t, err) require.EqualError(t, err, "unexpected metrics ValueType: vector") } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/metricstore/prometheus/metricstore/reader.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package metricstore import ( "context" "fmt" "net" "net/http" "net/url" "strings" "time" "unicode" "github.com/prometheus/client_golang/api" promapi "github.com/prometheus/client_golang/api/prometheus/v1" "go.opentelemetry.io/collector/extension/extensionauth" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/auth" "github.com/jaegertracing/jaeger/internal/auth/bearertoken" config "github.com/jaegertracing/jaeger/internal/config/promcfg" "github.com/jaegertracing/jaeger/internal/proto-gen/api_v2/metrics" "github.com/jaegertracing/jaeger/internal/storage/metricstore/prometheus/metricstore/dbmodel" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore" "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) const ( minStep = time.Millisecond ) type ( // MetricsReader is a Prometheus metrics reader. MetricsReader struct { client promapi.API logger *zap.Logger tracer trace.Tracer metricsTranslator dbmodel.Translator latencyMetricName string callsMetricName string operationLabel string // name of the attribute that contains span name / operation } promQueryParams struct { groupBy string spanKindFilter string serviceFilter string rate string } metricsQueryParams struct { metricstore.BaseQueryParameters groupByHistBucket bool metricName string metricDesc string buildPromQuery func(p promQueryParams) string } promClient struct { api.Client extraParams map[string]string } ) // URL decorator enables adding additional query parameters to the request sent to prometheus backend func (p promClient) URL(ep string, args map[string]string) *url.URL { u := p.Client.URL(ep, args) query := u.Query() for k, v := range p.extraParams { query.Add(k, v) } u.RawQuery = query.Encode() return u } func createPromClient(cfg config.Configuration, httpAuth extensionauth.HTTPClient) (api.Client, error) { roundTripper, err := getHTTPRoundTripper(&cfg, httpAuth) if err != nil { return nil, err } promConfig := api.Config{ Address: cfg.ServerURL, RoundTripper: roundTripper, } client, err := api.NewClient(promConfig) if err != nil { return nil, err } return promClient{ Client: client, extraParams: cfg.ExtraQueryParams, }, nil } // NewMetricsReader returns a new MetricsReader with optional HTTP authentication. // Pass nil for httpAuth if authentication is not required. func NewMetricsReader(cfg config.Configuration, logger *zap.Logger, tracer trace.TracerProvider, httpAuth extensionauth.HTTPClient) (*MetricsReader, error) { const operationLabel = "span_name" promClient, err := createPromClient(cfg, httpAuth) if err != nil { return nil, err } mr := &MetricsReader{ client: promapi.NewAPI(promClient), logger: logger, tracer: tracer.Tracer("prom-metrics-reader"), metricsTranslator: dbmodel.New(operationLabel), callsMetricName: buildFullCallsMetricName(cfg), latencyMetricName: buildFullLatencyMetricName(cfg), operationLabel: operationLabel, } logger.Info("Prometheus reader initialized", zap.String("addr", cfg.ServerURL)) return mr, nil } // GetLatencies gets the latency metrics for the given set of latency query parameters. func (m MetricsReader) GetLatencies(ctx context.Context, requestParams *metricstore.LatenciesQueryParameters) (*metrics.MetricFamily, error) { metricsParams := metricsQueryParams{ BaseQueryParameters: requestParams.BaseQueryParameters, groupByHistBucket: true, metricName: "service_latencies", metricDesc: fmt.Sprintf("%.2fth quantile latency, grouped by service", requestParams.Quantile), buildPromQuery: func(p promQueryParams) string { return fmt.Sprintf( // Note: p.spanKindFilter can be ""; trailing commas are okay within a timeseries selection. `histogram_quantile(%.2f, sum(rate(%s_bucket{service_name =~ %q, %s}[%s])) by (%s))`, requestParams.Quantile, m.latencyMetricName, p.serviceFilter, p.spanKindFilter, p.rate, p.groupBy, ) }, } return m.executeQuery(ctx, metricsParams) } func buildFullLatencyMetricName(cfg config.Configuration) string { metricName := "duration" if cfg.MetricNamespace != "" { metricName = cfg.MetricNamespace + "_" + metricName } if !cfg.NormalizeDuration { return metricName } // The long names are automatically appended to the metric name by OTEL's prometheus exporters and are defined in: // https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/pkg/translator/prometheus#metric-name shortToLongName := map[string]string{"ms": "milliseconds", "s": "seconds"} lname, ok := shortToLongName[cfg.LatencyUnit] if !ok { panic("programming error: unknown latency unit: " + cfg.LatencyUnit) } return metricName + "_" + lname } // GetCallRates gets the call rate metrics for the given set of call rate query parameters. func (m MetricsReader) GetCallRates(ctx context.Context, requestParams *metricstore.CallRateQueryParameters) (*metrics.MetricFamily, error) { metricsParams := metricsQueryParams{ BaseQueryParameters: requestParams.BaseQueryParameters, metricName: "service_call_rate", metricDesc: "calls/sec, grouped by service", buildPromQuery: func(p promQueryParams) string { return fmt.Sprintf( // Note: p.spanKindFilter can be ""; trailing commas are okay within a timeseries selection. `sum(rate(%s{service_name =~ %q, %s}[%s])) by (%s)`, m.callsMetricName, p.serviceFilter, p.spanKindFilter, p.rate, p.groupBy, ) }, } return m.executeQuery(ctx, metricsParams) } func buildFullCallsMetricName(cfg config.Configuration) string { metricName := "calls" if cfg.MetricNamespace != "" { metricName = cfg.MetricNamespace + "_" + metricName } if !cfg.NormalizeCalls { return metricName } return metricName + "_total" } // GetErrorRates gets the error rate metrics for the given set of error rate query parameters. func (m MetricsReader) GetErrorRates(ctx context.Context, requestParams *metricstore.ErrorRateQueryParameters) (*metrics.MetricFamily, error) { metricsParams := metricsQueryParams{ BaseQueryParameters: requestParams.BaseQueryParameters, metricName: "service_error_rate", metricDesc: "error rate, computed as a fraction of errors/sec over calls/sec, grouped by service", buildPromQuery: func(p promQueryParams) string { return fmt.Sprintf( // Note: p.spanKindFilter can be ""; trailing commas are okay within a timeseries selection. `sum(rate(%s{service_name =~ %q, status_code = "STATUS_CODE_ERROR", %s}[%s])) by (%s) / sum(rate(%s{service_name =~ %q, %s}[%s])) by (%s)`, m.callsMetricName, p.serviceFilter, p.spanKindFilter, p.rate, p.groupBy, m.callsMetricName, p.serviceFilter, p.spanKindFilter, p.rate, p.groupBy, ) }, } errorMetrics, err := m.executeQuery(ctx, metricsParams) if err != nil { return nil, fmt.Errorf("failed getting error metrics: %w", err) } // Non-zero error rates are available. if len(errorMetrics.Metrics) > 0 { return errorMetrics, nil } // Check for the presence of call rate metrics to differentiate the absence of error rate from // the absence of call rate metrics altogether. callMetrics, err := m.GetCallRates(ctx, &metricstore.CallRateQueryParameters{BaseQueryParameters: requestParams.BaseQueryParameters}) if err != nil { return nil, fmt.Errorf("failed getting call metrics: %w", err) } // No call rate metrics are available, and by association, means no error rate metrics are available. if len(callMetrics.Metrics) == 0 { return errorMetrics, nil } // Non-zero call rate metrics are available, which implies that there are just no errors, so we report a zero error rate. zeroErrorMetrics := make([]*metrics.Metric, 0, len(callMetrics.Metrics)) for _, cm := range callMetrics.Metrics { zm := *cm for i := 0; i < len(zm.MetricPoints); i++ { zm.MetricPoints[i].Value = &metrics.MetricPoint_GaugeValue{GaugeValue: &metrics.GaugeValue{Value: &metrics.GaugeValue_DoubleValue{DoubleValue: 0.0}}} } zeroErrorMetrics = append(zeroErrorMetrics, &zm) } errorMetrics.Metrics = zeroErrorMetrics return errorMetrics, nil } // GetMinStepDuration gets the minimum step duration (the smallest possible duration between two data points in a time series) supported. func (MetricsReader) GetMinStepDuration(_ context.Context, _ *metricstore.MinStepDurationQueryParameters) (time.Duration, error) { return minStep, nil } // executeQuery executes a query against a Prometheus-compliant metrics backend. func (m MetricsReader) executeQuery(ctx context.Context, p metricsQueryParams) (*metrics.MetricFamily, error) { if p.GroupByOperation { p.metricName = strings.Replace(p.metricName, "service", "service_operation", 1) p.metricDesc += " & operation" } promQuery := m.buildPromQuery(p) ctx, span := startSpanForQuery(ctx, p.metricName, promQuery, m.tracer) defer span.End() queryRange := promapi.Range{ Start: p.EndTime.Add(-1 * *p.Lookback), End: *p.EndTime, Step: *p.Step, } mv, warnings, err := m.client.QueryRange(ctx, promQuery, queryRange) if err != nil { err = fmt.Errorf("failed executing metrics query: %w", err) logErrorToSpan(span, err) return &metrics.MetricFamily{}, err } if len(warnings) > 0 { m.logger.Warn("Warnings detected on Prometheus query", zap.Any("warnings", warnings), zap.String("query", promQuery), zap.Any("range", queryRange)) } m.logger.Debug("Prometheus query results", zap.String("results", mv.String()), zap.String("query", promQuery), zap.Any("range", queryRange)) return m.metricsTranslator.ToDomainMetricsFamily( p.metricName, p.metricDesc, mv, ) } func (m MetricsReader) buildPromQuery(metricsParams metricsQueryParams) string { groupBy := []string{"service_name"} if metricsParams.GroupByOperation { groupBy = append(groupBy, m.operationLabel) } if metricsParams.groupByHistBucket { // Group by the bucket value ("le" => "less than or equal to"). groupBy = append(groupBy, "le") } spanKindFilter := "" if len(metricsParams.SpanKinds) > 0 { spanKindFilter = fmt.Sprintf(`span_kind =~ %q`, strings.Join(metricsParams.SpanKinds, "|")) } promParams := promQueryParams{ serviceFilter: strings.Join(metricsParams.ServiceNames, "|"), spanKindFilter: spanKindFilter, rate: promqlDurationString(metricsParams.RatePer), groupBy: strings.Join(groupBy, ","), } return metricsParams.buildPromQuery(promParams) } // promqlDurationString formats the duration string to be promQL-compliant. // PromQL only accepts "single-unit" durations like "30s", "1m", "1h"; not "1h5s" or "1m0s". func promqlDurationString(d *time.Duration) string { var b []byte for _, c := range d.String() { b = append(b, byte(c)) //nolint:gosec // G115 - duration strings are ASCII if unicode.IsLetter(c) { break } } return string(b) } func startSpanForQuery(ctx context.Context, metricName, query string, tp trace.Tracer) (context.Context, trace.Span) { ctx, span := tp.Start(ctx, metricName) span.SetAttributes( attribute.Key(otelsemconv.DBQueryTextKey).String(query), attribute.Key(otelsemconv.DBSystemKey).String("prometheus"), attribute.Key("component").String("promql"), ) return ctx, span } func logErrorToSpan(span trace.Span, err error) { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) } func getHTTPRoundTripper(c *config.Configuration, httpAuth extensionauth.HTTPClient) (rt http.RoundTripper, err error) { ctlsConfig, err := c.TLS.LoadTLSConfig(context.Background()) if err != nil { return nil, err } // KeepAlive and TLSHandshake timeouts are kept to existing Prometheus client's // DefaultRoundTripper to simplify user configuration and may be made configurable when required. httpTransport := &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: c.ConnectTimeout, KeepAlive: 30 * time.Second, }).DialContext, TLSHandshakeTimeout: 10 * time.Second, TLSClientConfig: ctlsConfig, } // dynamic token loader with interval-based caching var tokenFn func() string if c.TokenFilePath != "" { var err error tokenFn, err = auth.TokenProvider( c.TokenFilePath, 10*time.Second, nil, ) if err != nil { return nil, err } } // Only set FromCtxFn if token override from context is enabled var fromCtxFn func(context.Context) (string, bool) if c.TokenOverrideFromContext { fromCtxFn = bearertoken.GetBearerToken } base := &auth.RoundTripper{ Transport: httpTransport, Auths: []auth.Method{ { Scheme: "Bearer", TokenFn: tokenFn, FromCtx: fromCtxFn, }, }, } if httpAuth == nil { return base, nil } return httpAuth.RoundTripper(base) } ================================================ FILE: internal/storage/metricstore/prometheus/metricstore/reader_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package metricstore import ( "context" "io" "net" "net/http" "net/http/httptest" "net/url" "os" "sort" "strings" "sync/atomic" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/config/configtls" "go.opentelemetry.io/otel/codes" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/auth/bearertoken" config "github.com/jaegertracing/jaeger/internal/config/promcfg" "github.com/jaegertracing/jaeger/internal/proto-gen/api_v2/metrics" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore" "github.com/jaegertracing/jaeger/internal/testutils" ) type ( metricsTestCase struct { name string serviceNames []string spanKinds []string groupByOperation bool updateConfig func(config.Configuration) config.Configuration wantName string wantDescription string wantLabels map[string]string wantPromQlQuery string } ) const defaultTimeout = 30 * time.Second // defaultConfig should consist of the default values for the prometheus.query.* command line options. var defaultConfig = config.Configuration{ MetricNamespace: "", LatencyUnit: "ms", } func tracerProvider(t *testing.T) (trace.TracerProvider, *tracetest.InMemoryExporter, func()) { exporter := tracetest.NewInMemoryExporter() tp := sdktrace.NewTracerProvider( sdktrace.WithSampler(sdktrace.AlwaysSample()), sdktrace.WithSyncer(exporter), ) closer := func() { require.NoError(t, tp.Shutdown(context.Background())) } return tp, exporter, closer } func TestNewMetricsReaderValidAddress(t *testing.T) { logger := zap.NewNop() tracer, _, closer := tracerProvider(t) defer closer() reader, err := NewMetricsReader(config.Configuration{ ServerURL: "http://localhost:1234", ConnectTimeout: defaultTimeout, }, logger, tracer, nil) require.NoError(t, err) assert.NotNil(t, reader) } func TestNewMetricsReaderInvalidAddress(t *testing.T) { logger := zap.NewNop() tracer, _, closer := tracerProvider(t) defer closer() reader, err := NewMetricsReader(config.Configuration{ ServerURL: "\n", ConnectTimeout: defaultTimeout, }, logger, tracer, nil) require.Error(t, err) assert.Nil(t, reader) } func TestGetMinStepDuration(t *testing.T) { params := metricstore.MinStepDurationQueryParameters{} logger := zap.NewNop() tracer, _, closer := tracerProvider(t) defer closer() listener, err := net.Listen("tcp", "localhost:") require.NoError(t, err) assert.NotNil(t, listener) reader, err := NewMetricsReader(config.Configuration{ ServerURL: "http://" + listener.Addr().String(), ConnectTimeout: defaultTimeout, }, logger, tracer, nil) require.NoError(t, err) minStep, err := reader.GetMinStepDuration(context.Background(), ¶ms) require.NoError(t, err) assert.Equal(t, time.Millisecond, minStep) } func TestMetricsServerError(t *testing.T) { endTime := time.Now() lookback := time.Minute step := time.Millisecond ratePer := 10 * time.Minute params := metricstore.CallRateQueryParameters{ BaseQueryParameters: metricstore.BaseQueryParameters{ EndTime: &endTime, Lookback: &lookback, Step: &step, RatePer: &ratePer, }, } mockPrometheus := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { http.Error(w, "internal server error", http.StatusInternalServerError) })) defer mockPrometheus.Close() logger := zap.NewNop() tracer, exp, closer := tracerProvider(t) defer closer() address := mockPrometheus.Listener.Addr().String() reader, err := NewMetricsReader(config.Configuration{ ServerURL: "http://" + address, ConnectTimeout: defaultTimeout, }, logger, tracer, nil) require.NoError(t, err) m, err := reader.GetCallRates(context.Background(), ¶ms) assert.NotNil(t, m) require.ErrorContains(t, err, "failed executing metrics query") require.Len(t, exp.GetSpans(), 1, "HTTP request was traced and span reported") assert.Equal(t, codes.Error, exp.GetSpans()[0].Status.Code) } func TestGetLatencies(t *testing.T) { for _, tc := range []metricsTestCase{ { name: "group by service should be reflected in name/description and query group-by", serviceNames: []string{"emailservice"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOperation: false, wantName: "service_latencies", wantDescription: "0.95th quantile latency, grouped by service", wantLabels: map[string]string{ "service_name": "emailservice", }, wantPromQlQuery: `histogram_quantile(0.95, sum(rate(duration_bucket{service_name =~ "emailservice", ` + `span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name,le))`, }, { name: "group by service and operation should be reflected in name/description and query group-by", serviceNames: []string{"emailservice"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOperation: true, wantName: "service_operation_latencies", wantDescription: "0.95th quantile latency, grouped by service & operation", wantLabels: map[string]string{ "operation": "/OrderResult", "service_name": "emailservice", }, wantPromQlQuery: `histogram_quantile(0.95, sum(rate(duration_bucket{service_name =~ "emailservice", ` + `span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name,span_name,le))`, }, { name: "two services and span kinds result in regex 'or' symbol in query", serviceNames: []string{"frontend", "emailservice"}, spanKinds: []string{"SPAN_KIND_SERVER", "SPAN_KIND_CLIENT"}, groupByOperation: false, wantName: "service_latencies", wantDescription: "0.95th quantile latency, grouped by service", wantLabels: map[string]string{ "service_name": "emailservice", }, wantPromQlQuery: `histogram_quantile(0.95, sum(rate(duration_bucket{service_name =~ "frontend|emailservice", ` + `span_kind =~ "SPAN_KIND_SERVER|SPAN_KIND_CLIENT"}[10m])) by (service_name,le))`, }, { name: "enable support for spanmetrics connector with a namespace", serviceNames: []string{"emailservice"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOperation: true, updateConfig: func(cfg config.Configuration) config.Configuration { cfg.MetricNamespace = "span_metrics" cfg.LatencyUnit = "s" return cfg }, wantName: "service_operation_latencies", wantDescription: "0.95th quantile latency, grouped by service & operation", wantLabels: map[string]string{ "operation": "/OrderResult", "service_name": "emailservice", }, wantPromQlQuery: `histogram_quantile(0.95, sum(rate(span_metrics_duration_bucket{service_name =~ "emailservice", ` + `span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name,span_name,le))`, }, { name: "enable support for spanmetrics connector with normalized metric name", serviceNames: []string{"emailservice"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOperation: true, updateConfig: func(cfg config.Configuration) config.Configuration { cfg.NormalizeDuration = true cfg.LatencyUnit = "s" return cfg }, wantName: "service_operation_latencies", wantDescription: "0.95th quantile latency, grouped by service & operation", wantLabels: map[string]string{ "operation": "/OrderResult", "service_name": "emailservice", }, wantPromQlQuery: `histogram_quantile(0.95, sum(rate(duration_seconds_bucket{service_name =~ "emailservice", ` + `span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name,span_name,le))`, }, } { t.Run(tc.name, func(t *testing.T) { params := metricstore.LatenciesQueryParameters{ BaseQueryParameters: buildTestBaseQueryParametersFrom(tc), Quantile: 0.95, } tracer, exp, closer := tracerProvider(t) defer closer() cfg := defaultConfig if tc.updateConfig != nil { cfg = tc.updateConfig(cfg) } reader, mockPrometheus := prepareMetricsReaderAndServer(t, cfg, tc.wantPromQlQuery, nil, tracer) defer mockPrometheus.Close() m, err := reader.GetLatencies(context.Background(), ¶ms) require.NoError(t, err) assertMetrics(t, m, tc.wantLabels, tc.wantName, tc.wantDescription) assert.Len(t, exp.GetSpans(), 1, "HTTP request was traced and span reported") }) } } func TestGetCallRates(t *testing.T) { for _, tc := range []metricsTestCase{ { name: "group by service only should be reflected in name/description and query group-by", serviceNames: []string{"emailservice"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOperation: false, wantName: "service_call_rate", wantDescription: "calls/sec, grouped by service", wantLabels: map[string]string{ "service_name": "emailservice", }, wantPromQlQuery: `sum(rate(calls{service_name =~ "emailservice", ` + `span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name)`, }, { name: "group by service and operation should be reflected in name/description and query group-by", serviceNames: []string{"emailservice"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOperation: true, wantName: "service_operation_call_rate", wantDescription: "calls/sec, grouped by service & operation", wantLabels: map[string]string{ "operation": "/OrderResult", "service_name": "emailservice", }, wantPromQlQuery: `sum(rate(calls{service_name =~ "emailservice", ` + `span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name,span_name)`, }, { name: "two services and span kinds result in regex 'or' symbol in query", serviceNames: []string{"frontend", "emailservice"}, spanKinds: []string{"SPAN_KIND_SERVER", "SPAN_KIND_CLIENT"}, groupByOperation: false, wantName: "service_call_rate", wantDescription: "calls/sec, grouped by service", wantLabels: map[string]string{ "service_name": "emailservice", }, wantPromQlQuery: `sum(rate(calls{service_name =~ "frontend|emailservice", ` + `span_kind =~ "SPAN_KIND_SERVER|SPAN_KIND_CLIENT"}[10m])) by (service_name)`, }, { name: "enable support for spanmetrics connector with a namespace", serviceNames: []string{"emailservice"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOperation: true, updateConfig: func(cfg config.Configuration) config.Configuration { cfg.MetricNamespace = "span_metrics" return cfg }, wantName: "service_operation_call_rate", wantDescription: "calls/sec, grouped by service & operation", wantLabels: map[string]string{ "operation": "/OrderResult", "service_name": "emailservice", }, wantPromQlQuery: `sum(rate(span_metrics_calls{service_name =~ "emailservice", ` + `span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name,span_name)`, }, { name: "enable support for spanmetrics connector with normalized metric name", serviceNames: []string{"emailservice"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOperation: true, updateConfig: func(cfg config.Configuration) config.Configuration { cfg.NormalizeCalls = true return cfg }, wantName: "service_operation_call_rate", wantDescription: "calls/sec, grouped by service & operation", wantLabels: map[string]string{ "operation": "/OrderResult", "service_name": "emailservice", }, wantPromQlQuery: `sum(rate(calls_total{service_name =~ "emailservice", ` + `span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name,span_name)`, }, } { t.Run(tc.name, func(t *testing.T) { params := metricstore.CallRateQueryParameters{ BaseQueryParameters: buildTestBaseQueryParametersFrom(tc), } tracer, exp, closer := tracerProvider(t) defer closer() cfg := defaultConfig if tc.updateConfig != nil { cfg = tc.updateConfig(cfg) } reader, mockPrometheus := prepareMetricsReaderAndServer(t, cfg, tc.wantPromQlQuery, nil, tracer) defer mockPrometheus.Close() m, err := reader.GetCallRates(context.Background(), ¶ms) require.NoError(t, err) assertMetrics(t, m, tc.wantLabels, tc.wantName, tc.wantDescription) assert.Len(t, exp.GetSpans(), 1, "HTTP request was traced and span reported") }) } } func TestGetErrorRates(t *testing.T) { for _, tc := range []metricsTestCase{ { name: "group by service only should be reflected in name/description and query group-by", serviceNames: []string{"emailservice"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOperation: false, wantName: "service_error_rate", wantDescription: "error rate, computed as a fraction of errors/sec over calls/sec, grouped by service", wantLabels: map[string]string{ "service_name": "emailservice", }, wantPromQlQuery: `sum(rate(calls{service_name =~ "emailservice", status_code = "STATUS_CODE_ERROR", ` + `span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name) / ` + `sum(rate(calls{service_name =~ "emailservice", span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name)`, }, { name: "group by service and operation should be reflected in name/description and query group-by", serviceNames: []string{"emailservice"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOperation: true, wantName: "service_operation_error_rate", wantDescription: "error rate, computed as a fraction of errors/sec over calls/sec, grouped by service & operation", wantLabels: map[string]string{ "operation": "/OrderResult", "service_name": "emailservice", }, wantPromQlQuery: `sum(rate(calls{service_name =~ "emailservice", status_code = "STATUS_CODE_ERROR", ` + `span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name,span_name) / ` + `sum(rate(calls{service_name =~ "emailservice", span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name,span_name)`, }, { name: "two services and span kinds result in regex 'or' symbol in query", serviceNames: []string{"frontend", "emailservice"}, spanKinds: []string{"SPAN_KIND_SERVER", "SPAN_KIND_CLIENT"}, groupByOperation: false, wantName: "service_error_rate", wantDescription: "error rate, computed as a fraction of errors/sec over calls/sec, grouped by service", wantLabels: map[string]string{ "service_name": "emailservice", }, wantPromQlQuery: `sum(rate(calls{service_name =~ "frontend|emailservice", status_code = "STATUS_CODE_ERROR", ` + `span_kind =~ "SPAN_KIND_SERVER|SPAN_KIND_CLIENT"}[10m])) by (service_name) / ` + `sum(rate(calls{service_name =~ "frontend|emailservice", span_kind =~ "SPAN_KIND_SERVER|SPAN_KIND_CLIENT"}[10m])) by (service_name)`, }, { name: "neither metric namespace nor enabling normalized metric names have an impact when spanmetrics connector is not supported", serviceNames: []string{"emailservice"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOperation: false, updateConfig: func(cfg config.Configuration) config.Configuration { cfg.MetricNamespace = "span_metrics" cfg.NormalizeCalls = true return cfg }, wantName: "service_error_rate", wantDescription: "error rate, computed as a fraction of errors/sec over calls/sec, grouped by service", wantLabels: map[string]string{ "service_name": "emailservice", }, wantPromQlQuery: `sum(rate(span_metrics_calls_total{service_name =~ "emailservice", status_code = "STATUS_CODE_ERROR", ` + `span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name) / ` + `sum(rate(span_metrics_calls_total{service_name =~ "emailservice", span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name)`, }, { name: "enable support for spanmetrics connector with a metric namespace", serviceNames: []string{"emailservice"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOperation: true, updateConfig: func(cfg config.Configuration) config.Configuration { cfg.MetricNamespace = "span_metrics" return cfg }, wantName: "service_operation_error_rate", wantDescription: "error rate, computed as a fraction of errors/sec over calls/sec, grouped by service & operation", wantLabels: map[string]string{ "operation": "/OrderResult", "service_name": "emailservice", }, wantPromQlQuery: `sum(rate(span_metrics_calls{service_name =~ "emailservice", status_code = "STATUS_CODE_ERROR", ` + `span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name,span_name) / ` + `sum(rate(span_metrics_calls{service_name =~ "emailservice", span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name,span_name)`, }, { name: "enable support for spanmetrics connector with normalized metric name", serviceNames: []string{"emailservice"}, spanKinds: []string{"SPAN_KIND_SERVER"}, groupByOperation: true, updateConfig: func(cfg config.Configuration) config.Configuration { cfg.NormalizeCalls = true return cfg }, wantName: "service_operation_error_rate", wantDescription: "error rate, computed as a fraction of errors/sec over calls/sec, grouped by service & operation", wantLabels: map[string]string{ "operation": "/OrderResult", "service_name": "emailservice", }, wantPromQlQuery: `sum(rate(calls_total{service_name =~ "emailservice", status_code = "STATUS_CODE_ERROR", ` + `span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name,span_name) / ` + `sum(rate(calls_total{service_name =~ "emailservice", span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name,span_name)`, }, } { t.Run(tc.name, func(t *testing.T) { params := metricstore.ErrorRateQueryParameters{ BaseQueryParameters: buildTestBaseQueryParametersFrom(tc), } tracer, exp, closer := tracerProvider(t) defer closer() cfg := defaultConfig if tc.updateConfig != nil { cfg = tc.updateConfig(cfg) } reader, mockPrometheus := prepareMetricsReaderAndServer(t, cfg, tc.wantPromQlQuery, nil, tracer) defer mockPrometheus.Close() m, err := reader.GetErrorRates(context.Background(), ¶ms) require.NoError(t, err) assertMetrics(t, m, tc.wantLabels, tc.wantName, tc.wantDescription) assert.Len(t, exp.GetSpans(), 1, "HTTP request was traced and span reported") }) } } func TestGetErrorRatesZero(t *testing.T) { params := metricstore.ErrorRateQueryParameters{ BaseQueryParameters: buildTestBaseQueryParametersFrom(metricsTestCase{ serviceNames: []string{"emailservice"}, spanKinds: []string{"SPAN_KIND_SERVER"}, }), } tracer, exp, closer := tracerProvider(t) defer closer() const ( queryErrorRate = `sum(rate(calls{service_name =~ "emailservice", status_code = "STATUS_CODE_ERROR", ` + `span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name) / ` + `sum(rate(calls{service_name =~ "emailservice", span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name)` queryCallRate = `sum(rate(calls{service_name =~ "emailservice", ` + `span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name)` ) wantPromQLQueries := []string{queryErrorRate, queryCallRate} responses := []string{"testdata/empty_response.json", "testdata/service_datapoint_response.json"} var callCount int mockPrometheus := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) defer r.Body.Close() u, err := url.Parse("http://" + r.Host + r.RequestURI + "?" + string(body)) if assert.NoError(t, err) { q := u.Query() promQuery := q.Get("query") assert.Equal(t, wantPromQLQueries[callCount], promQuery) sendResponse(t, w, responses[callCount]) callCount++ } })) logger := zap.NewNop() address := mockPrometheus.Listener.Addr().String() cfg := defaultConfig cfg.ServerURL = "http://" + address cfg.ConnectTimeout = defaultTimeout reader, err := NewMetricsReader(cfg, logger, tracer, nil) require.NoError(t, err) defer mockPrometheus.Close() m, err := reader.GetErrorRates(context.Background(), ¶ms) require.NoError(t, err) require.Len(t, m.Metrics, 1) mps := m.Metrics[0].MetricPoints require.Len(t, mps, 1) // Assert that we essentially zeroed the call rate data point. // That is, the timestamp is the same as the call rate's data point, but the value is 0. actualVal := mps[0].Value.(*metrics.MetricPoint_GaugeValue).GaugeValue.Value.(*metrics.GaugeValue_DoubleValue).DoubleValue assert.Zero(t, actualVal) assert.Equal(t, int64(1620351786), mps[0].Timestamp.GetSeconds()) assert.Len(t, exp.GetSpans(), 2, "expected an error rate query and a call rate query to be made") } func TestGetErrorRatesNull(t *testing.T) { params := metricstore.ErrorRateQueryParameters{ BaseQueryParameters: buildTestBaseQueryParametersFrom(metricsTestCase{ serviceNames: []string{"emailservice"}, spanKinds: []string{"SPAN_KIND_SERVER"}, }), } tracer, exp, closer := tracerProvider(t) defer closer() const ( queryErrorRate = `sum(rate(calls{service_name =~ "emailservice", status_code = "STATUS_CODE_ERROR", ` + `span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name) / ` + `sum(rate(calls{service_name =~ "emailservice", span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name)` queryCallRate = `sum(rate(calls{service_name =~ "emailservice", ` + `span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name)` ) wantPromQLQueries := []string{queryErrorRate, queryCallRate} responses := []string{"testdata/empty_response.json", "testdata/empty_response.json"} var callCount int mockPrometheus := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) defer r.Body.Close() u, err := url.Parse("http://" + r.Host + r.RequestURI + "?" + string(body)) assert.NoError(t, err) q := u.Query() promQuery := q.Get("query") assert.Equal(t, wantPromQLQueries[callCount], promQuery) sendResponse(t, w, responses[callCount]) callCount++ })) logger := zap.NewNop() address := mockPrometheus.Listener.Addr().String() cfg := defaultConfig cfg.ServerURL = "http://" + address cfg.ConnectTimeout = defaultTimeout reader, err := NewMetricsReader(cfg, logger, tracer, nil) require.NoError(t, err) defer mockPrometheus.Close() m, err := reader.GetErrorRates(context.Background(), ¶ms) require.NoError(t, err) assert.Empty(t, m.Metrics, "expect no error data available") assert.Len(t, exp.GetSpans(), 2, "expected an error rate query and a call rate query to be made") } func TestGetErrorRatesErrors(t *testing.T) { for _, tc := range []struct { name string failErrorRateQuery bool failCallRateQuery bool wantErr string }{ { name: "error rate query failure", failErrorRateQuery: true, wantErr: "failed getting error metrics: failed executing metrics query: server_error: server error: 500", }, { name: "call rate query failure", failCallRateQuery: true, wantErr: "failed getting call metrics: failed executing metrics query: server_error: server error: 500", }, } { t.Run(tc.name, func(t *testing.T) { params := metricstore.ErrorRateQueryParameters{ BaseQueryParameters: buildTestBaseQueryParametersFrom(metricsTestCase{ serviceNames: []string{"emailservice"}, spanKinds: []string{"SPAN_KIND_SERVER"}, }), } tracer, _, closer := tracerProvider(t) defer closer() const ( queryErrorRate = `sum(rate(calls{service_name =~ "emailservice", status_code = "STATUS_CODE_ERROR", ` + `span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name) / ` + `sum(rate(calls{service_name =~ "emailservice", span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name)` queryCallRate = `sum(rate(calls{service_name =~ "emailservice", ` + `span_kind =~ "SPAN_KIND_SERVER"}[10m])) by (service_name)` ) wantPromQLQueries := []string{queryErrorRate, queryCallRate} responses := []string{"testdata/empty_response.json", "testdata/service_datapoint_response.json"} var callCount int mockPrometheus := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) defer r.Body.Close() u, err := url.Parse("http://" + r.Host + r.RequestURI + "?" + string(body)) assert.NoError(t, err) q := u.Query() promQuery := q.Get("query") assert.Equal(t, wantPromQLQueries[callCount], promQuery) switch promQuery { case queryErrorRate: if tc.failErrorRateQuery { w.WriteHeader(http.StatusInternalServerError) } else { sendResponse(t, w, responses[callCount]) } case queryCallRate: if tc.failCallRateQuery { w.WriteHeader(http.StatusInternalServerError) } else { sendResponse(t, w, responses[callCount]) } default: t.Errorf("Unexpected Prometheus query: %s", promQuery) } callCount++ })) logger := zap.NewNop() address := mockPrometheus.Listener.Addr().String() cfg := defaultConfig cfg.ServerURL = "http://" + address cfg.ConnectTimeout = defaultTimeout reader, err := NewMetricsReader(cfg, logger, tracer, nil) require.NoError(t, err) defer mockPrometheus.Close() _, err = reader.GetErrorRates(context.Background(), ¶ms) require.Error(t, err) require.EqualError(t, err, tc.wantErr) }) } } func TestInvalidLatencyUnit(t *testing.T) { defer func() { if r := recover(); r == nil { t.Error("Expected a panic due to invalid latency unit") } }() tracer, _, closer := tracerProvider(t) defer closer() cfg := config.Configuration{ NormalizeDuration: true, LatencyUnit: "something invalid", } _, _ = NewMetricsReader(cfg, zap.NewNop(), tracer, nil) } func TestWarningResponse(t *testing.T) { params := metricstore.ErrorRateQueryParameters{ BaseQueryParameters: buildTestBaseQueryParametersFrom(metricsTestCase{serviceNames: []string{"foo"}}), } tracer, exp, closer := tracerProvider(t) defer closer() reader, mockPrometheus := prepareMetricsReaderAndServer(t, config.Configuration{}, "", []string{"warning0", "warning1"}, tracer) defer mockPrometheus.Close() m, err := reader.GetErrorRates(context.Background(), ¶ms) require.NoError(t, err) assert.NotNil(t, m) assert.Len(t, exp.GetSpans(), 2, "expected an error rate query and a call rate query to be made") } type fakePromServer struct { *httptest.Server authReceived atomic.Pointer[string] } func newFakePromServer(t *testing.T) *fakePromServer { s := &fakePromServer{} s.Server = httptest.NewServer( http.HandlerFunc( func(_ http.ResponseWriter, r *http.Request) { t.Logf("Request to fake Prometheus server %+v", r) h := r.Header.Get("Authorization") s.authReceived.Store(&h) }, ), ) return s } func (s *fakePromServer) getAuth() string { return *s.authReceived.Load() } func TestGetRoundTripperTLSConfig(t *testing.T) { for _, tc := range []struct { name string tlsConfig configtls.ClientConfig wantErr bool }{ { name: "tls enabled with cert verification", tlsConfig: configtls.ClientConfig{ Insecure: false, }, }, { name: "tls enabled insecure", tlsConfig: configtls.ClientConfig{ Insecure: true, // Skip verify }, }, { name: "invalid tls config", tlsConfig: configtls.ClientConfig{ Config: configtls.Config{ CAFile: "foo", }, }, wantErr: true, }, } { t.Run(tc.name, func(t *testing.T) { config := &config.Configuration{ ConnectTimeout: 9 * time.Millisecond, TLS: tc.tlsConfig, TokenOverrideFromContext: true, } rt, err := getHTTPRoundTripper(config, nil) if tc.wantErr { require.Error(t, err) return } require.NoError(t, err) server := newFakePromServer(t) defer server.Close() req, err := http.NewRequestWithContext( bearertoken.ContextWithBearerToken(context.Background(), "foo"), http.MethodGet, server.URL, http.NoBody, ) require.NoError(t, err) resp, err := rt.RoundTrip(req) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Equal(t, "Bearer foo", server.getAuth()) }) } } func TestGetRoundTripperTokenFile(t *testing.T) { const wantBearer = "token from file" file, err := os.Create(t.TempDir() + "token_") require.NoError(t, err) _, err = file.WriteString(wantBearer) require.NoError(t, err) require.NoError(t, file.Close()) rt, err := getHTTPRoundTripper(&config.Configuration{ ConnectTimeout: time.Second, TokenFilePath: file.Name(), TokenOverrideFromContext: false, }, nil) require.NoError(t, err) server := newFakePromServer(t) defer server.Close() ctx := bearertoken.ContextWithBearerToken(context.Background(), "tokenFromRequest") req, err := http.NewRequestWithContext( ctx, http.MethodGet, server.URL, http.NoBody, ) require.NoError(t, err) resp, err := rt.RoundTrip(req) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Equal(t, "Bearer "+wantBearer, server.getAuth()) } func TestGetRoundTripperTokenFromContext(t *testing.T) { file, err := os.Create(t.TempDir() + "token_") require.NoError(t, err) _, err = file.WriteString("token from file") require.NoError(t, err) require.NoError(t, file.Close()) rt, err := getHTTPRoundTripper(&config.Configuration{ ConnectTimeout: time.Second, TokenFilePath: file.Name(), TokenOverrideFromContext: true, }, nil) require.NoError(t, err) server := newFakePromServer(t) defer server.Close() ctx := bearertoken.ContextWithBearerToken(context.Background(), "tokenFromRequest") req, err := http.NewRequestWithContext( ctx, http.MethodGet, server.URL, http.NoBody, ) require.NoError(t, err) resp, err := rt.RoundTrip(req) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Equal(t, "Bearer tokenFromRequest", server.getAuth()) } func TestGetRoundTripperTokenError(t *testing.T) { tokenFilePath := "this file does not exist" _, err := getHTTPRoundTripper(&config.Configuration{ TokenFilePath: tokenFilePath, }, nil) assert.ErrorContains(t, err, "failed to get token from file") } func TestInvalidCertFile(t *testing.T) { logger := zap.NewNop() tracer, _, closer := tracerProvider(t) defer closer() reader, err := NewMetricsReader(config.Configuration{ ServerURL: "https://localhost:1234", ConnectTimeout: defaultTimeout, TLS: configtls.ClientConfig{ Config: configtls.Config{ CAFile: "foo", }, }, }, logger, tracer, nil) require.Error(t, err) assert.Nil(t, reader) } func TestCreatePromClientWithExtraQueryParameters(t *testing.T) { extraParams := map[string]string{ "param1": "value1", "param2": "value2", } cfg := config.Configuration{ ServerURL: "http://localhost:1234?param1=value0", ExtraQueryParams: extraParams, } expParams := map[string][]string{ "param1": {"value0", "value1"}, "param2": {"value2"}, } customClient, err := createPromClient(cfg, nil) require.NoError(t, err) u := customClient.URL("", nil) q := u.Query() for k, v := range expParams { sort.Strings(q[k]) require.Equal(t, v, q[k]) } } func startMockPrometheusServer(t *testing.T, wantPromQlQuery string, wantWarnings []string) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if len(wantWarnings) > 0 { sendResponse(t, w, "testdata/warning_response.json") return } body, _ := io.ReadAll(r.Body) defer r.Body.Close() u, err := url.Parse("http://" + r.Host + r.RequestURI + "?" + string(body)) assert.NoError(t, err) q := u.Query() promQuery := q.Get("query") assert.Equal(t, wantPromQlQuery, promQuery) mockResponsePayloadFile := "testdata/service_datapoint_response.json" if strings.Contains(promQuery, "by (service_name,span_name") { mockResponsePayloadFile = "testdata/service_span_name_datapoint_response.json" } sendResponse(t, w, mockResponsePayloadFile) })) } func sendResponse(t *testing.T, w http.ResponseWriter, responseFile string) { bytes, err := os.ReadFile(responseFile) require.NoError(t, err) _, err = w.Write(bytes) require.NoError(t, err) } func buildTestBaseQueryParametersFrom(tc metricsTestCase) metricstore.BaseQueryParameters { endTime := time.Now() lookback := time.Minute step := time.Millisecond ratePer := 10 * time.Minute return metricstore.BaseQueryParameters{ ServiceNames: tc.serviceNames, GroupByOperation: tc.groupByOperation, EndTime: &endTime, Lookback: &lookback, Step: &step, RatePer: &ratePer, SpanKinds: tc.spanKinds, } } func prepareMetricsReaderAndServer(t *testing.T, cfg config.Configuration, wantPromQlQuery string, wantWarnings []string, tracer trace.TracerProvider) (metricstore.Reader, *httptest.Server) { mockPrometheus := startMockPrometheusServer(t, wantPromQlQuery, wantWarnings) logger := zap.NewNop() address := mockPrometheus.Listener.Addr().String() cfg.ServerURL = "http://" + address cfg.ConnectTimeout = defaultTimeout reader, err := NewMetricsReader(cfg, logger, tracer, nil) require.NoError(t, err) return reader, mockPrometheus } func assertMetrics(t *testing.T, gotMetrics *metrics.MetricFamily, wantLabels map[string]string, wantName, wantDescription string) { assert.Len(t, gotMetrics.Metrics, 1) assert.Equal(t, wantName, gotMetrics.Name) assert.Equal(t, wantDescription, gotMetrics.Help) mps := gotMetrics.Metrics[0].MetricPoints assert.Len(t, mps, 1) // logging for expected and actual labels t.Logf("Expected labels: %v\n", wantLabels) t.Logf("Actual labels: %v\n", gotMetrics.Metrics[0].Labels) // There is no guaranteed order of labels, so we need to take the approach of using a map of expected values. labels := gotMetrics.Metrics[0].Labels assert.Len(t, labels, len(wantLabels)) for _, l := range labels { assert.Contains(t, wantLabels, l.Name) assert.Equal(t, wantLabels[l.Name], l.Value) delete(wantLabels, l.Name) } assert.Empty(t, wantLabels) // Additional logging to show that all expected labels were found and matched t.Logf("Remaining expected labels after matching: %v\n", wantLabels) t.Log("\n") assert.Equal(t, int64(1620351786), mps[0].Timestamp.GetSeconds()) actualVal := mps[0].Value.(*metrics.MetricPoint_GaugeValue).GaugeValue.Value.(*metrics.GaugeValue_DoubleValue).DoubleValue assert.InDelta(t, float64(9223372036854), actualVal, 0.01) } func TestNewMetricsReaderWithHTTPAuth(t *testing.T) { tests := []struct { name string httpAuth *mockHTTPAuthenticator }{ { name: "with HTTP authenticator", httpAuth: &mockHTTPAuthenticator{}, }, { name: "without HTTP authenticator", httpAuth: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { authHeaderReceived := "" mockPrometheus := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authHeaderReceived = r.Header.Get("Authorization") sendResponse(t, w, "testdata/service_datapoint_response.json") })) defer mockPrometheus.Close() logger := zap.NewNop() tracer, _, closer := tracerProvider(t) defer closer() cfg := config.Configuration{ ServerURL: mockPrometheus.URL, ConnectTimeout: defaultTimeout, } reader, err := NewMetricsReader(cfg, logger, tracer, tt.httpAuth) require.NoError(t, err) require.NotNil(t, reader) endTime := time.Now() lookback := time.Minute step := time.Millisecond ratePer := 10 * time.Minute params := metricstore.CallRateQueryParameters{ BaseQueryParameters: metricstore.BaseQueryParameters{ ServiceNames: []string{"emailservice"}, EndTime: &endTime, Lookback: &lookback, Step: &step, RatePer: &ratePer, }, } _, err = reader.GetCallRates(context.Background(), ¶ms) require.NoError(t, err) if tt.httpAuth != nil { assert.Equal(t, "Bearer sigv4-token", authHeaderReceived) } }) } } type mockHTTPAuthenticator struct{} func (*mockHTTPAuthenticator) RoundTripper(base http.RoundTripper) (http.RoundTripper, error) { return &mockAuthRoundTripper{base: base}, nil } type mockAuthRoundTripper struct { base http.RoundTripper } func (m *mockAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { req.Header.Set("Authorization", "Bearer sigv4-token") if m.base != nil { return m.base.RoundTrip(req) } return &http.Response{ StatusCode: http.StatusOK, Body: http.NoBody, }, nil } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/metricstore/prometheus/metricstore/testdata/empty_response.json ================================================ { "status": "success", "data": { "resultType": "matrix", "result": [] } } ================================================ FILE: internal/storage/metricstore/prometheus/metricstore/testdata/service_datapoint_response.json ================================================ { "status": "success", "data": { "resultType": "matrix", "result": [ { "metric": { "service_name": "emailservice" }, "values": [ [ 1620351786, "9223372036854" ] ] } ] } } ================================================ FILE: internal/storage/metricstore/prometheus/metricstore/testdata/service_span_name_datapoint_response.json ================================================ { "status": "success", "data": { "resultType": "matrix", "result": [ { "metric": { "span_name": "/OrderResult", "service_name": "emailservice" }, "values": [ [ 1620351786, "9223372036854" ] ] } ] } } ================================================ FILE: internal/storage/metricstore/prometheus/metricstore/testdata/warning_response.json ================================================ { "status": "warning", "warnings": ["warning0", "warning1"], "data": { "resultType": "matrix", "result": [] } } ================================================ FILE: internal/storage/metricstore/prometheus/options.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package prometheus import ( "flag" "fmt" "strings" "time" "github.com/spf13/viper" config "github.com/jaegertracing/jaeger/internal/config/promcfg" "github.com/jaegertracing/jaeger/internal/config/tlscfg" ) const ( prefix = "prometheus" suffixServerURL = ".server-url" suffixConnectTimeout = ".connect-timeout" suffixTokenFilePath = ".token-file" suffixOverrideFromContext = ".token-override-from-context" suffixMetricNamespace = ".query.namespace" suffixLatencyUnit = ".query.duration-unit" suffixNormalizeCalls = ".query.normalize-calls" suffixNormalizeDuration = ".query.normalize-duration" suffixExtraQueryParams = ".query.extra-query-params" defaultServerURL = "http://localhost:9090" defaultConnectTimeout = 30 * time.Second defaultTokenFilePath = "" // the default configuration here matches the default namespace in the span metrics connector defaultMetricNamespace = "traces_span_metrics" defaultLatencyUnit = "ms" defaultNormalizeCalls = false defaultNormalizeDuration = false ) // Options stores the configuration entries for this storage. type Options struct { config.Configuration `mapstructure:",squash"` } var tlsFlagsCfg = tlscfg.ClientFlagsConfig{Prefix: prefix} func DefaultConfig() config.Configuration { return config.Configuration{ ServerURL: defaultServerURL, ConnectTimeout: defaultConnectTimeout, MetricNamespace: defaultMetricNamespace, LatencyUnit: defaultLatencyUnit, NormalizeCalls: defaultNormalizeCalls, NormalizeDuration: defaultNormalizeCalls, } } // NewOptions creates a new Options struct. func NewOptions() *Options { return &Options{ Configuration: DefaultConfig(), } } // AddFlags from this storage to the CLI. func (*Options) AddFlags(flagSet *flag.FlagSet) { flagSet.String(prefix+suffixServerURL, defaultServerURL, "The Prometheus server's URL, must include the protocol scheme e.g. http://localhost:9090") flagSet.Duration(prefix+suffixConnectTimeout, defaultConnectTimeout, "The period to wait for a connection to Prometheus when executing queries.") flagSet.String(prefix+suffixTokenFilePath, defaultTokenFilePath, "The path to a file containing the bearer token which will be included when executing queries against the Prometheus API.") flagSet.Bool(prefix+suffixOverrideFromContext, true, "Whether the bearer token should be overridden from context (incoming request)") flagSet.String(prefix+suffixMetricNamespace, defaultMetricNamespace, `The metric namespace that is prefixed to the metric name. A '.' separator will be added between `+ `the namespace and the metric name.`) flagSet.String(prefix+suffixLatencyUnit, defaultLatencyUnit, `The units used for the "latency" histogram. It can be either "ms" or "s" and should be consistent with the `+ `histogram unit value set in the spanmetrics connector (see: `+ `https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/connector/spanmetricsconnector#configurations). `+ `This also helps jaeger-query determine the metric name when querying for "latency" metrics.`) flagSet.Bool(prefix+suffixNormalizeCalls, defaultNormalizeCalls, `Whether to normalize the "calls" metric name according to `+ `https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/pkg/translator/prometheus/README.md. `+ `For example: `+ `"calls" (not normalized) -> "calls_total" (normalized), `) flagSet.Bool(prefix+suffixNormalizeDuration, defaultNormalizeDuration, `Whether to normalize the "duration" metric name according to `+ `https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/pkg/translator/prometheus/README.md. `+ `For example: `+ `"duration_bucket" (not normalized) -> "duration_milliseconds_bucket (normalized)"`) flagSet.String(prefix+suffixExtraQueryParams, "", "A comma separated list of param=value pairs of query parameters, which are appended on all API requests to the Prometheus API. "+ "Example: param1=value2,param2=value2") tlsFlagsCfg.AddFlags(flagSet) } // InitFromViper initializes the options struct with values from Viper. func (opt *Options) InitFromViper(v *viper.Viper) error { opt.ServerURL = stripWhiteSpace(v.GetString(prefix + suffixServerURL)) opt.ConnectTimeout = v.GetDuration(prefix + suffixConnectTimeout) opt.TokenFilePath = v.GetString(prefix + suffixTokenFilePath) opt.MetricNamespace = v.GetString(prefix + suffixMetricNamespace) opt.LatencyUnit = v.GetString(prefix + suffixLatencyUnit) opt.NormalizeCalls = v.GetBool(prefix + suffixNormalizeCalls) opt.NormalizeDuration = v.GetBool(prefix + suffixNormalizeDuration) opt.TokenOverrideFromContext = v.GetBool(prefix + suffixOverrideFromContext) var err error opt.ExtraQueryParams, err = parseKV(stripWhiteSpace(v.GetString(prefix + suffixExtraQueryParams))) if err != nil { return fmt.Errorf("failed to parse extra query params: %w", err) } isValidUnit := map[string]bool{"ms": true, "s": true} if _, ok := isValidUnit[opt.LatencyUnit]; !ok { return fmt.Errorf(`duration-unit must be one of "ms" or "s", not %q`, opt.LatencyUnit) } tlsCfg, err := tlsFlagsCfg.InitFromViper(v) if err != nil { return fmt.Errorf("failed to process Prometheus TLS options: %w", err) } opt.TLS = tlsCfg return nil } // stripWhiteSpace removes all whitespace characters from a string. func stripWhiteSpace(str string) string { return strings.ReplaceAll(str, " ", "") } // parseKV parses a comma separated list of key=value pairs into a map func parseKV(input string) (map[string]string, error) { if input == "" { return map[string]string{}, nil } ret := map[string]string{} for entry := range strings.SplitSeq(input, ",") { kv := strings.Split(entry, "=") if len(kv) != 2 { return map[string]string{}, fmt.Errorf("failed to parse '%s'. Expected format: 'param1=value1,param2=value2'", input) } ret[kv[0]] = kv[1] } return ret, nil } ================================================ FILE: internal/storage/metricstore/prometheus/options_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package prometheus import ( "errors" "strconv" "testing" "github.com/stretchr/testify/assert" config "github.com/jaegertracing/jaeger/internal/config/promcfg" ) func TestCLI(t *testing.T) { opts := Options{ Configuration: config.Configuration{ ExtraQueryParams: map[string]string{"key1": "value1"}, }, } assert.Equal(t, map[string]string{"key1": "value1"}, opts.ExtraQueryParams) } func TestCLIError(t *testing.T) { _, err := parseKV("key1") assert.ErrorContains(t, err, "failed to parse 'key1'. Expected format: 'param1=value1,param2=value2'") } func TestParseKV(t *testing.T) { tests := []struct { input string expected map[string]string err error }{ { input: "", expected: map[string]string{}, err: nil, }, { input: "key1=value1", expected: map[string]string{"key1": "value1"}, err: nil, }, { input: "key1=value1,key2=value2", expected: map[string]string{"key1": "value1", "key2": "value2"}, err: nil, }, { input: "key1=value1,key2", expected: map[string]string{}, err: errors.New("failed to parse 'key1=value1,key2'. Expected format: 'param1=value1,param2=value2'"), }, } for i, test := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { kv, err := parseKV(test.input) assert.Equal(t, test.expected, kv) assert.Equal(t, test.err, err) }) } } ================================================ FILE: internal/storage/v1/api/README.md ================================================ The collection of different storage interfaces that are shared by two or more components If a storage is used by only one component, its interface should be defined in the component package, and implementations under `./plugin/storage/{db_name}/{store_type}/...`. ================================================ FILE: internal/storage/v1/api/dependencystore/empty_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dependencystore import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/api/dependencystore/interface.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package dependencystore import ( "context" "time" "github.com/jaegertracing/jaeger-idl/model/v1" ) // Writer stores service dependencies into storage. type Writer interface { WriteDependencies(ts time.Time, dependencies []model.DependencyLink) error } // Reader can load service dependencies from storage. type Reader interface { GetDependencies(ctx context.Context, endTs time.Time, lookback time.Duration) ([]model.DependencyLink, error) } ================================================ FILE: internal/storage/v1/api/dependencystore/mocks/mocks.go ================================================ // Copyright (c) The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // // Run 'make generate-mocks' to regenerate. // Code generated by mockery; DO NOT EDIT. // github.com/vektra/mockery // template: testify package mocks import ( "context" "time" "github.com/jaegertracing/jaeger-idl/model/v1" mock "github.com/stretchr/testify/mock" ) // NewReader creates a new instance of Reader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewReader(t interface { mock.TestingT Cleanup(func()) }) *Reader { mock := &Reader{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // Reader is an autogenerated mock type for the Reader type type Reader struct { mock.Mock } type Reader_Expecter struct { mock *mock.Mock } func (_m *Reader) EXPECT() *Reader_Expecter { return &Reader_Expecter{mock: &_m.Mock} } // GetDependencies provides a mock function for the type Reader func (_mock *Reader) GetDependencies(ctx context.Context, endTs time.Time, lookback time.Duration) ([]model.DependencyLink, error) { ret := _mock.Called(ctx, endTs, lookback) if len(ret) == 0 { panic("no return value specified for GetDependencies") } var r0 []model.DependencyLink var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, time.Time, time.Duration) ([]model.DependencyLink, error)); ok { return returnFunc(ctx, endTs, lookback) } if returnFunc, ok := ret.Get(0).(func(context.Context, time.Time, time.Duration) []model.DependencyLink); ok { r0 = returnFunc(ctx, endTs, lookback) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]model.DependencyLink) } } if returnFunc, ok := ret.Get(1).(func(context.Context, time.Time, time.Duration) error); ok { r1 = returnFunc(ctx, endTs, lookback) } else { r1 = ret.Error(1) } return r0, r1 } // Reader_GetDependencies_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDependencies' type Reader_GetDependencies_Call struct { *mock.Call } // GetDependencies is a helper method to define mock.On call // - ctx context.Context // - endTs time.Time // - lookback time.Duration func (_e *Reader_Expecter) GetDependencies(ctx interface{}, endTs interface{}, lookback interface{}) *Reader_GetDependencies_Call { return &Reader_GetDependencies_Call{Call: _e.mock.On("GetDependencies", ctx, endTs, lookback)} } func (_c *Reader_GetDependencies_Call) Run(run func(ctx context.Context, endTs time.Time, lookback time.Duration)) *Reader_GetDependencies_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 time.Time if args[1] != nil { arg1 = args[1].(time.Time) } var arg2 time.Duration if args[2] != nil { arg2 = args[2].(time.Duration) } run( arg0, arg1, arg2, ) }) return _c } func (_c *Reader_GetDependencies_Call) Return(dependencyLinks []model.DependencyLink, err error) *Reader_GetDependencies_Call { _c.Call.Return(dependencyLinks, err) return _c } func (_c *Reader_GetDependencies_Call) RunAndReturn(run func(ctx context.Context, endTs time.Time, lookback time.Duration) ([]model.DependencyLink, error)) *Reader_GetDependencies_Call { _c.Call.Return(run) return _c } ================================================ FILE: internal/storage/v1/api/doc.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 // Package storage is the collection of different storage interfaces that are shared by two or more components. // // If a storage is used by only one component, its interface should be defined in the component package, and implementations under ./plugin/storage/{db_name}/{store_type}/.... package storage ================================================ FILE: internal/storage/v1/api/empty_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package storage import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/api/metricstore/empty_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package metricstore import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/api/metricstore/interface.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package metricstore import ( "context" "time" "github.com/jaegertracing/jaeger/internal/proto-gen/api_v2/metrics" ) // Reader can load aggregated trace metrics from storage. type Reader interface { // GetLatencies gets the latency metrics for a specific quantile (e.g. 0.99) and list of services // grouped by service and optionally grouped by operation. GetLatencies(ctx context.Context, params *LatenciesQueryParameters) (*metrics.MetricFamily, error) // GetCallRates gets the call rate metrics for a given list of services grouped by service // and optionally grouped by operation. GetCallRates(ctx context.Context, params *CallRateQueryParameters) (*metrics.MetricFamily, error) // GetErrorRates gets the error rate metrics for a given list of services grouped by service // and optionally grouped by operation. GetErrorRates(ctx context.Context, params *ErrorRateQueryParameters) (*metrics.MetricFamily, error) // GetMinStepDuration gets the min time resolution supported by the backing metrics store, // e.g. 10s means the backend can only return data points that are at least 10s apart, not closer. GetMinStepDuration(ctx context.Context, params *MinStepDurationQueryParameters) (time.Duration, error) } // BaseQueryParameters contains the common set of parameters used by all metrics queries: // latency, call rate or error rate. type BaseQueryParameters struct { // ServiceNames are the service names to fetch metrics from. The results will be grouped by service_name. ServiceNames []string // GroupByOperation determines if the metrics returned should be grouped by operation. GroupByOperation bool // EndTime is the ending time of the time series query range. EndTime *time.Time // Lookback is the duration from the end_time to look back on for metrics data points. // For example, if set to 1h, the query would span from end_time-1h to end_time. Lookback *time.Duration // Step size is the duration between data points of the query results. // For example, if set to 5s, the results would produce a data point every 5 seconds from the (EndTime - Lookback) to EndTime. Step *time.Duration // RatePer is the duration in which the per-second rate of change is calculated for a cumulative counter metric. RatePer *time.Duration // SpanKinds is the list of span kinds to include (logical OR) in the resulting metrics aggregation. SpanKinds []string } // LatenciesQueryParameters contains the parameters required for latency metrics queries. type LatenciesQueryParameters struct { BaseQueryParameters // Quantile is the quantile to compute from latency histogram metrics. // Valid range: 0 - 1 (inclusive). // // e.g. 0.99 will return the 99th percentile or P99 which is the worst latency // observed from 99% of all spans for the given service (and operation). Quantile float64 } // CallRateQueryParameters contains the parameters required for call rate metrics queries. type CallRateQueryParameters struct { BaseQueryParameters } // ErrorRateQueryParameters contains the parameters required for error rate metrics queries. type ErrorRateQueryParameters struct { BaseQueryParameters } // MinStepDurationQueryParameters contains the parameters required for fetching the minimum step duration. type MinStepDurationQueryParameters struct{} ================================================ FILE: internal/storage/v1/api/metricstore/metricstoremetrics/decorator.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package metricstoremetrics import ( "context" "time" "github.com/jaegertracing/jaeger/internal/metrics" protometrics "github.com/jaegertracing/jaeger/internal/proto-gen/api_v2/metrics" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore" ) // ReadMetricsDecorator wraps a metricstore.Reader and collects metrics around each read operation. type ReadMetricsDecorator struct { reader metricstore.Reader getLatenciesMetrics *queryMetrics getCallRatesMetrics *queryMetrics getErrorRatesMetrics *queryMetrics getMinStepDurationMetrics *queryMetrics } type queryMetrics struct { Errors metrics.Counter `metric:"requests" tags:"result=err"` Successes metrics.Counter `metric:"requests" tags:"result=ok"` ErrLatency metrics.Timer `metric:"latency" tags:"result=err"` OKLatency metrics.Timer `metric:"latency" tags:"result=ok"` } func (q *queryMetrics) emit(err error, latency time.Duration) { if err != nil { q.Errors.Inc(1) q.ErrLatency.Record(latency) } else { q.Successes.Inc(1) q.OKLatency.Record(latency) } } // NewReadMetricsDecorator returns a new ReadMetricsDecorator. func NewReaderDecorator(reader metricstore.Reader, metricsFactory metrics.Factory) *ReadMetricsDecorator { return &ReadMetricsDecorator{ reader: reader, getLatenciesMetrics: buildQueryMetrics("get_latencies", metricsFactory), getCallRatesMetrics: buildQueryMetrics("get_call_rates", metricsFactory), getErrorRatesMetrics: buildQueryMetrics("get_error_rates", metricsFactory), getMinStepDurationMetrics: buildQueryMetrics("get_min_step_duration", metricsFactory), } } func buildQueryMetrics(operation string, metricsFactory metrics.Factory) *queryMetrics { qMetrics := &queryMetrics{} scoped := metricsFactory.Namespace(metrics.NSOptions{Name: "", Tags: map[string]string{"operation": operation}}) metrics.Init(qMetrics, scoped, nil) return qMetrics } // GetLatencies implements metricstore.Reader#GetLatencies func (m *ReadMetricsDecorator) GetLatencies(ctx context.Context, params *metricstore.LatenciesQueryParameters) (*protometrics.MetricFamily, error) { start := time.Now() retMe, err := m.reader.GetLatencies(ctx, params) m.getLatenciesMetrics.emit(err, time.Since(start)) return retMe, err } // GetCallRates implements metricstore.Reader#GetCallRates func (m *ReadMetricsDecorator) GetCallRates(ctx context.Context, params *metricstore.CallRateQueryParameters) (*protometrics.MetricFamily, error) { start := time.Now() retMe, err := m.reader.GetCallRates(ctx, params) m.getCallRatesMetrics.emit(err, time.Since(start)) return retMe, err } // GetErrorRates implements metricstore.Reader#GetErrorRates func (m *ReadMetricsDecorator) GetErrorRates(ctx context.Context, params *metricstore.ErrorRateQueryParameters) (*protometrics.MetricFamily, error) { start := time.Now() retMe, err := m.reader.GetErrorRates(ctx, params) m.getErrorRatesMetrics.emit(err, time.Since(start)) return retMe, err } // GetMinStepDuration implements metricstore.Reader#GetMinStepDuration func (m *ReadMetricsDecorator) GetMinStepDuration(ctx context.Context, params *metricstore.MinStepDurationQueryParameters) (time.Duration, error) { start := time.Now() retMe, err := m.reader.GetMinStepDuration(ctx, params) m.getMinStepDurationMetrics.emit(err, time.Since(start)) return retMe, err } ================================================ FILE: internal/storage/v1/api/metricstore/metricstoremetrics/decorator_test.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package metricstoremetrics_test import ( "context" "errors" "testing" "time" "github.com/stretchr/testify/assert" "github.com/jaegertracing/jaeger/internal/metricstest" protometrics "github.com/jaegertracing/jaeger/internal/proto-gen/api_v2/metrics" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore/metricstoremetrics" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore/mocks" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestSuccessfulUnderlyingCalls(t *testing.T) { mf := metricstest.NewFactory(0) mockReader := mocks.Reader{} mrs := metricstoremetrics.NewReaderDecorator(&mockReader, mf) glParams := &metricstore.LatenciesQueryParameters{} mockReader.On("GetLatencies", context.Background(), glParams). Return(&protometrics.MetricFamily{}, nil) mrs.GetLatencies(context.Background(), glParams) gcrParams := &metricstore.CallRateQueryParameters{} mockReader.On("GetCallRates", context.Background(), gcrParams). Return(&protometrics.MetricFamily{}, nil) mrs.GetCallRates(context.Background(), gcrParams) gerParams := &metricstore.ErrorRateQueryParameters{} mockReader.On("GetErrorRates", context.Background(), gerParams). Return(&protometrics.MetricFamily{}, nil) mrs.GetErrorRates(context.Background(), gerParams) msdParams := &metricstore.MinStepDurationQueryParameters{} mockReader.On("GetMinStepDuration", context.Background(), msdParams). Return(time.Second, nil) mrs.GetMinStepDuration(context.Background(), msdParams) counters, gauges := mf.Snapshot() wantCounts := map[string]int64{ "requests|operation=get_latencies|result=ok": 1, "requests|operation=get_latencies|result=err": 0, "requests|operation=get_call_rates|result=ok": 1, "requests|operation=get_call_rates|result=err": 0, "requests|operation=get_error_rates|result=ok": 1, "requests|operation=get_error_rates|result=err": 0, "requests|operation=get_min_step_duration|result=ok": 1, "requests|operation=get_min_step_duration|result=err": 0, } // This is not exhaustive. wantExistingKeys := []string{ "latency|operation=get_latencies|result=ok.P50", "latency|operation=get_error_rates|result=ok.P50", } // This is not exhaustive. wantNonExistentKeys := []string{ "latency|operation=get_latencies|result=err.P50", } checkExpectedExistingAndNonExistentCounters(t, counters, wantCounts, gauges, wantExistingKeys, wantNonExistentKeys) } func checkExpectedExistingAndNonExistentCounters(t *testing.T, actualCounters, expectedCounters, actualGauges map[string]int64, existingKeys, nonExistentKeys []string, ) { for k, v := range expectedCounters { assert.Equal(t, v, actualCounters[k], k) } for _, k := range existingKeys { _, ok := actualGauges[k] assert.True(t, ok) } for _, k := range nonExistentKeys { _, ok := actualGauges[k] assert.False(t, ok) } } func TestFailingUnderlyingCalls(t *testing.T) { mf := metricstest.NewFactory(0) mockReader := mocks.Reader{} mrs := metricstoremetrics.NewReaderDecorator(&mockReader, mf) glParams := &metricstore.LatenciesQueryParameters{} mockReader.On("GetLatencies", context.Background(), glParams). Return(&protometrics.MetricFamily{}, errors.New("failure")) mrs.GetLatencies(context.Background(), glParams) gcrParams := &metricstore.CallRateQueryParameters{} mockReader.On("GetCallRates", context.Background(), gcrParams). Return(&protometrics.MetricFamily{}, errors.New("failure")) mrs.GetCallRates(context.Background(), gcrParams) gerParams := &metricstore.ErrorRateQueryParameters{} mockReader.On("GetErrorRates", context.Background(), gerParams). Return(&protometrics.MetricFamily{}, errors.New("failure")) mrs.GetErrorRates(context.Background(), gerParams) msdParams := &metricstore.MinStepDurationQueryParameters{} mockReader.On("GetMinStepDuration", context.Background(), msdParams). Return(time.Second, errors.New("failure")) mrs.GetMinStepDuration(context.Background(), msdParams) counters, gauges := mf.Snapshot() wantCounts := map[string]int64{ "requests|operation=get_latencies|result=ok": 0, "requests|operation=get_latencies|result=err": 1, "requests|operation=get_call_rates|result=ok": 0, "requests|operation=get_call_rates|result=err": 1, "requests|operation=get_error_rates|result=ok": 0, "requests|operation=get_error_rates|result=err": 1, "requests|operation=get_min_step_duration|result=ok": 0, "requests|operation=get_min_step_duration|result=err": 1, } // This is not exhaustive. wantExistingKeys := []string{ "latency|operation=get_latencies|result=err.P50", } // This is not exhaustive. wantNonExistentKeys := []string{ "latency|operation=get_latencies|result=ok.P50", "latency|operation=get_error_rates|result=ok.P50", } checkExpectedExistingAndNonExistentCounters(t, counters, wantCounts, gauges, wantExistingKeys, wantNonExistentKeys) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/api/metricstore/mocks/mocks.go ================================================ // Copyright (c) The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // // Run 'make generate-mocks' to regenerate. // Code generated by mockery; DO NOT EDIT. // github.com/vektra/mockery // template: testify package mocks import ( "context" "time" "github.com/jaegertracing/jaeger/internal/proto-gen/api_v2/metrics" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore" mock "github.com/stretchr/testify/mock" ) // NewReader creates a new instance of Reader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewReader(t interface { mock.TestingT Cleanup(func()) }) *Reader { mock := &Reader{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // Reader is an autogenerated mock type for the Reader type type Reader struct { mock.Mock } type Reader_Expecter struct { mock *mock.Mock } func (_m *Reader) EXPECT() *Reader_Expecter { return &Reader_Expecter{mock: &_m.Mock} } // GetCallRates provides a mock function for the type Reader func (_mock *Reader) GetCallRates(ctx context.Context, params *metricstore.CallRateQueryParameters) (*metrics.MetricFamily, error) { ret := _mock.Called(ctx, params) if len(ret) == 0 { panic("no return value specified for GetCallRates") } var r0 *metrics.MetricFamily var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *metricstore.CallRateQueryParameters) (*metrics.MetricFamily, error)); ok { return returnFunc(ctx, params) } if returnFunc, ok := ret.Get(0).(func(context.Context, *metricstore.CallRateQueryParameters) *metrics.MetricFamily); ok { r0 = returnFunc(ctx, params) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*metrics.MetricFamily) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *metricstore.CallRateQueryParameters) error); ok { r1 = returnFunc(ctx, params) } else { r1 = ret.Error(1) } return r0, r1 } // Reader_GetCallRates_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetCallRates' type Reader_GetCallRates_Call struct { *mock.Call } // GetCallRates is a helper method to define mock.On call // - ctx context.Context // - params *metricstore.CallRateQueryParameters func (_e *Reader_Expecter) GetCallRates(ctx interface{}, params interface{}) *Reader_GetCallRates_Call { return &Reader_GetCallRates_Call{Call: _e.mock.On("GetCallRates", ctx, params)} } func (_c *Reader_GetCallRates_Call) Run(run func(ctx context.Context, params *metricstore.CallRateQueryParameters)) *Reader_GetCallRates_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *metricstore.CallRateQueryParameters if args[1] != nil { arg1 = args[1].(*metricstore.CallRateQueryParameters) } run( arg0, arg1, ) }) return _c } func (_c *Reader_GetCallRates_Call) Return(metricFamily *metrics.MetricFamily, err error) *Reader_GetCallRates_Call { _c.Call.Return(metricFamily, err) return _c } func (_c *Reader_GetCallRates_Call) RunAndReturn(run func(ctx context.Context, params *metricstore.CallRateQueryParameters) (*metrics.MetricFamily, error)) *Reader_GetCallRates_Call { _c.Call.Return(run) return _c } // GetErrorRates provides a mock function for the type Reader func (_mock *Reader) GetErrorRates(ctx context.Context, params *metricstore.ErrorRateQueryParameters) (*metrics.MetricFamily, error) { ret := _mock.Called(ctx, params) if len(ret) == 0 { panic("no return value specified for GetErrorRates") } var r0 *metrics.MetricFamily var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *metricstore.ErrorRateQueryParameters) (*metrics.MetricFamily, error)); ok { return returnFunc(ctx, params) } if returnFunc, ok := ret.Get(0).(func(context.Context, *metricstore.ErrorRateQueryParameters) *metrics.MetricFamily); ok { r0 = returnFunc(ctx, params) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*metrics.MetricFamily) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *metricstore.ErrorRateQueryParameters) error); ok { r1 = returnFunc(ctx, params) } else { r1 = ret.Error(1) } return r0, r1 } // Reader_GetErrorRates_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetErrorRates' type Reader_GetErrorRates_Call struct { *mock.Call } // GetErrorRates is a helper method to define mock.On call // - ctx context.Context // - params *metricstore.ErrorRateQueryParameters func (_e *Reader_Expecter) GetErrorRates(ctx interface{}, params interface{}) *Reader_GetErrorRates_Call { return &Reader_GetErrorRates_Call{Call: _e.mock.On("GetErrorRates", ctx, params)} } func (_c *Reader_GetErrorRates_Call) Run(run func(ctx context.Context, params *metricstore.ErrorRateQueryParameters)) *Reader_GetErrorRates_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *metricstore.ErrorRateQueryParameters if args[1] != nil { arg1 = args[1].(*metricstore.ErrorRateQueryParameters) } run( arg0, arg1, ) }) return _c } func (_c *Reader_GetErrorRates_Call) Return(metricFamily *metrics.MetricFamily, err error) *Reader_GetErrorRates_Call { _c.Call.Return(metricFamily, err) return _c } func (_c *Reader_GetErrorRates_Call) RunAndReturn(run func(ctx context.Context, params *metricstore.ErrorRateQueryParameters) (*metrics.MetricFamily, error)) *Reader_GetErrorRates_Call { _c.Call.Return(run) return _c } // GetLatencies provides a mock function for the type Reader func (_mock *Reader) GetLatencies(ctx context.Context, params *metricstore.LatenciesQueryParameters) (*metrics.MetricFamily, error) { ret := _mock.Called(ctx, params) if len(ret) == 0 { panic("no return value specified for GetLatencies") } var r0 *metrics.MetricFamily var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *metricstore.LatenciesQueryParameters) (*metrics.MetricFamily, error)); ok { return returnFunc(ctx, params) } if returnFunc, ok := ret.Get(0).(func(context.Context, *metricstore.LatenciesQueryParameters) *metrics.MetricFamily); ok { r0 = returnFunc(ctx, params) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*metrics.MetricFamily) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *metricstore.LatenciesQueryParameters) error); ok { r1 = returnFunc(ctx, params) } else { r1 = ret.Error(1) } return r0, r1 } // Reader_GetLatencies_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLatencies' type Reader_GetLatencies_Call struct { *mock.Call } // GetLatencies is a helper method to define mock.On call // - ctx context.Context // - params *metricstore.LatenciesQueryParameters func (_e *Reader_Expecter) GetLatencies(ctx interface{}, params interface{}) *Reader_GetLatencies_Call { return &Reader_GetLatencies_Call{Call: _e.mock.On("GetLatencies", ctx, params)} } func (_c *Reader_GetLatencies_Call) Run(run func(ctx context.Context, params *metricstore.LatenciesQueryParameters)) *Reader_GetLatencies_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *metricstore.LatenciesQueryParameters if args[1] != nil { arg1 = args[1].(*metricstore.LatenciesQueryParameters) } run( arg0, arg1, ) }) return _c } func (_c *Reader_GetLatencies_Call) Return(metricFamily *metrics.MetricFamily, err error) *Reader_GetLatencies_Call { _c.Call.Return(metricFamily, err) return _c } func (_c *Reader_GetLatencies_Call) RunAndReturn(run func(ctx context.Context, params *metricstore.LatenciesQueryParameters) (*metrics.MetricFamily, error)) *Reader_GetLatencies_Call { _c.Call.Return(run) return _c } // GetMinStepDuration provides a mock function for the type Reader func (_mock *Reader) GetMinStepDuration(ctx context.Context, params *metricstore.MinStepDurationQueryParameters) (time.Duration, error) { ret := _mock.Called(ctx, params) if len(ret) == 0 { panic("no return value specified for GetMinStepDuration") } var r0 time.Duration var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *metricstore.MinStepDurationQueryParameters) (time.Duration, error)); ok { return returnFunc(ctx, params) } if returnFunc, ok := ret.Get(0).(func(context.Context, *metricstore.MinStepDurationQueryParameters) time.Duration); ok { r0 = returnFunc(ctx, params) } else { r0 = ret.Get(0).(time.Duration) } if returnFunc, ok := ret.Get(1).(func(context.Context, *metricstore.MinStepDurationQueryParameters) error); ok { r1 = returnFunc(ctx, params) } else { r1 = ret.Error(1) } return r0, r1 } // Reader_GetMinStepDuration_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetMinStepDuration' type Reader_GetMinStepDuration_Call struct { *mock.Call } // GetMinStepDuration is a helper method to define mock.On call // - ctx context.Context // - params *metricstore.MinStepDurationQueryParameters func (_e *Reader_Expecter) GetMinStepDuration(ctx interface{}, params interface{}) *Reader_GetMinStepDuration_Call { return &Reader_GetMinStepDuration_Call{Call: _e.mock.On("GetMinStepDuration", ctx, params)} } func (_c *Reader_GetMinStepDuration_Call) Run(run func(ctx context.Context, params *metricstore.MinStepDurationQueryParameters)) *Reader_GetMinStepDuration_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *metricstore.MinStepDurationQueryParameters if args[1] != nil { arg1 = args[1].(*metricstore.MinStepDurationQueryParameters) } run( arg0, arg1, ) }) return _c } func (_c *Reader_GetMinStepDuration_Call) Return(duration time.Duration, err error) *Reader_GetMinStepDuration_Call { _c.Call.Return(duration, err) return _c } func (_c *Reader_GetMinStepDuration_Call) RunAndReturn(run func(ctx context.Context, params *metricstore.MinStepDurationQueryParameters) (time.Duration, error)) *Reader_GetMinStepDuration_Call { _c.Call.Return(run) return _c } ================================================ FILE: internal/storage/v1/api/samplingstore/empty_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package samplingstore import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/api/samplingstore/interface.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package samplingstore import ( "time" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore/model" ) // Store writes and retrieves sampling data to and from storage. type Store interface { // InsertThroughput inserts aggregated throughput for operations into storage. InsertThroughput(throughput []*model.Throughput) error // InsertProbabilitiesAndQPS inserts calculated sampling probabilities and measured qps into storage. InsertProbabilitiesAndQPS(hostname string, probabilities model.ServiceOperationProbabilities, qps model.ServiceOperationQPS) error // GetThroughput retrieves aggregated throughput for operations within a time range. GetThroughput(start, end time.Time) ([]*model.Throughput, error) // GetLatestProbabilities retrieves the latest sampling probabilities. GetLatestProbabilities() (model.ServiceOperationProbabilities, error) } ================================================ FILE: internal/storage/v1/api/samplingstore/mocks/mocks.go ================================================ // Copyright (c) The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // // Run 'make generate-mocks' to regenerate. // Code generated by mockery; DO NOT EDIT. // github.com/vektra/mockery // template: testify package mocks import ( "time" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore/model" mock "github.com/stretchr/testify/mock" ) // NewStore creates a new instance of Store. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewStore(t interface { mock.TestingT Cleanup(func()) }) *Store { mock := &Store{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // Store is an autogenerated mock type for the Store type type Store struct { mock.Mock } type Store_Expecter struct { mock *mock.Mock } func (_m *Store) EXPECT() *Store_Expecter { return &Store_Expecter{mock: &_m.Mock} } // GetLatestProbabilities provides a mock function for the type Store func (_mock *Store) GetLatestProbabilities() (model.ServiceOperationProbabilities, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for GetLatestProbabilities") } var r0 model.ServiceOperationProbabilities var r1 error if returnFunc, ok := ret.Get(0).(func() (model.ServiceOperationProbabilities, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() model.ServiceOperationProbabilities); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(model.ServiceOperationProbabilities) } } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // Store_GetLatestProbabilities_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLatestProbabilities' type Store_GetLatestProbabilities_Call struct { *mock.Call } // GetLatestProbabilities is a helper method to define mock.On call func (_e *Store_Expecter) GetLatestProbabilities() *Store_GetLatestProbabilities_Call { return &Store_GetLatestProbabilities_Call{Call: _e.mock.On("GetLatestProbabilities")} } func (_c *Store_GetLatestProbabilities_Call) Run(run func()) *Store_GetLatestProbabilities_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *Store_GetLatestProbabilities_Call) Return(serviceOperationProbabilities model.ServiceOperationProbabilities, err error) *Store_GetLatestProbabilities_Call { _c.Call.Return(serviceOperationProbabilities, err) return _c } func (_c *Store_GetLatestProbabilities_Call) RunAndReturn(run func() (model.ServiceOperationProbabilities, error)) *Store_GetLatestProbabilities_Call { _c.Call.Return(run) return _c } // GetThroughput provides a mock function for the type Store func (_mock *Store) GetThroughput(start time.Time, end time.Time) ([]*model.Throughput, error) { ret := _mock.Called(start, end) if len(ret) == 0 { panic("no return value specified for GetThroughput") } var r0 []*model.Throughput var r1 error if returnFunc, ok := ret.Get(0).(func(time.Time, time.Time) ([]*model.Throughput, error)); ok { return returnFunc(start, end) } if returnFunc, ok := ret.Get(0).(func(time.Time, time.Time) []*model.Throughput); ok { r0 = returnFunc(start, end) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.Throughput) } } if returnFunc, ok := ret.Get(1).(func(time.Time, time.Time) error); ok { r1 = returnFunc(start, end) } else { r1 = ret.Error(1) } return r0, r1 } // Store_GetThroughput_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetThroughput' type Store_GetThroughput_Call struct { *mock.Call } // GetThroughput is a helper method to define mock.On call // - start time.Time // - end time.Time func (_e *Store_Expecter) GetThroughput(start interface{}, end interface{}) *Store_GetThroughput_Call { return &Store_GetThroughput_Call{Call: _e.mock.On("GetThroughput", start, end)} } func (_c *Store_GetThroughput_Call) Run(run func(start time.Time, end time.Time)) *Store_GetThroughput_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 time.Time if args[0] != nil { arg0 = args[0].(time.Time) } var arg1 time.Time if args[1] != nil { arg1 = args[1].(time.Time) } run( arg0, arg1, ) }) return _c } func (_c *Store_GetThroughput_Call) Return(throughputs []*model.Throughput, err error) *Store_GetThroughput_Call { _c.Call.Return(throughputs, err) return _c } func (_c *Store_GetThroughput_Call) RunAndReturn(run func(start time.Time, end time.Time) ([]*model.Throughput, error)) *Store_GetThroughput_Call { _c.Call.Return(run) return _c } // InsertProbabilitiesAndQPS provides a mock function for the type Store func (_mock *Store) InsertProbabilitiesAndQPS(hostname string, probabilities model.ServiceOperationProbabilities, qps model.ServiceOperationQPS) error { ret := _mock.Called(hostname, probabilities, qps) if len(ret) == 0 { panic("no return value specified for InsertProbabilitiesAndQPS") } var r0 error if returnFunc, ok := ret.Get(0).(func(string, model.ServiceOperationProbabilities, model.ServiceOperationQPS) error); ok { r0 = returnFunc(hostname, probabilities, qps) } else { r0 = ret.Error(0) } return r0 } // Store_InsertProbabilitiesAndQPS_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InsertProbabilitiesAndQPS' type Store_InsertProbabilitiesAndQPS_Call struct { *mock.Call } // InsertProbabilitiesAndQPS is a helper method to define mock.On call // - hostname string // - probabilities model.ServiceOperationProbabilities // - qps model.ServiceOperationQPS func (_e *Store_Expecter) InsertProbabilitiesAndQPS(hostname interface{}, probabilities interface{}, qps interface{}) *Store_InsertProbabilitiesAndQPS_Call { return &Store_InsertProbabilitiesAndQPS_Call{Call: _e.mock.On("InsertProbabilitiesAndQPS", hostname, probabilities, qps)} } func (_c *Store_InsertProbabilitiesAndQPS_Call) Run(run func(hostname string, probabilities model.ServiceOperationProbabilities, qps model.ServiceOperationQPS)) *Store_InsertProbabilitiesAndQPS_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } var arg1 model.ServiceOperationProbabilities if args[1] != nil { arg1 = args[1].(model.ServiceOperationProbabilities) } var arg2 model.ServiceOperationQPS if args[2] != nil { arg2 = args[2].(model.ServiceOperationQPS) } run( arg0, arg1, arg2, ) }) return _c } func (_c *Store_InsertProbabilitiesAndQPS_Call) Return(err error) *Store_InsertProbabilitiesAndQPS_Call { _c.Call.Return(err) return _c } func (_c *Store_InsertProbabilitiesAndQPS_Call) RunAndReturn(run func(hostname string, probabilities model.ServiceOperationProbabilities, qps model.ServiceOperationQPS) error) *Store_InsertProbabilitiesAndQPS_Call { _c.Call.Return(run) return _c } // InsertThroughput provides a mock function for the type Store func (_mock *Store) InsertThroughput(throughput []*model.Throughput) error { ret := _mock.Called(throughput) if len(ret) == 0 { panic("no return value specified for InsertThroughput") } var r0 error if returnFunc, ok := ret.Get(0).(func([]*model.Throughput) error); ok { r0 = returnFunc(throughput) } else { r0 = ret.Error(0) } return r0 } // Store_InsertThroughput_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InsertThroughput' type Store_InsertThroughput_Call struct { *mock.Call } // InsertThroughput is a helper method to define mock.On call // - throughput []*model.Throughput func (_e *Store_Expecter) InsertThroughput(throughput interface{}) *Store_InsertThroughput_Call { return &Store_InsertThroughput_Call{Call: _e.mock.On("InsertThroughput", throughput)} } func (_c *Store_InsertThroughput_Call) Run(run func(throughput []*model.Throughput)) *Store_InsertThroughput_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 []*model.Throughput if args[0] != nil { arg0 = args[0].([]*model.Throughput) } run( arg0, ) }) return _c } func (_c *Store_InsertThroughput_Call) Return(err error) *Store_InsertThroughput_Call { _c.Call.Return(err) return _c } func (_c *Store_InsertThroughput_Call) RunAndReturn(run func(throughput []*model.Throughput) error) *Store_InsertThroughput_Call { _c.Call.Return(run) return _c } ================================================ FILE: internal/storage/v1/api/samplingstore/model/empty_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package model import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/api/samplingstore/model/sampling.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package model // Throughput keeps track of the queries an operation received. type Throughput struct { Service string Operation string Count int64 Probabilities map[string]struct{} } // ServiceOperationProbabilities contains the sampling probabilities for all operations in a service. // ie [service][operation] = probability type ServiceOperationProbabilities map[string]map[string]float64 // ServiceOperationQPS contains the qps for all operations in a service. // ie [service][operation] = qps type ServiceOperationQPS map[string]map[string]float64 ================================================ FILE: internal/storage/v1/api/spanstore/interface.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "context" "errors" "time" "github.com/jaegertracing/jaeger-idl/model/v1" ) // ErrTraceNotFound is returned by Reader's GetTrace if no data is found for given trace ID. var ErrTraceNotFound = errors.New("trace not found") // Writer writes spans to storage. type Writer interface { WriteSpan(ctx context.Context, span *model.Span) error } // Reader finds and loads traces and other data from storage. type Reader interface { // GetTrace retrieves the trace with a given id. // // If no spans are stored for this trace, it returns ErrTraceNotFound. GetTrace(ctx context.Context, query GetTraceParameters) (*model.Trace, error) // GetServices returns all service names known to the backend from spans // within its retention period. GetServices(ctx context.Context) ([]string, error) // GetOperations returns all operation names for a given service // known to the backend from spans within its retention period. GetOperations(ctx context.Context, query OperationQueryParameters) ([]Operation, error) // FindTraces returns all traces matching query parameters. There's currently // an implementation-dependent abiguity whether all query filters (such as // multiple tags) must apply to the same span within a trace, or can be satisfied // by different spans. // // If no matching traces are found, the function returns (nil, nil). FindTraces(ctx context.Context, query *TraceQueryParameters) ([]*model.Trace, error) // FindTraceIDs does the same search as FindTraces, but returns only the list // of matching trace IDs. // // If no matching traces are found, the function returns (nil, nil). FindTraceIDs(ctx context.Context, query *TraceQueryParameters) ([]model.TraceID, error) } // GetTraceParameters contains parameters of a trace get. type GetTraceParameters struct { TraceID model.TraceID StartTime time.Time // optional EndTime time.Time // optional } // TraceQueryParameters contains parameters of a trace query. type TraceQueryParameters struct { ServiceName string OperationName string Tags map[string]string StartTimeMin time.Time StartTimeMax time.Time DurationMin time.Duration DurationMax time.Duration NumTraces int } // OperationQueryParameters contains parameters of query operations, empty spanKind means get operations for all kinds of span. type OperationQueryParameters struct { ServiceName string SpanKind string } // Operation contains operation name and span kind type Operation struct { Name string SpanKind string } ================================================ FILE: internal/storage/v1/api/spanstore/interface_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/api/spanstore/mocks/mocks.go ================================================ // Copyright (c) The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // // Run 'make generate-mocks' to regenerate. // Code generated by mockery; DO NOT EDIT. // github.com/vektra/mockery // template: testify package mocks import ( "context" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" mock "github.com/stretchr/testify/mock" ) // NewWriter creates a new instance of Writer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewWriter(t interface { mock.TestingT Cleanup(func()) }) *Writer { mock := &Writer{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // Writer is an autogenerated mock type for the Writer type type Writer struct { mock.Mock } type Writer_Expecter struct { mock *mock.Mock } func (_m *Writer) EXPECT() *Writer_Expecter { return &Writer_Expecter{mock: &_m.Mock} } // WriteSpan provides a mock function for the type Writer func (_mock *Writer) WriteSpan(ctx context.Context, span *model.Span) error { ret := _mock.Called(ctx, span) if len(ret) == 0 { panic("no return value specified for WriteSpan") } var r0 error if returnFunc, ok := ret.Get(0).(func(context.Context, *model.Span) error); ok { r0 = returnFunc(ctx, span) } else { r0 = ret.Error(0) } return r0 } // Writer_WriteSpan_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteSpan' type Writer_WriteSpan_Call struct { *mock.Call } // WriteSpan is a helper method to define mock.On call // - ctx context.Context // - span *model.Span func (_e *Writer_Expecter) WriteSpan(ctx interface{}, span interface{}) *Writer_WriteSpan_Call { return &Writer_WriteSpan_Call{Call: _e.mock.On("WriteSpan", ctx, span)} } func (_c *Writer_WriteSpan_Call) Run(run func(ctx context.Context, span *model.Span)) *Writer_WriteSpan_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *model.Span if args[1] != nil { arg1 = args[1].(*model.Span) } run( arg0, arg1, ) }) return _c } func (_c *Writer_WriteSpan_Call) Return(err error) *Writer_WriteSpan_Call { _c.Call.Return(err) return _c } func (_c *Writer_WriteSpan_Call) RunAndReturn(run func(ctx context.Context, span *model.Span) error) *Writer_WriteSpan_Call { _c.Call.Return(run) return _c } // NewReader creates a new instance of Reader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewReader(t interface { mock.TestingT Cleanup(func()) }) *Reader { mock := &Reader{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // Reader is an autogenerated mock type for the Reader type type Reader struct { mock.Mock } type Reader_Expecter struct { mock *mock.Mock } func (_m *Reader) EXPECT() *Reader_Expecter { return &Reader_Expecter{mock: &_m.Mock} } // FindTraceIDs provides a mock function for the type Reader func (_mock *Reader) FindTraceIDs(ctx context.Context, query *spanstore.TraceQueryParameters) ([]model.TraceID, error) { ret := _mock.Called(ctx, query) if len(ret) == 0 { panic("no return value specified for FindTraceIDs") } var r0 []model.TraceID var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *spanstore.TraceQueryParameters) ([]model.TraceID, error)); ok { return returnFunc(ctx, query) } if returnFunc, ok := ret.Get(0).(func(context.Context, *spanstore.TraceQueryParameters) []model.TraceID); ok { r0 = returnFunc(ctx, query) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]model.TraceID) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *spanstore.TraceQueryParameters) error); ok { r1 = returnFunc(ctx, query) } else { r1 = ret.Error(1) } return r0, r1 } // Reader_FindTraceIDs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindTraceIDs' type Reader_FindTraceIDs_Call struct { *mock.Call } // FindTraceIDs is a helper method to define mock.On call // - ctx context.Context // - query *spanstore.TraceQueryParameters func (_e *Reader_Expecter) FindTraceIDs(ctx interface{}, query interface{}) *Reader_FindTraceIDs_Call { return &Reader_FindTraceIDs_Call{Call: _e.mock.On("FindTraceIDs", ctx, query)} } func (_c *Reader_FindTraceIDs_Call) Run(run func(ctx context.Context, query *spanstore.TraceQueryParameters)) *Reader_FindTraceIDs_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *spanstore.TraceQueryParameters if args[1] != nil { arg1 = args[1].(*spanstore.TraceQueryParameters) } run( arg0, arg1, ) }) return _c } func (_c *Reader_FindTraceIDs_Call) Return(traceIDs []model.TraceID, err error) *Reader_FindTraceIDs_Call { _c.Call.Return(traceIDs, err) return _c } func (_c *Reader_FindTraceIDs_Call) RunAndReturn(run func(ctx context.Context, query *spanstore.TraceQueryParameters) ([]model.TraceID, error)) *Reader_FindTraceIDs_Call { _c.Call.Return(run) return _c } // FindTraces provides a mock function for the type Reader func (_mock *Reader) FindTraces(ctx context.Context, query *spanstore.TraceQueryParameters) ([]*model.Trace, error) { ret := _mock.Called(ctx, query) if len(ret) == 0 { panic("no return value specified for FindTraces") } var r0 []*model.Trace var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *spanstore.TraceQueryParameters) ([]*model.Trace, error)); ok { return returnFunc(ctx, query) } if returnFunc, ok := ret.Get(0).(func(context.Context, *spanstore.TraceQueryParameters) []*model.Trace); ok { r0 = returnFunc(ctx, query) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.Trace) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *spanstore.TraceQueryParameters) error); ok { r1 = returnFunc(ctx, query) } else { r1 = ret.Error(1) } return r0, r1 } // Reader_FindTraces_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindTraces' type Reader_FindTraces_Call struct { *mock.Call } // FindTraces is a helper method to define mock.On call // - ctx context.Context // - query *spanstore.TraceQueryParameters func (_e *Reader_Expecter) FindTraces(ctx interface{}, query interface{}) *Reader_FindTraces_Call { return &Reader_FindTraces_Call{Call: _e.mock.On("FindTraces", ctx, query)} } func (_c *Reader_FindTraces_Call) Run(run func(ctx context.Context, query *spanstore.TraceQueryParameters)) *Reader_FindTraces_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *spanstore.TraceQueryParameters if args[1] != nil { arg1 = args[1].(*spanstore.TraceQueryParameters) } run( arg0, arg1, ) }) return _c } func (_c *Reader_FindTraces_Call) Return(traces []*model.Trace, err error) *Reader_FindTraces_Call { _c.Call.Return(traces, err) return _c } func (_c *Reader_FindTraces_Call) RunAndReturn(run func(ctx context.Context, query *spanstore.TraceQueryParameters) ([]*model.Trace, error)) *Reader_FindTraces_Call { _c.Call.Return(run) return _c } // GetOperations provides a mock function for the type Reader func (_mock *Reader) GetOperations(ctx context.Context, query spanstore.OperationQueryParameters) ([]spanstore.Operation, error) { ret := _mock.Called(ctx, query) if len(ret) == 0 { panic("no return value specified for GetOperations") } var r0 []spanstore.Operation var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, spanstore.OperationQueryParameters) ([]spanstore.Operation, error)); ok { return returnFunc(ctx, query) } if returnFunc, ok := ret.Get(0).(func(context.Context, spanstore.OperationQueryParameters) []spanstore.Operation); ok { r0 = returnFunc(ctx, query) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]spanstore.Operation) } } if returnFunc, ok := ret.Get(1).(func(context.Context, spanstore.OperationQueryParameters) error); ok { r1 = returnFunc(ctx, query) } else { r1 = ret.Error(1) } return r0, r1 } // Reader_GetOperations_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetOperations' type Reader_GetOperations_Call struct { *mock.Call } // GetOperations is a helper method to define mock.On call // - ctx context.Context // - query spanstore.OperationQueryParameters func (_e *Reader_Expecter) GetOperations(ctx interface{}, query interface{}) *Reader_GetOperations_Call { return &Reader_GetOperations_Call{Call: _e.mock.On("GetOperations", ctx, query)} } func (_c *Reader_GetOperations_Call) Run(run func(ctx context.Context, query spanstore.OperationQueryParameters)) *Reader_GetOperations_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 spanstore.OperationQueryParameters if args[1] != nil { arg1 = args[1].(spanstore.OperationQueryParameters) } run( arg0, arg1, ) }) return _c } func (_c *Reader_GetOperations_Call) Return(operations []spanstore.Operation, err error) *Reader_GetOperations_Call { _c.Call.Return(operations, err) return _c } func (_c *Reader_GetOperations_Call) RunAndReturn(run func(ctx context.Context, query spanstore.OperationQueryParameters) ([]spanstore.Operation, error)) *Reader_GetOperations_Call { _c.Call.Return(run) return _c } // GetServices provides a mock function for the type Reader func (_mock *Reader) GetServices(ctx context.Context) ([]string, error) { ret := _mock.Called(ctx) if len(ret) == 0 { panic("no return value specified for GetServices") } var r0 []string var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context) ([]string, error)); ok { return returnFunc(ctx) } if returnFunc, ok := ret.Get(0).(func(context.Context) []string); ok { r0 = returnFunc(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]string) } } if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { r1 = returnFunc(ctx) } else { r1 = ret.Error(1) } return r0, r1 } // Reader_GetServices_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetServices' type Reader_GetServices_Call struct { *mock.Call } // GetServices is a helper method to define mock.On call // - ctx context.Context func (_e *Reader_Expecter) GetServices(ctx interface{}) *Reader_GetServices_Call { return &Reader_GetServices_Call{Call: _e.mock.On("GetServices", ctx)} } func (_c *Reader_GetServices_Call) Run(run func(ctx context.Context)) *Reader_GetServices_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } run( arg0, ) }) return _c } func (_c *Reader_GetServices_Call) Return(strings []string, err error) *Reader_GetServices_Call { _c.Call.Return(strings, err) return _c } func (_c *Reader_GetServices_Call) RunAndReturn(run func(ctx context.Context) ([]string, error)) *Reader_GetServices_Call { _c.Call.Return(run) return _c } // GetTrace provides a mock function for the type Reader func (_mock *Reader) GetTrace(ctx context.Context, query spanstore.GetTraceParameters) (*model.Trace, error) { ret := _mock.Called(ctx, query) if len(ret) == 0 { panic("no return value specified for GetTrace") } var r0 *model.Trace var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, spanstore.GetTraceParameters) (*model.Trace, error)); ok { return returnFunc(ctx, query) } if returnFunc, ok := ret.Get(0).(func(context.Context, spanstore.GetTraceParameters) *model.Trace); ok { r0 = returnFunc(ctx, query) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Trace) } } if returnFunc, ok := ret.Get(1).(func(context.Context, spanstore.GetTraceParameters) error); ok { r1 = returnFunc(ctx, query) } else { r1 = ret.Error(1) } return r0, r1 } // Reader_GetTrace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTrace' type Reader_GetTrace_Call struct { *mock.Call } // GetTrace is a helper method to define mock.On call // - ctx context.Context // - query spanstore.GetTraceParameters func (_e *Reader_Expecter) GetTrace(ctx interface{}, query interface{}) *Reader_GetTrace_Call { return &Reader_GetTrace_Call{Call: _e.mock.On("GetTrace", ctx, query)} } func (_c *Reader_GetTrace_Call) Run(run func(ctx context.Context, query spanstore.GetTraceParameters)) *Reader_GetTrace_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 spanstore.GetTraceParameters if args[1] != nil { arg1 = args[1].(spanstore.GetTraceParameters) } run( arg0, arg1, ) }) return _c } func (_c *Reader_GetTrace_Call) Return(trace *model.Trace, err error) *Reader_GetTrace_Call { _c.Call.Return(trace, err) return _c } func (_c *Reader_GetTrace_Call) RunAndReturn(run func(ctx context.Context, query spanstore.GetTraceParameters) (*model.Trace, error)) *Reader_GetTrace_Call { _c.Call.Return(run) return _c } ================================================ FILE: internal/storage/v1/api/spanstore/spanstoremetrics/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package spanstoremetrics import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/api/spanstore/spanstoremetrics/read_metrics.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstoremetrics import ( "context" "time" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" ) // ReadMetricsDecorator wraps a spanstore.Reader and collects metrics around each read operation. type ReadMetricsDecorator struct { spanReader spanstore.Reader findTracesMetrics *queryMetrics findTraceIDsMetrics *queryMetrics getTraceMetrics *queryMetrics getServicesMetrics *queryMetrics getOperationsMetrics *queryMetrics } type queryMetrics struct { Errors metrics.Counter `metric:"requests" tags:"result=err"` Successes metrics.Counter `metric:"requests" tags:"result=ok"` Responses metrics.Timer `metric:"responses"` // used as a histogram, not necessary for GetTrace ErrLatency metrics.Timer `metric:"latency" tags:"result=err"` OKLatency metrics.Timer `metric:"latency" tags:"result=ok"` } func (q *queryMetrics) emit(err error, latency time.Duration, responses int) { if err != nil { q.Errors.Inc(1) q.ErrLatency.Record(latency) } else { q.Successes.Inc(1) q.OKLatency.Record(latency) q.Responses.Record(time.Duration(responses)) } } // NewReaderDecorator returns a new ReadMetricsDecorator. func NewReaderDecorator(spanReader spanstore.Reader, metricsFactory metrics.Factory) *ReadMetricsDecorator { return &ReadMetricsDecorator{ spanReader: spanReader, findTracesMetrics: buildQueryMetrics("find_traces", metricsFactory), findTraceIDsMetrics: buildQueryMetrics("find_trace_ids", metricsFactory), getTraceMetrics: buildQueryMetrics("get_trace", metricsFactory), getServicesMetrics: buildQueryMetrics("get_services", metricsFactory), getOperationsMetrics: buildQueryMetrics("get_operations", metricsFactory), } } func buildQueryMetrics(operation string, metricsFactory metrics.Factory) *queryMetrics { qMetrics := &queryMetrics{} scoped := metricsFactory.Namespace(metrics.NSOptions{Name: "", Tags: map[string]string{"operation": operation}}) metrics.Init(qMetrics, scoped, nil) return qMetrics } // FindTraces implements spanstore.Reader#FindTraces func (m *ReadMetricsDecorator) FindTraces(ctx context.Context, traceQuery *spanstore.TraceQueryParameters) ([]*model.Trace, error) { start := time.Now() retMe, err := m.spanReader.FindTraces(ctx, traceQuery) m.findTracesMetrics.emit(err, time.Since(start), len(retMe)) return retMe, err } // FindTraceIDs implements spanstore.Reader#FindTraceIDs func (m *ReadMetricsDecorator) FindTraceIDs(ctx context.Context, traceQuery *spanstore.TraceQueryParameters) ([]model.TraceID, error) { start := time.Now() retMe, err := m.spanReader.FindTraceIDs(ctx, traceQuery) m.findTraceIDsMetrics.emit(err, time.Since(start), len(retMe)) return retMe, err } // GetTrace implements spanstore.Reader#GetTrace func (m *ReadMetricsDecorator) GetTrace(ctx context.Context, query spanstore.GetTraceParameters) (*model.Trace, error) { start := time.Now() retMe, err := m.spanReader.GetTrace(ctx, query) m.getTraceMetrics.emit(err, time.Since(start), 1) return retMe, err } // GetServices implements spanstore.Reader#GetServices func (m *ReadMetricsDecorator) GetServices(ctx context.Context) ([]string, error) { start := time.Now() retMe, err := m.spanReader.GetServices(ctx) m.getServicesMetrics.emit(err, time.Since(start), len(retMe)) return retMe, err } // GetOperations implements spanstore.Reader#GetOperations func (m *ReadMetricsDecorator) GetOperations( ctx context.Context, query spanstore.OperationQueryParameters, ) ([]spanstore.Operation, error) { start := time.Now() retMe, err := m.spanReader.GetOperations(ctx, query) m.getOperationsMetrics.emit(err, time.Since(start), len(retMe)) return retMe, err } ================================================ FILE: internal/storage/v1/api/spanstore/spanstoremetrics/read_metrics_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstoremetrics_test import ( "context" "errors" "testing" "github.com/stretchr/testify/assert" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/metricstest" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore/mocks" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore/spanstoremetrics" ) func TestSuccessfulUnderlyingCalls(t *testing.T) { mf := metricstest.NewFactory(0) mockReader := mocks.Reader{} mrs := spanstoremetrics.NewReaderDecorator(&mockReader, mf) mockReader.On("GetServices", context.Background()).Return([]string{}, nil) mrs.GetServices(context.Background()) operationQuery := spanstore.OperationQueryParameters{ServiceName: "something"} mockReader.On("GetOperations", context.Background(), operationQuery). Return([]spanstore.Operation{}, nil) mrs.GetOperations(context.Background(), operationQuery) mockReader.On("GetTrace", context.Background(), spanstore.GetTraceParameters{}).Return(&model.Trace{}, nil) mrs.GetTrace(context.Background(), spanstore.GetTraceParameters{}) mockReader.On("FindTraces", context.Background(), &spanstore.TraceQueryParameters{}). Return([]*model.Trace{}, nil) mrs.FindTraces(context.Background(), &spanstore.TraceQueryParameters{}) mockReader.On("FindTraceIDs", context.Background(), &spanstore.TraceQueryParameters{}). Return([]model.TraceID{}, nil) mrs.FindTraceIDs(context.Background(), &spanstore.TraceQueryParameters{}) counters, gauges := mf.Snapshot() expecteds := map[string]int64{ "requests|operation=get_operations|result=ok": 1, "requests|operation=get_operations|result=err": 0, "requests|operation=get_trace|result=ok": 1, "requests|operation=get_trace|result=err": 0, "requests|operation=find_traces|result=ok": 1, "requests|operation=find_traces|result=err": 0, "requests|operation=find_trace_ids|result=ok": 1, "requests|operation=find_trace_ids|result=err": 0, "requests|operation=get_services|result=ok": 1, "requests|operation=get_services|result=err": 0, } existingKeys := []string{ "latency|operation=get_operations|result=ok.P50", "responses|operation=get_trace.P50", "latency|operation=find_traces|result=ok.P50", // this is not exhaustive } nonExistentKeys := []string{ "latency|operation=get_operations|result=err.P50", } checkExpectedExistingAndNonExistentCounters(t, counters, expecteds, gauges, existingKeys, nonExistentKeys) } func checkExpectedExistingAndNonExistentCounters(t *testing.T, actualCounters, expectedCounters, actualGauges map[string]int64, existingKeys, nonExistentKeys []string, ) { for k, v := range expectedCounters { assert.Equal(t, v, actualCounters[k], k) } for _, k := range existingKeys { _, ok := actualGauges[k] assert.True(t, ok) } for _, k := range nonExistentKeys { _, ok := actualGauges[k] assert.False(t, ok) } } func TestFailingUnderlyingCalls(t *testing.T) { mf := metricstest.NewFactory(0) mockReader := mocks.Reader{} mrs := spanstoremetrics.NewReaderDecorator(&mockReader, mf) mockReader.On("GetServices", context.Background()). Return(nil, errors.New("Failure")) mrs.GetServices(context.Background()) operationQuery := spanstore.OperationQueryParameters{ServiceName: "something"} mockReader.On("GetOperations", context.Background(), operationQuery). Return(nil, errors.New("Failure")) mrs.GetOperations(context.Background(), operationQuery) mockReader.On("GetTrace", context.Background(), spanstore.GetTraceParameters{}). Return(nil, errors.New("Failure")) mrs.GetTrace(context.Background(), spanstore.GetTraceParameters{}) mockReader.On("FindTraces", context.Background(), &spanstore.TraceQueryParameters{}). Return(nil, errors.New("Failure")) mrs.FindTraces(context.Background(), &spanstore.TraceQueryParameters{}) mockReader.On("FindTraceIDs", context.Background(), &spanstore.TraceQueryParameters{}). Return(nil, errors.New("Failure")) mrs.FindTraceIDs(context.Background(), &spanstore.TraceQueryParameters{}) counters, gauges := mf.Snapshot() expecteds := map[string]int64{ "requests|operation=get_operations|result=ok": 0, "requests|operation=get_operations|result=err": 1, "requests|operation=get_trace|result=ok": 0, "requests|operation=get_trace|result=err": 1, "requests|operation=find_traces|result=ok": 0, "requests|operation=find_traces|result=err": 1, "requests|operation=find_trace_ids|result=ok": 0, "requests|operation=find_trace_ids|result=err": 1, "requests|operation=get_services|result=ok": 0, "requests|operation=get_services|result=err": 1, } existingKeys := []string{ "latency|operation=get_operations|result=err.P50", } nonExistentKeys := []string{ "latency|operation=get_operations|result=ok.P50", "responses|operation=get_trace.P50", "latency|operation=query|result=ok.P50", // this is not exhaustive } checkExpectedExistingAndNonExistentCounters(t, counters, expecteds, gauges, existingKeys, nonExistentKeys) } ================================================ FILE: internal/storage/v1/api/spanstore/spanstoremetrics/write_metrics.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstoremetrics import ( "time" "github.com/jaegertracing/jaeger/internal/metrics" ) // WriteMetrics is a collection of metrics for write operations. type WriteMetrics struct { Attempts metrics.Counter `metric:"attempts"` Inserts metrics.Counter `metric:"inserts"` Errors metrics.Counter `metric:"errors"` LatencyOk metrics.Timer `metric:"latency-ok"` LatencyErr metrics.Timer `metric:"latency-err"` } // NewWriter takes a metrics scope and creates a metrics struct func NewWriter(factory metrics.Factory, tableName string) *WriteMetrics { t := &WriteMetrics{} metrics.Init(t, factory.Namespace(metrics.NSOptions{Name: tableName, Tags: nil}), nil) return t } // Emit will record success or failure counts and latency metrics depending on the passed error. func (t *WriteMetrics) Emit(err error, latency time.Duration) { t.Attempts.Inc(1) if err != nil { t.LatencyErr.Record(latency) t.Errors.Inc(1) } else { t.LatencyOk.Record(latency) t.Inserts.Inc(1) } } ================================================ FILE: internal/storage/v1/api/spanstore/spanstoremetrics/write_metrics_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstoremetrics import ( "errors" "testing" "time" "github.com/stretchr/testify/assert" "github.com/jaegertracing/jaeger/internal/metricstest" ) func TestTableEmit(t *testing.T) { testCases := []struct { err error counts map[string]int64 gauges map[string]int64 }{ { err: nil, counts: map[string]int64{ "a_table.attempts": 1, "a_table.inserts": 1, }, gauges: map[string]int64{ "a_table.latency-ok.P999": 50, "a_table.latency-ok.P50": 50, "a_table.latency-ok.P75": 50, "a_table.latency-ok.P90": 50, "a_table.latency-ok.P95": 50, "a_table.latency-ok.P99": 50, }, }, { err: errors.New("some error"), counts: map[string]int64{ "a_table.attempts": 1, "a_table.errors": 1, }, gauges: map[string]int64{ "a_table.latency-err.P999": 50, "a_table.latency-err.P50": 50, "a_table.latency-err.P75": 50, "a_table.latency-err.P90": 50, "a_table.latency-err.P95": 50, "a_table.latency-err.P99": 50, }, }, } for _, tc := range testCases { mf := metricstest.NewFactory(time.Second) tm := NewWriter(mf, "a_table") tm.Emit(tc.err, 50*time.Millisecond) counts, gauges := mf.Snapshot() assert.Equal(t, tc.counts, counts) assert.Equal(t, tc.gauges, gauges) mf.Stop() } } ================================================ FILE: internal/storage/v1/badger/README.md ================================================ # Badger data storage ## Data modeling The key design in badger storage backend takes advantage of sorted nature of badger as well as the key only searching with badger, which does not require loading the values from the value log but only the keys in the LSM tree. This is used to implement efficient inverted index for both lookups as well as range searches. All the values in the keys must be stored in big endian ordering to make the sorting work properly. Index keys structure is created in the ``createIndexKey`` in ``spanstore/writer.go`` and the primary key for spans in ``createTraceKV``. ### Primary key design Primary keys are the only keys that have a value in the badger's storage. Each key presents a single span, thus a single trace is a collection of tuples. The value is the actual span, which is marshalled into bytes. The marshalling format is indicated by the last 4 bits of the meta encoding byte in the badger entry. Primary keys are sorted as follows: * TraceID High * TraceID Low * Timestamp * SpanID This allows quick lookup for a single TraceID by searching for prefix with: 0x80 + traceID high + traceID low and then iterating as long as that prefix is valid. Note that timestamp ordering does not allow fetching a range of traces in a time range. ### Index key design Each index key has a single byte first to indicate which field is indexed. The last 4 bits of the first byte in the key are used to indicate which index key is used, with the first 4 bits being zeroed. This sorts the LSM tree by index field which allows quicker range queries. Each inverted index key is then sorted in the following order: * Value * Timestamp * TraceID High * TraceID Low That means the scanning for a single value can continue until we reach the first timestamp which is not in the boundaries and then stop since we can guarantee the future keys are not going to be valid. ## Index searches If the lookup is a single traceID, the logic mentioned in the ``Primary key design`` section is used. If instead we have a TraceQueryParameters with one or more search keys to use, we need to combine the results of multiple index seeks to form an intersection of those results. Each search parameter (each tag is new search parameter) is used to scan single index key, thus we iterate the index until the ```` is no longer valid. We do this by checking the prefix for ```` for exactness and then ```` for range. As long as that one is valid, we fetch the keys. Once the timestamp goes beyond our maximum timestamp, the iteration stops. The keys are then sorted to ``TraceID`` order instead of their natural key ordering for the next part. Exception to the above is the duration index, since there are no exact duration values but a range of values. When scanning it, the prefix search lookups the starting point with ```` and scans the index until ```` is reached. Each key is then separately checked for valid ```` but the timestamp does not control the seek process and some keys are ignored because they did not match the given time range. Because each TraceID is stored as spans, the same TraceID can appear multiple times from a index query. Other than duration query, this means they are coming in order so each of them is discarded by easily checking if the previous one is equal to current one, but with the duration index the spans can come in random order and thus hash-join is used to filter the duplicates. After all the index keys have been scanned, the process is then sent to the merge-join where two index queries are compared and only matching IDs are taken. After that, the next one is compared to the result of the previous and so forth until all the index fetches have been processed. The resulting query set is the list of TraceIDs that matched all the requirements. ================================================ FILE: internal/storage/v1/badger/config.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package badger import ( "os" "path/filepath" "time" "github.com/asaskevich/govalidator" ) const ( defaultMaintenanceInterval time.Duration = 5 * time.Minute defaultMetricsUpdateInterval time.Duration = 10 * time.Second defaultTTL time.Duration = time.Hour * 72 defaultDataDir string = string(os.PathSeparator) + "data" ) var ( defaultValueDir string = filepath.Join(defaultDataDir, "values") defaultKeysDir string = filepath.Join(defaultDataDir, "keys") ) // Config is badger's internal configuration data. type Config struct { // TTL holds time-to-live configuration for the badger store. TTL TTL `mapstructure:"ttl"` // Directories contains the configuration for where items are stored. Ephemeral must be // set to false for this configuration to take effect. Directories Directories `mapstructure:"directories"` // Ephemeral, if set to true, will store data in a temporary file system. // If set to true, the configuration in Directories is ignored. Ephemeral bool `mapstructure:"ephemeral"` // SyncWrites, if set to true, will immediately sync all writes to disk. Note that // setting this field to true will affect write performance. SyncWrites bool `mapstructure:"consistency"` // MaintenanceInterval is the regular interval after which a maintenance job is // run on the values in the store. MaintenanceInterval time.Duration `mapstructure:"maintenance_interval"` // MetricsUpdateInterval is the regular interval after which metrics are collected // by Jaeger. MetricsUpdateInterval time.Duration `mapstructure:"metrics_update_interval"` // ReadOnly opens the data store in read-only mode. Multiple instances can open the same // store in read-only mode. Values still in the write-ahead-log must be replayed before opening. ReadOnly bool `mapstructure:"read_only"` } type TTL struct { // SpanStore holds the amount of time that the span store data is stored. // Once this duration has passed for a given key, span store data will // no longer be accessible. Spans time.Duration `mapstructure:"spans"` } type Directories struct { // Keys contains the directory in which the keys are stored. Keys string `mapstructure:"keys"` // Values contains the directory in which the values are stored. Values string `mapstructure:"values"` } func DefaultConfig() *Config { defaultBadgerDataDir := getCurrentExecutableDir() return &Config{ TTL: TTL{ Spans: defaultTTL, }, SyncWrites: false, // Performance over durability Ephemeral: true, // Default is ephemeral storage Directories: Directories{ Keys: defaultBadgerDataDir + defaultKeysDir, Values: defaultBadgerDataDir + defaultValueDir, }, MaintenanceInterval: defaultMaintenanceInterval, MetricsUpdateInterval: defaultMetricsUpdateInterval, } } func getCurrentExecutableDir() string { // We ignore the error, this will fail later when trying to start the store exec, _ := os.Executable() return filepath.Dir(exec) } func (c *Config) Validate() error { _, err := govalidator.ValidateStruct(c) return err } ================================================ FILE: internal/storage/v1/badger/config_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package badger import ( "testing" "time" "github.com/stretchr/testify/require" ) func TestValidate_DoesNotReturnErrorWhenValid(t *testing.T) { tests := []struct { name string cfg *Config }{ { name: "non-required fields not set", cfg: &Config{}, }, { name: "all fields are set", cfg: &Config{ TTL: TTL{ Spans: time.Second, }, Directories: Directories{ Keys: "some-key-directory", Values: "some-values-directory", }, Ephemeral: false, SyncWrites: false, MaintenanceInterval: time.Second, MetricsUpdateInterval: time.Second, ReadOnly: false, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { err := test.cfg.Validate() require.NoError(t, err) }) } } ================================================ FILE: internal/storage/v1/badger/dependencystore/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dependencystore import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/badger/dependencystore/storage.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dependencystore import ( "context" "time" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" ) // DependencyStore handles all queries and insertions to Badger dependencies type DependencyStore struct { reader spanstore.Reader } // NewDependencyStore returns a DependencyStore func NewDependencyStore(store spanstore.Reader) *DependencyStore { return &DependencyStore{ reader: store, } } // GetDependencies returns all interservice dependencies, implements DependencyReader func (s *DependencyStore) GetDependencies(ctx context.Context, endTs time.Time, lookback time.Duration) ([]model.DependencyLink, error) { deps := map[string]*model.DependencyLink{} params := &spanstore.TraceQueryParameters{ StartTimeMin: endTs.Add(-1 * lookback), StartTimeMax: endTs, } // We need to do a full table scan - if this becomes a bottleneck, we can write an index that describes // dependencyKeyPrefix + timestamp + parent + child key and do a key-only seek (which is fast - but requires additional writes) // GetDependencies is not shipped with a context like the SpanReader / SpanWriter traces, err := s.reader.FindTraces(ctx, params) if err != nil { return nil, err } for _, tr := range traces { processTrace(deps, tr) } return depMapToSlice(deps), err } // depMapToSlice modifies the spans to DependencyLink in the same way as the memory storage plugin func depMapToSlice(deps map[string]*model.DependencyLink) []model.DependencyLink { retMe := make([]model.DependencyLink, 0, len(deps)) for _, dep := range deps { retMe = append(retMe, *dep) } return retMe } // processTrace is copy from the memory storage plugin func processTrace(deps map[string]*model.DependencyLink, trace *model.Trace) { for _, s := range trace.Spans { parentSpan := seekToSpan(trace, s.ParentSpanID()) if parentSpan != nil { if parentSpan.Process.ServiceName == s.Process.ServiceName { continue } depKey := parentSpan.Process.ServiceName + "&&&" + s.Process.ServiceName if _, ok := deps[depKey]; !ok { deps[depKey] = &model.DependencyLink{ Parent: parentSpan.Process.ServiceName, Child: s.Process.ServiceName, CallCount: 1, } } else { deps[depKey].CallCount++ } } } } func seekToSpan(trace *model.Trace, spanID model.SpanID) *model.Span { for _, s := range trace.Spans { if s.SpanID == spanID { return s } } return nil } ================================================ FILE: internal/storage/v1/badger/dependencystore/storage_internal_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dependencystore import ( "testing" "github.com/stretchr/testify/assert" "github.com/jaegertracing/jaeger-idl/model/v1" ) func TestSeekToSpan(t *testing.T) { span := seekToSpan(&model.Trace{}, model.SpanID(uint64(1))) assert.Nil(t, span) } ================================================ FILE: internal/storage/v1/badger/dependencystore/storage_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dependencystore_test import ( "context" "fmt" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/storage/v1/api/dependencystore" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v1/badger" ) // Opens a badger db and runs a test on it. func runFactoryTest(tb testing.TB, test func(tb testing.TB, sw spanstore.Writer, dr dependencystore.Reader)) { f := badger.NewFactory() f.Config.Ephemeral = true f.Config.SyncWrites = false err := f.Initialize(metrics.NullFactory, zap.NewNop()) require.NoError(tb, err) defer func() { require.NoError(tb, f.Close()) }() sw, err := f.CreateSpanWriter() require.NoError(tb, err) dr, err := f.CreateDependencyReader() require.NoError(tb, err) test(tb, sw, dr) } func TestDependencyReader(t *testing.T) { runFactoryTest(t, func(_ testing.TB, sw spanstore.Writer, dr dependencystore.Reader) { tid := time.Now() links, err := dr.GetDependencies(context.Background(), tid, time.Hour) require.NoError(t, err) assert.Empty(t, links) traces := 40 spans := 3 for i := range traces { for j := range spans { s := model.Span{ TraceID: model.TraceID{ Low: uint64(i), High: 1, }, SpanID: model.SpanID(j), OperationName: "operation-a", Process: &model.Process{ ServiceName: fmt.Sprintf("service-%d", j), }, StartTime: tid.Add(time.Duration(i)), Duration: time.Duration(i + j), } if j > 0 { s.References = []model.SpanRef{model.NewChildOfRef(s.TraceID, model.SpanID(j-1))} } err := sw.WriteSpan(context.Background(), &s) require.NoError(t, err) } } links, err = dr.GetDependencies(context.Background(), time.Now(), time.Hour) require.NoError(t, err) assert.NotEmpty(t, links) assert.Len(t, links, spans-1) // First span does not create a dependency assert.Equal(t, uint64(traces), links[0].CallCount) // Each trace calls the same services }) } ================================================ FILE: internal/storage/v1/badger/docs/storage-file-non-root-permission.md ================================================ # Badger file permissions as non-root service After the release of 1.50, Jaeger's Docker image is no longer running with root privileges (in [#4783](https://github.com/jaegertracing/jaeger/pull/4783)). In some installations it may cause issues such as "permission denied" errors when writing data. A possible workaround for this ([proposed here](https://github.com/jaegertracing/jaeger/issues/4906#issuecomment-1991779425)) is to run an initialization step as `root` that pre-creates the Badger data directory and updates its owner to the user that will run the main Jaeger process. ```yaml version: "3.9" services: [...] jaeger: image: jaegertracing/all-in-one:latest command: - "--badger.ephemeral=false" - "--badger.directory-key=/badger/data/keys" - "--badger.directory-value=/badger/data/values" - "--badger.span-store-ttl=72h0m0s" # limit storage to 72hrs environment: - SPAN_STORAGE_TYPE=badger # Mount host directory "jaeger_badger_data" as "/badger" inside the container. # The actual data directory will be "/badger/data", # since we cannot change permissions on the mount. volumes: - jaeger_badger_data:/badger ports: - "16686:16686" - "14250" - "4317" depends_on: prepare-data-dir: condition: service_completed_successfully prepare-data-dir: # Run this step as root so that we can change the directory owner. user: root image: jaegertracing/all-in-one:latest command: "/bin/sh -c 'mkdir -p /badger/data && touch /badger/data/.initialized && chown -R 10001:10001 /badger/data'" volumes: - jaeger_badger_data:/badger volumes: jaeger_badger_data: ``` ================================================ FILE: internal/storage/v1/badger/docs/upgrade-v1-to-v3.md ================================================ # Upgrade Badger v1 to v3 In Jaeger 1.24.0, Badger is upgraded from v1.6.2 to v3.2103.0 which changes the underlying data format. Following steps will help in migrating your data: 1. In Badger v1, the data looks like: ```sh ❯ ls /tmp/badger/ data key ❯ ls /tmp/badger/data/ 000001.vlog 000004.vlog 000005.vlog 000008.vlog 000011.vlog 000012.vlog 000013.vlog 000014.vlog 000015.vlog 000016.vlog 000017.vlog ❯ ls /tmp/badger/key/ 000038.sst 000048.sst 000049.sst 000050.sst 000051.sst 000059.sst 000060.sst 000061.sst 000063.sst 000064.sst 000065.sst 000066.sst MANIFEST ``` 2. Make a backup of your data directory to have a copy incase migration didn't work successfully. ```sh ❯ cp -r /tmp/badger /tmp/badger.bk ``` 3. Download, extract and compile the source code of badger v1: https://github.com/dgraph-io/badger/archive/refs/tags/v1.6.2.tar.gz ```sh ❯ tar xvzf badger-1.6.2.tar ❯ cd badger-1.6.2/badger/ ❯ go install ``` This will install the badger command line utility into your $GOBIN path eg ~/go/bin/badger. 4. Use badger utility to take backup of data. ```sh ❯ ~/go/bin/badger backup --dir /tmp/badger/key --vlog-dir /tmp/badger/data/ Listening for /debug HTTP requests at port: 8080 badger 2021/06/24 22:04:30 INFO: All 12 tables opened in 907ms badger 2021/06/24 22:04:30 INFO: Replaying file id: 17 at offset: 64584535 badger 2021/06/24 22:04:30 INFO: Replay took: 12.303µs badger 2021/06/24 22:04:30 DEBUG: Value log discard stats empty badger 2021/06/24 22:04:30 INFO: DB.Backup Created batch of size: 9.7 kB in 75.907µs. badger 2021/06/24 22:04:31 INFO: DB.Backup Created batch of size: 4.3 MB in 8.003592ms. .... .... badger 2021/06/24 22:04:31 INFO: DB.Backup Created batch of size: 30 MB in 74.808075ms. badger 2021/06/24 22:04:36 INFO: DB.Backup Sent 15495232 keys badger 2021/06/24 22:04:36 INFO: Got compaction priority: {level:0 score:1.73 dropPrefixes:[]} ``` This will create a badger.bak file in the current directory. 5. Download, extract and compile the source code of badger v3: https://github.com/dgraph-io/badger/archive/refs/tags/v3.2103.0.tar.gz ```sh ❯ tar xvzf badger-3.2103.0.tar ❯ cd badger-3.2103.0/badger/ ❯ go install ``` This will install the badger command line utility into your $GOBIN path eg ~/go/bin/badger. 6. Restore the data from backup. ```sh ❯ ~/go/bin/badger restore --dir jaeger-v3 Listening for /debug HTTP requests at port: 8080 jemalloc enabled: false Using Go memory badger 2021/06/24 22:08:29 INFO: All 0 tables opened in 0s badger 2021/06/24 22:08:29 INFO: Discard stats nextEmptySlot: 0 badger 2021/06/24 22:08:29 INFO: Set nextTxnTs to 0 badger 2021/06/24 22:08:37 INFO: [0] [E] LOG Compact 0->6 (5, 0 -> 50 tables with 1 splits). [00001 00002 00003 00004 00005 . .] -> [00006 00007 00008 00009 00010 00011 00012 00013 00014 00015 00016 00017 00018 00019 00020 00021 00022 00023 00024 00025 00026 00028 00029 00030 00031 00032 00033 00034 00035 00036 00037 00038 00039 00040 00041 00043 00044 00045 00046 00047 00048 00049 00050 00051 00052 00053 00054 00055 00056 00057 .], took 2.597s badger 2021/06/24 22:08:53 INFO: Lifetime L0 stalled for: 0s badger 2021/06/24 22:08:55 INFO: Level 0 [ ]: NumTables: 00. Size: 0 B of 0 B. Score: 0.00->0.00 StaleData: 0 B Target FileSize: 64 MiB Level 1 [ ]: NumTables: 00. Size: 0 B of 10 MiB. Score: 0.00->0.00 StaleData: 0 B Target FileSize: 2.0 MiB Level 2 [ ]: NumTables: 00. Size: 0 B of 10 MiB. Score: 0.00->0.00 StaleData: 0 B Target FileSize: 2.0 MiB Level 3 [ ]: NumTables: 00. Size: 0 B of 10 MiB. Score: 0.00->0.00 StaleData: 0 B Target FileSize: 2.0 MiB Level 4 [B]: NumTables: 45. Size: 86 MiB of 10 MiB. Score: 8.64->10.21 StaleData: 0 B Target FileSize: 2.0 MiB Level 5 [ ]: NumTables: 08. Size: 29 MiB of 34 MiB. Score: 0.00->0.00 StaleData: 0 B Target FileSize: 4.0 MiB Level 6 [ ]: NumTables: 63. Size: 340 MiB of 340 MiB. Score: 0.00->0.00 StaleData: 0 B Target FileSize: 8.0 MiB Level Done Num Allocated Bytes at program end: 0 B ``` This will restore the data in jaeger-v3 directory. It will look like this ```sh ❯ ls ./jaeger-v3 000001.vlog 000180.sst 000257.sst 000276.sst 000294.sst 000327.sst 000336.sst 000349.sst 000356.sst 000364.sst 000371.sst 000378.sst 000385.sst 000392.sst 000399.sst 000406.sst 000413.sst MANIFEST 000006.sst 000181.sst 000259.sst 000277.sst 000302.sst 000328.sst 000339.sst 000350.sst 000357.sst 000365.sst 000372.sst 000379.sst 000386.sst 000393.sst 000400.sst 000407.sst 000414.sst 000007.sst 000195.sst 000261.sst 000278.sst 000305.sst 000330.sst 000340.sst 000351.sst 000359.sst 000366.sst 000373.sst 000380.sst 000387.sst 000394.sst 000401.sst 000408.sst 000415.sst 000008.sst 000218.sst 000265.sst 000279.sst 000315.sst 000331.sst 000341.sst 000352.sst 000360.sst 000367.sst 000374.sst 000381.sst 000388.sst 000395.sst 000402.sst 000409.sst 000416.sst 000061.sst 000227.sst 000267.sst 000282.sst 000324.sst 000332.sst 000343.sst 000353.sst 000361.sst 000368.sst 000375.sst 000382.sst 000389.sst 000396.sst 000403.sst 000410.sst 000417.sst 000134.sst 000249.sst 000272.sst 000285.sst 000325.sst 000333.sst 000344.sst 000354.sst 000362.sst 000369.sst 000376.sst 000383.sst 000390.sst 000397.sst 000404.sst 000411.sst DISCARD 000154.sst 000255.sst 000275.sst 000289.sst 000326.sst 000334.sst 000348.sst 000355.sst 000363.sst 000370.sst 000377.sst 000384.sst 000391.sst 000398.sst 000405.sst 000412.sst KEYREGISTRY ``` 7. Separate out the key and data directories. ```sh ❯ rm -rf /tmp/badger ❯ mv ./jaeger-v3 /tmp/badger ❯ mkdir /tmp/badger/data /tmp/badger/key ❯ mv /tmp/badger/*.vlog /tmp/badger/data/ ❯ mv /tmp/badger/*.sst /tmp/badger/key/ ❯ mv /tmp/badger/MANIFEST /tmp/badger/DISCARD /tmp/badger/KEYREGISTRY /tmp/badger/key/ ``` 8. Start Jaeger v1.24.0. It should start well. ================================================ FILE: internal/storage/v1/badger/factory.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package badger import ( "context" "errors" "expvar" "io" "os" "strings" "sync" "time" "github.com/dgraph-io/badger/v4" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/distributedlock" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/storage/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/api/dependencystore" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore/spanstoremetrics" depstore "github.com/jaegertracing/jaeger/internal/storage/v1/badger/dependencystore" badgersampling "github.com/jaegertracing/jaeger/internal/storage/v1/badger/samplingstore" badgerstore "github.com/jaegertracing/jaeger/internal/storage/v1/badger/spanstore" ) const ( valueLogSpaceAvailableName = "badger_value_log_bytes_available" keyLogSpaceAvailableName = "badger_key_log_bytes_available" lastMaintenanceRunName = "badger_storage_maintenance_last_run" lastValueLogCleanedName = "badger_storage_valueloggc_last_run" ) var ( // interface comformance checks _ io.Closer = (*Factory)(nil) _ storage.Purger = (*Factory)(nil) _ storage.SamplingStoreFactory = (*Factory)(nil) ) // Factory for Badger backend. type Factory struct { Config *Config store *badger.DB cache *badgerstore.CacheStore logger *zap.Logger metricsFactory metrics.Factory tmpDir string maintenanceDone chan bool bgWg sync.WaitGroup // TODO initialize via reflection; convert comments to tag 'description'. metrics struct { // ValueLogSpaceAvailable returns the amount of space left on the value log mount point in bytes ValueLogSpaceAvailable metrics.Gauge // KeyLogSpaceAvailable returns the amount of space left on the key log mount point in bytes KeyLogSpaceAvailable metrics.Gauge // LastMaintenanceRun stores the timestamp (UnixNano) of the previous maintenanceRun LastMaintenanceRun metrics.Gauge // LastValueLogCleaned stores the timestamp (UnixNano) of the previous ValueLogGC run LastValueLogCleaned metrics.Gauge // Expose badger's internal expvar metrics, which are all gauge's at this point badgerMetrics map[string]metrics.Gauge } } // NewFactory creates a new Factory. func NewFactory() *Factory { return &Factory{ Config: DefaultConfig(), maintenanceDone: make(chan bool), } } // Initialize performs internal initialization of the factory. func (f *Factory) Initialize(metricsFactory metrics.Factory, logger *zap.Logger) error { f.logger = logger f.metricsFactory = metricsFactory opts := badger.DefaultOptions("") if f.Config.Ephemeral { opts.SyncWrites = false // Error from TempDir is ignored to satisfy Codecov dir, _ := os.MkdirTemp("", "badger") f.tmpDir = dir opts.Dir = f.tmpDir opts.ValueDir = f.tmpDir f.Config.Directories.Keys = f.tmpDir f.Config.Directories.Values = f.tmpDir } else { // Errors are ignored as they're caught in the Open call initializeDir(f.Config.Directories.Keys) initializeDir(f.Config.Directories.Values) opts.SyncWrites = f.Config.SyncWrites opts.Dir = f.Config.Directories.Keys opts.ValueDir = f.Config.Directories.Values // These options make no sense with ephemeral data opts.ReadOnly = f.Config.ReadOnly } store, err := badger.Open(opts) if err != nil { return err } f.store = store f.cache = badgerstore.NewCacheStore(f.store, f.Config.TTL.Spans) f.metrics.ValueLogSpaceAvailable = metricsFactory.Gauge(metrics.Options{Name: valueLogSpaceAvailableName}) f.metrics.KeyLogSpaceAvailable = metricsFactory.Gauge(metrics.Options{Name: keyLogSpaceAvailableName}) f.metrics.LastMaintenanceRun = metricsFactory.Gauge(metrics.Options{Name: lastMaintenanceRunName}) f.metrics.LastValueLogCleaned = metricsFactory.Gauge(metrics.Options{Name: lastValueLogCleanedName}) f.registerBadgerExpvarMetrics(metricsFactory) f.bgWg.Add(2) go func() { defer f.bgWg.Done() f.maintenance() }() go func() { defer f.bgWg.Done() f.metricsCopier() }() logger.Info("Badger storage configuration", zap.Any("configuration", opts)) return nil } // initializeDir makes the directory and parent directories if the path doesn't exists yet. func initializeDir(path string) { if _, err := os.Stat(path); err != nil && os.IsNotExist(err) { os.MkdirAll(path, 0o700) } } // CreateSpanReader creates a spanstore.Reader. func (f *Factory) CreateSpanReader() (spanstore.Reader, error) { tr := badgerstore.NewTraceReader(f.store, f.cache, true) return spanstoremetrics.NewReaderDecorator(tr, f.metricsFactory), nil } // CreateSpanWriter creates a spanstore.Writer. func (f *Factory) CreateSpanWriter() (spanstore.Writer, error) { return badgerstore.NewSpanWriter(f.store, f.cache, f.Config.TTL.Spans), nil } // CreateDependencyReader creates a dependencystore.Reader. func (f *Factory) CreateDependencyReader() (dependencystore.Reader, error) { sr, _ := f.CreateSpanReader() // err is always nil return depstore.NewDependencyStore(sr), nil } // CreateSamplingStore implements storage.SamplingStoreFactory func (f *Factory) CreateSamplingStore(int /* maxBuckets */) (samplingstore.Store, error) { return badgersampling.NewSamplingStore(f.store), nil } // CreateLock implements storage.SamplingStoreFactory func (*Factory) CreateLock() (distributedlock.Lock, error) { return &lock{}, nil } // Close Implements io.Closer and closes the underlying storage func (f *Factory) Close() error { close(f.maintenanceDone) f.bgWg.Wait() // Wait for background goroutines to finish before closing store if f.store == nil { return nil } err := f.store.Close() // Remove tmp files if this was ephemeral storage if f.Config.Ephemeral { errSecondary := os.RemoveAll(f.tmpDir) if err == nil { err = errSecondary } } return err } // Maintenance starts a background maintenance job for the badger K/V store, such as ValueLogGC func (f *Factory) maintenance() { maintenanceTicker := time.NewTicker(f.Config.MaintenanceInterval) defer maintenanceTicker.Stop() for { select { case <-f.maintenanceDone: return case t := <-maintenanceTicker.C: var err error // After there's nothing to clean, the err is raised for err == nil { err = f.store.RunValueLogGC(0.5) // 0.5 is selected to rewrite a file if half of it can be discarded } if errors.Is(err, badger.ErrNoRewrite) { f.metrics.LastValueLogCleaned.Update(t.UnixNano()) } else { f.logger.Error("Failed to run ValueLogGC", zap.Error(err)) } f.metrics.LastMaintenanceRun.Update(t.UnixNano()) _ = f.diskStatisticsUpdate() } } } func (f *Factory) metricsCopier() { metricsTicker := time.NewTicker(f.Config.MetricsUpdateInterval) defer metricsTicker.Stop() for { select { case <-f.maintenanceDone: return case <-metricsTicker.C: expvar.Do(func(kv expvar.KeyValue) { if strings.HasPrefix(kv.Key, "badger") { switch val := kv.Value.(type) { case *expvar.Int: if g, found := f.metrics.badgerMetrics[kv.Key]; found { g.Update(val.Value()) } case *expvar.Map: val.Do(func(innerKv expvar.KeyValue) { // The metrics we're interested in have only a single inner key (dynamic name) // and we're only interested in its value if intVal, ok := innerKv.Value.(*expvar.Int); ok { if g, found := f.metrics.badgerMetrics[kv.Key]; found { g.Update(intVal.Value()) } } }) default: f.logger.Debug("skipping non-numeric badger expvar metric", zap.String("key", kv.Key)) } } }) } } } func (f *Factory) registerBadgerExpvarMetrics(metricsFactory metrics.Factory) { f.metrics.badgerMetrics = make(map[string]metrics.Gauge) expvar.Do(func(kv expvar.KeyValue) { if strings.HasPrefix(kv.Key, "badger") { switch val := kv.Value.(type) { case *expvar.Int: g := metricsFactory.Gauge(metrics.Options{Name: kv.Key}) f.metrics.badgerMetrics[kv.Key] = g case *expvar.Map: val.Do(func(innerKv expvar.KeyValue) { // The metrics we're interested in have only a single inner key (dynamic name) // and we're only interested in its value if _, ok := innerKv.Value.(*expvar.Int); ok { g := metricsFactory.Gauge(metrics.Options{Name: kv.Key}) f.metrics.badgerMetrics[kv.Key] = g } }) default: f.logger.Info("skipping non-numeric badger expvar metric", zap.String("key", kv.Key)) } } }) } // Purge removes all data from the Factory's underlying Badger store. // This function is intended for testing purposes only and should not be used in production environments. // Calling Purge in production will result in permanent data loss. func (f *Factory) Purge(_ context.Context) error { return f.store.Update(func(_ *badger.Txn) error { return f.store.DropAll() }) } ================================================ FILE: internal/storage/v1/badger/factory_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package badger import ( "expvar" "os" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/metricstest" ) func TestInitializationErrors(t *testing.T) { f := NewFactory() dir := "/root/this_should_fail" // If this test fails, you have some issues in your system f.Config.Ephemeral = false f.Config.SyncWrites = true f.Config.Directories.Keys = dir f.Config.Directories.Values = dir err := f.Initialize(metrics.NullFactory, zap.NewNop()) require.Error(t, err) } func TestForCodecov(t *testing.T) { // These tests are testing our vendor packages and are intended to satisfy Codecov. f := NewFactory() err := f.Initialize(metrics.NullFactory, zap.NewNop()) require.NoError(t, err) // Get all the writers, readers, etc _, err = f.CreateSpanReader() require.NoError(t, err) _, err = f.CreateSpanWriter() require.NoError(t, err) _, err = f.CreateDependencyReader() require.NoError(t, err) lock, err := f.CreateLock() require.NoError(t, err) assert.NotNil(t, lock) // Now, remove the badger directories err = os.RemoveAll(f.tmpDir) require.NoError(t, err) // Now try to close, since the files have been deleted this should throw an error err = f.Close() require.Error(t, err) } func TestMaintenanceRun(t *testing.T) { // For Codecov - this does not test anything f := NewFactory() f.Config.MaintenanceInterval = 10 * time.Millisecond // Safeguard mFactory := metricstest.NewFactory(0) _, gs := mFactory.Snapshot() assert.Equal(t, int64(0), gs[lastMaintenanceRunName]) err := f.Initialize(mFactory, zap.NewNop()) require.NoError(t, err) defer f.Close() waiter := func(previousValue int64) int64 { sleeps := 0 _, gs := mFactory.Snapshot() for gs[lastMaintenanceRunName] == previousValue && sleeps < 8 { // Wait for the scheduler time.Sleep(time.Duration(50) * time.Millisecond) sleeps++ _, gs = mFactory.Snapshot() } assert.Greater(t, gs[lastMaintenanceRunName], previousValue) return gs[lastMaintenanceRunName] } runtime := waiter(0) // First run, check that it was ran and caches previous size // This is to for codecov only. Can break without anything else breaking as it does test badger's // internal implementation vlogSize := expvar.Get("badger_size_bytes_vlog").(*expvar.Map).Get(f.tmpDir).(*expvar.Int) currSize := vlogSize.Value() vlogSize.Set(currSize + 1<<31) waiter(runtime) _, gs = mFactory.Snapshot() assert.Positive(t, gs[lastValueLogCleanedName]) } // TestMaintenanceCodecov this test is not intended to test anything, but hopefully increase coverage by triggering a log line func TestMaintenanceCodecov(t *testing.T) { // For Codecov - this does not test anything f := NewFactory() f.Config.MaintenanceInterval = 10 * time.Millisecond mFactory := metricstest.NewFactory(0) err := f.Initialize(mFactory, zap.NewNop()) require.NoError(t, err) defer f.Close() waiter := func() { for range 8 { // Wait for the scheduler time.Sleep(time.Duration(50) * time.Millisecond) } } err = f.store.Close() require.NoError(t, err) waiter() // This should trigger the logging of error } func TestBadgerMetrics(t *testing.T) { // The expvar is leaking keyparams between tests. We need to clean up a bit.. eMap := expvar.Get("badger_size_bytes_lsm").(*expvar.Map) eMap.Init() f := NewFactory() f.Config.MetricsUpdateInterval = 10 * time.Millisecond mFactory := metricstest.NewFactory(0) err := f.Initialize(mFactory, zap.NewNop()) require.NoError(t, err) assert.NotNil(t, f.metrics.badgerMetrics) _, found := f.metrics.badgerMetrics["badger_get_num_memtable"] assert.True(t, found) waiter := func(previousValue int64) int64 { sleeps := 0 _, gs := mFactory.Snapshot() for gs["badger_get_num_memtable"] == previousValue && sleeps < 8 { // Wait for the scheduler time.Sleep(time.Duration(50) * time.Millisecond) sleeps++ _, gs = mFactory.Snapshot() } assert.Equal(t, gs["badger_get_num_memtable"], previousValue) return gs["badger_get_num_memtable"] } vlogSize := waiter(0) _, gs := mFactory.Snapshot() assert.EqualValues(t, 0, vlogSize) assert.Equal(t, int64(0), gs["badger_get_num_memtable"]) // IntVal metric _, found = gs["badger_size_bytes_lsm"] // Map metric assert.True(t, found) require.NoError(t, f.Close()) } ================================================ FILE: internal/storage/v1/badger/lock.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package badger import "time" type lock struct{} // Acquire always returns true for badgerdb as no lock is needed func (*lock) Acquire(string /* resource */, time.Duration /* ttl */) (bool, error) { return true, nil } // Forfeit always returns true for badgerdb as no lock is needed func (*lock) Forfeit(string /* resource */) (bool, error) { return true, nil } ================================================ FILE: internal/storage/v1/badger/lock_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package badger import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestAcquire(t *testing.T) { l := &lock{} ok, err := l.Acquire("resource", time.Duration(1)) assert.True(t, ok) require.NoError(t, err) } func TestForfeit(t *testing.T) { l := &lock{} ok, err := l.Forfeit("resource") assert.True(t, ok) require.NoError(t, err) } ================================================ FILE: internal/storage/v1/badger/options.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package badger import ( "flag" "github.com/spf13/viper" "go.uber.org/zap" ) const ( prefix = "badger" suffixKeyDirectory = ".directory-key" suffixValueDirectory = ".directory-value" suffixEphemeral = ".ephemeral" suffixSpanstoreTTL = ".span-store-ttl" suffixSyncWrite = ".consistency" suffixMaintenanceInterval = ".maintenance-interval" suffixMetricsInterval = ".metrics-update-interval" // Intended only for testing purposes suffixReadOnly = ".read-only" ) // AddFlags adds flags for Config. func (c *Config) AddFlags(flagSet *flag.FlagSet) { flagSet.Bool( prefix+suffixEphemeral, c.Ephemeral, "Mark this storage ephemeral, data is stored in tmpfs.", ) flagSet.Duration( prefix+suffixSpanstoreTTL, c.TTL.Spans, "How long to store the data. Format is time.Duration (https://golang.org/pkg/time/#Duration)", ) flagSet.String( prefix+suffixKeyDirectory, c.Directories.Keys, "Path to store the keys (indexes), this directory should reside in SSD disk. Set ephemeral to false if you want to define this setting.", ) flagSet.String( prefix+suffixValueDirectory, c.Directories.Values, "Path to store the values (spans). Set ephemeral to false if you want to define this setting.", ) flagSet.Bool( prefix+suffixSyncWrite, c.SyncWrites, "If all writes should be synced immediately to physical disk. This will impact write performance.", ) flagSet.Duration( prefix+suffixMaintenanceInterval, c.MaintenanceInterval, "How often the maintenance thread for values is ran. Format is time.Duration (https://golang.org/pkg/time/#Duration)", ) flagSet.Duration( prefix+suffixMetricsInterval, c.MetricsUpdateInterval, "How often the badger metrics are collected by Jaeger. Format is time.Duration (https://golang.org/pkg/time/#Duration)", ) flagSet.Bool( prefix+suffixReadOnly, c.ReadOnly, "Allows to open badger database in read only mode. Multiple instances can open same database in read-only mode. Values still in the write-ahead-log must be replayed before opening.", ) } // InitFromViper initializes Config with properties from viper. func (c *Config) InitFromViper(v *viper.Viper, logger *zap.Logger) { initFromViper(c, v, logger) } func initFromViper(config *Config, v *viper.Viper, _ *zap.Logger) { config.Ephemeral = v.GetBool(prefix + suffixEphemeral) config.Directories.Keys = v.GetString(prefix + suffixKeyDirectory) config.Directories.Values = v.GetString(prefix + suffixValueDirectory) config.SyncWrites = v.GetBool(prefix + suffixSyncWrite) config.TTL.Spans = v.GetDuration(prefix + suffixSpanstoreTTL) config.MaintenanceInterval = v.GetDuration(prefix + suffixMaintenanceInterval) config.MetricsUpdateInterval = v.GetDuration(prefix + suffixMetricsInterval) config.ReadOnly = v.GetBool(prefix + suffixReadOnly) } ================================================ FILE: internal/storage/v1/badger/options_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package badger import ( "testing" "time" "github.com/stretchr/testify/assert" ) func TestDefaultConfigParsing(t *testing.T) { cfg := DefaultConfig() assert.True(t, cfg.Ephemeral) assert.False(t, cfg.SyncWrites) assert.Equal(t, time.Duration(72*time.Hour), cfg.TTL.Spans) } func TestParseConfig(t *testing.T) { cfg := &Config{ Ephemeral: false, SyncWrites: true, TTL: TTL{ Spans: 168 * time.Hour, }, Directories: Directories{ Keys: "/var/lib/badger", Values: "/mnt/slow/badger", }, ReadOnly: false, } assert.False(t, cfg.Ephemeral) assert.True(t, cfg.SyncWrites) assert.Equal(t, time.Duration(168*time.Hour), cfg.TTL.Spans) assert.Equal(t, "/var/lib/badger", cfg.Directories.Keys) assert.Equal(t, "/mnt/slow/badger", cfg.Directories.Values) assert.False(t, cfg.ReadOnly) } func TestReadOnlyConfig(t *testing.T) { cfg := DefaultConfig() cfg.ReadOnly = true assert.True(t, cfg.ReadOnly) } ================================================ FILE: internal/storage/v1/badger/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package badger import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/badger/samplingstore/storage.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package samplingstore import ( "bytes" "encoding/binary" "encoding/json" "time" "github.com/dgraph-io/badger/v4" jaegermodel "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore/model" ) const ( throughputKeyPrefix byte = 0x08 probabilitiesKeyPrefix byte = 0x09 ) type SamplingStore struct { store *badger.DB } type ProbabilitiesAndQPS struct { Hostname string Probabilities model.ServiceOperationProbabilities QPS model.ServiceOperationQPS } func NewSamplingStore(db *badger.DB) *SamplingStore { return &SamplingStore{ store: db, } } func (s *SamplingStore) InsertThroughput(throughput []*model.Throughput) error { startTime := jaegermodel.TimeAsEpochMicroseconds(time.Now()) entriesToStore := make([]*badger.Entry, 0) entries, err := s.createThroughputEntry(throughput, startTime) if err != nil { return err } entriesToStore = append(entriesToStore, entries) err = s.store.Update(func(txn *badger.Txn) error { for i := range entriesToStore { err = txn.SetEntry(entriesToStore[i]) if err != nil { return err } } return nil }) return nil } func (s *SamplingStore) GetThroughput(start, end time.Time) ([]*model.Throughput, error) { var retSlice []*model.Throughput prefix := []byte{throughputKeyPrefix} err := s.store.View(func(txn *badger.Txn) error { opts := badger.DefaultIteratorOptions it := txn.NewIterator(opts) defer it.Close() val := []byte{} for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { item := it.Item() k := item.Key() startTime := k[1:9] val, err := item.ValueCopy(val) if err != nil { return err } t, err := initalStartTime(startTime) if err != nil { return err } throughputs, err := decodeThroughputValue(val) if err != nil { return err } if t.After(start) && (t.Before(end) || t.Equal(end)) { retSlice = append(retSlice, throughputs...) } } return nil }) if err != nil { return nil, err } return retSlice, nil } func (s *SamplingStore) InsertProbabilitiesAndQPS(hostname string, probabilities model.ServiceOperationProbabilities, qps model.ServiceOperationQPS, ) error { startTime := jaegermodel.TimeAsEpochMicroseconds(time.Now()) entriesToStore := make([]*badger.Entry, 0) entries, err := s.createProbabilitiesEntry(hostname, probabilities, qps, startTime) if err != nil { return err } entriesToStore = append(entriesToStore, entries) err = s.store.Update(func(txn *badger.Txn) error { // Write the entries for i := range entriesToStore { err = txn.SetEntry(entriesToStore[i]) if err != nil { return err } } return nil }) return nil } // GetLatestProbabilities implements samplingstore.Reader#GetLatestProbabilities. func (s *SamplingStore) GetLatestProbabilities() (model.ServiceOperationProbabilities, error) { var retVal model.ServiceOperationProbabilities var unMarshalProbabilities ProbabilitiesAndQPS prefix := []byte{probabilitiesKeyPrefix} err := s.store.View(func(txn *badger.Txn) error { opts := badger.DefaultIteratorOptions it := txn.NewIterator(opts) defer it.Close() val := []byte{} for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { item := it.Item() val, err := item.ValueCopy(val) if err != nil { return err } unMarshalProbabilities, err = decodeProbabilitiesValue(val) if err != nil { return err } retVal = unMarshalProbabilities.Probabilities } return nil }) if err != nil { return nil, err } return retVal, nil } func (s *SamplingStore) createProbabilitiesEntry(hostname string, probabilities model.ServiceOperationProbabilities, qps model.ServiceOperationQPS, startTime uint64) (*badger.Entry, error) { pK, pV, err := s.createProbabilitiesKV(hostname, probabilities, qps, startTime) if err != nil { return nil, err } e := s.createBadgerEntry(pK, pV) return e, nil } func (*SamplingStore) createProbabilitiesKV(hostname string, probabilities model.ServiceOperationProbabilities, qps model.ServiceOperationQPS, startTime uint64) (key []byte, bb []byte, err error) { key = make([]byte, 16) key[0] = probabilitiesKeyPrefix pos := 1 binary.BigEndian.PutUint64(key[pos:], startTime) val := ProbabilitiesAndQPS{ Hostname: hostname, Probabilities: probabilities, QPS: qps, } bb, err = json.Marshal(val) return key, bb, err } func (s *SamplingStore) createThroughputEntry(throughput []*model.Throughput, startTime uint64) (*badger.Entry, error) { pK, pV, err := s.createThroughputKV(throughput, startTime) if err != nil { return nil, err } e := s.createBadgerEntry(pK, pV) return e, nil } func (*SamplingStore) createBadgerEntry(key []byte, value []byte) *badger.Entry { return &badger.Entry{ Key: key, Value: value, } } func (*SamplingStore) createThroughputKV(throughput []*model.Throughput, startTime uint64) (key []byte, bb []byte, err error) { key = make([]byte, 16) key[0] = throughputKeyPrefix pos := 1 binary.BigEndian.PutUint64(key[pos:], startTime) bb, err = json.Marshal(throughput) return key, bb, err } func decodeThroughputValue(val []byte) ([]*model.Throughput, error) { var throughput []*model.Throughput err := json.Unmarshal(val, &throughput) if err != nil { return nil, err } return throughput, err } func decodeProbabilitiesValue(val []byte) (ProbabilitiesAndQPS, error) { var probabilities ProbabilitiesAndQPS err := json.Unmarshal(val, &probabilities) if err != nil { return ProbabilitiesAndQPS{}, err } return probabilities, nil } func initalStartTime(timeBytes []byte) (time.Time, error) { var usec int64 buf := bytes.NewReader(timeBytes) if err := binary.Read(buf, binary.BigEndian, &usec); err != nil { return time.Time{}, err } t := time.UnixMicro(usec) return t, nil } ================================================ FILE: internal/storage/v1/badger/samplingstore/storage_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package samplingstore import ( "encoding/json" "testing" "time" "github.com/dgraph-io/badger/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" samplemodel "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore/model" "github.com/jaegertracing/jaeger/internal/testutils" ) func newTestSamplingStore(db *badger.DB) *SamplingStore { return NewSamplingStore(db) } func TestInsertThroughput(t *testing.T) { runWithBadger(t, func(t *testing.T, store *SamplingStore) { throughputs := []*samplemodel.Throughput{ {Service: "my-svc", Operation: "op"}, {Service: "our-svc", Operation: "op2"}, } err := store.InsertThroughput(throughputs) require.NoError(t, err) }) } func TestGetThroughput(t *testing.T) { runWithBadger(t, func(t *testing.T, store *SamplingStore) { start := time.Now() expected := []*samplemodel.Throughput{ {Service: "my-svc", Operation: "op"}, {Service: "our-svc", Operation: "op2"}, } err := store.InsertThroughput(expected) require.NoError(t, err) actual, err := store.GetThroughput(start.Add(-time.Millisecond), start.Add(time.Second*time.Duration(10))) require.NoError(t, err) assert.Equal(t, expected, actual) }) } func TestInsertProbabilitiesAndQPS(t *testing.T) { runWithBadger(t, func(t *testing.T, store *SamplingStore) { err := store.InsertProbabilitiesAndQPS( "dell11eg843d", samplemodel.ServiceOperationProbabilities{"new-srv": {"op": 0.1}}, samplemodel.ServiceOperationQPS{"new-srv": {"op": 4}}, ) require.NoError(t, err) }) } func TestGetLatestProbabilities(t *testing.T) { runWithBadger(t, func(t *testing.T, store *SamplingStore) { err := store.InsertProbabilitiesAndQPS( "dell11eg843d", samplemodel.ServiceOperationProbabilities{"new-srv": {"op": 0.1}}, samplemodel.ServiceOperationQPS{"new-srv": {"op": 4}}, ) require.NoError(t, err) err = store.InsertProbabilitiesAndQPS( "newhostname", samplemodel.ServiceOperationProbabilities{"new-srv2": {"op": 0.123}}, samplemodel.ServiceOperationQPS{"new-srv2": {"op": 1}}, ) require.NoError(t, err) expected := samplemodel.ServiceOperationProbabilities{"new-srv2": {"op": 0.123}} actual, err := store.GetLatestProbabilities() require.NoError(t, err) assert.Equal(t, expected, actual) }) } func TestDecodeProbabilitiesValue(t *testing.T) { expected := ProbabilitiesAndQPS{ Hostname: "dell11eg843d", Probabilities: samplemodel.ServiceOperationProbabilities{"new-srv": {"op": 0.1}}, QPS: samplemodel.ServiceOperationQPS{"new-srv": {"op": 4}}, } marshalBytes, err := json.Marshal(expected) require.NoError(t, err) // This should pass without error actual, err := decodeProbabilitiesValue(marshalBytes) require.NoError(t, err) assert.Equal(t, expected, actual) // Simulate data corruption by removing the first byte. corruptedBytes := marshalBytes[1:] _, err = decodeProbabilitiesValue(corruptedBytes) require.Error(t, err) // Expect an error } func TestDecodeThroughtputValue(t *testing.T) { expected := []*samplemodel.Throughput{ {Service: "my-svc", Operation: "op"}, {Service: "our-svc", Operation: "op2"}, } marshalBytes, err := json.Marshal(expected) require.NoError(t, err) acrual, err := decodeThroughputValue(marshalBytes) require.NoError(t, err) assert.Equal(t, expected, acrual) } func runWithBadger(t *testing.T, test func(t *testing.T, store *SamplingStore)) { opts := badger.DefaultOptions("") opts.SyncWrites = false dir := t.TempDir() opts.Dir = dir opts.ValueDir = dir store, err := badger.Open(opts) require.NoError(t, err) defer func() { require.NoError(t, store.Close()) }() ss := newTestSamplingStore(store) test(t, ss) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/badger/spanstore/cache.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "sort" "sync" "time" "github.com/dgraph-io/badger/v4" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" ) // CacheStore saves expensive calculations from the K/V store type CacheStore struct { // Given the small amount of data these will store, we use the same structure as the memory store cacheLock sync.Mutex // write heavy - Mutex is faster than RWMutex for writes services map[string]uint64 operations map[string]map[string]uint64 store *badger.DB ttl time.Duration } // NewCacheStore returns initialized CacheStore for badger use func NewCacheStore(db *badger.DB, ttl time.Duration) *CacheStore { cs := &CacheStore{ services: make(map[string]uint64), operations: make(map[string]map[string]uint64), ttl: ttl, store: db, } return cs } // AddService fills the services into the cache with the most updated expiration time func (c *CacheStore) AddService(service string, keyTTL uint64) { c.cacheLock.Lock() defer c.cacheLock.Unlock() if v, found := c.services[service]; found { if v > keyTTL { return } } c.services[service] = keyTTL } // AddOperation adds the cache with operation names with most updated expiration time func (c *CacheStore) AddOperation(service, operation string, keyTTL uint64) { c.cacheLock.Lock() defer c.cacheLock.Unlock() if _, found := c.operations[service]; !found { c.operations[service] = make(map[string]uint64) } if v, found := c.operations[service][operation]; found { if v > keyTTL { return } } c.operations[service][operation] = keyTTL } // Update caches the results of service and service + operation indexes and maintains their TTL func (c *CacheStore) Update(service, operation string, expireTime uint64) { c.cacheLock.Lock() c.services[service] = expireTime if _, ok := c.operations[service]; !ok { c.operations[service] = make(map[string]uint64) } c.operations[service][operation] = expireTime c.cacheLock.Unlock() } // GetOperations returns all operations for a specific service & spanKind traced by Jaeger func (c *CacheStore) GetOperations(service string) ([]spanstore.Operation, error) { operations := make([]string, 0, len(c.services)) //nolint:gosec // G115 t := uint64(time.Now().Unix()) c.cacheLock.Lock() defer c.cacheLock.Unlock() if v, ok := c.services[service]; ok { if v < t { // Expired, remove delete(c.services, service) delete(c.operations, service) return []spanstore.Operation{}, nil // empty slice rather than nil } for o, e := range c.operations[service] { if e > t { operations = append(operations, o) } else { delete(c.operations[service], o) } } } sort.Strings(operations) // TODO: https://github.com/jaegertracing/jaeger/issues/1922 // - return the operations with actual spanKind result := make([]spanstore.Operation, 0, len(operations)) for _, op := range operations { result = append(result, spanstore.Operation{ Name: op, }) } return result, nil } // GetServices returns all services traced by Jaeger func (c *CacheStore) GetServices() ([]string, error) { services := make([]string, 0, len(c.services)) //nolint:gosec // G115 t := uint64(time.Now().Unix()) c.cacheLock.Lock() // Fetch the items for k, v := range c.services { if v > t { services = append(services, k) } else { // Service has expired, remove it delete(c.services, k) } } c.cacheLock.Unlock() sort.Strings(services) return services, nil } ================================================ FILE: internal/storage/v1/badger/spanstore/cache_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "testing" "time" "github.com/dgraph-io/badger/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) /* Additional cache store tests that need to access internal parts. As such, package must be spanstore and not spanstore_test */ func TestExpiredItems(t *testing.T) { runWithBadger(t, func(store *badger.DB, t *testing.T) { cache := NewCacheStore(store, time.Duration(-1*time.Hour)) expireTime := uint64(time.Now().Add(cache.ttl).Unix()) // Expired service cache.Update("service1", "op1", expireTime) cache.Update("service1", "op2", expireTime) services, err := cache.GetServices() require.NoError(t, err) assert.Empty(t, services) // Everything should be expired // Expired service for operations cache.Update("service1", "op1", expireTime) cache.Update("service1", "op2", expireTime) operations, err := cache.GetOperations("service1") require.NoError(t, err) assert.Empty(t, operations) // Everything should be expired // Expired operations, stable service cache.Update("service1", "op1", expireTime) cache.Update("service1", "op2", expireTime) cache.services["service1"] = uint64(time.Now().Unix() + 1e10) operations, err = cache.GetOperations("service1") require.NoError(t, err) assert.Empty(t, operations) // Everything should be expired }) } // func runFactoryTest(tb testing.TB, test func(tb testing.TB, sw spanstore.Writer, sr spanstore.Reader)) { func runWithBadger(t *testing.T, test func(store *badger.DB, t *testing.T)) { opts := badger.DefaultOptions("") opts.SyncWrites = false dir := t.TempDir() opts.Dir = dir opts.ValueDir = dir store, err := badger.Open(opts) defer func() { store.Close() }() require.NoError(t, err) test(store, t) } ================================================ FILE: internal/storage/v1/badger/spanstore/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/badger/spanstore/read_write_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package spanstore_test import ( "context" "fmt" "log" "math/rand" "os" "path/filepath" "runtime/pprof" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v1/badger" ) func TestWriteReadBack(t *testing.T) { runFactoryTest(t, func(_ testing.TB, sw spanstore.Writer, sr spanstore.Reader) { tid := time.Now() traces := 40 spans := 3 dummyKv := []model.KeyValue{ { Key: "key", VType: model.StringType, VStr: "value", }, } for i := range traces { for j := range spans { s := model.Span{ TraceID: model.TraceID{ Low: uint64(i), High: 1, }, SpanID: model.SpanID(j), OperationName: "operation", Process: &model.Process{ ServiceName: "service", Tags: dummyKv, }, StartTime: tid.Add(time.Duration(i)), Duration: time.Duration(i + j), Tags: dummyKv, Logs: []model.Log{ { Timestamp: tid, Fields: dummyKv, }, }, } err := sw.WriteSpan(context.Background(), &s) require.NoError(t, err) } } for i := range traces { tr, err := sr.GetTrace(context.Background(), spanstore.GetTraceParameters{TraceID: model.TraceID{ Low: uint64(i), High: 1, }}) require.NoError(t, err) assert.Len(t, tr.Spans, spans) } }) } func TestValidation(t *testing.T) { runFactoryTest(t, func(_ testing.TB, _ spanstore.Writer, sr spanstore.Reader) { tid := time.Now() params := &spanstore.TraceQueryParameters{ StartTimeMin: tid, StartTimeMax: tid.Add(time.Duration(10)), } params.OperationName = "no-service" _, err := sr.FindTraces(context.Background(), params) require.EqualError(t, err, "service name must be set") params.ServiceName = "find-service" _, err = sr.FindTraces(context.Background(), nil) require.EqualError(t, err, "malformed request object") params.StartTimeMin = params.StartTimeMax.Add(1 * time.Hour) _, err = sr.FindTraces(context.Background(), params) require.EqualError(t, err, "min start time is above max") params.StartTimeMin = tid params.DurationMax = time.Duration(1 * time.Millisecond) params.DurationMin = time.Duration(1 * time.Minute) _, err = sr.FindTraces(context.Background(), params) require.EqualError(t, err, "min duration is above max") params = &spanstore.TraceQueryParameters{ StartTimeMin: tid, } _, err = sr.FindTraces(context.Background(), params) require.EqualError(t, err, "start and end time must be set") params.StartTimeMax = tid.Add(1 * time.Minute) params.Tags = map[string]string{"A": "B"} _, err = sr.FindTraces(context.Background(), params) require.EqualError(t, err, "service name must be set") }) } func TestIndexSeeks(t *testing.T) { runFactoryTest(t, func(_ testing.TB, sw spanstore.Writer, sr spanstore.Reader) { startT := time.Now() traces := 60 spans := 3 tid := startT traceOrder := make([]uint64, traces) for i := range traces { lowId := rand.Uint64() traceOrder[i] = lowId tid = tid.Add(time.Duration(time.Millisecond * time.Duration(i))) for j := range spans { s := model.Span{ TraceID: model.TraceID{ Low: lowId, High: 1, }, SpanID: model.SpanID(rand.Uint64()), OperationName: fmt.Sprintf("operation-%d", j), Process: &model.Process{ ServiceName: fmt.Sprintf("service-%d", i%4), }, StartTime: tid, Duration: time.Duration(time.Duration(i+j) * time.Millisecond), Tags: model.KeyValues{ model.KeyValue{ Key: fmt.Sprintf("k%d", i), VStr: fmt.Sprintf("val%d", j), VType: model.StringType, }, { Key: "error", VType: model.BoolType, VBool: true, }, }, } err := sw.WriteSpan(context.Background(), &s) require.NoError(t, err) } } testOrder := func(trs []*model.Trace) { // Assert that we returned correctly in DESC time order for l := 1; l < len(trs); l++ { assert.True(t, trs[l].Spans[spans-1].StartTime.Before(trs[l-1].Spans[spans-1].StartTime)) } } params := &spanstore.TraceQueryParameters{ StartTimeMin: startT, StartTimeMax: startT.Add(time.Duration(time.Millisecond * 10)), ServiceName: "service-1", } trs, err := sr.FindTraces(context.Background(), params) require.NoError(t, err) assert.Len(t, trs, 1) assert.Len(t, trs[0].Spans, spans) params.OperationName = "operation-1" trs, err = sr.FindTraces(context.Background(), params) require.NoError(t, err) assert.Len(t, trs, 1) params.ServiceName = "service-10" // this should not match trs, err = sr.FindTraces(context.Background(), params) require.NoError(t, err) assert.Empty(t, trs) params.OperationName = "operation-4" trs, err = sr.FindTraces(context.Background(), params) require.NoError(t, err) assert.Empty(t, trs) // Multi-index hits params.StartTimeMax = startT.Add(time.Duration(time.Millisecond * 666)) params.ServiceName = "service-3" params.OperationName = "operation-1" tags := make(map[string]string) tags["k11"] = "val0" tags["error"] = "true" params.Tags = tags params.DurationMin = time.Duration(1 * time.Millisecond) trs, err = sr.FindTraces(context.Background(), params) require.NoError(t, err) assert.Len(t, trs, 1) assert.Len(t, trs[0].Spans, spans) // Query limited amount of hits params.StartTimeMax = startT.Add(time.Duration(time.Hour * 1)) delete(params.Tags, "k11") params.NumTraces = 2 trs, err = sr.FindTraces(context.Background(), params) require.NoError(t, err) assert.Len(t, trs, 2) assert.Equal(t, traceOrder[59], trs[0].Spans[0].TraceID.Low) assert.Equal(t, traceOrder[55], trs[1].Spans[0].TraceID.Low) testOrder(trs) // Check for DESC return order with duration index params = &spanstore.TraceQueryParameters{ StartTimeMin: startT, StartTimeMax: startT.Add(time.Duration(time.Hour * 1)), DurationMin: time.Duration(30 * time.Millisecond), // Filters one DurationMax: time.Duration(50 * time.Millisecond), // Filters three NumTraces: 9, } trs, err = sr.FindTraces(context.Background(), params) require.NoError(t, err) assert.Len(t, trs, 9) // Returns 23, we limited to 9 // Check the newest items are returned assert.Equal(t, traceOrder[50], trs[0].Spans[0].TraceID.Low) assert.Equal(t, traceOrder[42], trs[8].Spans[0].TraceID.Low) testOrder(trs) // Check for DESC return order without duration index, but still with limit params.DurationMin = 0 params.DurationMax = 0 params.NumTraces = 7 trs, err = sr.FindTraces(context.Background(), params) require.NoError(t, err) assert.Len(t, trs, 7) assert.Equal(t, traceOrder[59], trs[0].Spans[0].TraceID.Low) assert.Equal(t, traceOrder[53], trs[6].Spans[0].TraceID.Low) testOrder(trs) // StartTime, endTime scan - full table scan (so technically no index seek) params = &spanstore.TraceQueryParameters{ StartTimeMin: startT, StartTimeMax: startT.Add(time.Duration(time.Millisecond * 10)), } trs, err = sr.FindTraces(context.Background(), params) require.NoError(t, err) assert.Len(t, trs, 5) assert.Len(t, trs[0].Spans, spans) testOrder(trs) // StartTime and Duration queries params.StartTimeMax = startT.Add(time.Duration(time.Hour * 10)) params.DurationMin = time.Duration(53 * time.Millisecond) // trace 51 (min) params.DurationMax = time.Duration(56 * time.Millisecond) // trace 56 (max) trs, err = sr.FindTraces(context.Background(), params) require.NoError(t, err) assert.Len(t, trs, 6) assert.Equal(t, traceOrder[56], trs[0].Spans[0].TraceID.Low) assert.Equal(t, traceOrder[51], trs[5].Spans[0].TraceID.Low) testOrder(trs) }) } func TestFindNothing(t *testing.T) { runFactoryTest(t, func(_ testing.TB, _ spanstore.Writer, sr spanstore.Reader) { startT := time.Now() params := &spanstore.TraceQueryParameters{ StartTimeMin: startT, StartTimeMax: startT.Add(time.Duration(time.Millisecond * 10)), ServiceName: "service-1", } trs, err := sr.FindTraces(context.Background(), params) require.NoError(t, err) assert.Empty(t, trs) tr, err := sr.GetTrace(context.Background(), spanstore.GetTraceParameters{TraceID: model.TraceID{Low: 0, High: 0}}) assert.Equal(t, spanstore.ErrTraceNotFound, err) assert.Nil(t, tr) }) } func TestWriteDuplicates(t *testing.T) { runFactoryTest(t, func(_ testing.TB, sw spanstore.Writer, _ spanstore.Reader) { tid := time.Now() times := 40 spans := 3 for i := range times { for j := range spans { s := model.Span{ TraceID: model.TraceID{ Low: uint64(0), High: 1, }, SpanID: model.SpanID(j), OperationName: "operation", Process: &model.Process{ ServiceName: "service", }, StartTime: tid.Add(time.Duration(10)), Duration: time.Duration(i + j), } err := sw.WriteSpan(context.Background(), &s) require.NoError(t, err) } } }) } func TestMenuSeeks(t *testing.T) { runFactoryTest(t, func(_ testing.TB, sw spanstore.Writer, sr spanstore.Reader) { tid := time.Now() traces := 40 services := 4 spans := 3 for i := range traces { for j := range spans { s := model.Span{ TraceID: model.TraceID{ Low: uint64(i), High: 1, }, SpanID: model.SpanID(j), OperationName: fmt.Sprintf("operation-%d", j), Process: &model.Process{ ServiceName: fmt.Sprintf("service-%d", i%services), }, StartTime: tid.Add(time.Duration(i)), Duration: time.Duration(i + j), } err := sw.WriteSpan(context.Background(), &s) require.NoError(t, err) } } operations, err := sr.GetOperations( context.Background(), spanstore.OperationQueryParameters{ServiceName: "service-1"}, ) require.NoError(t, err) serviceList, err := sr.GetServices(context.Background()) require.NoError(t, err) assert.Len(t, operations, spans) assert.Len(t, serviceList, services) }) } func TestPersist(t *testing.T) { dir := t.TempDir() p := func(t *testing.T, dir string, test func(t *testing.T, sw spanstore.Writer, sr spanstore.Reader)) { f := badger.NewFactory() f.Config.Ephemeral = false f.Config.Directories.Keys = dir f.Config.Directories.Values = dir err := f.Initialize(metrics.NullFactory, zap.NewNop()) require.NoError(t, err) defer func() { require.NoError(t, f.Close()) }() sw, err := f.CreateSpanWriter() require.NoError(t, err) sr, err := f.CreateSpanReader() require.NoError(t, err) test(t, sw, sr) } p(t, dir, func(t *testing.T, sw spanstore.Writer, _ spanstore.Reader) { s := model.Span{ TraceID: model.TraceID{ Low: uint64(1), High: 1, }, SpanID: model.SpanID(4), OperationName: "operation-p", Process: &model.Process{ ServiceName: "service-p", }, StartTime: time.Now(), Duration: time.Duration(1 * time.Hour), } err := sw.WriteSpan(context.Background(), &s) require.NoError(t, err) }) p(t, dir, func(t *testing.T, _ spanstore.Writer, sr spanstore.Reader) { trace, err := sr.GetTrace(context.Background(), spanstore.GetTraceParameters{TraceID: model.TraceID{ Low: uint64(1), High: 1, }}) require.NoError(t, err) assert.Equal(t, "operation-p", trace.Spans[0].OperationName) services, err := sr.GetServices(context.Background()) require.NoError(t, err) assert.Len(t, services, 1) }) } // Opens a badger db and runs a test on it. func runFactoryTest(tb testing.TB, test func(tb testing.TB, sw spanstore.Writer, sr spanstore.Reader)) { f := badger.NewFactory() f.Config.Ephemeral = true f.Config.SyncWrites = false err := f.Initialize(metrics.NullFactory, zap.NewNop()) require.NoError(tb, err) defer func() { require.NoError(tb, f.Close()) }() sw, err := f.CreateSpanWriter() require.NoError(tb, err) sr, err := f.CreateSpanReader() require.NoError(tb, err) test(tb, sw, sr) } // Benchmarks intended for profiling func writeSpans(sw spanstore.Writer, tags []model.KeyValue, services, operations []string, traces, spans int, high uint64, tid time.Time) { for i := range traces { for j := range spans { s := model.Span{ TraceID: model.TraceID{ Low: uint64(i), High: high, }, SpanID: model.SpanID(j), OperationName: operations[j], Process: &model.Process{ ServiceName: services[j], }, Tags: tags, StartTime: tid.Add(time.Duration(time.Millisecond)), Duration: time.Duration(time.Millisecond * time.Duration(i+j)), } _ = sw.WriteSpan(context.Background(), &s) } } } func BenchmarkWrites(b *testing.B) { runFactoryTest(b, func(_ testing.TB, sw spanstore.Writer, _ spanstore.Reader) { tid := time.Now() traces := 1000 spans := 32 tagsCount := 64 tags, services, operations := makeWriteSupports(tagsCount, spans) f, err := os.Create("writes.out") if err != nil { log.Fatal("could not create CPU profile: ", err) } if err := pprof.StartCPUProfile(f); err != nil { log.Fatal("could not start CPU profile: ", err) } defer pprof.StopCPUProfile() b.ResetTimer() for a := 0; a < b.N; a++ { writeSpans(sw, tags, services, operations, traces, spans, uint64(0), tid) } b.StopTimer() }) } func makeWriteSupports(tagsCount, spans int) (tags []model.KeyValue, services []string, operations []string) { tags = make([]model.KeyValue, tagsCount) for i := range tagsCount { tags[i] = model.KeyValue{ Key: fmt.Sprintf("a%d", i), VStr: fmt.Sprintf("b%d", i), } } operations = make([]string, spans) for j := range spans { operations[j] = fmt.Sprintf("operation-%d", j) } services = make([]string, spans) for i := range spans { services[i] = fmt.Sprintf("service-%d", i) } return tags, services, operations } func makeReadBenchmark(b *testing.B, _ time.Time, params *spanstore.TraceQueryParameters, outputFile string) { runLargeFactoryTest(b, func(_ testing.TB, sw spanstore.Writer, sr spanstore.Reader) { tid := time.Now() // Total amount of traces is traces * tracesTimes traces := 1000 tracesTimes := 1 // Total amount of spans written is traces * tracesTimes * spans spans := 32 // Default is 160k tagsCount := 64 tags, services, operations := makeWriteSupports(tagsCount, spans) for h := range tracesTimes { writeSpans(sw, tags, services, operations, traces, spans, uint64(h), tid) } f, err := os.Create(outputFile) if err != nil { log.Fatal("could not create CPU profile: ", err) } if err := pprof.StartCPUProfile(f); err != nil { log.Fatal("could not start CPU profile: ", err) } defer pprof.StopCPUProfile() b.ResetTimer() for a := 0; a < b.N; a++ { sr.FindTraces(context.Background(), params) } b.StopTimer() }) } func BenchmarkServiceTagsRangeQueryLimitIndexFetch(b *testing.B) { tid := time.Now() params := &spanstore.TraceQueryParameters{ StartTimeMin: tid, StartTimeMax: tid.Add(time.Duration(time.Millisecond * 2000)), ServiceName: "service-1", Tags: map[string]string{ "a8": "b8", }, } params.DurationMin = time.Duration(1 * time.Millisecond) // durationQuery takes 53% of total execution time.. params.NumTraces = 50 makeReadBenchmark(b, tid, params, "scanrangeandindexlimit.out") } func BenchmarkServiceIndexLimitFetch(b *testing.B) { tid := time.Now() params := &spanstore.TraceQueryParameters{ StartTimeMin: tid, StartTimeMax: tid.Add(time.Duration(time.Millisecond * 2000)), ServiceName: "service-1", } params.NumTraces = 50 makeReadBenchmark(b, tid, params, "serviceindexlimit.out") } // Opens a badger db and runs a test on it. func runLargeFactoryTest(tb testing.TB, test func(tb testing.TB, sw spanstore.Writer, sr spanstore.Reader)) { assertion := require.New(tb) f := badger.NewFactory() dir := filepath.Join(tb.TempDir(), "badger-testRun") err := os.MkdirAll(dir, 0o700) assertion.NoError(err) f.Config.Directories.Keys = dir f.Config.Directories.Values = dir f.Config.Ephemeral = false f.Config.SyncWrites = false err = f.Initialize(metrics.NullFactory, zap.NewNop()) assertion.NoError(err) defer func() { err := f.Close() os.RemoveAll(dir) require.NoError(tb, err) }() sw, err := f.CreateSpanWriter() assertion.NoError(err) sr, err := f.CreateSpanReader() assertion.NoError(err) test(tb, sw, sr) } // TestRandomTraceID from issue #1808 func TestRandomTraceID(t *testing.T) { runFactoryTest(t, func(_ testing.TB, sw spanstore.Writer, sr spanstore.Reader) { s1 := model.Span{ TraceID: model.TraceID{ Low: uint64(14767110704788176287), High: 0, }, SpanID: model.SpanID(14976775253976086374), OperationName: "/", Process: &model.Process{ ServiceName: "nginx", }, Tags: model.KeyValues{ model.KeyValue{ Key: "http.request_id", VStr: "first", VType: model.StringType, }, }, StartTime: time.Now(), Duration: 1 * time.Second, } err := sw.WriteSpan(context.Background(), &s1) require.NoError(t, err) s2 := model.Span{ TraceID: model.TraceID{ Low: uint64(4775132888371984950), High: 0, }, SpanID: model.SpanID(13576481569227028654), OperationName: "/", Process: &model.Process{ ServiceName: "nginx", }, Tags: model.KeyValues{ model.KeyValue{ Key: "http.request_id", VStr: "second", VType: model.StringType, }, }, StartTime: time.Now(), Duration: 1 * time.Second, } err = sw.WriteSpan(context.Background(), &s2) require.NoError(t, err) params := &spanstore.TraceQueryParameters{ StartTimeMin: time.Now().Add(-1 * time.Minute), StartTimeMax: time.Now(), ServiceName: "nginx", Tags: map[string]string{ "http.request_id": "second", }, } traces, err := sr.FindTraces(context.Background(), params) require.NoError(t, err) assert.Len(t, traces, 1) }) } ================================================ FILE: internal/storage/v1/badger/spanstore/reader.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "bytes" "context" "encoding/binary" "encoding/json" "errors" "fmt" "math" "sort" "github.com/dgraph-io/badger/v4" "golang.org/x/exp/maps" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" ) // Most of these errors are common with the ES and Cassandra backends. Each backend has slightly different validation rules. var ( // ErrServiceNameNotSet occurs when attempting to query with an empty service name ErrServiceNameNotSet = errors.New("service name must be set") // ErrStartTimeMinGreaterThanMax occurs when start time min is above start time max ErrStartTimeMinGreaterThanMax = errors.New("min start time is above max") // ErrDurationMinGreaterThanMax occurs when duration min is above duration max ErrDurationMinGreaterThanMax = errors.New("min duration is above max") // ErrMalformedRequestObject occurs when a request object is nil ErrMalformedRequestObject = errors.New("malformed request object") // ErrStartAndEndTimeNotSet occurs when start time and end time are not set ErrStartAndEndTimeNotSet = errors.New("start and end time must be set") // ErrUnableToFindTraceIDAggregation occurs when an aggregation query for TraceIDs fail. ErrUnableToFindTraceIDAggregation = errors.New("could not find aggregation of traceIDs") // ErrNotSupported during development, don't support every option - yet ErrNotSupported = errors.New("this query parameter is not supported yet") // ErrInternalConsistencyError indicates internal data consistency issue ErrInternalConsistencyError = errors.New("internal data consistency issue") ) const ( defaultNumTraces = 100 sizeOfTraceID = 16 encodingTypeBits = 0x0F ) // TraceReader reads traces from the local badger store type TraceReader struct { store *badger.DB cache *CacheStore } // executionPlan is internal structure to track the index filtering type executionPlan struct { startTimeMin []byte startTimeMax []byte limit int // mergeOuter is the result of merge-join of inner and outer result sets mergeOuter [][]byte // hashOuter is the hashmap for hash-join of outer resultset hashOuter map[model.TraceID]struct{} } // NewTraceReader returns a TraceReader with cache func NewTraceReader(db *badger.DB, c *CacheStore, prefillCache bool) *TraceReader { reader := &TraceReader{ store: db, cache: c, } if prefillCache { services := reader.preloadServices() for _, service := range services { reader.preloadOperations(service) } } return reader } func decodeValue(val []byte, encodeType byte) (*model.Span, error) { sp := model.Span{} switch encodeType { case jsonEncoding: if err := json.Unmarshal(val, &sp); err != nil { return nil, err } case protoEncoding: if err := sp.Unmarshal(val); err != nil { return nil, err } default: return nil, fmt.Errorf("unknown encoding type: %#02x", encodeType) } return &sp, nil } // getTraces enriches TraceIDs to Traces func (r *TraceReader) getTraces(traceIDs []model.TraceID) ([]*model.Trace, error) { // Get by PK traces := make([]*model.Trace, 0, len(traceIDs)) prefixes := make([][]byte, 0, len(traceIDs)) for _, traceID := range traceIDs { prefixes = append(prefixes, createPrimaryKeySeekPrefix(traceID)) } err := r.store.View(func(txn *badger.Txn) error { opts := badger.DefaultIteratorOptions it := txn.NewIterator(opts) defer it.Close() val := []byte{} for _, prefix := range prefixes { spans := make([]*model.Span, 0, 32) // reduce reallocation requirements by defining some initial length for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { // Add value to the span store (decode from JSON / defined encoding first) // These are in the correct order because of the sorted nature item := it.Item() val, err := item.ValueCopy(val) if err != nil { return err } sp, err := decodeValue(val, item.UserMeta()&encodingTypeBits) if err != nil { return err } spans = append(spans, sp) } if len(spans) > 0 { trace := &model.Trace{ Spans: spans, } traces = append(traces, trace) } } return nil }) return traces, err } // GetTrace takes a traceID and returns a Trace associated with that traceID func (r *TraceReader) GetTrace(_ context.Context, query spanstore.GetTraceParameters) (*model.Trace, error) { traces, err := r.getTraces([]model.TraceID{query.TraceID}) if err != nil { return nil, err } if len(traces) == 0 { return nil, spanstore.ErrTraceNotFound } if len(traces) == 1 { return traces[0], nil } return nil, ErrInternalConsistencyError } // scanTimeRange returns all the Traces found between startTs and endTs func (r *TraceReader) scanTimeRange(plan *executionPlan) ([]model.TraceID, error) { // We need to do a full table scan traceKeys := make([][]byte, 0) err := r.store.View(func(txn *badger.Txn) error { opts := badger.DefaultIteratorOptions opts.PrefetchValues = false it := txn.NewIterator(opts) defer it.Close() startIndex := []byte{spanKeyPrefix} prevTraceID := []byte{} for it.Seek(startIndex); it.ValidForPrefix(startIndex); it.Next() { item := it.Item() key := []byte{} key = item.KeyCopy(key) timestamp := key[sizeOfTraceID+1 : sizeOfTraceID+1+8] traceID := key[1 : sizeOfTraceID+1] if bytes.Compare(timestamp, plan.startTimeMin) >= 0 && bytes.Compare(timestamp, plan.startTimeMax) <= 0 { if !bytes.Equal(traceID, prevTraceID) { if plan.hashOuter != nil { trID := bytesToTraceID(traceID) if _, exists := plan.hashOuter[trID]; exists { traceKeys = append(traceKeys, key) } } else { traceKeys = append(traceKeys, key) } prevTraceID = traceID } } } return nil }) sort.Slice(traceKeys, func(k, h int) bool { // This sorts by timestamp to descending order return bytes.Compare(traceKeys[k][sizeOfTraceID+1:sizeOfTraceID+1+8], traceKeys[h][sizeOfTraceID+1:sizeOfTraceID+1+8]) > 0 }) sizeCount := len(traceKeys) if plan.limit > 0 && plan.limit < sizeCount { sizeCount = plan.limit } traceIDs := make([]model.TraceID, sizeCount) for i := 0; i < sizeCount; i++ { traceIDs[i] = bytesToTraceID(traceKeys[i][1 : sizeOfTraceID+1]) } return traceIDs, err } func createPrimaryKeySeekPrefix(traceID model.TraceID) []byte { key := make([]byte, 1+sizeOfTraceID) key[0] = spanKeyPrefix pos := 1 binary.BigEndian.PutUint64(key[pos:], traceID.High) pos += 8 binary.BigEndian.PutUint64(key[pos:], traceID.Low) return key } // GetServices fetches the sorted service list that have not expired func (r *TraceReader) GetServices(context.Context) ([]string, error) { return r.cache.GetServices() } // GetOperations fetches operations in the service and empty slice if service does not exists func (r *TraceReader) GetOperations( _ context.Context, query spanstore.OperationQueryParameters, ) ([]spanstore.Operation, error) { return r.cache.GetOperations(query.ServiceName) } // setQueryDefaults alters the query with defaults if certain parameters are not set func setQueryDefaults(query *spanstore.TraceQueryParameters) { if query.NumTraces <= 0 { query.NumTraces = defaultNumTraces } } // serviceQueries parses the query to index seeks which are unique index seeks func serviceQueries(query *spanstore.TraceQueryParameters, indexSeeks [][]byte) [][]byte { if query.ServiceName != "" { indexSearchKey := make([]byte, 0, 64) // 64 is a magic guess tagQueryUsed := false for k, v := range query.Tags { tagSearch := []byte(query.ServiceName + k + v) tagSearchKey := make([]byte, 0, len(tagSearch)+1) tagSearchKey = append(tagSearchKey, tagIndexKey) tagSearchKey = append(tagSearchKey, tagSearch...) indexSeeks = append(indexSeeks, tagSearchKey) tagQueryUsed = true } if query.OperationName != "" { indexSearchKey = append(indexSearchKey, operationNameIndexKey) indexSearchKey = append(indexSearchKey, []byte(query.ServiceName+query.OperationName)...) } else if !tagQueryUsed { // Tag query already reduces the search set with a serviceName indexSearchKey = append(indexSearchKey, serviceNameIndexKey) indexSearchKey = append(indexSearchKey, []byte(query.ServiceName)...) } if len(indexSearchKey) > 0 { indexSeeks = append(indexSeeks, indexSearchKey) } } return indexSeeks } // indexSeeksToTraceIDs does the index scanning against badger based on the parsed index queries func (r *TraceReader) indexSeeksToTraceIDs(plan *executionPlan, indexSeeks [][]byte) ([]model.TraceID, error) { for i := len(indexSeeks) - 1; i > 0; i-- { indexResults, err := r.scanIndexKeys(indexSeeks[i], plan) if err != nil { return nil, err } sort.Slice(indexResults, func(k, h int) bool { return bytes.Compare(indexResults[k], indexResults[h]) < 0 }) // Same traceID can be returned multiple times, but always in sorted order so checking the previous key is enough prevTraceID := []byte{} innerIDs := make([][]byte, 0, len(indexSeeks)) for j := range indexResults { traceID := indexResults[j] if !bytes.Equal(prevTraceID, traceID) { innerIDs = append(innerIDs, traceID) prevTraceID = traceID } } // Merge-join current results if plan.mergeOuter == nil { plan.mergeOuter = innerIDs } else { plan.mergeOuter = mergeJoinIds(plan.mergeOuter, innerIDs) } } // Last scan should get us in correct timestamp order ids, err := r.scanIndexKeys(indexSeeks[0], plan) if err != nil { return nil, err } if plan.mergeOuter != nil { // Build hash of the current merged data plan.hashOuter = buildHash(plan, plan.mergeOuter) plan.mergeOuter = nil } else { // We filter the last elements plan.hashOuter = buildHash(plan, ids) } traceIDs := filterIDs(plan, ids) return traceIDs, nil } func filterIDs(plan *executionPlan, innerIDs [][]byte) []model.TraceID { traces := make([]model.TraceID, 0, plan.limit) items := 0 for i := range innerIDs { trID := bytesToTraceID(innerIDs[i]) if _, found := plan.hashOuter[trID]; found { traces = append(traces, trID) delete(plan.hashOuter, trID) // Prevent duplicate add items++ } if items == plan.limit { return traces } } return traces } func bytesToTraceID(key []byte) model.TraceID { return model.TraceID{ High: binary.BigEndian.Uint64(key[:8]), Low: binary.BigEndian.Uint64(key[8:sizeOfTraceID]), } } func buildHash(plan *executionPlan, outerIDs [][]byte) map[model.TraceID]struct{} { var empty struct{} hashed := make(map[model.TraceID]struct{}) for i := range outerIDs { trID := bytesToTraceID(outerIDs[i]) if plan.hashOuter != nil { if _, exists := plan.hashOuter[trID]; exists { hashed[trID] = empty delete(plan.hashOuter, trID) // Filter duplications } } else { hashed[trID] = empty } } return hashed } // durationQueries checks non unique index of durations and returns a map for further filtering purposes func (r *TraceReader) durationQueries(plan *executionPlan, query *spanstore.TraceQueryParameters) map[model.TraceID]struct{} { durMax := uint64(model.DurationAsMicroseconds(query.DurationMax)) durMin := uint64(model.DurationAsMicroseconds(query.DurationMin)) startKey := make([]byte, 1+8) endKey := make([]byte, 1+8) startKey[0] = durationIndexKey endKey[0] = durationIndexKey if query.DurationMax == 0 { // Set MAX to infinite, if Min is missing, 0 is a fine search result for us durMax = math.MaxUint64 } binary.BigEndian.PutUint64(endKey[1:], durMax) binary.BigEndian.PutUint64(startKey[1:], durMin) // This is not unique index result - same TraceID can be matched from multiple spans indexResults, _ := r.scanRangeIndex(plan, startKey, endKey) hashFilter := make(map[model.TraceID]struct{}) var value struct{} for _, k := range indexResults { key := k[len(k)-sizeOfTraceID:] id := model.TraceID{ High: binary.BigEndian.Uint64(key[:8]), Low: binary.BigEndian.Uint64(key[8:]), } if _, exists := hashFilter[id]; !exists { hashFilter[id] = value } } return hashFilter } func mergeJoinIds(left, right [][]byte) [][]byte { // len(left) or len(right) is the maximum, whichever is the smallest allocateSize := min(len(right), len(left)) merged := make([][]byte, 0, allocateSize) lMax := len(left) - 1 rMax := len(right) - 1 for r, l := 0, 0; r <= rMax && l <= lMax; { switch bytes.Compare(left[l], right[r]) { case 1: // left > right, increase right one r++ case -1: // left < right, increase left one l++ default: // Left matches right (case 0) - merge // #nosec G602 loop condition ensures l < len(left) merged = append(merged, left[l]) // Advance both l++ r++ } } return merged } // FindTraces retrieves traces that match the traceQuery func (r *TraceReader) FindTraces(ctx context.Context, query *spanstore.TraceQueryParameters) ([]*model.Trace, error) { keys, err := r.FindTraceIDs(ctx, query) if err != nil { return nil, err } return r.getTraces(keys) } // FindTraceIDs retrieves only the TraceIDs that match the traceQuery, but not the trace data func (r *TraceReader) FindTraceIDs(_ context.Context, query *spanstore.TraceQueryParameters) ([]model.TraceID, error) { // Validate and set query defaults which were not defined if err := validateQuery(query); err != nil { return nil, err } setQueryDefaults(query) // Find matches using indexes that are using service as part of the key indexSeeks := make([][]byte, 0, 1) indexSeeks = serviceQueries(query, indexSeeks) startStampBytes := make([]byte, 8) binary.BigEndian.PutUint64(startStampBytes, model.TimeAsEpochMicroseconds(query.StartTimeMin)) endStampBytes := make([]byte, 8) binary.BigEndian.PutUint64(endStampBytes, model.TimeAsEpochMicroseconds(query.StartTimeMax)) plan := &executionPlan{ startTimeMin: startStampBytes, startTimeMax: endStampBytes, limit: query.NumTraces, } if query.DurationMax != 0 || query.DurationMin != 0 { plan.hashOuter = r.durationQueries(plan, query) } if len(indexSeeks) > 0 { keys, err := r.indexSeeksToTraceIDs(plan, indexSeeks) if err != nil { return nil, err } return keys, nil } return r.scanTimeRange(plan) } // validateQuery returns an error if certain restrictions are not met func validateQuery(p *spanstore.TraceQueryParameters) error { if p == nil { return ErrMalformedRequestObject } if p.ServiceName == "" && len(p.Tags) > 0 { return ErrServiceNameNotSet } if p.ServiceName == "" && p.OperationName != "" { return ErrServiceNameNotSet } if p.StartTimeMin.IsZero() || p.StartTimeMax.IsZero() { return ErrStartAndEndTimeNotSet } if !p.StartTimeMax.IsZero() && p.StartTimeMax.Before(p.StartTimeMin) { return ErrStartTimeMinGreaterThanMax } if p.DurationMin != 0 && p.DurationMax != 0 && p.DurationMin > p.DurationMax { return ErrDurationMinGreaterThanMax } return nil } // scanIndexKeys scans the time range for index keys matching the given prefix. func (r *TraceReader) scanIndexKeys(indexKeyValue []byte, plan *executionPlan) ([][]byte, error) { indexResults := make([][]byte, 0) err := r.store.View(func(txn *badger.Txn) error { opts := badger.DefaultIteratorOptions opts.PrefetchValues = false // Don't fetch values since we're only interested in the keys opts.Reverse = true it := txn.NewIterator(opts) defer it.Close() // Create starting point for sorted index scan startIndex := make([]byte, len(indexKeyValue)+8+1) startIndex[len(startIndex)-1] = 0xFF copy(startIndex, indexKeyValue) copy(startIndex[len(indexKeyValue):], plan.startTimeMax) for it.Seek(startIndex); scanFunction(it, indexKeyValue, plan.startTimeMin); it.Next() { item := it.Item() // ScanFunction is a prefix scanning (since we could have for example service1 & service12) // Now we need to match only the exact key if we want to add it timestampStartIndex := len(it.Item().Key()) - (sizeOfTraceID + 8) // timestamp is stored with 8 bytes if bytes.Equal(indexKeyValue, it.Item().Key()[:timestampStartIndex]) { traceIDBytes := item.Key()[len(item.Key())-sizeOfTraceID:] traceIDCopy := make([]byte, sizeOfTraceID) copy(traceIDCopy, traceIDBytes) indexResults = append(indexResults, traceIDCopy) } } return nil }) return indexResults, err } // scanFunction compares the index name as well as the time range in the index key func scanFunction(it *badger.Iterator, indexPrefix []byte, timeBytesEnd []byte) bool { if it.Valid() { // We can't use the indexPrefix length, because we might have the same prefixValue for non-matching cases also timestampStartIndex := len(it.Item().Key()) - (sizeOfTraceID + 8) // timestamp is stored with 8 bytes timestamp := it.Item().Key()[timestampStartIndex : timestampStartIndex+8] timestampInRange := bytes.Compare(timeBytesEnd, timestamp) <= 0 // Check length as well to prevent theoretical case where timestamp might match with wrong index key if len(it.Item().Key()) != len(indexPrefix)+24 { return false } return bytes.HasPrefix(it.Item().Key()[:timestampStartIndex], indexPrefix) && timestampInRange } return false } // scanRangeIndex scans the time range for index keys matching the given prefix. func (r *TraceReader) scanRangeIndex(plan *executionPlan, indexStartValue []byte, indexEndValue []byte) ([][]byte, error) { indexResults := make([][]byte, 0) err := r.store.View(func(txn *badger.Txn) error { opts := badger.DefaultIteratorOptions opts.PrefetchValues = false // Don't fetch values since we're only interested in the keys it := txn.NewIterator(opts) defer it.Close() // Create starting point for sorted index scan startIndex := make([]byte, len(indexStartValue)+len(plan.startTimeMin)) copy(startIndex, indexStartValue) copy(startIndex[len(indexStartValue):], plan.startTimeMin) for it.Seek(startIndex); scanRangeFunction(it, indexEndValue); it.Next() { item := it.Item() // ScanFunction is a prefix scanning (since we could have for example service1 & service12) // Now we need to match only the exact key if we want to add it timestampStartIndex := len(it.Item().Key()) - (sizeOfTraceID + 8) // timestamp is stored with 8 bytes timestamp := it.Item().Key()[timestampStartIndex : timestampStartIndex+8] if bytes.Compare(timestamp, plan.startTimeMax) <= 0 { key := make([]byte, len(item.Key())) copy(key, item.Key()) indexResults = append(indexResults, key) } } return nil }) return indexResults, err } // scanRangeFunction seeks until the index end has been reached func scanRangeFunction(it *badger.Iterator, indexEndValue []byte) bool { if it.Valid() { compareSlice := it.Item().Key()[:len(indexEndValue)] return bytes.Compare(indexEndValue, compareSlice) >= 0 } return false } // preloadServices fills the cache with services after extracting from badger func (r *TraceReader) preloadServices() []string { services := map[string]struct{}{} r.store.View(func(txn *badger.Txn) error { opts := badger.DefaultIteratorOptions it := txn.NewIterator(opts) defer it.Close() serviceKey := []byte{serviceNameIndexKey} // Seek all the services first for it.Seek(serviceKey); it.ValidForPrefix(serviceKey); it.Next() { timestampStartIndex := len(it.Item().Key()) - (sizeOfTraceID + 8) // 8 = sizeof(uint64) serviceName := string(it.Item().Key()[len(serviceKey):timestampStartIndex]) keyTTL := it.Item().ExpiresAt() services[serviceName] = struct{}{} r.cache.AddService(serviceName, keyTTL) } return nil }) return maps.Keys(services) } // preloadOperations extract all operations for a specified service func (r *TraceReader) preloadOperations(service string) { r.store.View(func(txn *badger.Txn) error { opts := badger.DefaultIteratorOptions it := txn.NewIterator(opts) defer it.Close() serviceKey := make([]byte, len(service)+1) serviceKey[0] = operationNameIndexKey copy(serviceKey[1:], service) // Seek all the services first for it.Seek(serviceKey); it.ValidForPrefix(serviceKey); it.Next() { timestampStartIndex := len(it.Item().Key()) - (sizeOfTraceID + 8) // 8 = sizeof(uint64) operationName := string(it.Item().Key()[len(serviceKey):timestampStartIndex]) keyTTL := it.Item().ExpiresAt() r.cache.AddOperation(service, operationName, keyTTL) } return nil }) } ================================================ FILE: internal/storage/v1/badger/spanstore/rw_internal_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "context" "encoding/binary" "math/rand" "testing" "time" "github.com/dgraph-io/badger/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" ) func TestEncodingTypes(t *testing.T) { // JSON encoding runWithBadger(t, func(store *badger.DB, t *testing.T) { testSpan := createDummySpan() cache := NewCacheStore(store, time.Duration(1*time.Hour)) sw := NewSpanWriter(store, cache, time.Duration(1*time.Hour)) rw := NewTraceReader(store, cache, true) sw.encodingType = jsonEncoding err := sw.WriteSpan(context.Background(), &testSpan) require.NoError(t, err) tr, err := rw.GetTrace(context.Background(), spanstore.GetTraceParameters{TraceID: model.TraceID{Low: 0, High: 1}}) require.NoError(t, err) assert.Len(t, tr.Spans, 1) }) // Unknown encoding write runWithBadger(t, func(store *badger.DB, t *testing.T) { testSpan := createDummySpan() cache := NewCacheStore(store, time.Duration(1*time.Hour)) sw := NewSpanWriter(store, cache, time.Duration(1*time.Hour)) // rw := NewTraceReader(store, cache) sw.encodingType = 0x04 err := sw.WriteSpan(context.Background(), &testSpan) require.EqualError(t, err, "unknown encoding type: 0x04") }) // Unknown encoding reader runWithBadger(t, func(store *badger.DB, t *testing.T) { testSpan := createDummySpan() cache := NewCacheStore(store, time.Duration(1*time.Hour)) sw := NewSpanWriter(store, cache, time.Duration(1*time.Hour)) rw := NewTraceReader(store, cache, true) err := sw.WriteSpan(context.Background(), &testSpan) require.NoError(t, err) startTime := model.TimeAsEpochMicroseconds(testSpan.StartTime) key, _, _ := createTraceKV(&testSpan, protoEncoding, startTime) e := &badger.Entry{ Key: key, ExpiresAt: uint64(time.Now().Add(1 * time.Hour).Unix()), } e.UserMeta = byte(0x04) store.Update(func(txn *badger.Txn) error { txn.SetEntry(e) return nil }) _, err = rw.GetTrace(context.Background(), spanstore.GetTraceParameters{TraceID: model.TraceID{Low: 0, High: 1}}) require.EqualError(t, err, "unknown encoding type: 0x04") }) } func TestDecodeErrorReturns(t *testing.T) { garbage := []byte{0x08} _, err := decodeValue(garbage, protoEncoding) require.Error(t, err) _, err = decodeValue(garbage, jsonEncoding) require.Error(t, err) } func TestDuplicateTraceIDDetection(t *testing.T) { runWithBadger(t, func(store *badger.DB, t *testing.T) { testSpan := createDummySpan() cache := NewCacheStore(store, time.Duration(1*time.Hour)) sw := NewSpanWriter(store, cache, time.Duration(1*time.Hour)) rw := NewTraceReader(store, cache, true) origStartTime := testSpan.StartTime traceCount := 128 for range traceCount { testSpan.TraceID.Low = rand.Uint64() for range 32 { testSpan.SpanID = model.SpanID(rand.Uint64()) testSpan.StartTime = origStartTime.Add(time.Duration(rand.Int31n(8000)) * time.Millisecond) err := sw.WriteSpan(context.Background(), &testSpan) require.NoError(t, err) } } traces, err := rw.FindTraceIDs(context.Background(), &spanstore.TraceQueryParameters{ ServiceName: "service", NumTraces: 256, // Default is 100, we want to fetch more than there should be StartTimeMax: time.Now().Add(time.Hour), StartTimeMin: testSpan.StartTime.Add(-1 * time.Hour), }) require.NoError(t, err) assert.Len(t, traces, 128) }) } func createDummySpan() model.Span { tid := time.Now() dummyKv := []model.KeyValue{ { Key: "key", VType: model.StringType, VStr: "value", }, } testSpan := model.Span{ TraceID: model.TraceID{ Low: uint64(0), High: 1, }, SpanID: model.SpanID(0), OperationName: "operation", Process: &model.Process{ ServiceName: "service", Tags: dummyKv, }, StartTime: tid.Add(time.Duration(1 * time.Millisecond)), Duration: time.Duration(1 * time.Millisecond), Tags: dummyKv, Logs: []model.Log{ { Timestamp: tid, Fields: dummyKv, }, }, } return testSpan } func TestMergeJoin(t *testing.T) { chk := assert.New(t) // Test equals left := make([][]byte, 16) right := make([][]byte, 16) for i := range 16 { left[i] = make([]byte, 4) binary.BigEndian.PutUint32(left[i], uint32(i)) right[i] = make([]byte, 4) binary.BigEndian.PutUint32(right[i], uint32(i)) } merged := mergeJoinIds(left, right) chk.Len(merged, 16) // Check order chk.Equal(uint32(15), binary.BigEndian.Uint32(merged[15])) // Test simple non-equality different size merged = mergeJoinIds(left[1:2], right[13:]) chk.Empty(merged) // Different size, some equalities merged = mergeJoinIds(left[0:3], right[1:7]) chk.Len(merged, 2) chk.Equal(uint32(2), binary.BigEndian.Uint32(merged[1])) } func TestOldReads(t *testing.T) { runWithBadger(t, func(store *badger.DB, t *testing.T) { timeNow := model.TimeAsEpochMicroseconds(time.Now()) s1Key := createIndexKey(serviceNameIndexKey, []byte("service1"), timeNow, model.TraceID{High: 0, Low: 0}) s1o1Key := createIndexKey(operationNameIndexKey, []byte("service1operation1"), timeNow, model.TraceID{High: 0, Low: 0}) tid := time.Now().Add(1 * time.Minute) writer := func() { store.Update(func(txn *badger.Txn) error { txn.SetEntry(&badger.Entry{ Key: s1Key, ExpiresAt: uint64(tid.Unix()), }) txn.SetEntry(&badger.Entry{ Key: s1o1Key, ExpiresAt: uint64(tid.Unix()), }) return nil }) } cache := NewCacheStore(store, time.Duration(-1*time.Hour)) writer() nuTid := tid.Add(1 * time.Hour) cache.Update("service1", "operation1", uint64(tid.Unix())) cache.services["service1"] = uint64(nuTid.Unix()) cache.operations["service1"]["operation1"] = uint64(nuTid.Unix()) // This is equivalent to populate caches of cache _ = NewTraceReader(store, cache, true) // Now make sure we didn't use the older timestamps from the DB assert.Equal(t, uint64(nuTid.Unix()), cache.services["service1"]) assert.Equal(t, uint64(nuTid.Unix()), cache.operations["service1"]["operation1"]) }) } ================================================ FILE: internal/storage/v1/badger/spanstore/writer.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "context" "encoding/binary" "encoding/json" "fmt" "time" "github.com/dgraph-io/badger/v4" "github.com/gogo/protobuf/proto" "github.com/jaegertracing/jaeger-idl/model/v1" ) /* This store should be easily modified to use any sorted KV-store, which allows set/get/iterators. That includes RocksDB also (this key structure should work as-is with RocksDB) Keys are written in BigEndian order to allow lexicographic sorting of keys */ const ( spanKeyPrefix byte = 0x80 // All span keys should have first bit set to 1 indexKeyRange byte = 0x0F // Secondary indexes use last 4 bits serviceNameIndexKey byte = 0x81 operationNameIndexKey byte = 0x82 tagIndexKey byte = 0x83 durationIndexKey byte = 0x84 jsonEncoding byte = 0x01 // Last 4 bits of the meta byte are for encoding type protoEncoding byte = 0x02 // Last 4 bits of the meta byte are for encoding type defaultEncoding byte = protoEncoding ) // SpanWriter for writing spans to badger type SpanWriter struct { store *badger.DB ttl time.Duration cache *CacheStore encodingType byte } // NewSpanWriter returns a SpawnWriter with cache func NewSpanWriter(db *badger.DB, c *CacheStore, ttl time.Duration) *SpanWriter { return &SpanWriter{ store: db, ttl: ttl, cache: c, encodingType: defaultEncoding, // TODO Make configurable } } // WriteSpan writes the encoded span as well as creates indexes with defined TTL func (w *SpanWriter) WriteSpan(_ context.Context, span *model.Span) error { //nolint:gosec // G115 expireTime := uint64(time.Now().Add(w.ttl).Unix()) startTime := model.TimeAsEpochMicroseconds(span.StartTime) // Avoid doing as much as possible inside the transaction boundary, create entries here entriesToStore := make([]*badger.Entry, 0, len(span.Tags)+4+len(span.Process.Tags)+len(span.Logs)*4) trace, err := w.createTraceEntry(span, startTime, expireTime) if err != nil { return err } entriesToStore = append(entriesToStore, trace, w.createBadgerEntry(createIndexKey(serviceNameIndexKey, []byte(span.Process.ServiceName), startTime, span.TraceID), nil, expireTime), w.createBadgerEntry(createIndexKey(operationNameIndexKey, []byte(span.Process.ServiceName+span.OperationName), startTime, span.TraceID), nil, expireTime), ) // It doesn't matter if we overwrite Duration index keys, everything is read at Trace level in any case durationValue := make([]byte, 8) binary.BigEndian.PutUint64(durationValue, uint64(model.DurationAsMicroseconds(span.Duration))) entriesToStore = append(entriesToStore, w.createBadgerEntry(createIndexKey(durationIndexKey, durationValue, startTime, span.TraceID), nil, expireTime)) for _, kv := range span.Tags { // Convert everything to string since queries are done that way also // KEY: it VALUE: entriesToStore = append(entriesToStore, w.createBadgerEntry(createIndexKey(tagIndexKey, []byte(span.Process.ServiceName+kv.Key+kv.AsString()), startTime, span.TraceID), nil, expireTime)) } for _, kv := range span.Process.Tags { entriesToStore = append(entriesToStore, w.createBadgerEntry(createIndexKey(tagIndexKey, []byte(span.Process.ServiceName+kv.Key+kv.AsString()), startTime, span.TraceID), nil, expireTime)) } for _, log := range span.Logs { for _, kv := range log.Fields { entriesToStore = append(entriesToStore, w.createBadgerEntry(createIndexKey(tagIndexKey, []byte(span.Process.ServiceName+kv.Key+kv.AsString()), startTime, span.TraceID), nil, expireTime)) } } err = w.store.Update(func(txn *badger.Txn) error { // Write the entries for i := range entriesToStore { err = txn.SetEntry(entriesToStore[i]) if err != nil { // Most likely primary key conflict, but let the caller check this return err } } // TODO Alternative option is to use simpler keys with the merge value interface. // Requires at least this to be solved: https://github.com/dgraph-io/badger/issues/373 return nil }) // Do cache refresh here to release the transaction earlier w.cache.Update(span.Process.ServiceName, span.OperationName, expireTime) return err } func createIndexKey(indexPrefixKey byte, value []byte, startTime uint64, traceID model.TraceID) []byte { // KEY: indexKey (traceId is last 16 bytes of the key) key := make([]byte, 1+len(value)+8+sizeOfTraceID) key[0] = (indexPrefixKey & indexKeyRange) | spanKeyPrefix pos := len(value) + 1 copy(key[1:pos], value) binary.BigEndian.PutUint64(key[pos:], startTime) pos += 8 // sizeOfTraceID / 2 binary.BigEndian.PutUint64(key[pos:], traceID.High) pos += 8 // sizeOfTraceID / 2 binary.BigEndian.PutUint64(key[pos:], traceID.Low) return key } func (*SpanWriter) createBadgerEntry(key []byte, value []byte, expireTime uint64) *badger.Entry { return &badger.Entry{ Key: key, Value: value, ExpiresAt: expireTime, } } func (w *SpanWriter) createTraceEntry(span *model.Span, startTime, expireTime uint64) (*badger.Entry, error) { pK, pV, err := createTraceKV(span, w.encodingType, startTime) if err != nil { return nil, err } e := w.createBadgerEntry(pK, pV, expireTime) e.UserMeta = w.encodingType return e, nil } func createTraceKV(span *model.Span, encodingType byte, startTime uint64) (key []byte, bb []byte, err error) { // TODO Add Hash for Zipkin compatibility? // Note, KEY must include startTime for proper sorting order for span-ids // KEY: ti VALUE: All the details (json for now) METADATA: Encoding key = make([]byte, 1+sizeOfTraceID+8+8) key[0] = spanKeyPrefix pos := 1 binary.BigEndian.PutUint64(key[pos:], span.TraceID.High) pos += 8 binary.BigEndian.PutUint64(key[pos:], span.TraceID.Low) pos += 8 binary.BigEndian.PutUint64(key[pos:], startTime) pos += 8 binary.BigEndian.PutUint64(key[pos:], uint64(span.SpanID)) switch encodingType { case protoEncoding: bb, err = proto.Marshal(span) case jsonEncoding: bb, err = json.Marshal(span) default: return nil, nil, fmt.Errorf("unknown encoding type: %#02x", encodingType) } return key, bb, err } ================================================ FILE: internal/storage/v1/badger/stats.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 //go:build !linux package badger func (*Factory) diskStatisticsUpdate() error { return nil } ================================================ FILE: internal/storage/v1/badger/stats_linux.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package badger import ( "golang.org/x/sys/unix" ) func (f *Factory) diskStatisticsUpdate() error { // These stats are not interesting with Windows as there's no separate tmpfs // In case of ephemeral these are the same, but we'll report them separately for consistency var keyDirStatfs unix.Statfs_t // Error ignored to satisfy Codecov _ = unix.Statfs(f.Config.Directories.Keys, &keyDirStatfs) var valDirStatfs unix.Statfs_t // Error ignored to satisfy Codecov _ = unix.Statfs(f.Config.Directories.Values, &valDirStatfs) // Using Bavail instead of Bfree to get non-priviledged user space available //nolint:gosec // G115 f.metrics.ValueLogSpaceAvailable.Update(int64(valDirStatfs.Bavail) * int64(valDirStatfs.Bsize)) //nolint:gosec // G115 f.metrics.KeyLogSpaceAvailable.Update(int64(keyDirStatfs.Bavail) * int64(keyDirStatfs.Bsize)) /* TODO If we wanted to clean up oldest data to free up diskspace, we need at a minimum an index to the StartTime Additionally to that, the deletion might not save anything if the ratio of removed values is lower than the RunValueLogGC's deletion ratio and with the keys the LSM compaction must remove the offending files also. Thus, there's no guarantee the clean up would actually reduce the amount of diskspace used any faster than allowing TTL to remove them. If badger supports TimeWindow based compaction, then this should be resolved. Not available in 1.5.3 */ return nil } ================================================ FILE: internal/storage/v1/badger/stats_linux_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package badger import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/metricstest" ) func TestDiskStatisticsUpdate(t *testing.T) { f := NewFactory() f.Config.Ephemeral = true f.Config.SyncWrites = false mFactory := metricstest.NewFactory(0) err := f.Initialize(mFactory, zap.NewNop()) require.NoError(t, err) defer f.Close() err = f.diskStatisticsUpdate() require.NoError(t, err) _, gs := mFactory.Snapshot() assert.Positive(t, gs[keyLogSpaceAvailableName]) assert.Positive(t, gs[valueLogSpaceAvailableName]) } ================================================ FILE: internal/storage/v1/badger/stats_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 //go:build !linux package badger import ( "testing" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/metrics" ) func TestDiskStatisticsUpdate(t *testing.T) { f := NewFactory() f.Config.Ephemeral = true f.Config.SyncWrites = false err := f.Initialize(metrics.NullFactory, zap.NewNop()) require.NoError(t, err) defer f.Close() // We're not expecting any value in !linux, just no error err = f.diskStatisticsUpdate() require.NoError(t, err) } ================================================ FILE: internal/storage/v1/blackhole/blackhole.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package blackhole import ( "context" "time" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" ) // Store is a blackhole. It creates an artificial micro-singularity // and forwards all writes to it. We do not know what happens to the // data once it reaches the singulatiry, but we know that we cannot // get it back. type Store struct { // nothing, just darkness } // NewStore creates a blackhole store. func NewStore() *Store { return &Store{} } // GetDependencies returns nothing. func (*Store) GetDependencies(context.Context, time.Time /* endTs */, time.Duration /* lookback */) ([]model.DependencyLink, error) { return []model.DependencyLink{}, nil } // WriteSpan writes the given span to blackhole. func (*Store) WriteSpan(context.Context, *model.Span) error { return nil } // GetTrace gets nothing. func (*Store) GetTrace(context.Context, spanstore.GetTraceParameters) (*model.Trace, error) { return nil, spanstore.ErrTraceNotFound } // GetServices returns nothing. func (*Store) GetServices(context.Context) ([]string, error) { return []string{}, nil } // GetOperations returns nothing. func (*Store) GetOperations( context.Context, spanstore.OperationQueryParameters, ) ([]spanstore.Operation, error) { return []spanstore.Operation{}, nil } // FindTraces returns nothing. func (*Store) FindTraces(context.Context, *spanstore.TraceQueryParameters) ([]*model.Trace, error) { return []*model.Trace{}, nil } // FindTraceIDs returns nothing. func (*Store) FindTraceIDs(context.Context, *spanstore.TraceQueryParameters) ([]model.TraceID, error) { return []model.TraceID{}, nil } ================================================ FILE: internal/storage/v1/blackhole/blackhole_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package blackhole import ( "context" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" ) func withBlackhole(f func(store *Store)) { f(NewStore()) } func TestStoreGetDependencies(t *testing.T) { withBlackhole(func(store *Store) { links, err := store.GetDependencies(context.Background(), time.Now(), time.Hour) require.NoError(t, err) assert.Empty(t, links) }) } func TestStoreWriteSpan(t *testing.T) { withBlackhole(func(store *Store) { err := store.WriteSpan(context.Background(), nil) require.NoError(t, err) }) } func TestStoreGetTrace(t *testing.T) { withBlackhole(func(store *Store) { trace, err := store.GetTrace(context.Background(), spanstore.GetTraceParameters{TraceID: model.NewTraceID(1, 2)}) require.Error(t, err) assert.Nil(t, trace) }) } func TestStoreGetServices(t *testing.T) { withBlackhole(func(store *Store) { serviceNames, err := store.GetServices(context.Background()) require.NoError(t, err) assert.Empty(t, serviceNames) }) } func TestStoreGetAllOperations(t *testing.T) { withBlackhole(func(store *Store) { operations, err := store.GetOperations( context.Background(), spanstore.OperationQueryParameters{}, ) require.NoError(t, err) assert.Empty(t, operations) }) } func TestStoreFindTraces(t *testing.T) { withBlackhole(func(store *Store) { traces, err := store.FindTraces(context.Background(), nil) require.NoError(t, err) assert.Empty(t, traces) }) } func TestStoreFindTraceIDs(t *testing.T) { withBlackhole(func(store *Store) { traceIDs, err := store.FindTraceIDs(context.Background(), nil) require.NoError(t, err) assert.Empty(t, traceIDs) }) } ================================================ FILE: internal/storage/v1/blackhole/factory.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package blackhole ================================================ FILE: internal/storage/v1/blackhole/factory_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package blackhole ================================================ FILE: internal/storage/v1/blackhole/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package blackhole import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/cassandra/Dockerfile ================================================ # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 FROM cassandra:5.0.6@sha256:5e2c85d2d5db759c28c3efb50905f8d237f958321d6dfd8c176cb148700d9ade COPY schema/* /cassandra-schema/ ENV CQLSH_HOST=cassandra RUN groupadd -g 65532 nonroot && \ useradd -u 65532 -g nonroot nonroot --create-home USER 65532:65532 ENTRYPOINT ["/cassandra-schema/docker.sh"] ================================================ FILE: internal/storage/v1/cassandra/dependencystore/bootstrap.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2019 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package dependencystore import ( "github.com/jaegertracing/jaeger/internal/storage/cassandra" ) // GetDependencyVersion attempts to determine the version of the dependencies table. // TODO: Remove this once we've migrated to V2 permanently. https://github.com/jaegertracing/jaeger/issues/1344 func GetDependencyVersion(s cassandra.Session) Version { if err := s.Query("SELECT ts from dependencies_v2 limit 1;").Exec(); err != nil { return V1 } return V2 } ================================================ FILE: internal/storage/v1/cassandra/dependencystore/bootstrap_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2019 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package dependencystore import ( "errors" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/jaegertracing/jaeger/internal/storage/cassandra/mocks" ) func TestGetDependencyVersionV1(t *testing.T) { var ( session = &mocks.Session{} query = &mocks.Query{} ) session.On("Query", mock.AnythingOfType("string"), mock.Anything).Return(query) query.On("Exec").Return(errors.New("error")) assert.Equal(t, V1, GetDependencyVersion(session)) } func TestGetDependencyVersionV2(t *testing.T) { var ( session = &mocks.Session{} query = &mocks.Query{} ) session.On("Query", mock.AnythingOfType("string"), mock.Anything).Return(query) query.On("Exec").Return(nil) assert.Equal(t, V2, GetDependencyVersion(session)) } ================================================ FILE: internal/storage/v1/cassandra/dependencystore/model.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package dependencystore import ( "fmt" gocql "github.com/apache/cassandra-gocql-driver/v2" ) // Dependency is the UDT representation of a Jaeger Dependency. type Dependency struct { Parent string `cql:"parent"` Child string `cql:"child"` CallCount int64 `cql:"call_count"` // always unsigned, but we cannot explicitly read uint64 from Cassandra Source string `cql:"source"` } // MarshalUDT handles marshalling a Dependency. func (d *Dependency) MarshalUDT(name string, info gocql.TypeInfo) ([]byte, error) { switch name { case "parent": return gocql.Marshal(info, d.Parent) case "child": return gocql.Marshal(info, d.Child) case "call_count": return gocql.Marshal(info, d.CallCount) case "source": return gocql.Marshal(info, d.Source) default: return nil, fmt.Errorf("unknown column for position: %q", name) } } // UnmarshalUDT handles unmarshalling a Dependency. func (d *Dependency) UnmarshalUDT(name string, info gocql.TypeInfo, data []byte) error { switch name { case "parent": return gocql.Unmarshal(info, data, &d.Parent) case "child": return gocql.Unmarshal(info, data, &d.Child) case "call_count": return gocql.Unmarshal(info, data, &d.CallCount) case "source": return gocql.Unmarshal(info, data, &d.Source) default: return fmt.Errorf("unknown column for position: %q", name) } } ================================================ FILE: internal/storage/v1/cassandra/dependencystore/model_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package dependencystore import ( "testing" gocql "github.com/apache/cassandra-gocql-driver/v2" "github.com/jaegertracing/jaeger/internal/storage/cassandra/gocql/testutils" ) func TestDependencyUDT(t *testing.T) { dependency := &Dependency{ Parent: "bi", Child: "ng", CallCount: 123, Source: "jaeger", } testCase := testutils.UDTTestCase{ Obj: dependency, New: func() gocql.UDTUnmarshaler { return &Dependency{} }, ObjName: "Dependency", Fields: []testutils.UDTField{ {Name: "parent", Type: gocql.TypeAscii, ValIn: []byte("bi"), Err: false}, {Name: "child", Type: gocql.TypeAscii, ValIn: []byte("ng"), Err: false}, {Name: "call_count", Type: gocql.TypeBigInt, ValIn: []byte{0, 0, 0, 0, 0, 0, 0, 123}, Err: false}, {Name: "source", Type: gocql.TypeAscii, ValIn: []byte("jaeger"), Err: false}, {Name: "wrong-field", Err: true}, }, } testCase.Run(t) } ================================================ FILE: internal/storage/v1/cassandra/dependencystore/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dependencystore import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/cassandra/dependencystore/storage.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package dependencystore import ( "context" "errors" "fmt" "time" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/storage/cassandra" casmetrics "github.com/jaegertracing/jaeger/internal/storage/cassandra/metrics" ) // Version determines which version of the dependencies table to use. type Version int // IsValid returns true if the Version is a valid one. func (i Version) IsValid() bool { return i >= 0 && i < versionEnumEnd } const ( // V1 is used when the dependency table is SASI indexed. V1 Version = iota // V2 is used when the dependency table is NOT SASI indexed. V2 versionEnumEnd depsInsertStmtV1 = "INSERT INTO dependencies(ts, ts_index, dependencies) VALUES (?, ?, ?)" depsInsertStmtV2 = "INSERT INTO dependencies_v2(ts, ts_bucket, dependencies) VALUES (?, ?, ?)" depsSelectStmtV1 = "SELECT ts, dependencies FROM dependencies WHERE ts_index >= ? AND ts_index < ?" depsSelectStmtV2 = "SELECT ts, dependencies FROM dependencies_v2 WHERE ts_bucket IN ? AND ts >= ? AND ts < ?" // TODO: Make this customizable. tsBucket = 24 * time.Hour ) var errInvalidVersion = errors.New("invalid version") // DependencyStore handles all queries and insertions to Cassandra dependencies type DependencyStore struct { session cassandra.Session dependenciesTableMetrics *casmetrics.Table logger *zap.Logger version Version } // NewDependencyStore returns a DependencyStore func NewDependencyStore( session cassandra.Session, metricsFactory metrics.Factory, logger *zap.Logger, version Version, ) (*DependencyStore, error) { if !version.IsValid() { return nil, errInvalidVersion } return &DependencyStore{ session: session, dependenciesTableMetrics: casmetrics.NewTable(metricsFactory, "dependencies"), logger: logger, version: version, }, nil } // WriteDependencies implements dependencystore.Writer#WriteDependencies. func (s *DependencyStore) WriteDependencies(ts time.Time, dependencies []model.DependencyLink) error { deps := make([]Dependency, len(dependencies)) for i, d := range dependencies { deps[i] = Dependency{ Parent: d.Parent, Child: d.Child, //nolint:gosec // G115 CallCount: int64(d.CallCount), Source: string(d.Source), } } var query cassandra.Query switch s.version { case V1: query = s.session.Query(depsInsertStmtV1, ts, ts, deps) case V2: query = s.session.Query(depsInsertStmtV2, ts, ts.Truncate(tsBucket), deps) default: return fmt.Errorf("unsupported schema version: %v", s.version) } return s.dependenciesTableMetrics.Exec(query, s.logger) } // GetDependencies returns all interservice dependencies func (s *DependencyStore) GetDependencies(_ context.Context, endTs time.Time, lookback time.Duration) ([]model.DependencyLink, error) { startTs := endTs.Add(-1 * lookback) var query cassandra.Query switch s.version { case V1: query = s.session.Query(depsSelectStmtV1, startTs, endTs) case V2: query = s.session.Query(depsSelectStmtV2, getBuckets(startTs, endTs), startTs, endTs) default: return nil, fmt.Errorf("unsupported schema version: %v", s.version) } iter := query.Consistency(cassandra.One).Iter() var mDependency []model.DependencyLink var dependencies []Dependency var ts time.Time for iter.Scan(&ts, &dependencies) { for _, dependency := range dependencies { dl := model.DependencyLink{ Parent: dependency.Parent, Child: dependency.Child, //nolint:gosec // G115 CallCount: uint64(dependency.CallCount), Source: dependency.Source, }.ApplyDefaults() mDependency = append(mDependency, dl) } } if err := iter.Close(); err != nil { s.logger.Error("Failure to read Dependencies", zap.Time("endTs", endTs), zap.Duration("lookback", lookback), zap.Error(err)) return nil, fmt.Errorf("error reading dependencies from storage: %w", err) } return mDependency, nil } func getBuckets(startTs time.Time, endTs time.Time) []time.Time { // TODO: Preallocate the array using some maths and maybe use a pool? This endpoint probably isn't used enough to warrant this. var tsBuckets []time.Time for ts := startTs.Truncate(tsBucket); ts.Before(endTs); ts = ts.Add(tsBucket) { tsBuckets = append(tsBuckets, ts) } return tsBuckets } ================================================ FILE: internal/storage/v1/cassandra/dependencystore/storage_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package dependencystore import ( "context" "errors" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/metricstest" "github.com/jaegertracing/jaeger/internal/storage/cassandra" casmetrics "github.com/jaegertracing/jaeger/internal/storage/cassandra/metrics" "github.com/jaegertracing/jaeger/internal/storage/cassandra/mocks" "github.com/jaegertracing/jaeger/internal/storage/v1/api/dependencystore" "github.com/jaegertracing/jaeger/internal/testutils" ) type depStorageTest struct { session *mocks.Session logger *zap.Logger logBuffer *testutils.Buffer storage *DependencyStore } func withDepStore(version Version, fn func(s *depStorageTest)) { session := &mocks.Session{} logger, logBuffer := testutils.NewLogger() metricsFactory := metricstest.NewFactory(time.Second) defer metricsFactory.Stop() store, _ := NewDependencyStore(session, metricsFactory, logger, version) s := &depStorageTest{ session: session, logger: logger, logBuffer: logBuffer, storage: store, } fn(s) } var ( _ dependencystore.Reader = &DependencyStore{} // check API conformance _ dependencystore.Writer = &DependencyStore{} // check API conformance ) func TestVersionIsValid(t *testing.T) { assert.True(t, V1.IsValid()) assert.True(t, V2.IsValid()) assert.False(t, versionEnumEnd.IsValid()) } func TestInvalidVersion(t *testing.T) { _, err := NewDependencyStore(&mocks.Session{}, metrics.NullFactory, zap.NewNop(), versionEnumEnd) require.Error(t, err) } func TestDependencyStoreWrite(t *testing.T) { testCases := []struct { caption string version Version }{ { caption: "V1", version: V1, }, { caption: "V2", version: V2, }, } for _, tc := range testCases { testCase := tc // capture loop var t.Run(testCase.caption, func(t *testing.T) { withDepStore(testCase.version, func(s *depStorageTest) { query := &mocks.Query{} query.On("Exec").Return(nil) var args []any captureArgs := mock.MatchedBy(func(v []any) bool { args = v return true }) s.session.On("Query", mock.AnythingOfType("string"), captureArgs).Return(query) ts := time.Date(2017, time.January, 24, 11, 15, 17, 12345, time.UTC) dependencies := []model.DependencyLink{ { Parent: "a", Child: "b", CallCount: 42, Source: model.JaegerDependencyLinkSource, }, } err := s.storage.WriteDependencies(ts, dependencies) require.NoError(t, err) assert.Len(t, args, 3) if d, ok := args[0].(time.Time); ok { assert.Equal(t, ts, d) } else { assert.Fail(t, "expecting first arg as time.Time", "received: %+v", args) } if testCase.version == V2 { if d, ok := args[1].(time.Time); ok { assert.Equal(t, time.Date(2017, time.January, 24, 0, 0, 0, 0, time.UTC), d) } else { assert.Fail(t, "expecting second arg as time", "received: %+v", args) } } else { if d, ok := args[1].(time.Time); ok { assert.Equal(t, ts, d) } else { assert.Fail(t, "expecting second arg as time.Time", "received: %+v", args) } } if d, ok := args[2].([]Dependency); ok { assert.Equal(t, []Dependency{ { Parent: "a", Child: "b", CallCount: 42, Source: "jaeger", }, }, d) } else { assert.Fail(t, "expecting third arg as []Dependency", "received: %+v", args) } }) }) } } func TestDependencyStoreGetDependencies(t *testing.T) { testCases := []struct { caption string queryError error expectedError string expectedLogs []string version Version }{ { caption: "success V1", version: V1, }, { caption: "success V2", version: V2, }, { caption: "failure V1", queryError: errors.New("query error"), expectedError: "error reading dependencies from storage: query error", expectedLogs: []string{ "Failure to read Dependencies", }, version: V1, }, { caption: "failure V2", queryError: errors.New("query error"), expectedError: "error reading dependencies from storage: query error", expectedLogs: []string{ "Failure to read Dependencies", }, version: V2, }, } for _, tc := range testCases { testCase := tc // capture loop var t.Run(testCase.caption, func(t *testing.T) { withDepStore(testCase.version, func(s *depStorageTest) { scanMatcher := func() any { deps := [][]Dependency{ { {Parent: "a", Child: "b", CallCount: 1}, {Parent: "b", Child: "c", CallCount: 1}, }, { {Parent: "a", Child: "b", CallCount: 1}, {Parent: "b", Child: "c", CallCount: 1}, }, } scanFunc := func(args []any) bool { if len(deps) == 0 { return false } for _, arg := range args { if ptr, ok := arg.(*[]Dependency); ok { *ptr = deps[0] break } } deps = deps[1:] return true } return mock.MatchedBy(scanFunc) } iter := &mocks.Iterator{} iter.On("Scan", scanMatcher()).Return(true) iter.On("Scan", matchEverything()).Return(false) iter.On("Close").Return(testCase.queryError) query := &mocks.Query{} query.On("Exec").Return(nil) query.On("Consistency", cassandra.One).Return(query) query.On("Iter").Return(iter) s.session.On("Query", mock.AnythingOfType("string"), matchEverything()).Return(query) deps, err := s.storage.GetDependencies(context.Background(), time.Now(), 48*time.Hour) if testCase.expectedError == "" { require.NoError(t, err) expected := []model.DependencyLink{ {Parent: "a", Child: "b", CallCount: 1, Source: model.JaegerDependencyLinkSource}, {Parent: "b", Child: "c", CallCount: 1, Source: model.JaegerDependencyLinkSource}, {Parent: "a", Child: "b", CallCount: 1, Source: model.JaegerDependencyLinkSource}, {Parent: "b", Child: "c", CallCount: 1, Source: model.JaegerDependencyLinkSource}, } assert.Equal(t, expected, deps) } else { require.EqualError(t, err, testCase.expectedError) } for _, expectedLog := range testCase.expectedLogs { assert.Contains(t, s.logBuffer.String(), expectedLog, "Log must contain %s, but was %s", expectedLog, s.logBuffer.String()) } if len(testCase.expectedLogs) == 0 { assert.Empty(t, s.logBuffer.String()) } }) }) } } func TestGetBuckets(t *testing.T) { var ( start = time.Date(2017, time.January, 24, 11, 15, 17, 12345, time.UTC) end = time.Date(2017, time.January, 26, 11, 15, 17, 12345, time.UTC) expected = []time.Time{ time.Date(2017, time.January, 24, 0, 0, 0, 0, time.UTC), time.Date(2017, time.January, 25, 0, 0, 0, 0, time.UTC), time.Date(2017, time.January, 26, 0, 0, 0, 0, time.UTC), } ) assert.Equal(t, expected, getBuckets(start, end)) } func matchEverything() any { return mock.MatchedBy(func([]any) bool { return true }) } func TestDependencyStore_UnsupportedVersion(t *testing.T) { logger := zap.NewNop() metricsFactory := metrics.NullFactory session := &mocks.Session{} store := &DependencyStore{ session: session, dependenciesTableMetrics: casmetrics.NewTable(metricsFactory, "dependencies"), logger: logger, version: Version(999), } deps := []model.DependencyLink{ {Parent: "parent", Child: "child", CallCount: 1}, } err := store.WriteDependencies(time.Now(), deps) require.Error(t, err) assert.Contains(t, err.Error(), "unsupported schema version") _, err = store.GetDependencies(context.Background(), time.Now(), time.Hour) require.Error(t, err) assert.Contains(t, err.Error(), "unsupported schema version") } ================================================ FILE: internal/storage/v1/cassandra/factory.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package cassandra import ( "context" "errors" "io" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/distributedlock" "github.com/jaegertracing/jaeger/internal/hostname" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/storage/cassandra" "github.com/jaegertracing/jaeger/internal/storage/cassandra/config" gocqlw "github.com/jaegertracing/jaeger/internal/storage/cassandra/gocql" caslock "github.com/jaegertracing/jaeger/internal/storage/distributedlock/cassandra" "github.com/jaegertracing/jaeger/internal/storage/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/api/dependencystore" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" cdepstore "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/dependencystore" csamplingstore "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/samplingstore" "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/schema" cspanstore "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/spanstore/dbmodel" ) var ( // interface comformance checks _ storage.Purger = (*Factory)(nil) _ storage.SamplingStoreFactory = (*Factory)(nil) _ io.Closer = (*Factory)(nil) _ storage.ArchiveCapable = (*Factory)(nil) ) // Factory for Cassandra backend. type Factory struct { Options *Options metricsFactory metrics.Factory logger *zap.Logger tracer trace.TracerProvider config config.Configuration session cassandra.Session // tests can override this sessionBuilderFn func(*config.Configuration) (cassandra.Session, error) } // NewFactory creates a new Factory. func NewFactory() *Factory { return &Factory{ Options: NewOptions(), sessionBuilderFn: NewSession, } } // InitFromOptions initializes factory from options. func (f *Factory) ConfigureFromOptions(o *Options) { f.Options = o f.config = o.GetConfig() } // Initialize performs internal initialization of the factory. func (f *Factory) Initialize(metricsFactory metrics.Factory, logger *zap.Logger, tracer trace.TracerProvider) error { f.metricsFactory = metricsFactory f.logger = logger f.tracer = tracer session, err := f.sessionBuilderFn(&f.config) if err != nil { return err } f.session = session return nil } // createSession creates session from a configuration func createSession(c *config.Configuration) (cassandra.Session, error) { cluster, err := c.NewCluster() if err != nil { return nil, err } session, err := cluster.CreateSession() if err != nil { return nil, err } return gocqlw.WrapCQLSession(session), nil } // newSessionPrerequisites creates tables and types before creating a session func newSessionPrerequisites(c *config.Configuration) error { if !c.Schema.CreateSchema { return nil } cfg := *c // clone because we need to connect without specifying a keyspace cfg.Schema.Keyspace = "" session, err := createSession(&cfg) if err != nil { return err } sc := schema.NewSchemaCreator(session, c.Schema) return sc.CreateSchemaIfNotPresent() } // NewSession creates a new Cassandra session func NewSession(c *config.Configuration) (cassandra.Session, error) { if err := newSessionPrerequisites(c); err != nil { return nil, err } return createSession(c) } // CreateSpanReader implements storage.Factory func (*Factory) CreateSpanReader() (spanstore.Reader, error) { return nil, errors.New("not implemented") } // CreateSpanWriter creates a spanstore.Writer. func (f *Factory) CreateSpanWriter() (spanstore.Writer, error) { options, err := writerOptions(f.Options) if err != nil { return nil, err } return cspanstore.NewSpanWriter(f.session, f.Options.SpanStoreWriteCacheTTL, f.metricsFactory, f.logger, options...) } // CreateDependencyReader creates a dependencystore.Reader. func (f *Factory) CreateDependencyReader() (dependencystore.Reader, error) { version := cdepstore.GetDependencyVersion(f.session) return cdepstore.NewDependencyStore(f.session, f.metricsFactory, f.logger, version) } // CreateLock implements storage.SamplingStoreFactory func (f *Factory) CreateLock() (distributedlock.Lock, error) { hostId, err := hostname.AsIdentifier() if err != nil { return nil, err } f.logger.Info("Using unique participantName in the distributed lock", zap.String("participantName", hostId)) return caslock.NewLock(f.session, hostId), nil } // CreateSamplingStore implements storage.SamplingStoreFactory func (f *Factory) CreateSamplingStore(int /* maxBuckets */) (samplingstore.Store, error) { samplingMetricsFactory := f.metricsFactory.Namespace( metrics.NSOptions{ Tags: map[string]string{ "role": "sampling", }, }, ) return csamplingstore.New(f.session, samplingMetricsFactory, f.logger), nil } func writerOptions(opts *Options) ([]cspanstore.Option, error) { var tagFilters []dbmodel.TagFilter // drop all tag filters if !opts.Index.Tags || !opts.Index.ProcessTags || !opts.Index.Logs { tagFilters = append(tagFilters, dbmodel.NewTagFilterDropAll(!opts.Index.Tags, !opts.Index.ProcessTags, !opts.Index.Logs)) } // black/white list tag filters tagIndexBlacklist := opts.TagIndexBlacklist() tagIndexWhitelist := opts.TagIndexWhitelist() if len(tagIndexBlacklist) > 0 && len(tagIndexWhitelist) > 0 { return nil, errors.New("only one of TagIndexBlacklist and TagIndexWhitelist can be specified") } if len(tagIndexBlacklist) > 0 { tagFilters = append(tagFilters, dbmodel.NewBlacklistFilter(tagIndexBlacklist)) } else if len(tagIndexWhitelist) > 0 { tagFilters = append(tagFilters, dbmodel.NewWhitelistFilter(tagIndexWhitelist)) } if len(tagFilters) == 0 { return nil, nil } else if len(tagFilters) == 1 { return []cspanstore.Option{cspanstore.TagFilter(tagFilters[0])}, nil } return []cspanstore.Option{cspanstore.TagFilter(dbmodel.NewChainedTagFilter(tagFilters...))}, nil } var _ io.Closer = (*Factory)(nil) // Close closes the resources held by the factory func (f *Factory) Close() error { if f.session != nil { f.session.Close() } return nil } func (f *Factory) Purge(_ context.Context) error { return f.session.Query("TRUNCATE traces").Exec() } func (f *Factory) IsArchiveCapable() bool { return f.Options.ArchiveEnabled } func (f *Factory) GetSession() cassandra.Session { return f.session } ================================================ FILE: internal/storage/v1/cassandra/factory_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package cassandra import ( "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/config/configtls" "github.com/jaegertracing/jaeger/internal/storage/cassandra/config" "github.com/jaegertracing/jaeger/internal/storage/cassandra/mocks" ) func TestCreateSpanReaderError(t *testing.T) { f := NewFactory() _, err := f.CreateSpanReader() require.ErrorContains(t, err, "not implemented") } func TestConfigureFromOptions(t *testing.T) { f := NewFactory() o := NewOptions() f.ConfigureFromOptions(o) assert.Equal(t, o, f.Options) assert.Equal(t, o.GetConfig(), f.config) } func TestFactory_Purge(t *testing.T) { f := NewFactory() var ( session = &mocks.Session{} query = &mocks.Query{} ) session.On("Query", mock.AnythingOfType("string"), mock.Anything).Return(query) query.On("Exec").Return(nil) f.session = session err := f.Purge(context.Background()) require.NoError(t, err) session.AssertCalled(t, "Query", mock.AnythingOfType("string"), mock.Anything) query.AssertCalled(t, "Exec") } func TestNewSessionErrors(t *testing.T) { t.Run("NewCluster error", func(t *testing.T) { cfg := &config.Configuration{ Connection: config.Connection{ TLS: configtls.ClientConfig{ Config: configtls.Config{ CAFile: "foobar", }, }, }, } _, err := NewSession(cfg) require.ErrorContains(t, err, "failed to load TLS config") }) t.Run("CreateSession error", func(t *testing.T) { cfg := &config.Configuration{} _, err := NewSession(cfg) require.ErrorContains(t, err, "no hosts provided") }) t.Run("CreateSession error with schema", func(t *testing.T) { cfg := &config.Configuration{ Schema: config.Schema{ CreateSchema: true, }, } _, err := NewSession(cfg) require.ErrorContains(t, err, "no hosts provided") }) } func TestIsArchiveCapable(t *testing.T) { tests := []struct { name string archiveEnabled bool expected bool }{ { name: "archive capable", archiveEnabled: true, expected: true, }, { name: "not capable", archiveEnabled: false, expected: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { factory := &Factory{ Options: &Options{ ArchiveEnabled: test.archiveEnabled, }, } result := factory.IsArchiveCapable() require.Equal(t, test.expected, result) }) } } ================================================ FILE: internal/storage/v1/cassandra/helper.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package cassandra import ( "github.com/jaegertracing/jaeger/internal/storage/cassandra" "github.com/jaegertracing/jaeger/internal/storage/cassandra/config" "github.com/jaegertracing/jaeger/internal/storage/cassandra/mocks" ) type mockSessionBuilder struct { index int sessions []*mocks.Session errors []error } func (m *mockSessionBuilder) add(session *mocks.Session, err error) *mockSessionBuilder { m.sessions = append(m.sessions, session) m.errors = append(m.errors, err) return m } func (m *mockSessionBuilder) build(*config.Configuration) (cassandra.Session, error) { session := m.sessions[m.index] err := m.errors[m.index] m.index++ return session, err } func MockSession(f *Factory, session *mocks.Session, err error) { f.sessionBuilderFn = new(mockSessionBuilder).add(session, err).build } ================================================ FILE: internal/storage/v1/cassandra/options.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package cassandra import ( "strings" "time" "github.com/jaegertracing/jaeger/internal/storage/cassandra/config" ) // Options contains various type of Cassandra configs and provides the ability // to bind them to command line flag and apply overlays, so that some configurations // (e.g. archive) may be underspecified and infer the rest of its parameters from primary. type Options struct { config.Configuration `mapstructure:",squash"` SpanStoreWriteCacheTTL time.Duration `mapstructure:"span_store_write_cache_ttl"` Index IndexConfig `mapstructure:"index"` ArchiveEnabled bool `mapstructure:"-"` } // IndexConfig configures indexing. // By default all indexing is enabled. type IndexConfig struct { Logs bool `mapstructure:"logs"` Tags bool `mapstructure:"tags"` ProcessTags bool `mapstructure:"process_tags"` TagBlackList string `mapstructure:"tag_blacklist"` TagWhiteList string `mapstructure:"tag_whitelist"` } // NewOptions creates a new Options struct. func NewOptions() *Options { // TODO all default values should be defined via cobra flags options := &Options{ Configuration: config.DefaultConfiguration(), SpanStoreWriteCacheTTL: time.Hour * 12, ArchiveEnabled: false, } return options } func (opt *Options) GetConfig() config.Configuration { return opt.Configuration } // TagIndexBlacklist returns the list of blacklisted tags func (opt *Options) TagIndexBlacklist() []string { if opt.Index.TagBlackList != "" { return strings.Split(opt.Index.TagBlackList, ",") } return nil } // TagIndexWhitelist returns the list of whitelisted tags func (opt *Options) TagIndexWhitelist() []string { if opt.Index.TagWhiteList != "" { return strings.Split(opt.Index.TagWhiteList, ",") } return nil } ================================================ FILE: internal/storage/v1/cassandra/options_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package cassandra import ( "testing" "github.com/stretchr/testify/assert" ) func TestOptions(t *testing.T) { opts := NewOptions() primary := opts.GetConfig() assert.NotEmpty(t, primary.Schema.Keyspace) assert.NotEmpty(t, primary.Connection.Servers) assert.Equal(t, 2, primary.Connection.ConnectionsPerHost) } ================================================ FILE: internal/storage/v1/cassandra/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package cassandra import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/cassandra/samplingstore/storage.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package samplingstore import ( "bytes" "encoding/csv" "errors" "fmt" "io" "strconv" "strings" "time" gocql "github.com/apache/cassandra-gocql-driver/v2" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/storage/cassandra" casmetrics "github.com/jaegertracing/jaeger/internal/storage/cassandra/metrics" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore/model" ) const ( buckets = `(0,1,2,3,4,5,6,7,8,9)` constBucket = 1 constBucketStr = `1` insertThroughput = `INSERT INTO operation_throughput(bucket, ts, throughput) VALUES (?, ?, ?)` getThroughput = `SELECT throughput FROM operation_throughput WHERE bucket IN ` + buckets + ` AND ts > ? AND ts <= ?` insertProbabilities = `INSERT INTO sampling_probabilities(bucket, ts, hostname, probabilities) VALUES (?, ?, ?, ?)` getLatestProbabilities = `SELECT probabilities FROM sampling_probabilities WHERE bucket = ` + constBucketStr + ` LIMIT 1` ) // probabilityAndQPS contains the sampling probability and measured qps for an operation. type probabilityAndQPS struct { Probability float64 QPS float64 } // serviceOperationData contains the sampling probabilities and measured qps for all operations in a service. // ie [service][operation] = ProbabilityAndQPS type serviceOperationData map[string]map[string]*probabilityAndQPS type samplingStoreMetrics struct { operationThroughput *casmetrics.Table probabilities *casmetrics.Table } // SamplingStore handles all insertions and queries for sampling data to and from Cassandra type SamplingStore struct { session cassandra.Session metrics samplingStoreMetrics logger *zap.Logger } // New creates a new cassandra sampling store. func New(session cassandra.Session, factory metrics.Factory, logger *zap.Logger) *SamplingStore { return &SamplingStore{ session: session, metrics: samplingStoreMetrics{ operationThroughput: casmetrics.NewTable(factory, "operation_throughput"), probabilities: casmetrics.NewTable(factory, "probabilities"), }, logger: logger, } } // InsertThroughput implements samplingstore.Writer#InsertThroughput. func (s *SamplingStore) InsertThroughput(throughput []*model.Throughput) error { throughputStr := throughputToString(throughput) query := s.session.Query(insertThroughput, generateRandomBucket(), gocql.TimeUUID(), throughputStr) return s.metrics.operationThroughput.Exec(query, s.logger) } // GetThroughput implements samplingstore.Reader#GetThroughput. func (s *SamplingStore) GetThroughput(start, end time.Time) ([]*model.Throughput, error) { iter := s.session.Query(getThroughput, gocql.UUIDFromTime(start), gocql.UUIDFromTime(end)).Iter() var throughput []*model.Throughput var throughputStr string for iter.Scan(&throughputStr) { throughput = append(throughput, s.stringToThroughput(throughputStr)...) } if err := iter.Close(); err != nil { err = fmt.Errorf("error reading throughput from storage: %w", err) return nil, err } return throughput, nil } // InsertProbabilitiesAndQPS implements samplingstore.Writer#InsertProbabilitiesAndQPS. func (s *SamplingStore) InsertProbabilitiesAndQPS( hostname string, probabilities model.ServiceOperationProbabilities, qps model.ServiceOperationQPS, ) error { probabilitiesAndQPSStr := probabilitiesAndQPSToString(probabilities, qps) query := s.session.Query(insertProbabilities, constBucket, gocql.TimeUUID(), hostname, probabilitiesAndQPSStr) return s.metrics.probabilities.Exec(query, s.logger) } // GetLatestProbabilities implements samplingstore.Reader#GetLatestProbabilities. func (s *SamplingStore) GetLatestProbabilities() (model.ServiceOperationProbabilities, error) { iter := s.session.Query(getLatestProbabilities).Iter() var probabilitiesStr string iter.Scan(&probabilitiesStr) if err := iter.Close(); err != nil { err = fmt.Errorf("error reading probabilities from storage: %w", err) return nil, err } return s.stringToProbabilities(probabilitiesStr), nil } // This is random enough for storage purposes func generateRandomBucket() int64 { return time.Now().UnixNano() % 10 } func probabilitiesAndQPSToString(probabilities model.ServiceOperationProbabilities, qps model.ServiceOperationQPS) string { var buf bytes.Buffer writer := csv.NewWriter(&buf) for svc, opProbabilities := range probabilities { for op, probability := range opProbabilities { opQPS := 0.0 if _, ok := qps[svc]; ok { opQPS = qps[svc][op] } writer.Write([]string{ svc, op, strconv.FormatFloat(probability, 'f', -1, 64), strconv.FormatFloat(opQPS, 'f', -1, 64), }) } } writer.Flush() return buf.String() } func (s *SamplingStore) stringToProbabilitiesAndQPS(probabilitiesAndQPSStr string) serviceOperationData { probabilitiesAndQPS := make(serviceOperationData) appendFunc := s.appendProbabilityAndQPS(probabilitiesAndQPS) s.parseString(probabilitiesAndQPSStr, 4, appendFunc) return probabilitiesAndQPS } func (s *SamplingStore) stringToProbabilities(probabilitiesStr string) model.ServiceOperationProbabilities { probabilities := make(model.ServiceOperationProbabilities) appendFunc := s.appendProbability(probabilities) s.parseString(probabilitiesStr, 4, appendFunc) return probabilities } func throughputToString(throughput []*model.Throughput) string { var buf bytes.Buffer writer := csv.NewWriter(&buf) for _, t := range throughput { writer.Write([]string{t.Service, t.Operation, strconv.Itoa(int(t.Count)), probabilitiesSetToString(t.Probabilities)}) } writer.Flush() return buf.String() } func probabilitiesSetToString(probabilities map[string]struct{}) string { var buf bytes.Buffer for probability := range probabilities { buf.WriteString(probability) buf.WriteString(",") } return strings.TrimSuffix(buf.String(), ",") } func (s *SamplingStore) stringToThroughput(throughputStr string) []*model.Throughput { var throughput []*model.Throughput appendFunc := s.appendThroughput(&throughput) s.parseString(throughputStr, 4, appendFunc) return throughput } func (s *SamplingStore) appendProbabilityAndQPS(svcOpData serviceOperationData) func(csvFields []string) { return func(csvFields []string) { probability, err := strconv.ParseFloat(csvFields[2], 64) if err != nil { s.logger.Warn("probability cannot be parsed", zap.Any("entries", csvFields), zap.Error(err)) return } qps, err := strconv.ParseFloat(csvFields[3], 64) if err != nil { s.logger.Warn("qps cannot be parsed", zap.Any("entries", csvFields), zap.Error(err)) return } service := csvFields[0] operation := csvFields[1] if _, ok := svcOpData[service]; !ok { svcOpData[service] = make(map[string]*probabilityAndQPS) } svcOpData[service][operation] = &probabilityAndQPS{ Probability: probability, QPS: qps, } } } func (s *SamplingStore) appendProbability(probabilities model.ServiceOperationProbabilities) func(csvFields []string) { return func(csvFields []string) { probability, err := strconv.ParseFloat(csvFields[2], 64) if err != nil { s.logger.Warn("probability cannot be parsed", zap.Any("entries", csvFields), zap.Error(err)) return } service := csvFields[0] operation := csvFields[1] if _, ok := probabilities[service]; !ok { probabilities[service] = make(map[string]float64) } probabilities[service][operation] = probability } } func (s *SamplingStore) appendThroughput(throughput *[]*model.Throughput) func(csvFields []string) { return func(csvFields []string) { count, err := strconv.Atoi(csvFields[2]) if err != nil { s.logger.Warn("throughput count cannot be parsed", zap.Any("entries", csvFields), zap.Error(err)) return } *throughput = append(*throughput, &model.Throughput{ Service: csvFields[0], Operation: csvFields[1], Count: int64(count), Probabilities: parseProbabilitiesSet(csvFields[3]), }) } } func parseProbabilitiesSet(probabilitiesStr string) map[string]struct{} { ret := map[string]struct{}{} for probability := range strings.SplitSeq(probabilitiesStr, ",") { if probability != "" { ret[probability] = struct{}{} } } return ret } func (s *SamplingStore) parseString(str string, numColumns int, appendFunc func(csvFields []string)) { reader := csv.NewReader(strings.NewReader(str)) for { csvFields, err := reader.Read() if errors.Is(err, io.EOF) { break } if err != nil { s.logger.Error("failed to read csv", zap.Error(err)) } if len(csvFields) != numColumns { s.logger.Warn("incomplete throughput data", zap.Int("expected_columns", numColumns), zap.Any("entries", csvFields)) continue } appendFunc(csvFields) } } ================================================ FILE: internal/storage/v1/cassandra/samplingstore/storage_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package samplingstore import ( "errors" "net/http" "testing" "time" gocql "github.com/apache/cassandra-gocql-driver/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/metricstest" "github.com/jaegertracing/jaeger/internal/storage/cassandra/mocks" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore/model" "github.com/jaegertracing/jaeger/internal/testutils" ) var testTime = time.Date(2017, time.January, 24, 11, 15, 17, 12345, time.UTC) type samplingStoreTest struct { session *mocks.Session logger *zap.Logger logBuffer *testutils.Buffer store *SamplingStore } func withSamplingStore(fn func(r *samplingStoreTest)) { session := &mocks.Session{} logger, logBuffer := testutils.NewLogger() metricsFactory := metricstest.NewFactory(0) r := &samplingStoreTest{ session: session, logger: logger, logBuffer: logBuffer, store: New(session, metricsFactory, logger), } fn(r) } var _ samplingstore.Store = &SamplingStore{} // check API conformance func TestInsertThroughput(t *testing.T) { withSamplingStore(func(s *samplingStoreTest) { query := &mocks.Query{} query.On("Exec").Return(nil) var args []any captureArgs := mock.MatchedBy(func(v []any) bool { args = v return true }) s.session.On("Query", mock.AnythingOfType("string"), captureArgs).Return(query) throughput := []*model.Throughput{ { Service: "svc,withcomma", Operation: "op,withcomma", Count: 40, }, } err := s.store.InsertThroughput(throughput) require.NoError(t, err) assert.Len(t, args, 3) if _, ok := args[0].(int64); !ok { assert.Fail(t, "expecting first arg as int64", "received: %+v", args) } if _, ok := args[1].(gocql.UUID); !ok { assert.Fail(t, "expecting second arg as gocql.UUID", "received: %+v", args) } if d, ok := args[2].(string); ok { assert.Equal(t, "\"svc,withcomma\",\"op,withcomma\",40,\n", d) } else { assert.Fail(t, "expecting third arg as string", "received: %+v", args) } }) } func TestInsertProbabilitiesAndQPS(t *testing.T) { withSamplingStore(func(s *samplingStoreTest) { query := &mocks.Query{} query.On("Exec").Return(nil) var args []any captureArgs := mock.MatchedBy(func(v []any) bool { args = v return true }) s.session.On("Query", mock.AnythingOfType("string"), captureArgs).Return(query) hostname := "hostname" probabilities := model.ServiceOperationProbabilities{ "svc": map[string]float64{ "op": 0.84, }, } qps := model.ServiceOperationQPS{ "svc": map[string]float64{ "op": 40, }, } err := s.store.InsertProbabilitiesAndQPS(hostname, probabilities, qps) require.NoError(t, err) assert.Len(t, args, 4) if d, ok := args[0].(int); ok { assert.Equal(t, 1, d) } else { assert.Fail(t, "expecting first arg as int", "received: %+v", args) } if _, ok := args[1].(gocql.UUID); !ok { assert.Fail(t, "expecting second arg as gocql.UUID", "received: %+v", args) } if d, ok := args[2].(string); ok { assert.Equal(t, hostname, d) } else { assert.Fail(t, "expecting third arg as string", "received: %+v", args) } if d, ok := args[3].(string); ok { assert.Equal(t, "svc,op,0.84,40\n", d) } else { assert.Fail(t, "expecting fourth arg as string", "received: %+v", args) } }) } func TestGetThroughput(t *testing.T) { testCases := []struct { caption string queryError error expectedError string }{ { caption: "success", }, { caption: "failure", queryError: errors.New("query error"), expectedError: "error reading throughput from storage: query error", }, } for _, tc := range testCases { testCase := tc // capture loop var t.Run(testCase.caption, func(t *testing.T) { withSamplingStore(func(s *samplingStoreTest) { scanMatcher := func() any { throughputStr := []string{ "\"svc,withcomma\",\"op,withcomma\",40,\"0.1,\"\n", "svc,op,50,\n", } scanFunc := func(args []any) bool { if len(throughputStr) == 0 { return false } for _, arg := range args { if ptr, ok := arg.(*string); ok { *ptr = throughputStr[0] break } } throughputStr = throughputStr[1:] return true } return mock.MatchedBy(scanFunc) } iter := &mocks.Iterator{} iter.On("Scan", scanMatcher()).Return(true) iter.On("Scan", matchEverything()).Return(false) iter.On("Close").Return(testCase.queryError) query := &mocks.Query{} query.On("Iter").Return(iter) s.session.On("Query", mock.AnythingOfType("string"), matchEverything()).Return(query) throughput, err := s.store.GetThroughput(testTime, testTime) if testCase.expectedError == "" { require.NoError(t, err) assert.Len(t, throughput, 2) assert.Equal(t, model.Throughput{ Service: "svc,withcomma", Operation: "op,withcomma", Count: 40, Probabilities: map[string]struct{}{"0.1": {}}, }, *throughput[0], ) assert.Equal(t, model.Throughput{ Service: "svc", Operation: "op", Count: 50, Probabilities: map[string]struct{}{}, }, *throughput[1], ) } else { require.EqualError(t, err, testCase.expectedError) } }) }) } } func TestGetLatestProbabilities(t *testing.T) { testCases := []struct { caption string queryError error expectedError string }{ { caption: "success", }, { caption: "failure", queryError: errors.New("query error"), expectedError: "error reading probabilities from storage: query error", }, } for _, tc := range testCases { testCase := tc // capture loop var t.Run(testCase.caption, func(t *testing.T) { withSamplingStore(func(s *samplingStoreTest) { scanMatcher := func() any { probabilitiesStr := []string{ "svc,op,0.84,40\n", } scanFunc := func(args []any) bool { if len(probabilitiesStr) == 0 { return false } for _, arg := range args { if ptr, ok := arg.(*string); ok { *ptr = probabilitiesStr[0] break } } probabilitiesStr = probabilitiesStr[1:] return true } return mock.MatchedBy(scanFunc) } iter := &mocks.Iterator{} iter.On("Scan", scanMatcher()).Return(true) iter.On("Scan", scanMatcher()).Return(false) iter.On("Close").Return(testCase.queryError) query := &mocks.Query{} query.On("Iter").Return(iter) s.session.On("Query", mock.AnythingOfType("string")).Return(query) probabilities, err := s.store.GetLatestProbabilities() if testCase.expectedError == "" { require.NoError(t, err) assert.InDelta(t, 0.84, probabilities["svc"]["op"], 0.01) } else { require.EqualError(t, err, testCase.expectedError) } }) }) } } func matchEverything() any { return mock.MatchedBy(func(_ []any) bool { return true }) } func TestGenerateRandomBucket(t *testing.T) { assert.Contains(t, []int64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, generateRandomBucket()) } func TestThroughputToString(t *testing.T) { throughput := []*model.Throughput{ {Service: "svc1", Operation: "op,1", Count: 1, Probabilities: map[string]struct{}{"1": {}}}, {Service: "svc2", Operation: "op2", Count: 2, Probabilities: map[string]struct{}{}}, } str := throughputToString(throughput) assert.True(t, str == "svc1,\"op,1\",1,1\nsvc2,op2,2,\n" || str == "svc2,op2,2,\nsvc1,1\"op,1\",1,1\n") throughput = []*model.Throughput{ {Service: "svc1", Operation: "op,1", Count: 1, Probabilities: map[string]struct{}{"1": {}, "2": {}}}, } str = throughputToString(throughput) assert.True(t, str == "svc1,\"op,1\",1,\"1,2\"\n" || str == "svc1,\"op,1\",1,\"2,1\"\n") } func TestStringToThroughput(t *testing.T) { s := &SamplingStore{logger: zap.NewNop()} testStr := "svc1,\"op,1\",1,\"0.1,0.2\"\nsvc2,op2,2,\nsvc3,op3,string,\nsvc4,op4\nsvc5\n" throughput := s.stringToThroughput(testStr) assert.Len(t, throughput, 2) assert.Equal(t, model.Throughput{ Service: "svc1", Operation: "op,1", Count: 1, Probabilities: map[string]struct{}{"0.1": {}, "0.2": {}}, }, *throughput[0], ) assert.Equal(t, model.Throughput{ Service: "svc2", Operation: "op2", Count: 2, Probabilities: map[string]struct{}{}, }, *throughput[1], ) } func TestProbabilitiesAndQPSToString(t *testing.T) { probabilities := model.ServiceOperationProbabilities{ "svc,1": map[string]float64{ http.MethodGet: 0.001, }, } qps := model.ServiceOperationQPS{ "svc,1": map[string]float64{ http.MethodGet: 62.3, }, } str := probabilitiesAndQPSToString(probabilities, qps) assert.Equal(t, "\"svc,1\",GET,0.001,62.3\n", str) } func TestStringToProbabilitiesAndQPS(t *testing.T) { s := &SamplingStore{logger: zap.NewNop()} testStr := "svc1,GET,0.001,63.2\nsvc1,PUT,0.002,0.0\nsvc2,GET,0.5,34.2\nsvc2\nsvc2,PUT,string,22.2\nsvc2,DELETE,0.3,string\n" probabilities := s.stringToProbabilitiesAndQPS(testStr) assert.Len(t, probabilities, 2) assert.Equal(t, map[string]*probabilityAndQPS{ http.MethodGet: { Probability: 0.001, QPS: 63.2, }, http.MethodPut: { Probability: 0.002, QPS: 0.0, }, }, probabilities["svc1"]) assert.Equal(t, map[string]*probabilityAndQPS{ http.MethodGet: { Probability: 0.5, QPS: 34.2, }, }, probabilities["svc2"]) } func TestStringToProbabilities(t *testing.T) { s := &SamplingStore{logger: zap.NewNop()} testStr := "svc1,GET,0.001,63.2\nsvc1,PUT,0.002,0.0\nsvc2,GET,0.5,34.2\nsvc2\nsvc2,PUT,string,34.2\n" probabilities := s.stringToProbabilities(testStr) assert.Len(t, probabilities, 2) assert.Equal(t, map[string]float64{http.MethodGet: 0.001, http.MethodPut: 0.002}, probabilities["svc1"]) assert.Equal(t, map[string]float64{http.MethodGet: 0.5}, probabilities["svc2"]) } func TestProbabilitiesSetToString(t *testing.T) { s := probabilitiesSetToString(map[string]struct{}{"0.000001": {}, "0.000002": {}}) assert.True(t, s == "0.000001,0.000002" || s == "0.000002,0.000001") assert.Empty(t, probabilitiesSetToString(nil)) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/cassandra/savetracetest/main.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "time" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/jtracer" "github.com/jaegertracing/jaeger/internal/metrics" cascfg "github.com/jaegertracing/jaeger/internal/storage/cassandra/config" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra" cspanstore "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/spanstore" ) var logger, _ = zap.NewDevelopment() // TODO: this is going morph into a load testing framework for cassandra 3.7 func main() { noScope := metrics.NullFactory cConfig := &cascfg.Configuration{ Schema: cascfg.Schema{ Keyspace: "jaeger_v1_test", }, Connection: cascfg.Connection{ Servers: []string{"127.0.0.1"}, ConnectionsPerHost: 10, ProtoVersion: 4, }, Query: cascfg.Query{ Timeout: time.Millisecond * 750, }, } cqlSession, err := cassandra.NewSession(cConfig) if err != nil { logger.Fatal("Cannot create Cassandra session", zap.Error(err)) } tracerProvider, tracerCloser, err := jtracer.NewProvider(context.Background(), "savetracetest") if err != nil { logger.Fatal("Failed to initialize tracer", zap.Error(err)) } defer tracerCloser(context.Background()) spanStore, err := cspanstore.NewSpanWriter(cqlSession, time.Hour*12, noScope, logger) if err != nil { logger.Fatal("Failed to create span writer", zap.Error(err)) } spanReader, err := cspanstore.NewSpanReader(cqlSession, noScope, logger, tracerProvider.Tracer("cspanstore.SpanReader")) if err != nil { logger.Fatal("Failed to create span reader", zap.Error(err)) } ctx := context.Background() if err = spanStore.WriteSpan(ctx, getSomeSpan()); err != nil { logger.Fatal("Failed to save", zap.Error(err)) } else { logger.Info("Saved span", zap.String("spanID", getSomeSpan().SpanID.String())) } s := getSomeSpan() trace, err := spanReader.GetTrace(ctx, spanstore.GetTraceParameters{TraceID: s.TraceID}) if err != nil { logger.Fatal("Failed to read", zap.Error(err)) } else { logger.Info("Loaded trace", zap.Any("trace", trace)) } tqp := &spanstore.TraceQueryParameters{ ServiceName: "someServiceName", StartTimeMin: time.Now().Add(time.Hour * -1), StartTimeMax: time.Now().Add(time.Hour), } logger.Info("Check main query") queryAndPrint(ctx, spanReader, tqp) tqp.OperationName = "opName" logger.Info("Check query with operation") queryAndPrint(ctx, spanReader, tqp) tqp.Tags = map[string]string{ "someKey": "someVal", } logger.Info("Check query with operation name and tags") queryAndPrint(ctx, spanReader, tqp) tqp.DurationMin = 0 tqp.DurationMax = time.Hour tqp.Tags = map[string]string{} logger.Info("check query with duration") queryAndPrint(ctx, spanReader, tqp) } func queryAndPrint(ctx context.Context, spanReader *cspanstore.SpanReader, tqp *spanstore.TraceQueryParameters) { traces, err := spanReader.FindTraces(ctx, tqp) if err != nil { logger.Fatal("Failed to query", zap.Error(err)) } else { logger.Info("Found trace(s)", zap.Any("traces", traces)) } } func getSomeProcess() *model.Process { processTagVal := "indexMe" return &model.Process{ ServiceName: "someServiceName", Tags: model.KeyValues{ model.String("processTagKey", processTagVal), }, } } func getSomeSpan() *model.Span { traceID := model.NewTraceID(1, 2) return &model.Span{ TraceID: traceID, SpanID: model.NewSpanID(3), OperationName: "opName", References: model.MaybeAddParentSpanID(traceID, 4, getReferences()), Flags: model.Flags(uint32(5)), StartTime: time.Now(), Duration: 50000 * time.Microsecond, Tags: getTags(), Logs: getLogs(), Process: getSomeProcess(), } } func getReferences() []model.SpanRef { return []model.SpanRef{ { RefType: model.ChildOf, TraceID: model.NewTraceID(1, 1), SpanID: model.NewSpanID(4), }, } } func getTags() model.KeyValues { someVal := "someVal" return model.KeyValues{ model.String("someKey", someVal), } } func getLogs() []model.Log { logTag := "this is a msg" return []model.Log{ { Timestamp: time.Now(), Fields: model.KeyValues{ model.String("event", logTag), }, }, } } ================================================ FILE: internal/storage/v1/cassandra/schema/README.md ================================================ # Cassandra Schema Management The table below lists Jaeger releases in which new cassandra schema were introduced. | Jaeger Version | Cassandra Schema File | Notes | |------------------------------------------------------------------------|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------| | [0.5.0](https://github.com/jaegertracing/jaeger/releases/tag/v0.5.0) | `v001.cql.tmpl` | | | [1.10.0](https://github.com/jaegertracing/jaeger/releases/tag/v1.10.0) | `v002.cql.tmpl` | See [CHANGELOG.md](https://github.com/jaegertracing/jaeger/blob/main/CHANGELOG.md#1100-2019-02-15) for more details on the migration. | | [1.16.0](https://github.com/jaegertracing/jaeger/releases/tag/v1.16.0) | `v003.cql.tmpl` | See [CHANGELOG.md](https://github.com/jaegertracing/jaeger/blob/main/CHANGELOG.md#1160-2019-12-17) for more details on the migration. | | [1.26.0](https://github.com/jaegertracing/jaeger/releases/tag/v1.26.0) | `v004.cql.tmpl` | See [CHANGELOG.md](https://github.com/jaegertracing/jaeger/blob/main/CHANGELOG.md#1260-2021-09-06) for more details on the migration. | ================================================ FILE: internal/storage/v1/cassandra/schema/create.sh ================================================ #!/usr/bin/env bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 function usage { >&2 echo "Error: $1" >&2 echo "" >&2 echo "Usage: MODE=(prod|test) [PARAM=value ...] $0 [template-file] | cqlsh" >&2 echo "" >&2 echo "The following parameters can be set via environment:" >&2 echo " MODE - prod or test. Test keyspace is usable on a single node cluster (no replication)" >&2 echo " DATACENTER - datacenter name for network topology used in prod (optional in MODE=test)" >&2 echo " TRACE_TTL - time to live for trace data, in seconds (default: 172800, 2 days)" >&2 echo " DEPENDENCIES_TTL - time to live for dependencies data, in seconds (default: 0, no TTL)" >&2 echo " KEYSPACE - keyspace (default: jaeger_v1_{datacenter})" >&2 echo " REPLICATION - complete replication configuration (if set, overrides REPLICATION_FACTOR and DATACENTER)" >&2 echo " REPLICATION_FACTOR - replication factor for prod (default: 2 for prod, 1 for test)" >&2 echo " VERSION - Cassandra backend version, 3 or 4 (default: 4). Ignored if template is provided." >&2 echo "" >&2 echo "The template-file argument must be fully qualified path to a v00#.cql.tmpl template file." >&2 echo "If omitted, the template file with the highest available version will be used." exit 1 } trace_ttl=${TRACE_TTL:-172800} dependencies_ttl=${DEPENDENCIES_TTL:-0} cas_version=${VERSION:-4} cas_version=$(echo "$cas_version" | tr -cd '0-9') template=$1 if [[ "$template" == "" ]]; then case "$cas_version" in 3) template=$(dirname $0)/v003.cql.tmpl ;; 4) template=$(dirname $0)/v004.cql.tmpl ;; 5) template=$(dirname $0)/v004.cql.tmpl ;; *) template=$(ls $(dirname $0)/*cql.tmpl | sort | tail -1) ;; esac fi if [[ "$MODE" == "" ]]; then usage "missing MODE parameter" elif [[ "$MODE" == "prod" ]]; then if [[ -n "$REPLICATION" ]]; then replication="$REPLICATION" else if [[ "$DATACENTER" == "" ]]; then usage "missing DATACENTER parameter for prod mode"; fi datacenter=$DATACENTER replication_factor=${REPLICATION_FACTOR:-2} replication="{'class': 'NetworkTopologyStrategy', '$datacenter': '${replication_factor}' }" fi elif [[ "$MODE" == "test" ]]; then if [[ -n "$REPLICATION" ]]; then replication="$REPLICATION" else datacenter=${DATACENTER:-'test'} replication_factor=${REPLICATION_FACTOR:-1} replication="{'class': 'SimpleStrategy', 'replication_factor': '${replication_factor}'}" fi else usage "invalid MODE=$MODE, expecting 'prod' or 'test'" fi keyspace=${KEYSPACE:-"jaeger_v1_${datacenter}"} if [[ $keyspace =~ [^a-zA-Z0-9_] ]]; then usage "invalid characters in KEYSPACE=$keyspace parameter, please use letters, digits or underscores" fi if [ ! -z "$COMPACTION_WINDOW" ]; then if echo "$COMPACTION_WINDOW" | grep -E -q '^[0-9]+[mhd]$'; then compaction_window_size="$(echo "$COMPACTION_WINDOW" | sed 's/[mhd]//')" compaction_window_unit="$(echo "$COMPACTION_WINDOW" | sed 's/[0-9]//g')" else usage "Invalid compaction window size format. Please use numeric value followed by 'm' for minutes, 'h' for hours, or 'd' for days." fi else trace_ttl_minutes=$(( $trace_ttl / 60 )) # Taking the ceiling of the result compaction_window_size=$(( ($trace_ttl_minutes + 30 - 1) / 30 )) compaction_window_unit="m" fi case "$compaction_window_unit" in m) compaction_window_unit="MINUTES" ;; h) compaction_window_unit="HOURS" ;; d) compaction_window_unit="DAYS" ;; esac >&2 cat <&1) assertContains "$err" "missing MODE parameter" } testInvalidMode() { err=$(MODE=invalid bash "$createScript" 2>&1) assertContains "$err" "invalid MODE=invalid, expecting 'prod' or 'test'" } testProdModeRequiresDatacenter() { err=$(MODE=prod bash "$createScript" 2>&1) assertContains "$err" "missing DATACENTER parameter for prod mode" } testProdModeWithDatacenter() { out=$(MODE=prod DATACENTER=dc1 bash "$createScript" 2>&1) assertContains "$out" "mode = prod" assertContains "$out" "datacenter = dc1" assertContains "$out" "replication = {'class': 'NetworkTopologyStrategy', 'dc1': '2' }" } testTestMode() { out=$(MODE=test bash "$createScript" 2>&1) assertContains "$out" "mode = test" assertContains "$out" "datacenter = test" assertContains "$out" "replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}" } testCustomTTL() { out=$(MODE=test TRACE_TTL=86400 DEPENDENCIES_TTL=172800 bash "$createScript" 2>&1) assertContains "$out" "trace_ttl = 86400" assertContains "$out" "dependencies_ttl = 172800" } testInvalidKeyspace() { err=$(MODE=test KEYSPACE=invalid-keyspace bash "$createScript" 2>&1) assertContains "$err" "invalid characters in KEYSPACE" } testValidKeyspace() { out=$(MODE=test KEYSPACE=valid_keyspace_123 bash "$createScript" 2>&1) assertContains "$out" "keyspace = valid_keyspace_123" } testCustomCompactionWindow() { out=$(MODE=test COMPACTION_WINDOW=24h bash "$createScript" 2>&1) assertContains "$out" "compaction_window_size = 24" assertContains "$out" "compaction_window_unit = HOURS" } testInvalidCompactionWindow() { err=$(MODE=test COMPACTION_WINDOW=24x bash "$createScript" 2>&1) assertContains "$err" "Invalid compaction window size format" } testCustomVersion() { out=$(MODE=test VERSION=3 bash "$createScript" 2>&1) assertContains "$out" "v003.cql.tmpl" } source "${SHUNIT2}/shunit2" ================================================ FILE: internal/storage/v1/cassandra/schema/docker.sh ================================================ #!/usr/bin/env bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # # This script is used in the Docker image jaegertracing/jaeger-cassandra-schema # that allows installing Jaeger keyspace and schema without installing cqlsh. CQLSH=${CQLSH:-"/opt/cassandra/bin/cqlsh"} CQLSH_HOST=${CQLSH_HOST:-"cassandra"} CQLSH_PORT=${CQLSH_PORT:-"9042"} CQLSH_SSL=${CQLSH_SSL:-""} CASSANDRA_WAIT_TIMEOUT=${CASSANDRA_WAIT_TIMEOUT:-"60"} DATACENTER=${DATACENTER:-"dc1"} KEYSPACE=${KEYSPACE:-"jaeger_v1_${DATACENTER}"} MODE=${MODE:-"test"} TEMPLATE=${TEMPLATE:-""} USER=${CASSANDRA_USERNAME:-""} PASSWORD=${CASSANDRA_PASSWORD:-""} SCHEMA_SCRIPT=${SCHEMA_SCRIPT:-"/cassandra-schema/create.sh"} CQLSH_CMD="${CQLSH} ${CQLSH_SSL} ${CQLSH_HOST} ${CQLSH_PORT}" if [ ! -z "$PASSWORD" ]; then CQLSH_CMD="${CQLSH_CMD} -u ${USER} -p ${PASSWORD}" fi total_wait=0 while true do echo "Checking if Cassandra is up at ${CQLSH_HOST}:${CQLSH_PORT}." ${CQLSH_CMD} -e "describe keyspaces" if (( $? == 0 )); then echo "Cassandra connection established." break else if (( total_wait >= ${CASSANDRA_WAIT_TIMEOUT} )); then echo "Timed out waiting for Cassandra." exit 1 fi echo "Cassandra is still not up at ${CQLSH_HOST}:${CQLSH_PORT}. Waiting 1 second." sleep 1s ((total_wait++)) fi done # Extract cassandra version # # $ cqlsh -e "show version" # [cqlsh 5.0.1 | Cassandra 3.11.11 | CQL spec 3.4.4 | Native protocol v4] VERSION= if [ -z "$TEMPLATE" ]; then VERSION=$(${CQLSH_CMD} -e "show version" \ | awk -F "|" '{print $2}' \ | awk -F " " '{print $2}' \ | awk -F "." '{print $1}' \ ) echo "Cassandra version detected: ${VERSION}" fi echo "Generating the schema for the keyspace ${KEYSPACE} and datacenter ${DATACENTER}." set -e -o pipefail MODE="${MODE}" DATACENTER="${DATACENTER}" KEYSPACE="${KEYSPACE}" VERSION="${VERSION}" ${SCHEMA_SCRIPT} "${TEMPLATE}" | ${CQLSH_CMD} echo "Schema generated." ================================================ FILE: internal/storage/v1/cassandra/schema/migration/V002toV003.sh ================================================ #!/usr/bin/env bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # Create a new operation_names_v2 table and copy all data from operation_names table # Sample usage: KEYSPACE=jaeger_v1 CQL_CMD='cqlsh host 9042 -u test_user -p test_password --request-timeout=3000' bash # ./v002tov003.sh set -euo pipefail function usage { >&2 echo "Error: $1" >&2 echo "" >&2 echo "Usage: KEYSPACE={keyspace} CQL_CMD={cql_cmd} $0" >&2 echo "" >&2 echo "The following parameters can be set via environment:" >&2 echo " KEYSPACE - keyspace" >&2 echo " CQL_CMD - cqlsh host port -u user -p password" >&2 echo "" exit 1 } confirm() { read -r -p "${1:-Continue? [y/N]} " response case "$response" in [yY][eE][sS]|[yY]) true ;; *) exit 1 ;; esac } if [[ ${KEYSPACE} == "" ]]; then usage "missing KEYSPACE parameter" fi if [[ ${KEYSPACE} =~ [^a-zA-Z0-9_] ]]; then usage "invalid characters in KEYSPACE=$KEYSPACE parameter, please use letters, digits or underscores" fi keyspace=${KEYSPACE} old_table=operation_names new_table=operation_names_v2 cqlsh_cmd=${CQL_CMD} if [[ ${cqlsh_cmd} == "" ]]; then cqlsh_cmd=cqlsh fi echo "Using cql command: $cqlsh_cmd" row_count=$(${cqlsh_cmd} -e "select count(*) from $keyspace.$old_table;"|head -4|tail -1| tr -d ' ') echo "About to copy $row_count rows to new table..." confirm ${cqlsh_cmd} -e "COPY $keyspace.$old_table (service_name, operation_name) to '$old_table.csv';" if [[ ! -f ${old_table}.csv ]]; then echo "Could not find $old_table.csv. Backup from cassandra was probably not successful" exit 1 fi csv_rows=$(wc -l ${old_table}.csv | tr -dc '0-9') echo "Generating data for new table..." while IFS="," read service_name operation_name; do echo "$service_name,,$operation_name" done < ${old_table}.csv > ${new_table}.csv ttl=$(${cqlsh_cmd} -e "select default_time_to_live from system_schema.tables WHERE keyspace_name='$keyspace' AND table_name='$old_table';"|head -4|tail -1|tr -d ' ') echo "Creating new table $new_table with ttl: $ttl" ${cqlsh_cmd} -e "CREATE TABLE IF NOT EXISTS $keyspace.$new_table ( service_name text, span_kind text, operation_name text, PRIMARY KEY ((service_name), span_kind, operation_name) ) WITH compaction = { 'min_threshold': '4', 'max_threshold': '32', 'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = $ttl AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800;" echo "Import data to new table: $keyspace.$new_table from $new_table.csv" # empty string will be inserted as empty string instead of null ${cqlsh_cmd} -e "COPY $keyspace.$new_table (service_name, span_kind, operation_name) FROM '$new_table.csv' WITH NULL='NIL';" echo "Data from old table are successfully imported to new table!" echo "Before finish, do you want to delete old table: $keyspace.$old_table?" confirm ${cqlsh_cmd} -e "DROP TABLE IF EXISTS $keyspace.$old_table;" ================================================ FILE: internal/storage/v1/cassandra/schema/migration/v001tov002part1.sh ================================================ #!/usr/bin/env bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 set -euo pipefail function usage { >&2 echo "Error: $1" >&2 echo "" >&2 echo "Usage: KEYSPACE={keyspace} $0" >&2 echo "" >&2 echo "The following parameters can be set via environment:" >&2 echo " KEYSPACE - keyspace" >&2 echo " TIMEOUT - cqlsh request timeout" >&2 echo "" exit 1 } confirm() { read -r -p "${1:-Are you sure? [y/N]} " response case "$response" in [yY][eE][sS]|[yY]) true ;; *) exit 1 ;; esac } keyspace=${KEYSPACE} timeout=${TIMEOUT:-"60"} cqlsh_cmd="cqlsh --request-timeout=$timeout" if [[ ${keyspace} == "" ]]; then usage "missing KEYSPACE parameter" fi if [[ ${keyspace} =~ [^a-zA-Z0-9_] ]]; then usage "invalid characters in KEYSPACE=$keyspace parameter, please use letters, digits or underscores" fi row_count=$($cqlsh_cmd -e "select count(*) from $keyspace.dependencies;"|head -4|tail -1| tr -d ' ') echo "About to copy $row_count rows." confirm $cqlsh_cmd -e "COPY $keyspace.dependencies (ts, dependencies) to 'dependencies.csv';" if [ ! -f dependencies.csv ]; then echo "Could not find dependencies.csv. Backup from cassandra was probably not successful" exit 1 fi if [ ${row_count} -ne $(wc -l dependencies.csv | cut -f 1 -d ' ') ]; then echo "Number of rows in file is not equal to number of rows in cassandra" exit 1 fi while IFS="," read ts dependency; do bucket=`date +"%Y-%m-%d%z" -d "$ts"` echo "$bucket,$ts,$dependency" done < dependencies.csv > dependencies_datebucket.csv dependencies_ttl=$($cqlsh_cmd -e "select default_time_to_live from system_schema.tables WHERE keyspace_name='$keyspace' AND table_name='dependencies';"|head -4|tail -1|tr -d ' ') echo "Setting dependencies_ttl to $dependencies_ttl" $cqlsh_cmd -e "ALTER TYPE $keyspace.dependency ADD source text;" $cqlsh_cmd -e "CREATE TABLE $keyspace.dependencies_v2 ( ts_bucket timestamp, ts timestamp, dependencies list>, PRIMARY KEY (ts_bucket, ts) ) WITH CLUSTERING ORDER BY (ts DESC) AND compaction = { 'min_threshold': '4', 'max_threshold': '32', 'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy' } AND default_time_to_live = $dependencies_ttl; " $cqlsh_cmd -e "COPY $keyspace.dependencies_v2 (ts_bucket, ts, dependencies) FROM 'dependencies_datebucket.csv';" ================================================ FILE: internal/storage/v1/cassandra/schema/migration/v001tov002part2.sh ================================================ #!/usr/bin/env bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 set -euo pipefail function usage { >&2 echo "Error: $1" >&2 echo "" >&2 echo "Usage: KEYSPACE={keyspace} $0" >&2 echo "" >&2 echo "The following parameters can be set via environment:" >&2 echo " KEYSPACE - keyspace" >&2 echo " TIMEOUT - cqlsh request timeout" >&2 echo "" exit 1 } confirm() { read -r -p "${1:-Are you sure? [y/N]} " response case "$response" in [yY][eE][sS]|[yY]) true ;; *) exit 1 ;; esac } keyspace=${KEYSPACE} timeout=${TIMEOUT} cqlsh_cmd=cqlsh --request-timeout=$timeout if [[ ${keyspace} == "" ]]; then usage "missing KEYSPACE parameter" fi if [[ ${keyspace} =~ [^a-zA-Z0-9_] ]]; then usage "invalid characters in KEYSPACE=$keyspace parameter, please use letters, digits or underscores" fi row_count=$($cqlsh_cmd -e "select count(*) from $keyspace.dependencies;"|head -4|tail -1| tr -d ' ') echo "About to delete $row_count rows." confirm $cqlsh_cmd -e "DROP INDEX IF EXISTS $keyspace.ts_index;" $cqlsh_cmd -e "DROP TABLE IF EXISTS $keyspace.dependencies;" ================================================ FILE: internal/storage/v1/cassandra/schema/package_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package schema import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/cassandra/schema/schema.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package schema import ( "bytes" "embed" "errors" "fmt" "text/template" "time" "github.com/jaegertracing/jaeger/internal/storage/cassandra" "github.com/jaegertracing/jaeger/internal/storage/cassandra/config" ) //go:embed v004-go-tmpl.cql.tmpl var schemaFile embed.FS type templateParams struct { // Keyspace in which tables and types will be created for storage Keyspace string // Replication is the replication strategy used. Ex: "{'class': 'NetworkTopologyStrategy', 'replication_factor': '1' }" Replication string // CompactionWindowInMinutes is constructed from CompactionWindow for using in template CompactionWindowInMinutes int64 // TraceTTLInSeconds is constructed from TraceTTL for using in template TraceTTLInSeconds int64 // DependenciesTTLInSeconds is constructed from DependenciesTTL for using in template DependenciesTTLInSeconds int64 } type Creator struct { session cassandra.Session schema config.Schema } // NewSchemaCreator returns a new SchemaCreator func NewSchemaCreator(session cassandra.Session, schema config.Schema) *Creator { return &Creator{ session: session, schema: schema, } } func (sc *Creator) constructTemplateParams() templateParams { replicationConfig := fmt.Sprintf("{'class': 'SimpleStrategy', 'replication_factor': '%d'}", sc.schema.ReplicationFactor) if sc.schema.Datacenter != "" { replicationConfig = fmt.Sprintf("{'class': 'NetworkTopologyStrategy', '%s': '%d' }", sc.schema.Datacenter, sc.schema.ReplicationFactor) } return templateParams{ Keyspace: sc.schema.Keyspace, Replication: replicationConfig, CompactionWindowInMinutes: int64(sc.schema.CompactionWindow / time.Minute), TraceTTLInSeconds: int64(sc.schema.TraceTTL / time.Second), DependenciesTTLInSeconds: int64(sc.schema.DependenciesTTL / time.Second), } } func (*Creator) getQueryFileAsBytes(fileName string, params templateParams) ([]byte, error) { tmpl, err := template.ParseFS(schemaFile, fileName) if err != nil { return nil, err } var result bytes.Buffer err = tmpl.Execute(&result, params) if err != nil { return nil, err } return result.Bytes(), nil } func (*Creator) getQueriesFromBytes(queryFile []byte) ([]string, error) { lines := bytes.Split(queryFile, []byte("\n")) var extractedLines [][]byte for _, line := range lines { // Remove any comments, if at the end of the line commentIndex := bytes.Index(line, []byte(`--`)) if commentIndex != -1 { // remove everything after comment line = line[0:commentIndex] } trimmedLine := bytes.TrimSpace(line) if len(trimmedLine) == 0 { continue } extractedLines = append(extractedLines, trimmedLine) } var queries []string // Construct individual queries strings var queryString string for _, line := range extractedLines { queryString += string(line) + "\n" if bytes.HasSuffix(line, []byte(";")) { queries = append(queries, queryString) queryString = "" } } if queryString != "" { return nil, errors.New(`query exists in template without ";"`) } return queries, nil } func (sc *Creator) getCassandraQueriesFromQueryStrings(queries []string) []cassandra.Query { var casQueries []cassandra.Query for _, query := range queries { casQueries = append(casQueries, sc.session.Query(query)) } return casQueries } func (sc *Creator) contructSchemaQueries() ([]cassandra.Query, error) { params := sc.constructTemplateParams() queryFile, err := sc.getQueryFileAsBytes(`v004-go-tmpl.cql.tmpl`, params) if err != nil { return nil, err } queryStrings, err := sc.getQueriesFromBytes(queryFile) if err != nil { return nil, err } casQueries := sc.getCassandraQueriesFromQueryStrings(queryStrings) return casQueries, nil } func (sc *Creator) CreateSchemaIfNotPresent() error { casQueries, err := sc.contructSchemaQueries() if err != nil { return err } for _, query := range casQueries { if err := query.Exec(); err != nil { return err } } return nil } ================================================ FILE: internal/storage/v1/cassandra/schema/schema_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package schema import ( "testing" "github.com/stretchr/testify/require" ) func TestQueryGenerationFromBytes(t *testing.T) { queriesAsString := ` query1 -- comment (this should be removed) query1-continue query1-finished; -- query2; query-3 query-3-continue query-3-finished; ` expGeneratedQueries := []string{ `query1 query1-continue query1-finished; `, `query2; `, `query-3 query-3-continue query-3-finished; `, } sc := Creator{} queriesAsBytes := []byte(queriesAsString) queries, err := sc.getQueriesFromBytes(queriesAsBytes) require.NoError(t, err) require.Len(t, queries, len(expGeneratedQueries)) for i := range len(expGeneratedQueries) { require.Equal(t, expGeneratedQueries[i], queries[i]) } } func TestInvalidQueryTemplate(t *testing.T) { queriesAsString := ` query1 -- comment (this should be removed) query1-continue query1-finished; -- query2; query-3 query-3-continue query-3-finished -- missing semicolon ` sc := Creator{} queriesAsBytes := []byte(queriesAsString) _, err := sc.getQueriesFromBytes(queriesAsBytes) require.Error(t, err) } ================================================ FILE: internal/storage/v1/cassandra/schema/v001.cql.tmpl ================================================ -- -- Creates Cassandra keyspace with tables for traces and dependencies. -- -- Required parameters: -- -- keyspace -- name of the keyspace -- replication -- replication strategy for the keyspace, such as -- for prod environments -- {'class': 'NetworkTopologyStrategy', '$datacenter': '${replication_factor}' } -- for test environments -- {'class': 'SimpleStrategy', 'replication_factor': '1'} -- trace_ttl -- default time to live for trace data, in seconds -- dependencies_ttl -- default time to live for dependencies data, in seconds (0 for no TTL) -- -- Non-configurable settings: -- gc_grace_seconds is non-zero, see: http://www.uberobert.com/cassandra_gc_grace_disables_hinted_handoff/ -- For TTL of 2 days, compaction window is 1 hour, rule of thumb here: http://thelastpickle.com/blog/2016/12/08/TWCS-part1.html CREATE KEYSPACE IF NOT EXISTS ${keyspace} WITH replication = ${replication}; CREATE TYPE IF NOT EXISTS ${keyspace}.keyvalue ( key text, value_type text, value_string text, value_bool boolean, value_long bigint, value_double double, value_binary blob, ); CREATE TYPE IF NOT EXISTS ${keyspace}.log ( ts bigint, // microseconds since epoch fields list>, ); CREATE TYPE IF NOT EXISTS ${keyspace}.span_ref ( ref_type text, trace_id blob, span_id bigint, ); CREATE TYPE IF NOT EXISTS ${keyspace}.process ( service_name text, tags list>, ); -- Notice we have span_hash. This exists only for zipkin backwards compat. Zipkin allows spans with the same ID. -- Note: Cassandra re-orders non-PK columns alphabetically, so the table looks differently in CQLSH "describe table". -- start_time is bigint instead of timestamp as we require microsecond precision CREATE TABLE IF NOT EXISTS ${keyspace}.traces ( trace_id blob, span_id bigint, span_hash bigint, parent_id bigint, operation_name text, flags int, start_time bigint, // microseconds since epoch duration bigint, // microseconds tags list>, logs list>, refs list>, process frozen, PRIMARY KEY (trace_id, span_id, span_hash) ) WITH compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TABLE IF NOT EXISTS ${keyspace}.service_names ( service_name text, PRIMARY KEY (service_name) ) WITH compaction = { 'min_threshold': '4', 'max_threshold': '32', 'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TABLE IF NOT EXISTS ${keyspace}.operation_names ( service_name text, operation_name text, PRIMARY KEY ((service_name), operation_name) ) WITH compaction = { 'min_threshold': '4', 'max_threshold': '32', 'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes -- index of trace IDs by service + operation names, sorted by span start_time. CREATE TABLE IF NOT EXISTS ${keyspace}.service_operation_index ( service_name text, operation_name text, start_time bigint, // microseconds since epoch trace_id blob, PRIMARY KEY ((service_name, operation_name), start_time) ) WITH CLUSTERING ORDER BY (start_time DESC) AND compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TABLE IF NOT EXISTS ${keyspace}.service_name_index ( service_name text, bucket int, start_time bigint, // microseconds since epoch trace_id blob, PRIMARY KEY ((service_name, bucket), start_time) ) WITH CLUSTERING ORDER BY (start_time DESC) AND compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TABLE IF NOT EXISTS ${keyspace}.duration_index ( service_name text, // service name operation_name text, // operation name, or blank for queries without span name bucket timestamp, // time bucket, - the start_time of the given span rounded to an hour duration bigint, // span duration, in microseconds start_time bigint, // microseconds since epoch trace_id blob, PRIMARY KEY ((service_name, operation_name, bucket), duration, start_time, trace_id) ) WITH CLUSTERING ORDER BY (duration DESC, start_time DESC) AND compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes -- a bucketing strategy may have to be added for tag queries -- we can make this table even better by adding a timestamp to it CREATE TABLE IF NOT EXISTS ${keyspace}.tag_index ( service_name text, tag_key text, tag_value text, start_time bigint, // microseconds since epoch trace_id blob, span_id bigint, PRIMARY KEY ((service_name, tag_key, tag_value), start_time, trace_id, span_id) ) WITH CLUSTERING ORDER BY (start_time DESC) AND compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TYPE IF NOT EXISTS ${keyspace}.dependency ( parent text, child text, call_count bigint, ); -- compaction strategy is intentionally different as compared to other tables due to the size of dependencies data -- note we have to write ts twice (once as ts_index). This is because we cannot make a SASI index on the primary key CREATE TABLE IF NOT EXISTS ${keyspace}.dependencies ( ts timestamp, ts_index timestamp, dependencies list>, PRIMARY KEY (ts) ) WITH compaction = { 'min_threshold': '4', 'max_threshold': '32', 'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy' } AND default_time_to_live = ${dependencies_ttl}; CREATE CUSTOM INDEX IF NOT EXISTS ON ${keyspace}.dependencies (ts_index) USING 'org.apache.cassandra.index.sasi.SASIIndex' WITH OPTIONS = {'mode': 'SPARSE'}; ================================================ FILE: internal/storage/v1/cassandra/schema/v002.cql.tmpl ================================================ -- -- Creates Cassandra keyspace with tables for traces and dependencies. -- -- Required parameters: -- -- keyspace -- name of the keyspace -- replication -- replication strategy for the keyspace, such as -- for prod environments -- {'class': 'NetworkTopologyStrategy', '$datacenter': '${replication_factor}' } -- for test environments -- {'class': 'SimpleStrategy', 'replication_factor': '1'} -- trace_ttl -- default time to live for trace data, in seconds -- dependencies_ttl -- default time to live for dependencies data, in seconds (0 for no TTL) -- -- Non-configurable settings: -- gc_grace_seconds is non-zero, see: http://www.uberobert.com/cassandra_gc_grace_disables_hinted_handoff/ -- For TTL of 2 days, compaction window is 1 hour, rule of thumb here: http://thelastpickle.com/blog/2016/12/08/TWCS-part1.html CREATE KEYSPACE IF NOT EXISTS ${keyspace} WITH replication = ${replication}; CREATE TYPE IF NOT EXISTS ${keyspace}.keyvalue ( key text, value_type text, value_string text, value_bool boolean, value_long bigint, value_double double, value_binary blob, ); CREATE TYPE IF NOT EXISTS ${keyspace}.log ( ts bigint, // microseconds since epoch fields list>, ); CREATE TYPE IF NOT EXISTS ${keyspace}.span_ref ( ref_type text, trace_id blob, span_id bigint, ); CREATE TYPE IF NOT EXISTS ${keyspace}.process ( service_name text, tags list>, ); -- Notice we have span_hash. This exists only for zipkin backwards compat. Zipkin allows spans with the same ID. -- Note: Cassandra re-orders non-PK columns alphabetically, so the table looks differently in CQLSH "describe table". -- start_time is bigint instead of timestamp as we require microsecond precision CREATE TABLE IF NOT EXISTS ${keyspace}.traces ( trace_id blob, span_id bigint, span_hash bigint, parent_id bigint, operation_name text, flags int, start_time bigint, // microseconds since epoch duration bigint, // microseconds tags list>, logs list>, refs list>, process frozen, PRIMARY KEY (trace_id, span_id, span_hash) ) WITH compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TABLE IF NOT EXISTS ${keyspace}.service_names ( service_name text, PRIMARY KEY (service_name) ) WITH compaction = { 'min_threshold': '4', 'max_threshold': '32', 'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TABLE IF NOT EXISTS ${keyspace}.operation_names ( service_name text, operation_name text, PRIMARY KEY ((service_name), operation_name) ) WITH compaction = { 'min_threshold': '4', 'max_threshold': '32', 'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes -- index of trace IDs by service + operation names, sorted by span start_time. CREATE TABLE IF NOT EXISTS ${keyspace}.service_operation_index ( service_name text, operation_name text, start_time bigint, // microseconds since epoch trace_id blob, PRIMARY KEY ((service_name, operation_name), start_time) ) WITH CLUSTERING ORDER BY (start_time DESC) AND compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TABLE IF NOT EXISTS ${keyspace}.service_name_index ( service_name text, bucket int, start_time bigint, // microseconds since epoch trace_id blob, PRIMARY KEY ((service_name, bucket), start_time) ) WITH CLUSTERING ORDER BY (start_time DESC) AND compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TABLE IF NOT EXISTS ${keyspace}.duration_index ( service_name text, // service name operation_name text, // operation name, or blank for queries without span name bucket timestamp, // time bucket, - the start_time of the given span rounded to an hour duration bigint, // span duration, in microseconds start_time bigint, // microseconds since epoch trace_id blob, PRIMARY KEY ((service_name, operation_name, bucket), duration, start_time, trace_id) ) WITH CLUSTERING ORDER BY (duration DESC, start_time DESC) AND compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes -- a bucketing strategy may have to be added for tag queries -- we can make this table even better by adding a timestamp to it CREATE TABLE IF NOT EXISTS ${keyspace}.tag_index ( service_name text, tag_key text, tag_value text, start_time bigint, // microseconds since epoch trace_id blob, span_id bigint, PRIMARY KEY ((service_name, tag_key, tag_value), start_time, trace_id, span_id) ) WITH CLUSTERING ORDER BY (start_time DESC) AND compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TYPE IF NOT EXISTS ${keyspace}.dependency ( parent text, child text, call_count bigint, source text, ); -- compaction strategy is intentionally different as compared to other tables due to the size of dependencies data CREATE TABLE IF NOT EXISTS ${keyspace}.dependencies_v2 ( ts_bucket timestamp, ts timestamp, dependencies list>, PRIMARY KEY (ts_bucket, ts) ) WITH CLUSTERING ORDER BY (ts DESC) AND compaction = { 'min_threshold': '4', 'max_threshold': '32', 'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy' } AND default_time_to_live = ${dependencies_ttl}; ================================================ FILE: internal/storage/v1/cassandra/schema/v003.cql.tmpl ================================================ -- -- Creates Cassandra keyspace with tables for traces and dependencies. -- -- Required parameters: -- -- keyspace -- name of the keyspace -- replication -- replication strategy for the keyspace, such as -- for prod environments -- {'class': 'NetworkTopologyStrategy', '$datacenter': '${replication_factor}' } -- for test environments -- {'class': 'SimpleStrategy', 'replication_factor': '1'} -- trace_ttl -- default time to live for trace data, in seconds -- dependencies_ttl -- default time to live for dependencies data, in seconds (0 for no TTL) -- -- Non-configurable settings: -- gc_grace_seconds is non-zero, see: http://www.uberobert.com/cassandra_gc_grace_disables_hinted_handoff/ -- For TTL of 2 days, compaction window is 1 hour, rule of thumb here: http://thelastpickle.com/blog/2016/12/08/TWCS-part1.html CREATE KEYSPACE IF NOT EXISTS ${keyspace} WITH replication = ${replication}; CREATE TYPE IF NOT EXISTS ${keyspace}.keyvalue ( key text, value_type text, value_string text, value_bool boolean, value_long bigint, value_double double, value_binary blob, ); CREATE TYPE IF NOT EXISTS ${keyspace}.log ( ts bigint, // microseconds since epoch fields list>, ); CREATE TYPE IF NOT EXISTS ${keyspace}.span_ref ( ref_type text, trace_id blob, span_id bigint, ); CREATE TYPE IF NOT EXISTS ${keyspace}.process ( service_name text, tags list>, ); -- Notice we have span_hash. This exists only for zipkin backwards compat. Zipkin allows spans with the same ID. -- Note: Cassandra re-orders non-PK columns alphabetically, so the table looks differently in CQLSH "describe table". -- start_time is bigint instead of timestamp as we require microsecond precision CREATE TABLE IF NOT EXISTS ${keyspace}.traces ( trace_id blob, span_id bigint, span_hash bigint, parent_id bigint, operation_name text, flags int, start_time bigint, // microseconds since epoch duration bigint, // microseconds tags list>, logs list>, refs list>, process frozen, PRIMARY KEY (trace_id, span_id, span_hash) ) WITH compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TABLE IF NOT EXISTS ${keyspace}.service_names ( service_name text, PRIMARY KEY (service_name) ) WITH compaction = { 'min_threshold': '4', 'max_threshold': '32', 'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TABLE IF NOT EXISTS ${keyspace}.operation_names_v2 ( service_name text, span_kind text, operation_name text, PRIMARY KEY ((service_name), span_kind, operation_name) ) WITH compaction = { 'min_threshold': '4', 'max_threshold': '32', 'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes -- index of trace IDs by service + operation names, sorted by span start_time. CREATE TABLE IF NOT EXISTS ${keyspace}.service_operation_index ( service_name text, operation_name text, start_time bigint, // microseconds since epoch trace_id blob, PRIMARY KEY ((service_name, operation_name), start_time) ) WITH CLUSTERING ORDER BY (start_time DESC) AND compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TABLE IF NOT EXISTS ${keyspace}.service_name_index ( service_name text, bucket int, start_time bigint, // microseconds since epoch trace_id blob, PRIMARY KEY ((service_name, bucket), start_time) ) WITH CLUSTERING ORDER BY (start_time DESC) AND compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TABLE IF NOT EXISTS ${keyspace}.duration_index ( service_name text, // service name operation_name text, // operation name, or blank for queries without span name bucket timestamp, // time bucket, - the start_time of the given span rounded to an hour duration bigint, // span duration, in microseconds start_time bigint, // microseconds since epoch trace_id blob, PRIMARY KEY ((service_name, operation_name, bucket), duration, start_time, trace_id) ) WITH CLUSTERING ORDER BY (duration DESC, start_time DESC) AND compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes -- a bucketing strategy may have to be added for tag queries -- we can make this table even better by adding a timestamp to it CREATE TABLE IF NOT EXISTS ${keyspace}.tag_index ( service_name text, tag_key text, tag_value text, start_time bigint, // microseconds since epoch trace_id blob, span_id bigint, PRIMARY KEY ((service_name, tag_key, tag_value), start_time, trace_id, span_id) ) WITH CLUSTERING ORDER BY (start_time DESC) AND compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TYPE IF NOT EXISTS ${keyspace}.dependency ( parent text, child text, call_count bigint, source text, ); -- compaction strategy is intentionally different as compared to other tables due to the size of dependencies data CREATE TABLE IF NOT EXISTS ${keyspace}.dependencies_v2 ( ts_bucket timestamp, ts timestamp, dependencies list>, PRIMARY KEY (ts_bucket, ts) ) WITH CLUSTERING ORDER BY (ts DESC) AND compaction = { 'min_threshold': '4', 'max_threshold': '32', 'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy' } AND default_time_to_live = ${dependencies_ttl}; -- adaptive sampling tables -- ./internal/storage/v1/cassandra/samplingstore/storage.go CREATE TABLE IF NOT EXISTS ${keyspace}.operation_throughput ( bucket int, ts timeuuid, throughput text, PRIMARY KEY(bucket, ts) ) WITH CLUSTERING ORDER BY (ts desc); CREATE TABLE IF NOT EXISTS ${keyspace}.sampling_probabilities ( bucket int, ts timeuuid, hostname text, probabilities text, PRIMARY KEY(bucket, ts) ) WITH CLUSTERING ORDER BY (ts desc); -- distributed lock -- ./plugin/pkg/distributedlock/cassandra/lock.go CREATE TABLE IF NOT EXISTS ${keyspace}.leases ( name text, owner text, PRIMARY KEY (name) ); ================================================ FILE: internal/storage/v1/cassandra/schema/v004-go-tmpl.cql.tmpl ================================================ CREATE KEYSPACE IF NOT EXISTS {{.Keyspace}} WITH replication = {{.Replication}}; CREATE TYPE IF NOT EXISTS {{.Keyspace}}.keyvalue ( key text, value_type text, value_string text, value_bool boolean, value_long bigint, value_double double, value_binary blob ); CREATE TYPE IF NOT EXISTS {{.Keyspace}}.log ( ts bigint, -- microseconds since epoch fields frozen>> ); CREATE TYPE IF NOT EXISTS {{.Keyspace}}.span_ref ( ref_type text, trace_id blob, span_id bigint ); CREATE TYPE IF NOT EXISTS {{.Keyspace}}.process ( service_name text, tags frozen>> ); -- Notice we have span_hash. This exists only for zipkin backwards compat. Zipkin allows spans with the same ID. -- Note: Cassandra re-orders non-PK columns alphabetically, so the table looks differently in CQLSH "describe table". -- start_time is bigint instead of timestamp as we require microsecond precision CREATE TABLE IF NOT EXISTS {{.Keyspace}}.traces ( trace_id blob, span_id bigint, span_hash bigint, parent_id bigint, operation_name text, flags int, start_time bigint, -- microseconds since epoch duration bigint, -- microseconds tags list>, logs list>, refs list>, process frozen, PRIMARY KEY (trace_id, span_id, span_hash) ) WITH compaction = { 'compaction_window_size': '{{.CompactionWindowInMinutes}}', 'compaction_window_unit': 'MINUTES', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND default_time_to_live = {{.TraceTTLInSeconds}} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TABLE IF NOT EXISTS {{.Keyspace}}.service_names ( service_name text, PRIMARY KEY (service_name) ) WITH compaction = { 'min_threshold': '4', 'max_threshold': '32', 'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy' } AND default_time_to_live = {{.TraceTTLInSeconds}} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TABLE IF NOT EXISTS {{.Keyspace}}.operation_names_v2 ( service_name text, span_kind text, operation_name text, PRIMARY KEY ((service_name), span_kind, operation_name) ) WITH compaction = { 'min_threshold': '4', 'max_threshold': '32', 'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy' } AND default_time_to_live = {{.TraceTTLInSeconds}} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes -- index of trace IDs by service + operation names, sorted by span start_time. CREATE TABLE IF NOT EXISTS {{.Keyspace}}.service_operation_index ( service_name text, operation_name text, start_time bigint, -- microseconds since epoch trace_id blob, PRIMARY KEY ((service_name, operation_name), start_time) ) WITH CLUSTERING ORDER BY (start_time DESC) AND compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND default_time_to_live = {{.TraceTTLInSeconds}} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TABLE IF NOT EXISTS {{.Keyspace}}.service_name_index ( service_name text, bucket int, start_time bigint, -- microseconds since epoch trace_id blob, PRIMARY KEY ((service_name, bucket), start_time) ) WITH CLUSTERING ORDER BY (start_time DESC) AND compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND default_time_to_live = {{.TraceTTLInSeconds}} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TABLE IF NOT EXISTS {{.Keyspace}}.duration_index ( service_name text, -- service name operation_name text, -- operation name, or blank for queries without span name bucket timestamp, -- time bucket, - the start_time of the given span rounded to an hour duration bigint, -- span duration, in microseconds start_time bigint, -- microseconds since epoch trace_id blob, PRIMARY KEY ((service_name, operation_name, bucket), duration, start_time, trace_id) ) WITH CLUSTERING ORDER BY (duration DESC, start_time DESC) AND compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND default_time_to_live = {{.TraceTTLInSeconds}} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes -- a bucketing strategy may have to be added for tag queries -- we can make this table even better by adding a timestamp to it CREATE TABLE IF NOT EXISTS {{.Keyspace}}.tag_index ( service_name text, tag_key text, tag_value text, start_time bigint, -- microseconds since epoch trace_id blob, span_id bigint, PRIMARY KEY ((service_name, tag_key, tag_value), start_time, trace_id, span_id) ) WITH CLUSTERING ORDER BY (start_time DESC) AND compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND default_time_to_live = {{.TraceTTLInSeconds}} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TYPE IF NOT EXISTS {{.Keyspace}}.dependency ( parent text, child text, call_count bigint, source text ); -- compaction strategy is intentionally different as compared to other tables due to the size of dependencies data CREATE TABLE IF NOT EXISTS {{.Keyspace}}.dependencies_v2 ( ts_bucket timestamp, ts timestamp, dependencies list>, PRIMARY KEY (ts_bucket, ts) ) WITH CLUSTERING ORDER BY (ts DESC) AND compaction = { 'min_threshold': '4', 'max_threshold': '32', 'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy' } AND default_time_to_live = {{.DependenciesTTLInSeconds}}; -- adaptive sampling tables -- ./internal/storage/v1/cassandra/samplingstore/storage.go CREATE TABLE IF NOT EXISTS {{.Keyspace}}.operation_throughput ( bucket int, ts timeuuid, throughput text, PRIMARY KEY(bucket, ts) ) WITH CLUSTERING ORDER BY (ts desc); CREATE TABLE IF NOT EXISTS {{.Keyspace}}.sampling_probabilities ( bucket int, ts timeuuid, hostname text, probabilities text, PRIMARY KEY(bucket, ts) ) WITH CLUSTERING ORDER BY (ts desc); -- distributed lock -- ./plugin/pkg/distributedlock/cassandra/lock.go CREATE TABLE IF NOT EXISTS {{.Keyspace}}.leases ( name text, owner text, PRIMARY KEY (name) ); ================================================ FILE: internal/storage/v1/cassandra/schema/v004.cql.tmpl ================================================ -- -- Creates Cassandra keyspace with tables for traces and dependencies. -- -- Required parameters: -- -- keyspace -- name of the keyspace -- replication -- replication strategy for the keyspace, such as -- for prod environments -- {'class': 'NetworkTopologyStrategy', '$datacenter': '${replication_factor}' } -- for test environments -- {'class': 'SimpleStrategy', 'replication_factor': '1'} -- trace_ttl -- default time to live for trace data, in seconds -- dependencies_ttl -- default time to live for dependencies data, in seconds (0 for no TTL) -- -- Non-configurable settings: -- gc_grace_seconds is non-zero, see: http://www.uberobert.com/cassandra_gc_grace_disables_hinted_handoff/ -- For TTL of 2 days, compaction window is 1 hour, rule of thumb here: http://thelastpickle.com/blog/2016/12/08/TWCS-part1.html CREATE KEYSPACE IF NOT EXISTS ${keyspace} WITH replication = ${replication}; CREATE TYPE IF NOT EXISTS ${keyspace}.keyvalue ( key text, value_type text, value_string text, value_bool boolean, value_long bigint, value_double double, value_binary blob ); CREATE TYPE IF NOT EXISTS ${keyspace}.log ( ts bigint, -- microseconds since epoch fields frozen>> ); CREATE TYPE IF NOT EXISTS ${keyspace}.span_ref ( ref_type text, trace_id blob, span_id bigint ); CREATE TYPE IF NOT EXISTS ${keyspace}.process ( service_name text, tags frozen>> ); -- Notice we have span_hash. This exists only for zipkin backwards compat. Zipkin allows spans with the same ID. -- Note: Cassandra re-orders non-PK columns alphabetically, so the table looks differently in CQLSH "describe table". -- start_time is bigint instead of timestamp as we require microsecond precision CREATE TABLE IF NOT EXISTS ${keyspace}.traces ( trace_id blob, span_id bigint, span_hash bigint, parent_id bigint, operation_name text, flags int, start_time bigint, -- microseconds since epoch duration bigint, -- microseconds tags list>, logs list>, refs list>, process frozen, PRIMARY KEY (trace_id, span_id, span_hash) ) WITH compaction = { 'compaction_window_size': '${compaction_window_size}', 'compaction_window_unit': '${compaction_window_unit}', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TABLE IF NOT EXISTS ${keyspace}.service_names ( service_name text, PRIMARY KEY (service_name) ) WITH compaction = { 'min_threshold': '4', 'max_threshold': '32', 'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy' } AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TABLE IF NOT EXISTS ${keyspace}.operation_names_v2 ( service_name text, span_kind text, operation_name text, PRIMARY KEY ((service_name), span_kind, operation_name) ) WITH compaction = { 'min_threshold': '4', 'max_threshold': '32', 'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy' } AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes -- index of trace IDs by service + operation names, sorted by span start_time. CREATE TABLE IF NOT EXISTS ${keyspace}.service_operation_index ( service_name text, operation_name text, start_time bigint, -- microseconds since epoch trace_id blob, PRIMARY KEY ((service_name, operation_name), start_time) ) WITH CLUSTERING ORDER BY (start_time DESC) AND compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TABLE IF NOT EXISTS ${keyspace}.service_name_index ( service_name text, bucket int, start_time bigint, -- microseconds since epoch trace_id blob, PRIMARY KEY ((service_name, bucket), start_time) ) WITH CLUSTERING ORDER BY (start_time DESC) AND compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TABLE IF NOT EXISTS ${keyspace}.duration_index ( service_name text, -- service name operation_name text, -- operation name, or blank for queries without span name bucket timestamp, -- time bucket, - the start_time of the given span rounded to an hour duration bigint, -- span duration, in microseconds start_time bigint, -- microseconds since epoch trace_id blob, PRIMARY KEY ((service_name, operation_name, bucket), duration, start_time, trace_id) ) WITH CLUSTERING ORDER BY (duration DESC, start_time DESC) AND compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes -- a bucketing strategy may have to be added for tag queries -- we can make this table even better by adding a timestamp to it CREATE TABLE IF NOT EXISTS ${keyspace}.tag_index ( service_name text, tag_key text, tag_value text, start_time bigint, -- microseconds since epoch trace_id blob, span_id bigint, PRIMARY KEY ((service_name, tag_key, tag_value), start_time, trace_id, span_id) ) WITH CLUSTERING ORDER BY (start_time DESC) AND compaction = { 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy' } AND default_time_to_live = ${trace_ttl} AND speculative_retry = 'NONE' AND gc_grace_seconds = 10800; -- 3 hours of downtime acceptable on nodes CREATE TYPE IF NOT EXISTS ${keyspace}.dependency ( parent text, child text, call_count bigint, source text ); -- compaction strategy is intentionally different as compared to other tables due to the size of dependencies data CREATE TABLE IF NOT EXISTS ${keyspace}.dependencies_v2 ( ts_bucket timestamp, ts timestamp, dependencies list>, PRIMARY KEY (ts_bucket, ts) ) WITH CLUSTERING ORDER BY (ts DESC) AND compaction = { 'min_threshold': '4', 'max_threshold': '32', 'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy' } AND default_time_to_live = ${dependencies_ttl}; -- adaptive sampling tables -- ./internal/storage/v1/cassandra/samplingstore/storage.go CREATE TABLE IF NOT EXISTS ${keyspace}.operation_throughput ( bucket int, ts timeuuid, throughput text, PRIMARY KEY(bucket, ts) ) WITH CLUSTERING ORDER BY (ts desc); CREATE TABLE IF NOT EXISTS ${keyspace}.sampling_probabilities ( bucket int, ts timeuuid, hostname text, probabilities text, PRIMARY KEY(bucket, ts) ) WITH CLUSTERING ORDER BY (ts desc); -- distributed lock -- ./plugin/pkg/distributedlock/cassandra/lock.go CREATE TABLE IF NOT EXISTS ${keyspace}.leases ( name text, owner text, PRIMARY KEY (name) ); ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/converter.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "fmt" "strings" "github.com/jaegertracing/jaeger-idl/model/v1" ) const ( // warningStringPrefix is a magic string prefix for tag names to store span warnings. warningStringPrefix = "$$span.warning." ) var ( dbToDomainRefMap = map[string]model.SpanRefType{ ChildOf: model.SpanRefType_CHILD_OF, FollowsFrom: model.SpanRefType_FOLLOWS_FROM, } domainToDBRefMap = map[model.SpanRefType]string{ model.SpanRefType_CHILD_OF: ChildOf, model.SpanRefType_FOLLOWS_FROM: FollowsFrom, } domainToDBValueTypeMap = map[model.ValueType]string{ model.StringType: StringType, model.BoolType: BoolType, model.Int64Type: Int64Type, model.Float64Type: Float64Type, model.BinaryType: BinaryType, } ) // FromDomain converts a domain model.Span to a database Span func FromDomain(span *model.Span) *Span { return converter{}.fromDomain(span) } // ToDomain converts a database Span to a domain model.Span func ToDomain(dbSpan *Span) (*model.Span, error) { return converter{}.toDomain(dbSpan) } // converter converts Spans between domain and database representations. // It primarily exists to namespace the conversion functions. type converter struct{} func (c converter) fromDomain(span *model.Span) *Span { tags := c.toDBTags(span.Tags) warnings := c.toDBWarnings(span.Warnings) logs := c.toDBLogs(span.Logs) refs := c.toDBRefs(span.References) udtProcess := c.toDBProcess(span.Process) spanHash, _ := model.HashCode(span) tags = append(tags, warnings...) //nolint:gosec // G115 return &Span{ TraceID: TraceIDFromDomain(span.TraceID), SpanID: int64(span.SpanID), OperationName: span.OperationName, Flags: int32(span.Flags), StartTime: int64(model.TimeAsEpochMicroseconds(span.StartTime)), Duration: int64(model.DurationAsMicroseconds(span.Duration)), Tags: tags, Logs: logs, Refs: refs, Process: udtProcess, ServiceName: span.Process.ServiceName, SpanHash: int64(spanHash), } } func (c converter) toDomain(dbSpan *Span) (*model.Span, error) { tags, err := c.fromDBTags(dbSpan.Tags) if err != nil { return nil, err } warnings, err := c.fromDBWarnings(dbSpan.Tags) if err != nil { return nil, err } logs, err := c.fromDBLogs(dbSpan.Logs) if err != nil { return nil, err } refs, err := c.fromDBRefs(dbSpan.Refs) if err != nil { return nil, err } process, err := c.fromDBProcess(dbSpan.Process) if err != nil { return nil, err } traceID := dbSpan.TraceID.ToDomain() span := &model.Span{ TraceID: traceID, //nolint:gosec // G115 SpanID: model.NewSpanID(uint64(dbSpan.SpanID)), OperationName: dbSpan.OperationName, //nolint:gosec // G115 References: model.MaybeAddParentSpanID(traceID, model.NewSpanID(uint64(dbSpan.ParentID)), refs), //nolint:gosec // G115 Flags: model.Flags(uint32(dbSpan.Flags)), //nolint:gosec // G115 StartTime: model.EpochMicrosecondsAsTime(uint64(dbSpan.StartTime)), //nolint:gosec // G115 Duration: model.MicrosecondsAsDuration(uint64(dbSpan.Duration)), Tags: tags, Warnings: warnings, Logs: logs, Process: process, } return span, nil } func (c converter) fromDBTags(tags []KeyValue) ([]model.KeyValue, error) { retMe := make([]model.KeyValue, 0, len(tags)) for i := range tags { if strings.HasPrefix(tags[i].Key, warningStringPrefix) { continue } kv, err := c.fromDBTag(&tags[i]) if err != nil { return nil, err } retMe = append(retMe, kv) } return retMe, nil } func (c converter) fromDBWarnings(tags []KeyValue) ([]string, error) { var retMe []string for _, tag := range tags { if !strings.HasPrefix(tag.Key, warningStringPrefix) { continue } kv, err := c.fromDBTag(&tag) if err != nil { return nil, err } retMe = append(retMe, kv.VStr) } return retMe, nil } func (converter) fromDBTag(tag *KeyValue) (model.KeyValue, error) { switch tag.ValueType { case StringType: return model.String(tag.Key, tag.ValueString), nil case BoolType: return model.Bool(tag.Key, tag.ValueBool), nil case Int64Type: return model.Int64(tag.Key, tag.ValueInt64), nil case Float64Type: return model.Float64(tag.Key, tag.ValueFloat64), nil case BinaryType: return model.Binary(tag.Key, tag.ValueBinary), nil default: return model.KeyValue{}, fmt.Errorf("invalid ValueType in %+v", tag) } } func (c converter) fromDBLogs(logs []Log) ([]model.Log, error) { retMe := make([]model.Log, len(logs)) for i, l := range logs { fields, err := c.fromDBTags(l.Fields) if err != nil { return nil, err } retMe[i] = model.Log{ //nolint:gosec // G115 Timestamp: model.EpochMicrosecondsAsTime(uint64(l.Timestamp)), Fields: fields, } } return retMe, nil } func (converter) fromDBRefs(refs []SpanRef) ([]model.SpanRef, error) { retMe := make([]model.SpanRef, len(refs)) for i, r := range refs { refType, ok := dbToDomainRefMap[r.RefType] if !ok { return nil, fmt.Errorf("invalid SpanRefType in %+v", r) } retMe[i] = model.SpanRef{ RefType: refType, TraceID: r.TraceID.ToDomain(), //nolint:gosec // G115 SpanID: model.NewSpanID(uint64(r.SpanID)), } } return retMe, nil } func (c converter) fromDBProcess(process Process) (*model.Process, error) { tags, err := c.fromDBTags(process.Tags) if err != nil { return nil, err } return &model.Process{ Tags: tags, ServiceName: process.ServiceName, }, nil } func (converter) toDBTags(tags []model.KeyValue) []KeyValue { retMe := make([]KeyValue, len(tags)) for i, t := range tags { // do we want to validate a jaeger tag here? Making sure that the type and value matches up? retMe[i] = KeyValue{ Key: t.Key, ValueType: domainToDBValueTypeMap[t.VType], ValueString: t.VStr, ValueBool: t.Bool(), ValueInt64: t.Int64(), ValueFloat64: t.Float64(), ValueBinary: t.Binary(), } } return retMe } func (converter) toDBWarnings(warnings []string) []KeyValue { retMe := make([]KeyValue, len(warnings)) for i, w := range warnings { kv := model.String(fmt.Sprintf("%s%d", warningStringPrefix, i+1), w) retMe[i] = KeyValue{ Key: kv.Key, ValueType: domainToDBValueTypeMap[kv.VType], ValueString: kv.VStr, } } return retMe } func (c converter) toDBLogs(logs []model.Log) []Log { retMe := make([]Log, len(logs)) for i, l := range logs { retMe[i] = Log{ //nolint:gosec // G115 Timestamp: int64(model.TimeAsEpochMicroseconds(l.Timestamp)), Fields: c.toDBTags(l.Fields), } } return retMe } func (converter) toDBRefs(refs []model.SpanRef) []SpanRef { retMe := make([]SpanRef, len(refs)) for i, r := range refs { retMe[i] = SpanRef{ TraceID: TraceIDFromDomain(r.TraceID), //nolint:gosec // G115 SpanID: int64(r.SpanID), RefType: domainToDBRefMap[r.RefType], } } return retMe } func (c converter) toDBProcess(process *model.Process) Process { return Process{ ServiceName: process.ServiceName, Tags: c.toDBTags(process.Tags), } } ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/converter_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "testing" "github.com/kr/pretty" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger-idl/model/v1" ) var ( someTraceID = model.NewTraceID(22222, 44444) someSpanID = model.SpanID(3333) someParentSpanID = model.SpanID(11111) someOperationName = "someOperationName" someStartTime = model.EpochMicrosecondsAsTime(55555) someDuration = model.MicrosecondsAsDuration(50000) someFlags = model.Flags(1) someLogTimestamp = model.EpochMicrosecondsAsTime(12345) someServiceName = "someServiceName" someStringTagValue = "someTagValue" someBoolTagValue = true someLongTagValue = int64(123) someDoubleTagValue = float64(1.4) someBinaryTagValue = []byte("someBinaryValue") someStringTagKey = "someStringTag" someBoolTagKey = "someBoolTag" someLongTagKey = "someLongTag" someDoubleTagKey = "someDoubleTag" someBinaryTagKey = "someBinaryTag" someTags = model.KeyValues{ model.String(someStringTagKey, someStringTagValue), model.Bool(someBoolTagKey, someBoolTagValue), model.Int64(someLongTagKey, someLongTagValue), model.Float64(someDoubleTagKey, someDoubleTagValue), model.Binary(someBinaryTagKey, someBinaryTagValue), } someWarnings = []string{"warning text 1", "warning text 2"} someDBTags = []KeyValue{ { Key: someStringTagKey, ValueType: StringType, ValueString: someStringTagValue, }, { Key: someBoolTagKey, ValueType: BoolType, ValueBool: someBoolTagValue, }, { Key: someLongTagKey, ValueType: Int64Type, ValueInt64: someLongTagValue, }, { Key: someDoubleTagKey, ValueType: Float64Type, ValueFloat64: someDoubleTagValue, }, { Key: someBinaryTagKey, ValueType: BinaryType, ValueBinary: someBinaryTagValue, }, } someLogs = []model.Log{ { Timestamp: someLogTimestamp, Fields: someTags, }, } someDBLogs = []Log{ { Timestamp: int64(model.TimeAsEpochMicroseconds(someLogTimestamp)), Fields: someDBTags, }, } someRefs = []model.SpanRef{ { TraceID: someTraceID, SpanID: someParentSpanID, RefType: model.ChildOf, }, } someDBProcess = Process{ ServiceName: someServiceName, Tags: someDBTags, } badDBTags = []KeyValue{ { Key: "sneh", ValueType: "krustytheklown", }, } someDBTraceID = TraceIDFromDomain(someTraceID) someDBRefs = []SpanRef{ { RefType: "child-of", SpanID: int64(someParentSpanID), TraceID: someDBTraceID, }, } notValidTagTypeErrStr = "invalid ValueType in" ) func getTestJaegerSpan() *model.Span { return &model.Span{ TraceID: someTraceID, SpanID: someSpanID, OperationName: someOperationName, References: someRefs, Flags: someFlags, StartTime: someStartTime, Duration: someDuration, Tags: someTags, Logs: someLogs, Process: getTestJaegerProcess(), } } func getTestJaegerProcess() *model.Process { return &model.Process{ ServiceName: someServiceName, Tags: someTags, } } func getTestSpan() *Span { span := &Span{ TraceID: someDBTraceID, SpanID: int64(someSpanID), OperationName: someOperationName, Flags: int32(someFlags), StartTime: int64(model.TimeAsEpochMicroseconds(someStartTime)), Duration: int64(model.DurationAsMicroseconds(someDuration)), Tags: someDBTags, Logs: someDBLogs, Refs: someDBRefs, Process: someDBProcess, ServiceName: someServiceName, } // there is no way to validate if the hash code is "correct" or not, // other than comparing it with some magic number that keeps changing // as the model changes. So let's just make sure the code is being // calculated during the conversion. spanHash, _ := model.HashCode(getTestJaegerSpan()) span.SpanHash = int64(spanHash) return span } func getCustomSpan(dbTags []KeyValue, dbProcess Process, dbLogs []Log, dbRefs []SpanRef) *Span { span := getTestSpan() span.Tags = dbTags span.Logs = dbLogs span.Refs = dbRefs span.Process = dbProcess return span } func getTestUniqueTags() []TagInsertion { return []TagInsertion{ {ServiceName: "someServiceName", TagKey: "someBoolTag", TagValue: "true"}, {ServiceName: "someServiceName", TagKey: "someDoubleTag", TagValue: "1.4"}, {ServiceName: "someServiceName", TagKey: "someLongTag", TagValue: "123"}, {ServiceName: "someServiceName", TagKey: "someStringTag", TagValue: "someTagValue"}, } } func TestToSpan(t *testing.T) { expectedSpan := getTestSpan() actualDBSpan := FromDomain(getTestJaegerSpan()) if !assert.Equal(t, expectedSpan, actualDBSpan) { for _, diff := range pretty.Diff(expectedSpan, actualDBSpan) { t.Log(diff) } } } func TestFromSpan(t *testing.T) { for _, testParentID := range []bool{false, true} { testDBSpan := getTestSpan() if testParentID { testDBSpan.ParentID = testDBSpan.Refs[0].SpanID testDBSpan.Refs = nil } expectedSpan := getTestJaegerSpan() actualJSpan, err := ToDomain(testDBSpan) require.NoError(t, err) if !assert.Equal(t, expectedSpan, actualJSpan) { for _, diff := range pretty.Diff(expectedSpan, actualJSpan) { t.Log(diff) } } } } func TestFailingFromDBSpanBadTags(t *testing.T) { faultyDBTags := getCustomSpan(badDBTags, someDBProcess, someDBLogs, someDBRefs) failingDBSpanTransform(t, faultyDBTags, notValidTagTypeErrStr) } func TestFailingFromDBSpanBadLogs(t *testing.T) { faultyDBLogs := getCustomSpan(someDBTags, someDBProcess, []Log{ { Timestamp: 0, Fields: badDBTags, }, }, someDBRefs) failingDBSpanTransform(t, faultyDBLogs, notValidTagTypeErrStr) } func TestFailingFromDBSpanBadProcess(t *testing.T) { faultyDBProcess := getCustomSpan(someDBTags, Process{ ServiceName: someServiceName, Tags: badDBTags, }, someDBLogs, someDBRefs) failingDBSpanTransform(t, faultyDBProcess, notValidTagTypeErrStr) } func TestFailingFromDBSpanBadRefs(t *testing.T) { faultyDBRefs := getCustomSpan(someDBTags, someDBProcess, someDBLogs, []SpanRef{ { RefType: "makeOurOwnCasino", TraceID: someDBTraceID, }, }) failingDBSpanTransform(t, faultyDBRefs, "invalid SpanRefType in") } func failingDBSpanTransform(t *testing.T, dbSpan *Span, errMsg string) { jSpan, err := ToDomain(dbSpan) assert.Nil(t, jSpan) assert.ErrorContains(t, err, errMsg) } func TestFailingFromDBLogs(t *testing.T) { someDBLogs := []Log{ { Timestamp: 0, Fields: []KeyValue{ { Key: "sneh", ValueType: "krustytheklown", }, }, }, } jLogs, err := converter{}.fromDBLogs(someDBLogs) assert.Nil(t, jLogs) assert.ErrorContains(t, err, notValidTagTypeErrStr) } func TestDBTagTypeError(t *testing.T) { _, err := converter{}.fromDBTag(&KeyValue{ValueType: "x"}) assert.ErrorContains(t, err, notValidTagTypeErrStr) } func TestGenerateHashCode(t *testing.T) { span1 := getTestJaegerSpan() span2 := getTestJaegerSpan() hc1, err1 := model.HashCode(span1) hc2, err2 := model.HashCode(span2) assert.Equal(t, hc1, hc2) require.NoError(t, err1) require.NoError(t, err2) span2.Tags = append(span2.Tags, model.String("xyz", "some new tag")) hc2, err2 = model.HashCode(span2) assert.NotEqual(t, hc1, hc2) require.NoError(t, err2) } func TestFromDBTagsWithoutWarnings(t *testing.T) { span := getTestJaegerSpan() dbSpan := FromDomain(span) tags, err := converter{}.fromDBTags(dbSpan.Tags) require.NoError(t, err) assert.Equal(t, tags, span.Tags) } func TestFromDBTagsWithWarnings(t *testing.T) { span := getTestJaegerSpan() span.Warnings = someWarnings dbSpan := FromDomain(span) tags, err := converter{}.fromDBTags(dbSpan.Tags) require.NoError(t, err) assert.Equal(t, tags, span.Tags) } func TestFromDBLogsWithWarnings(t *testing.T) { span := getTestJaegerSpan() span.Warnings = someWarnings dbSpan := FromDomain(span) logs, err := converter{}.fromDBLogs(dbSpan.Logs) require.NoError(t, err) assert.Equal(t, logs, span.Logs) } func TestFromDBProcessWithWarnings(t *testing.T) { span := getTestJaegerSpan() span.Warnings = someWarnings dbSpan := FromDomain(span) process, err := converter{}.fromDBProcess(dbSpan.Process) require.NoError(t, err) assert.Equal(t, process, span.Process) } func TestFromDBWarnings(t *testing.T) { span := getTestJaegerSpan() span.Warnings = someWarnings dbSpan := FromDomain(span) warnings, err := converter{}.fromDBWarnings(dbSpan.Tags) require.NoError(t, err) assert.Equal(t, warnings, span.Warnings) } func TestFailingFromDBWarnings(t *testing.T) { badDBWarningTags := []KeyValue{{Key: warningStringPrefix + "1", ValueType: "invalidValueType"}} span := getCustomSpan(badDBWarningTags, someDBProcess, someDBLogs, someDBRefs) failingDBSpanTransform(t, span, notValidTagTypeErrStr) } func TestFromDBTag_DefaultCase(t *testing.T) { tag := &KeyValue{ Key: "test-key", ValueType: "unknown-type", ValueString: "test-value", } converter := converter{} result, err := converter.fromDBTag(tag) require.Error(t, err) assert.Contains(t, err.Error(), "invalid ValueType") assert.Equal(t, model.KeyValue{}, result) } ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/cql_udt.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "errors" "fmt" gocql "github.com/apache/cassandra-gocql-driver/v2" ) // ErrTraceIDWrongLength is an error that occurs when cassandra has a TraceID that's not 128 bits long var ErrTraceIDWrongLength = errors.New("TraceID is not a 128bit integer") // MarshalCQL handles marshaling DBTraceID (e.g. in SpanRef) func (t TraceID) MarshalCQL(gocql.TypeInfo) ([]byte, error) { return t[:], nil } // UnmarshalCQL handles unmarshaling DBTraceID (e.g. in SpanRef) func (t *TraceID) UnmarshalCQL(_ gocql.TypeInfo, data []byte) error { if len(data) != 16 { return ErrTraceIDWrongLength } copy(t[:], data) return nil } // MarshalUDT handles marshalling a Tag. func (t *KeyValue) MarshalUDT(name string, info gocql.TypeInfo) ([]byte, error) { switch name { case "key": return gocql.Marshal(info, t.Key) case "value_type": return gocql.Marshal(info, t.ValueType) case "value_string": return gocql.Marshal(info, t.ValueString) case "value_bool": return gocql.Marshal(info, t.ValueBool) case "value_long": return gocql.Marshal(info, t.ValueInt64) case "value_double": return gocql.Marshal(info, t.ValueFloat64) case "value_binary": return gocql.Marshal(info, t.ValueBinary) default: return nil, fmt.Errorf("unknown column for position: %q", name) } } // UnmarshalUDT handles unmarshalling a Tag. func (t *KeyValue) UnmarshalUDT(name string, info gocql.TypeInfo, data []byte) error { switch name { case "key": return gocql.Unmarshal(info, data, &t.Key) case "value_type": return gocql.Unmarshal(info, data, &t.ValueType) case "value_string": return gocql.Unmarshal(info, data, &t.ValueString) case "value_bool": return gocql.Unmarshal(info, data, &t.ValueBool) case "value_long": return gocql.Unmarshal(info, data, &t.ValueInt64) case "value_double": return gocql.Unmarshal(info, data, &t.ValueFloat64) case "value_binary": return gocql.Unmarshal(info, data, &t.ValueBinary) default: return fmt.Errorf("unknown column for position: %q", name) } } // MarshalUDT handles marshalling a Log. func (l *Log) MarshalUDT(name string, info gocql.TypeInfo) ([]byte, error) { switch name { case "ts": return gocql.Marshal(info, l.Timestamp) case "fields": return gocql.Marshal(info, l.Fields) default: return nil, fmt.Errorf("unknown column for position: %q", name) } } // UnmarshalUDT handles unmarshalling a Log. func (l *Log) UnmarshalUDT(name string, info gocql.TypeInfo, data []byte) error { switch name { case "ts": return gocql.Unmarshal(info, data, &l.Timestamp) case "fields": return gocql.Unmarshal(info, data, &l.Fields) default: return fmt.Errorf("unknown column for position: %q", name) } } // MarshalUDT handles marshalling a SpanRef. func (s *SpanRef) MarshalUDT(name string, info gocql.TypeInfo) ([]byte, error) { switch name { case "ref_type": return gocql.Marshal(info, s.RefType) case "trace_id": return gocql.Marshal(info, s.TraceID) case "span_id": return gocql.Marshal(info, s.SpanID) default: return nil, fmt.Errorf("unknown column for position: %q", name) } } // UnmarshalUDT handles unmarshalling a SpanRef. func (s *SpanRef) UnmarshalUDT(name string, info gocql.TypeInfo, data []byte) error { switch name { case "ref_type": return gocql.Unmarshal(info, data, &s.RefType) case "trace_id": return gocql.Unmarshal(info, data, &s.TraceID) case "span_id": return gocql.Unmarshal(info, data, &s.SpanID) default: return fmt.Errorf("unknown column for position: %q", name) } } // MarshalUDT handles marshalling a Process. func (p *Process) MarshalUDT(name string, info gocql.TypeInfo) ([]byte, error) { switch name { case "service_name": return gocql.Marshal(info, p.ServiceName) case "tags": return gocql.Marshal(info, p.Tags) default: return nil, fmt.Errorf("unknown column for position: %q", name) } } // UnmarshalUDT handles unmarshalling a Process. func (p *Process) UnmarshalUDT(name string, info gocql.TypeInfo, data []byte) error { switch name { case "service_name": return gocql.Unmarshal(info, data, &p.ServiceName) case "tags": return gocql.Unmarshal(info, data, &p.Tags) default: return fmt.Errorf("unknown column for position: %q", name) } } ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/cql_udt_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "encoding/binary" "math" "testing" gocql "github.com/apache/cassandra-gocql-driver/v2" "github.com/stretchr/testify/assert" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/cassandra/gocql/testutils" ) func TestDBModelUDTMarshall(t *testing.T) { floatVal := float64(123.456) floatBytes := make([]byte, 8) binary.BigEndian.PutUint64(floatBytes, uint64(math.Float64bits(floatVal))) kv := &KeyValue{ Key: "key", ValueType: "value_type", ValueString: "string_value", ValueBool: true, ValueInt64: 123, ValueFloat64: floatVal, ValueBinary: []byte("value_binary"), } log := &Log{ Timestamp: 123, } spanRef := &SpanRef{ RefType: "childOf", TraceID: TraceIDFromDomain(model.NewTraceID(0, 1)), SpanID: 123, } proc := &Process{ ServiceName: "google", } testCases := []testutils.UDTTestCase{ { Obj: kv, New: func() gocql.UDTUnmarshaler { return &KeyValue{} }, ObjName: "KeyValue", Fields: []testutils.UDTField{ {Name: "key", Type: gocql.TypeAscii, ValIn: []byte("key"), Err: false}, {Name: "value_type", Type: gocql.TypeAscii, ValIn: []byte("value_type"), Err: false}, {Name: "value_string", Type: gocql.TypeAscii, ValIn: []byte("string_value"), Err: false}, {Name: "value_bool", Type: gocql.TypeBoolean, ValIn: []byte{1}, Err: false}, {Name: "value_long", Type: gocql.TypeBigInt, ValIn: []byte{0, 0, 0, 0, 0, 0, 0, 123}, Err: false}, {Name: "value_double", Type: gocql.TypeDouble, ValIn: floatBytes, Err: false}, {Name: "value_binary", Type: gocql.TypeBlob, ValIn: []byte("value_binary"), Err: false}, {Name: "wrong-field", Err: true}, }, }, { Obj: log, New: func() gocql.UDTUnmarshaler { return &Log{} }, ObjName: "Log", Fields: []testutils.UDTField{ {Name: "ts", Type: gocql.TypeBigInt, ValIn: []byte{0, 0, 0, 0, 0, 0, 0, 123}, Err: false}, // Wrong type TypeAscii for an array field {Name: "fields", Type: gocql.TypeAscii, ValIn: []byte{}, Err: true}, {Name: "wrong-field", Err: true}, }, }, { Obj: spanRef, New: func() gocql.UDTUnmarshaler { return &SpanRef{} }, ObjName: "SpanRef", Fields: []testutils.UDTField{ {Name: "ref_type", Type: gocql.TypeAscii, ValIn: []byte("childOf"), Err: false}, {Name: "trace_id", Type: gocql.TypeBlob, ValIn: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Err: false}, {Name: "span_id", Type: gocql.TypeBigInt, ValIn: []byte{0, 0, 0, 0, 0, 0, 0, 123}, Err: false}, {Name: "wrong-field", Err: true}, }, }, { Obj: proc, New: func() gocql.UDTUnmarshaler { return &Process{} }, ObjName: "Process", Fields: []testutils.UDTField{ {Name: "service_name", Type: gocql.TypeAscii, ValIn: []byte("google"), Err: false}, // Wrong type TypeAscii for an array field {Name: "tags", Type: gocql.TypeAscii, ValIn: []byte{}, Err: true}, {Name: "wrong-field", Err: true}, }, }, } for _, testCase := range testCases { testCase.Run(t) } dbid := TraceID{} assert.Equal(t, ErrTraceIDWrongLength, dbid.UnmarshalCQL(nil, nil)) } ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/ids.go ================================================ // Copyright (c) 2026 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "encoding/base64" "encoding/binary" "encoding/json" "fmt" "github.com/jaegertracing/jaeger-idl/model/v1" ) // TraceID is a serializable form of model.TraceID type TraceID [16]byte // ToDomain converts trace ID from db-serializable form to domain TradeID func (t TraceID) ToDomain() model.TraceID { traceIDHigh := binary.BigEndian.Uint64(t[:8]) traceIDLow := binary.BigEndian.Uint64(t[8:]) return model.NewTraceID(traceIDHigh, traceIDLow) } // String returns hex string representation of the trace ID. func (t TraceID) String() string { traceIDHigh := binary.BigEndian.Uint64(t[:8]) traceIDLow := binary.BigEndian.Uint64(t[8:]) if traceIDHigh == 0 { return fmt.Sprintf("%016x", traceIDLow) } return fmt.Sprintf("%016x%016x", traceIDHigh, traceIDLow) } // MarshalJSON converts trace id into a base64 string enclosed in quotes. func (t TraceID) MarshalJSON() ([]byte, error) { var out [26]byte out[0] = '"' base64.StdEncoding.Encode(out[1:25], t[:]) out[25] = '"' return out[:], nil } func (t *TraceID) UnmarshalJSON(data []byte) error { var s string if err := json.Unmarshal(data, &s); err != nil { return err } b, err := base64.StdEncoding.DecodeString(s) if err != nil { return err } if len(b) != 16 { return fmt.Errorf("invalid TraceID length: %d", len(b)) } copy(t[:], b) return nil } ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/ids_test.go ================================================ // Copyright (c) 2026 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestTraceIDJSONRoundTrip(t *testing.T) { original := TraceID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} b, err := json.Marshal(original) require.NoError(t, err) expectedStr := "\"AAAAAAAAAAAAAAAAAAAAAQ==\"" assert.Equal(t, expectedStr, string(b)) var decoded TraceID err = json.Unmarshal(b, &decoded) require.NoError(t, err) assert.Equal(t, original, decoded) } func TestTraceIDJSONUnmarshal_Errors(t *testing.T) { tests := []struct { name string input string err string }{ { name: "not a JSON string", input: `123`, err: "json: cannot unmarshal number into Go value of type string", }, { name: "invalid base64", input: `"@@@@"`, err: "illegal base64 data at input byte 0", }, { name: "invalid decoded length", input: `"AQ=="`, err: "invalid TraceID length: 1", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var traceId TraceID err := json.Unmarshal([]byte(tt.input), &traceId) assert.ErrorContains(t, err, tt.err) }) } } ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/index_filter.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel const ( // DurationIndex represents the flag for indexing by duration. DurationIndex = iota // ServiceIndex represents the flag for indexing by service. ServiceIndex // OperationIndex represents the flag for indexing by service-operation. OperationIndex ) // IndexFilter filters out any spans that should not be indexed depending on the index specified. type IndexFilter func(span *Span, index int) bool // DefaultIndexFilter is a filter that indexes everything. var DefaultIndexFilter = func(_ *Span, _ /* index */ int) bool { return true } ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/index_filter_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "testing" "github.com/stretchr/testify/assert" ) func TestDefaultIndexFilter(t *testing.T) { span := &Span{} filter := DefaultIndexFilter assert.True(t, filter(span, DurationIndex)) assert.True(t, filter(span, ServiceIndex)) assert.True(t, filter(span, OperationIndex)) } ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/model.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "bytes" "encoding/binary" "encoding/hex" "sort" "strconv" "strings" "github.com/jaegertracing/jaeger-idl/model/v1" ) const ( ChildOf = "child-of" FollowsFrom = "follows-from" StringType = "string" BoolType = "bool" Int64Type = "int64" Float64Type = "float64" BinaryType = "binary" ) // Span is the database representation of a span. type Span struct { TraceID TraceID SpanID int64 ParentID int64 // deprecated OperationName string Flags int32 StartTime int64 // microseconds since epoch Duration int64 // microseconds Tags []KeyValue Logs []Log Refs []SpanRef Process Process ServiceName string SpanHash int64 } // KeyValue is the UDT representation of a Jaeger KeyValue. type KeyValue struct { Key string `cql:"key"` ValueType string `cql:"value_type"` ValueString string `cql:"value_string" json:"value_string,omitempty"` ValueBool bool `cql:"value_bool" json:"value_bool,omitempty"` ValueInt64 int64 `cql:"value_long" json:"value_long,omitempty"` // using more natural column name for Cassandra ValueFloat64 float64 `cql:"value_double" json:"value_double,omitempty"` // using more natural column name for Cassandra ValueBinary []byte `cql:"value_binary" json:"value_binary,omitempty"` } func (t *KeyValue) compareValues(that *KeyValue) int { switch t.ValueType { case StringType: return strings.Compare(t.ValueString, that.ValueString) case BoolType: if t.ValueBool != that.ValueBool { if !t.ValueBool { return -1 } return 1 } case Int64Type: return int(t.ValueInt64 - that.ValueInt64) case Float64Type: if t.ValueFloat64 != that.ValueFloat64 { if t.ValueFloat64 < that.ValueFloat64 { return -1 } return 1 } case BinaryType: return bytes.Compare(t.ValueBinary, that.ValueBinary) default: return -1 // theoretical case, not stating them equal but placing the base pointer before other } return 0 } func (t *KeyValue) Compare(that any) int { if that == nil { if t == nil { return 0 } return 1 } that1, ok := that.(*KeyValue) if !ok { that2, ok := that.(KeyValue) if !ok { return 1 } that1 = &that2 } if that1 == nil { if t == nil { return 0 } return 1 } else if t == nil { return -1 } if cmp := strings.Compare(t.Key, that1.Key); cmp != 0 { return cmp } if cmp := strings.Compare(t.ValueType, that1.ValueType); cmp != 0 { return cmp } return t.compareValues(that1) } func (t *KeyValue) Equal(that any) bool { return t.Compare(that) == 0 } func (t *KeyValue) AsString() string { switch t.ValueType { case StringType: return t.ValueString case BoolType: if t.ValueBool { return "true" } return "false" case Int64Type: return strconv.FormatInt(t.ValueInt64, 10) case Float64Type: return strconv.FormatFloat(t.ValueFloat64, 'g', 10, 64) case BinaryType: return hex.EncodeToString(t.ValueBinary) default: return "unknown type " + t.ValueType } } func SortKVs(kvs []KeyValue) { sort.Slice(kvs, func(i, j int) bool { return kvs[i].Compare(kvs[j]) < 0 }) } // Log is the UDT representation of a Jaeger Log. type Log struct { Timestamp int64 `cql:"ts"` // microseconds since epoch Fields []KeyValue `cql:"fields"` } // SpanRef is the UDT representation of a Jaeger Span Reference. type SpanRef struct { RefType string `cql:"ref_type"` TraceID TraceID `cql:"trace_id"` SpanID int64 `cql:"span_id"` } // Process is the UDT representation of a Jaeger Process. type Process struct { ServiceName string `cql:"service_name"` Tags []KeyValue `cql:"tags"` } // TagInsertion contains the items necessary to insert a tag for a given span type TagInsertion struct { ServiceName string TagKey string TagValue string } func (t TagInsertion) String() string { const uniqueTagDelimiter = ":" var buffer bytes.Buffer buffer.WriteString(t.ServiceName) buffer.WriteString(uniqueTagDelimiter) buffer.WriteString(t.TagKey) buffer.WriteString(uniqueTagDelimiter) buffer.WriteString(t.TagValue) return buffer.String() } // TraceIDFromDomain converts domain TraceID into serializable DB representation. func TraceIDFromDomain(traceID model.TraceID) TraceID { dbTraceID := TraceID{} binary.BigEndian.PutUint64(dbTraceID[:8], uint64(traceID.High)) binary.BigEndian.PutUint64(dbTraceID[8:], uint64(traceID.Low)) return dbTraceID } ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/model_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "bytes" "testing" "github.com/stretchr/testify/assert" "github.com/jaegertracing/jaeger-idl/model/v1" ) func TestTagInsertionString(t *testing.T) { v := TagInsertion{"x", "y", "z"} assert.Equal(t, "x:y:z", v.String()) } func TestTraceIDString(t *testing.T) { id := TraceIDFromDomain(model.NewTraceID(1, 1)) assert.Equal(t, "00000000000000010000000000000001", id.String()) } func TestKeyValueCompare(t *testing.T) { tests := []struct { name string kv1 *KeyValue kv2 any result int }{ { name: "BothNil", kv1: nil, kv2: nil, result: 0, }, { name: "Nil_vs_Value", kv1: nil, kv2: &KeyValue{Key: "k", ValueType: "string"}, result: -1, }, { name: "Pointer_vs_Value", kv1: &KeyValue{Key: "k", ValueType: "string"}, kv2: KeyValue{Key: "m", ValueType: "string"}, result: -1, }, { name: "Value_vs_Nil", kv1: &KeyValue{Key: "k", ValueType: "string"}, kv2: nil, result: 1, }, { name: "TypedNil_vs_Value", kv1: (*KeyValue)(nil), kv2: &KeyValue{Key: "k", ValueType: "string"}, result: -1, }, { name: "TypedNil_vs_TypedNil", kv1: (*KeyValue)(nil), kv2: (*KeyValue)(nil), result: 0, }, { name: "Value_vs_TypedNil", kv1: &KeyValue{Key: "k", ValueType: "string"}, kv2: (*KeyValue)(nil), result: 1, }, { name: "InvalidType", kv1: &KeyValue{Key: "k", ValueType: "string"}, kv2: 123, result: 1, }, { name: "Equal", kv1: &KeyValue{ Key: "k", ValueType: "string", ValueString: "hello", }, kv2: &KeyValue{ Key: "k", ValueType: "string", ValueString: "hello", }, result: 0, }, { name: "KeyMismatch", kv1: &KeyValue{Key: "k", ValueType: "string"}, kv2: &KeyValue{Key: "a", ValueType: "string"}, result: 1, }, { name: "ValueTypeMismatch", kv1: &KeyValue{Key: "k", ValueType: "z"}, kv2: &KeyValue{Key: "k", ValueType: "a"}, result: 1, }, { name: "ValueStringMismatch", kv1: &KeyValue{Key: "k", ValueType: "string", ValueString: "zzz"}, kv2: &KeyValue{Key: "k", ValueType: "string", ValueString: "aaa"}, result: 1, }, { name: "ValueBoolMismatch_After", kv1: &KeyValue{Key: "k", ValueType: "bool", ValueBool: true}, kv2: &KeyValue{Key: "k", ValueType: "bool", ValueBool: false}, result: 1, }, { name: "ValueBoolMismatch_Before", kv1: &KeyValue{Key: "k", ValueType: "bool", ValueBool: false}, kv2: &KeyValue{Key: "k", ValueType: "bool", ValueBool: true}, result: -1, }, { name: "ValueInt64Mismatch_After", kv1: &KeyValue{Key: "k", ValueType: "int64", ValueInt64: 10}, kv2: &KeyValue{Key: "k", ValueType: "int64", ValueInt64: 5}, result: 5, }, { name: "ValueFloat64Mismatch_After", kv1: &KeyValue{Key: "k", ValueType: "float64", ValueFloat64: 1.5}, kv2: &KeyValue{Key: "k", ValueType: "float64", ValueFloat64: 0.5}, result: 1, }, { name: "ValueFloat64Mismatch_Before", kv1: &KeyValue{Key: "k", ValueType: "float64", ValueFloat64: 0.5}, kv2: &KeyValue{Key: "k", ValueType: "float64", ValueFloat64: 1.5}, result: -1, }, { name: "ValueBinaryMismatch", kv1: &KeyValue{Key: "k", ValueType: "binary", ValueBinary: []byte{1, 2, 3}}, kv2: &KeyValue{Key: "k", ValueType: "binary", ValueBinary: []byte{1, 2, 4}}, result: bytes.Compare([]byte{1, 2, 3}, []byte{1, 2, 4}), }, { name: "ValueBinaryEqual", kv1: &KeyValue{Key: "k", ValueType: "binary", ValueBinary: []byte{1, 2, 3}}, kv2: &KeyValue{Key: "k", ValueType: "binary", ValueBinary: []byte{1, 2, 3}}, result: 0, }, { name: "UnknownType", kv1: &KeyValue{Key: "k", ValueType: "random", ValueString: "hello"}, kv2: &KeyValue{Key: "k", ValueType: "random", ValueString: "hellobig"}, result: -1, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { assert.Equal(t, tc.result, tc.kv1.Compare(tc.kv2)) }) } } func TestKeyValueEqual(t *testing.T) { tests := []struct { name string kv1 *KeyValue kv2 any result bool }{ { name: "BothNil", kv1: nil, kv2: nil, result: true, }, { name: "Nil_vs_Value", kv1: nil, kv2: &KeyValue{Key: "k", ValueType: "string"}, result: false, }, { name: "Value_vs_Nil", kv1: &KeyValue{Key: "k", ValueType: "string"}, kv2: nil, result: false, }, { name: "TypedNil_vs_Value", kv1: (*KeyValue)(nil), kv2: &KeyValue{Key: "k", ValueType: "string"}, result: false, }, { name: "Value_vs_TypedNil", kv1: &KeyValue{Key: "k", ValueType: "string"}, kv2: (*KeyValue)(nil), result: false, }, { name: "InvalidType", kv1: &KeyValue{Key: "k", ValueType: "string"}, kv2: 123, result: false, }, { name: "Equal", kv1: &KeyValue{ Key: "k", ValueType: "string", ValueString: "hello", }, kv2: &KeyValue{ Key: "k", ValueType: "string", ValueString: "hello", }, result: true, }, { name: "KeyMismatch", kv1: &KeyValue{Key: "k", ValueType: "string"}, kv2: &KeyValue{Key: "a", ValueType: "string"}, result: false, }, { name: "ValueTypeMismatch", kv1: &KeyValue{Key: "k", ValueType: "z"}, kv2: &KeyValue{Key: "k", ValueType: "a"}, result: false, }, { name: "ValueStringMismatch", kv1: &KeyValue{Key: "k", ValueType: "string", ValueString: "zzz"}, kv2: &KeyValue{Key: "k", ValueType: "string", ValueString: "aaa"}, result: false, }, { name: "ValueBoolMismatch", kv1: &KeyValue{Key: "k", ValueType: "bool", ValueBool: true}, kv2: &KeyValue{Key: "k", ValueType: "bool", ValueBool: false}, result: false, }, { name: "ValueInt64Mismatch", kv1: &KeyValue{Key: "k", ValueType: "int64", ValueInt64: 10}, kv2: &KeyValue{Key: "k", ValueType: "int64", ValueInt64: 5}, result: false, }, { name: "ValueFloat64Mismatch", kv1: &KeyValue{Key: "k", ValueType: "float64", ValueFloat64: 1.5}, kv2: &KeyValue{Key: "k", ValueType: "float64", ValueFloat64: 0.5}, result: false, }, { name: "ValueBinaryMismatch", kv1: &KeyValue{Key: "k", ValueType: "binary", ValueBinary: []byte{1, 2, 3}}, kv2: &KeyValue{Key: "k", ValueType: "binary", ValueBinary: []byte{1, 2, 4}}, result: false, }, { name: "ValueBinaryEqual", kv1: &KeyValue{Key: "k", ValueType: "binary", ValueBinary: []byte{1, 2, 3}}, kv2: &KeyValue{Key: "k", ValueType: "binary", ValueBinary: []byte{1, 2, 3}}, result: true, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { assert.Equal(t, tc.result, tc.kv1.Equal(tc.kv2)) }) } } func TestKeyValueAsString(t *testing.T) { tests := []struct { name string kv KeyValue expect string }{ { name: "StringType", kv: KeyValue{ Key: "k", ValueType: StringType, ValueString: "hello", }, expect: "hello", }, { name: "BoolTrue", kv: KeyValue{ Key: "k", ValueType: BoolType, ValueBool: true, }, expect: "true", }, { name: "BoolFalse", kv: KeyValue{ Key: "k", ValueType: BoolType, ValueBool: false, }, expect: "false", }, { name: "Int64Type", kv: KeyValue{ Key: "k", ValueType: Int64Type, ValueInt64: 12345, }, expect: "12345", }, { name: "Float64Type", kv: KeyValue{ Key: "k", ValueType: Float64Type, ValueFloat64: 12.34, }, expect: "12.34", }, { name: "BinaryType", kv: KeyValue{ Key: "k", ValueType: BinaryType, ValueBinary: []byte{0xAB, 0xCD, 0xEF}, }, expect: "abcdef", }, { name: "UnknownType", kv: KeyValue{ Key: "k", ValueType: "random-type", }, expect: "unknown type random-type", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { assert.Equal(t, tc.expect, tc.kv.AsString()) }) } } func TestSortKVs_WithKey(t *testing.T) { kvs := []KeyValue{ {Key: "z", ValueType: "string", ValueString: "hello"}, {Key: "y", ValueType: "bool", ValueBool: true}, {Key: "x", ValueType: "int64", ValueInt64: 99}, {Key: "w", ValueType: "double", ValueFloat64: 1.23}, {Key: "v", ValueType: "binary", ValueBinary: []byte{1, 2, 3}}, {Key: "m", ValueType: "string", ValueString: "abc"}, } SortKVs(kvs) want := []string{"m", "v", "w", "x", "y", "z"} for i, kv := range kvs { assert.Equal(t, want[i], kv.Key) } } func TestSortKVs_WithType(t *testing.T) { kvs := []KeyValue{ {Key: "m", ValueType: "string", ValueString: "hello"}, {Key: "m", ValueType: "bool", ValueBool: true}, {Key: "m", ValueType: "int64", ValueInt64: 99}, {Key: "m", ValueType: "double", ValueFloat64: 1.23}, {Key: "m", ValueType: "binary", ValueBinary: []byte{1, 2, 3}}, } SortKVs(kvs) want := []string{"binary", "bool", "double", "int64", "string"} for i, kv := range kvs { assert.Equal(t, want[i], kv.ValueType) } } func TestSortKVs_WithValue(t *testing.T) { kvs := []KeyValue{ {Key: "m", ValueType: "string", ValueString: "a"}, {Key: "m", ValueType: "string", ValueString: "b"}, {Key: "m", ValueType: "string", ValueString: "c"}, {Key: "m", ValueType: "string", ValueString: "d"}, {Key: "m", ValueType: "string", ValueString: "e"}, } SortKVs(kvs) want := []string{"a", "b", "c", "d", "e"} for i, kv := range kvs { assert.Equal(t, want[i], kv.ValueString) } } ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/operation.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel // Operation defines schema for records saved in operation_names_v2 table type Operation struct { ServiceName string SpanKind string OperationName string } ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/tag_filter.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package dbmodel // TagFilter filters out any tags that should not be indexed. type TagFilter interface { FilterProcessTags(span *Span, processTags []KeyValue) []KeyValue FilterTags(span *Span, tags []KeyValue) []KeyValue FilterLogFields(span *Span, logFields []KeyValue) []KeyValue } // ChainedTagFilter applies multiple tag filters in serial fashion. type ChainedTagFilter []TagFilter // NewChainedTagFilter creates a TagFilter from the variadic list of passed TagFilter. func NewChainedTagFilter(filters ...TagFilter) ChainedTagFilter { return filters } // FilterProcessTags calls each FilterProcessTags. func (tf ChainedTagFilter) FilterProcessTags(span *Span, processTags []KeyValue) []KeyValue { for _, f := range tf { processTags = f.FilterProcessTags(span, processTags) } return processTags } // FilterTags calls each FilterTags func (tf ChainedTagFilter) FilterTags(span *Span, tags []KeyValue) []KeyValue { for _, f := range tf { tags = f.FilterTags(span, tags) } return tags } // FilterLogFields calls each FilterLogFields func (tf ChainedTagFilter) FilterLogFields(span *Span, logFields []KeyValue) []KeyValue { for _, f := range tf { logFields = f.FilterLogFields(span, logFields) } return logFields } // DefaultTagFilter returns a filter that retrieves all tags from span.Tags, span.Logs, and span.Process. var DefaultTagFilter = tagFilterImpl{} type tagFilterImpl struct{} func (tagFilterImpl) FilterProcessTags(_ *Span, processTags []KeyValue) []KeyValue { return processTags } func (tagFilterImpl) FilterTags(_ *Span, tags []KeyValue) []KeyValue { return tags } func (tagFilterImpl) FilterLogFields(_ *Span, logFields []KeyValue) []KeyValue { return logFields } ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/tag_filter_drop_all.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel // TagFilterDropAll filters all fields of a given type. type TagFilterDropAll struct { dropTags bool dropProcessTags bool dropLogs bool } // NewTagFilterDropAll return a filter that filters all of the specified type func NewTagFilterDropAll(dropTags bool, dropProcessTags bool, dropLogs bool) *TagFilterDropAll { return &TagFilterDropAll{ dropTags: dropTags, dropProcessTags: dropProcessTags, dropLogs: dropLogs, } } // FilterProcessTags implements TagFilter func (f *TagFilterDropAll) FilterProcessTags(_ *Span, processTags []KeyValue) []KeyValue { if f.dropProcessTags { return []KeyValue{} } return processTags } // FilterTags implements TagFilter func (f *TagFilterDropAll) FilterTags(_ *Span, tags []KeyValue) []KeyValue { if f.dropTags { return []KeyValue{} } return tags } // FilterLogFields implements TagFilter func (f *TagFilterDropAll) FilterLogFields(_ *Span, logFields []KeyValue) []KeyValue { if f.dropLogs { return []KeyValue{} } return logFields } ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/tag_filter_drop_all_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "testing" "github.com/stretchr/testify/assert" ) var _ TagFilter = &TagFilterDropAll{} // Check API compliance func TestDropAll(t *testing.T) { tt := []struct { filter *TagFilterDropAll expectedTags []KeyValue expectedProcessTags []KeyValue expectedLogs []KeyValue }{ { filter: NewTagFilterDropAll(false, false, false), expectedTags: someDBTags, expectedProcessTags: someDBTags, expectedLogs: someDBTags, }, { filter: NewTagFilterDropAll(true, false, false), expectedTags: []KeyValue{}, expectedProcessTags: someDBTags, expectedLogs: someDBTags, }, { filter: NewTagFilterDropAll(false, true, false), expectedTags: someDBTags, expectedProcessTags: []KeyValue{}, expectedLogs: someDBTags, }, { filter: NewTagFilterDropAll(false, false, true), expectedTags: someDBTags, expectedProcessTags: someDBTags, expectedLogs: []KeyValue{}, }, { filter: NewTagFilterDropAll(true, false, true), expectedTags: []KeyValue{}, expectedProcessTags: someDBTags, expectedLogs: []KeyValue{}, }, { filter: NewTagFilterDropAll(true, true, true), expectedTags: []KeyValue{}, expectedProcessTags: []KeyValue{}, expectedLogs: []KeyValue{}, }, } for _, test := range tt { actualTags := test.filter.FilterTags(nil, someDBTags) assert.Equal(t, test.expectedTags, actualTags) actualProcessTags := test.filter.FilterProcessTags(nil, someDBTags) assert.Equal(t, test.expectedProcessTags, actualProcessTags) actualLogs := test.filter.FilterLogFields(nil, someDBTags) assert.Equal(t, test.expectedLogs, actualLogs) } } ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/tag_filter_exact_match.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel // ExactMatchTagFilter filters out all tags in its tags slice type ExactMatchTagFilter struct { tags map[string]struct{} dropMatches bool } // newExactMatchTagFilter creates a ExactMatchTagFilter with the provided tags. Passing // dropMatches true will exhibit blacklist behavior. Passing dropMatches false // will exhibit whitelist behavior. func newExactMatchTagFilter(tags []string, dropMatches bool) ExactMatchTagFilter { mapTags := make(map[string]struct{}) for _, t := range tags { mapTags[t] = struct{}{} } return ExactMatchTagFilter{ tags: mapTags, dropMatches: dropMatches, } } // NewBlacklistFilter is a convenience method for creating a blacklist ExactMatchTagFilter func NewBlacklistFilter(tags []string) ExactMatchTagFilter { return newExactMatchTagFilter(tags, true) } // NewWhitelistFilter is a convenience method for creating a whitelist ExactMatchTagFilter func NewWhitelistFilter(tags []string) ExactMatchTagFilter { return newExactMatchTagFilter(tags, false) } // FilterProcessTags implements TagFilter func (tf ExactMatchTagFilter) FilterProcessTags(_ *Span, processTags []KeyValue) []KeyValue { return tf.filter(processTags) } // FilterTags implements TagFilter func (tf ExactMatchTagFilter) FilterTags(_ *Span, tags []KeyValue) []KeyValue { return tf.filter(tags) } // FilterLogFields implements TagFilter func (tf ExactMatchTagFilter) FilterLogFields(_ *Span, logFields []KeyValue) []KeyValue { return tf.filter(logFields) } func (tf ExactMatchTagFilter) filter(tags []KeyValue) []KeyValue { var filteredTags []KeyValue for _, t := range tags { if _, ok := tf.tags[t.Key]; ok == !tf.dropMatches { filteredTags = append(filteredTags, t) } } return filteredTags } ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/tag_filter_exact_match_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "testing" "github.com/stretchr/testify/assert" ) func TestBlacklistFilter(t *testing.T) { tt := []struct { input []string filter []string expected []string }{ { input: []string{"a", "b", "c"}, filter: []string{"a"}, expected: []string{"b", "c"}, }, { input: []string{"a", "b", "c"}, filter: []string{"A"}, expected: []string{"a", "b", "c"}, }, } for _, test := range tt { var inputKVs []KeyValue for _, i := range test.input { inputKVs = append(inputKVs, KeyValue{Key: i, ValueType: StringType, ValueString: ""}) } var expectedKVs []KeyValue for _, e := range test.expected { expectedKVs = append(expectedKVs, KeyValue{Key: e, ValueType: StringType, ValueString: ""}) } SortKVs(expectedKVs) tf := NewBlacklistFilter(test.filter) actualKVs := tf.filter(inputKVs) SortKVs(actualKVs) assert.Equal(t, expectedKVs, actualKVs) actualKVs = tf.FilterLogFields(nil, inputKVs) SortKVs(actualKVs) assert.Equal(t, expectedKVs, actualKVs) actualKVs = tf.FilterProcessTags(nil, inputKVs) SortKVs(actualKVs) assert.Equal(t, expectedKVs, actualKVs) actualKVs = tf.FilterTags(nil, inputKVs) SortKVs(actualKVs) assert.Equal(t, expectedKVs, actualKVs) } } func TestWhitelistFilter(t *testing.T) { tt := []struct { input []string filter []string expected []string }{ { input: []string{"a", "b", "c"}, filter: []string{"a"}, expected: []string{"a"}, }, { input: []string{"a", "b", "c"}, filter: []string{"A"}, expected: []string{}, }, } for _, test := range tt { var inputKVs []KeyValue for _, i := range test.input { inputKVs = append(inputKVs, KeyValue{Key: i, ValueType: StringType, ValueString: ""}) } var expectedKVs []KeyValue for _, e := range test.expected { expectedKVs = append(expectedKVs, KeyValue{Key: e, ValueType: StringType, ValueString: ""}) } SortKVs(expectedKVs) tf := NewWhitelistFilter(test.filter) actualKVs := tf.filter(inputKVs) SortKVs(actualKVs) assert.Equal(t, expectedKVs, actualKVs) actualKVs = tf.FilterLogFields(nil, inputKVs) SortKVs(actualKVs) assert.Equal(t, expectedKVs, actualKVs) actualKVs = tf.FilterProcessTags(nil, inputKVs) SortKVs(actualKVs) assert.Equal(t, expectedKVs, actualKVs) actualKVs = tf.FilterTags(nil, inputKVs) SortKVs(actualKVs) assert.Equal(t, expectedKVs, actualKVs) } } ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/tag_filter_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "testing" "github.com/kr/pretty" "github.com/stretchr/testify/assert" ) func TestDefaultTagFilter(t *testing.T) { span := getTestSpan() expectedTags := append(append(someDBTags, someDBTags...), someDBTags...) filteredTags := DefaultTagFilter.FilterProcessTags(span, span.Process.Tags) filteredTags = append(filteredTags, DefaultTagFilter.FilterTags(span, span.Tags)...) for _, log := range span.Logs { filteredTags = append(filteredTags, DefaultTagFilter.FilterLogFields(span, log.Fields)...) } compareTags(t, expectedTags, filteredTags) } type onlyStringsFilter struct{} func (onlyStringsFilter) filterStringTags(tags []KeyValue) []KeyValue { var ret []KeyValue for _, tag := range tags { if tag.ValueType == StringType { ret = append(ret, tag) } } return ret } func (f onlyStringsFilter) FilterProcessTags(_ *Span, processTags []KeyValue) []KeyValue { return f.filterStringTags(processTags) } func (f onlyStringsFilter) FilterTags(_ *Span, tags []KeyValue) []KeyValue { return f.filterStringTags(tags) } func (f onlyStringsFilter) FilterLogFields(_ *Span, logFields []KeyValue) []KeyValue { return f.filterStringTags(logFields) } func TestChainedTagFilter(t *testing.T) { expectedTags := []KeyValue{{Key: someStringTagKey, ValueType: StringType, ValueString: someStringTagValue}} filter := NewChainedTagFilter(DefaultTagFilter, onlyStringsFilter{}) filteredTags := filter.FilterProcessTags(nil, someDBTags) compareTags(t, expectedTags, filteredTags) filteredTags = filter.FilterTags(nil, someDBTags) compareTags(t, expectedTags, filteredTags) filteredTags = filter.FilterLogFields(nil, someDBTags) compareTags(t, expectedTags, filteredTags) } func compareTags(t *testing.T, expected, actual []KeyValue) { if !assert.Equal(t, expected, actual) { for _, diff := range pretty.Diff(expected, actual) { t.Log(diff) } } } ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/unique_ids.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package dbmodel // UniqueTraceIDs is a set of unique dbmodel TraceIDs, implemented via map. type UniqueTraceIDs map[TraceID]struct{} // UniqueTraceIDsFromList Takes a list of traceIDs and returns the unique set func UniqueTraceIDsFromList(traceIDs []TraceID) UniqueTraceIDs { uniqueTraceIDs := UniqueTraceIDs{} for _, traceID := range traceIDs { uniqueTraceIDs[traceID] = struct{}{} } return uniqueTraceIDs } // Add adds a traceID to the existing map func (u UniqueTraceIDs) Add(traceID TraceID) { u[traceID] = struct{}{} } // IntersectTraceIDs takes a list of UniqueTraceIDs and intersects them. func IntersectTraceIDs(uniqueTraceIdsList []UniqueTraceIDs) UniqueTraceIDs { retMe := UniqueTraceIDs{} for key, value := range uniqueTraceIdsList[0] { keyExistsInAll := true for _, otherTraceIds := range uniqueTraceIdsList[1:] { if _, ok := otherTraceIds[key]; !ok { keyExistsInAll = false break } } if keyExistsInAll { retMe[key] = value } } return retMe } ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/unique_ids_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "testing" "github.com/stretchr/testify/assert" "github.com/jaegertracing/jaeger-idl/model/v1" ) func TestGetIntersectedTraceIDs(t *testing.T) { firstTraceID := TraceIDFromDomain(model.NewTraceID(1, 1)) secondTraceID := TraceIDFromDomain(model.NewTraceID(2, 2)) listOfUniqueTraceIDs := []UniqueTraceIDs{ { firstTraceID: struct{}{}, secondTraceID: struct{}{}, }, { firstTraceID: struct{}{}, secondTraceID: struct{}{}, }, { firstTraceID: struct{}{}, }, } expected := UniqueTraceIDs{ firstTraceID: struct{}{}, } actual := IntersectTraceIDs(listOfUniqueTraceIDs) assert.Equal(t, expected, actual) } func TestAdd(t *testing.T) { u := UniqueTraceIDs{} someID := TraceIDFromDomain(model.NewTraceID(1, 1)) u.Add(someID) assert.Contains(t, u, someID) } func TestFromList(t *testing.T) { someID := TraceIDFromDomain(model.NewTraceID(1, 1)) traceIDList := []TraceID{ someID, } uniqueTraceIDs := UniqueTraceIDsFromList(traceIDList) assert.Len(t, uniqueTraceIDs, 1) assert.Contains(t, uniqueTraceIDs, someID) } ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/unique_tags.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package dbmodel // GetAllUniqueTags creates a list of all unique tags from a set of filtered tags. func GetAllUniqueTags(span *Span, tagFilter TagFilter) []TagInsertion { allTags := append([]KeyValue{}, tagFilter.FilterProcessTags(span, span.Process.Tags)...) allTags = append(allTags, tagFilter.FilterTags(span, span.Tags)...) for _, log := range span.Logs { allTags = append(allTags, tagFilter.FilterLogFields(span, log.Fields)...) } SortKVs(allTags) uniqueTags := make([]TagInsertion, 0, len(allTags)) for i := range allTags { if allTags[i].ValueType == BinaryType { continue // do not index binary tags } if i > 0 && allTags[i-1].Equal(&allTags[i]) { continue // skip identical tags } uniqueTags = append(uniqueTags, TagInsertion{ ServiceName: span.Process.ServiceName, TagKey: allTags[i].Key, TagValue: allTags[i].AsString(), }) } return uniqueTags } ================================================ FILE: internal/storage/v1/cassandra/spanstore/dbmodel/unique_tags_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "testing" "github.com/kr/pretty" "github.com/stretchr/testify/assert" ) func TestGetUniqueTags(t *testing.T) { expectedTags := getTestUniqueTags() uniqueTags := GetAllUniqueTags(getTestSpan(), DefaultTagFilter) if !assert.Equal(t, expectedTags, uniqueTags) { for _, diff := range pretty.Diff(expectedTags, uniqueTags) { t.Log(diff) } } } ================================================ FILE: internal/storage/v1/cassandra/spanstore/matchers_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "strings" "github.com/stretchr/testify/mock" ) func matchOnce() any { return matchOnceWithSideEffect(nil) } func matchOnceWithSideEffect(fn func(v []any)) any { var matched bool return mock.MatchedBy(func(v []any) bool { if matched { return false } matched = true if fn != nil { fn(v) } return true }) } // stringMatcher can match a string argument when it contains a specific substring q func stringMatcher(q string) any { matchFunc := func(s string) bool { return strings.Contains(s, q) } return mock.MatchedBy(matchFunc) } ================================================ FILE: internal/storage/v1/cassandra/spanstore/mocks/mocks.go ================================================ // Copyright (c) The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // // Run 'make generate-mocks' to regenerate. // Code generated by mockery; DO NOT EDIT. // github.com/vektra/mockery // template: testify package mocks import ( "context" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" mock "github.com/stretchr/testify/mock" ) // NewCoreSpanReader creates a new instance of CoreSpanReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewCoreSpanReader(t interface { mock.TestingT Cleanup(func()) }) *CoreSpanReader { mock := &CoreSpanReader{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // CoreSpanReader is an autogenerated mock type for the CoreSpanReader type type CoreSpanReader struct { mock.Mock } type CoreSpanReader_Expecter struct { mock *mock.Mock } func (_m *CoreSpanReader) EXPECT() *CoreSpanReader_Expecter { return &CoreSpanReader_Expecter{mock: &_m.Mock} } // FindTraceIDs provides a mock function for the type CoreSpanReader func (_mock *CoreSpanReader) FindTraceIDs(ctx context.Context, traceQuery *spanstore.TraceQueryParameters) ([]model.TraceID, error) { ret := _mock.Called(ctx, traceQuery) if len(ret) == 0 { panic("no return value specified for FindTraceIDs") } var r0 []model.TraceID var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *spanstore.TraceQueryParameters) ([]model.TraceID, error)); ok { return returnFunc(ctx, traceQuery) } if returnFunc, ok := ret.Get(0).(func(context.Context, *spanstore.TraceQueryParameters) []model.TraceID); ok { r0 = returnFunc(ctx, traceQuery) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]model.TraceID) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *spanstore.TraceQueryParameters) error); ok { r1 = returnFunc(ctx, traceQuery) } else { r1 = ret.Error(1) } return r0, r1 } // CoreSpanReader_FindTraceIDs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindTraceIDs' type CoreSpanReader_FindTraceIDs_Call struct { *mock.Call } // FindTraceIDs is a helper method to define mock.On call // - ctx context.Context // - traceQuery *spanstore.TraceQueryParameters func (_e *CoreSpanReader_Expecter) FindTraceIDs(ctx interface{}, traceQuery interface{}) *CoreSpanReader_FindTraceIDs_Call { return &CoreSpanReader_FindTraceIDs_Call{Call: _e.mock.On("FindTraceIDs", ctx, traceQuery)} } func (_c *CoreSpanReader_FindTraceIDs_Call) Run(run func(ctx context.Context, traceQuery *spanstore.TraceQueryParameters)) *CoreSpanReader_FindTraceIDs_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *spanstore.TraceQueryParameters if args[1] != nil { arg1 = args[1].(*spanstore.TraceQueryParameters) } run( arg0, arg1, ) }) return _c } func (_c *CoreSpanReader_FindTraceIDs_Call) Return(traceIDs []model.TraceID, err error) *CoreSpanReader_FindTraceIDs_Call { _c.Call.Return(traceIDs, err) return _c } func (_c *CoreSpanReader_FindTraceIDs_Call) RunAndReturn(run func(ctx context.Context, traceQuery *spanstore.TraceQueryParameters) ([]model.TraceID, error)) *CoreSpanReader_FindTraceIDs_Call { _c.Call.Return(run) return _c } // FindTraces provides a mock function for the type CoreSpanReader func (_mock *CoreSpanReader) FindTraces(ctx context.Context, traceQuery *spanstore.TraceQueryParameters) ([]*model.Trace, error) { ret := _mock.Called(ctx, traceQuery) if len(ret) == 0 { panic("no return value specified for FindTraces") } var r0 []*model.Trace var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *spanstore.TraceQueryParameters) ([]*model.Trace, error)); ok { return returnFunc(ctx, traceQuery) } if returnFunc, ok := ret.Get(0).(func(context.Context, *spanstore.TraceQueryParameters) []*model.Trace); ok { r0 = returnFunc(ctx, traceQuery) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.Trace) } } if returnFunc, ok := ret.Get(1).(func(context.Context, *spanstore.TraceQueryParameters) error); ok { r1 = returnFunc(ctx, traceQuery) } else { r1 = ret.Error(1) } return r0, r1 } // CoreSpanReader_FindTraces_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindTraces' type CoreSpanReader_FindTraces_Call struct { *mock.Call } // FindTraces is a helper method to define mock.On call // - ctx context.Context // - traceQuery *spanstore.TraceQueryParameters func (_e *CoreSpanReader_Expecter) FindTraces(ctx interface{}, traceQuery interface{}) *CoreSpanReader_FindTraces_Call { return &CoreSpanReader_FindTraces_Call{Call: _e.mock.On("FindTraces", ctx, traceQuery)} } func (_c *CoreSpanReader_FindTraces_Call) Run(run func(ctx context.Context, traceQuery *spanstore.TraceQueryParameters)) *CoreSpanReader_FindTraces_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 *spanstore.TraceQueryParameters if args[1] != nil { arg1 = args[1].(*spanstore.TraceQueryParameters) } run( arg0, arg1, ) }) return _c } func (_c *CoreSpanReader_FindTraces_Call) Return(traces []*model.Trace, err error) *CoreSpanReader_FindTraces_Call { _c.Call.Return(traces, err) return _c } func (_c *CoreSpanReader_FindTraces_Call) RunAndReturn(run func(ctx context.Context, traceQuery *spanstore.TraceQueryParameters) ([]*model.Trace, error)) *CoreSpanReader_FindTraces_Call { _c.Call.Return(run) return _c } // GetOperations provides a mock function for the type CoreSpanReader func (_mock *CoreSpanReader) GetOperations(ctx context.Context, query spanstore.OperationQueryParameters) ([]spanstore.Operation, error) { ret := _mock.Called(ctx, query) if len(ret) == 0 { panic("no return value specified for GetOperations") } var r0 []spanstore.Operation var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, spanstore.OperationQueryParameters) ([]spanstore.Operation, error)); ok { return returnFunc(ctx, query) } if returnFunc, ok := ret.Get(0).(func(context.Context, spanstore.OperationQueryParameters) []spanstore.Operation); ok { r0 = returnFunc(ctx, query) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]spanstore.Operation) } } if returnFunc, ok := ret.Get(1).(func(context.Context, spanstore.OperationQueryParameters) error); ok { r1 = returnFunc(ctx, query) } else { r1 = ret.Error(1) } return r0, r1 } // CoreSpanReader_GetOperations_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetOperations' type CoreSpanReader_GetOperations_Call struct { *mock.Call } // GetOperations is a helper method to define mock.On call // - ctx context.Context // - query spanstore.OperationQueryParameters func (_e *CoreSpanReader_Expecter) GetOperations(ctx interface{}, query interface{}) *CoreSpanReader_GetOperations_Call { return &CoreSpanReader_GetOperations_Call{Call: _e.mock.On("GetOperations", ctx, query)} } func (_c *CoreSpanReader_GetOperations_Call) Run(run func(ctx context.Context, query spanstore.OperationQueryParameters)) *CoreSpanReader_GetOperations_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 spanstore.OperationQueryParameters if args[1] != nil { arg1 = args[1].(spanstore.OperationQueryParameters) } run( arg0, arg1, ) }) return _c } func (_c *CoreSpanReader_GetOperations_Call) Return(operations []spanstore.Operation, err error) *CoreSpanReader_GetOperations_Call { _c.Call.Return(operations, err) return _c } func (_c *CoreSpanReader_GetOperations_Call) RunAndReturn(run func(ctx context.Context, query spanstore.OperationQueryParameters) ([]spanstore.Operation, error)) *CoreSpanReader_GetOperations_Call { _c.Call.Return(run) return _c } // GetOperationsV2 provides a mock function for the type CoreSpanReader func (_mock *CoreSpanReader) GetOperationsV2(ctx context.Context, query tracestore.OperationQueryParams) ([]tracestore.Operation, error) { ret := _mock.Called(ctx, query) if len(ret) == 0 { panic("no return value specified for GetOperationsV2") } var r0 []tracestore.Operation var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, tracestore.OperationQueryParams) ([]tracestore.Operation, error)); ok { return returnFunc(ctx, query) } if returnFunc, ok := ret.Get(0).(func(context.Context, tracestore.OperationQueryParams) []tracestore.Operation); ok { r0 = returnFunc(ctx, query) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]tracestore.Operation) } } if returnFunc, ok := ret.Get(1).(func(context.Context, tracestore.OperationQueryParams) error); ok { r1 = returnFunc(ctx, query) } else { r1 = ret.Error(1) } return r0, r1 } // CoreSpanReader_GetOperationsV2_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetOperationsV2' type CoreSpanReader_GetOperationsV2_Call struct { *mock.Call } // GetOperationsV2 is a helper method to define mock.On call // - ctx context.Context // - query tracestore.OperationQueryParams func (_e *CoreSpanReader_Expecter) GetOperationsV2(ctx interface{}, query interface{}) *CoreSpanReader_GetOperationsV2_Call { return &CoreSpanReader_GetOperationsV2_Call{Call: _e.mock.On("GetOperationsV2", ctx, query)} } func (_c *CoreSpanReader_GetOperationsV2_Call) Run(run func(ctx context.Context, query tracestore.OperationQueryParams)) *CoreSpanReader_GetOperationsV2_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 tracestore.OperationQueryParams if args[1] != nil { arg1 = args[1].(tracestore.OperationQueryParams) } run( arg0, arg1, ) }) return _c } func (_c *CoreSpanReader_GetOperationsV2_Call) Return(operations []tracestore.Operation, err error) *CoreSpanReader_GetOperationsV2_Call { _c.Call.Return(operations, err) return _c } func (_c *CoreSpanReader_GetOperationsV2_Call) RunAndReturn(run func(ctx context.Context, query tracestore.OperationQueryParams) ([]tracestore.Operation, error)) *CoreSpanReader_GetOperationsV2_Call { _c.Call.Return(run) return _c } // GetServices provides a mock function for the type CoreSpanReader func (_mock *CoreSpanReader) GetServices(ctx context.Context) ([]string, error) { ret := _mock.Called(ctx) if len(ret) == 0 { panic("no return value specified for GetServices") } var r0 []string var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context) ([]string, error)); ok { return returnFunc(ctx) } if returnFunc, ok := ret.Get(0).(func(context.Context) []string); ok { r0 = returnFunc(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]string) } } if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { r1 = returnFunc(ctx) } else { r1 = ret.Error(1) } return r0, r1 } // CoreSpanReader_GetServices_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetServices' type CoreSpanReader_GetServices_Call struct { *mock.Call } // GetServices is a helper method to define mock.On call // - ctx context.Context func (_e *CoreSpanReader_Expecter) GetServices(ctx interface{}) *CoreSpanReader_GetServices_Call { return &CoreSpanReader_GetServices_Call{Call: _e.mock.On("GetServices", ctx)} } func (_c *CoreSpanReader_GetServices_Call) Run(run func(ctx context.Context)) *CoreSpanReader_GetServices_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } run( arg0, ) }) return _c } func (_c *CoreSpanReader_GetServices_Call) Return(strings []string, err error) *CoreSpanReader_GetServices_Call { _c.Call.Return(strings, err) return _c } func (_c *CoreSpanReader_GetServices_Call) RunAndReturn(run func(ctx context.Context) ([]string, error)) *CoreSpanReader_GetServices_Call { _c.Call.Return(run) return _c } // GetTrace provides a mock function for the type CoreSpanReader func (_mock *CoreSpanReader) GetTrace(ctx context.Context, query spanstore.GetTraceParameters) (*model.Trace, error) { ret := _mock.Called(ctx, query) if len(ret) == 0 { panic("no return value specified for GetTrace") } var r0 *model.Trace var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, spanstore.GetTraceParameters) (*model.Trace, error)); ok { return returnFunc(ctx, query) } if returnFunc, ok := ret.Get(0).(func(context.Context, spanstore.GetTraceParameters) *model.Trace); ok { r0 = returnFunc(ctx, query) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Trace) } } if returnFunc, ok := ret.Get(1).(func(context.Context, spanstore.GetTraceParameters) error); ok { r1 = returnFunc(ctx, query) } else { r1 = ret.Error(1) } return r0, r1 } // CoreSpanReader_GetTrace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTrace' type CoreSpanReader_GetTrace_Call struct { *mock.Call } // GetTrace is a helper method to define mock.On call // - ctx context.Context // - query spanstore.GetTraceParameters func (_e *CoreSpanReader_Expecter) GetTrace(ctx interface{}, query interface{}) *CoreSpanReader_GetTrace_Call { return &CoreSpanReader_GetTrace_Call{Call: _e.mock.On("GetTrace", ctx, query)} } func (_c *CoreSpanReader_GetTrace_Call) Run(run func(ctx context.Context, query spanstore.GetTraceParameters)) *CoreSpanReader_GetTrace_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 spanstore.GetTraceParameters if args[1] != nil { arg1 = args[1].(spanstore.GetTraceParameters) } run( arg0, arg1, ) }) return _c } func (_c *CoreSpanReader_GetTrace_Call) Return(trace *model.Trace, err error) *CoreSpanReader_GetTrace_Call { _c.Call.Return(trace, err) return _c } func (_c *CoreSpanReader_GetTrace_Call) RunAndReturn(run func(ctx context.Context, query spanstore.GetTraceParameters) (*model.Trace, error)) *CoreSpanReader_GetTrace_Call { _c.Call.Return(run) return _c } ================================================ FILE: internal/storage/v1/cassandra/spanstore/operation_names.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "fmt" "time" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/cache" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/storage/cassandra" casmetrics "github.com/jaegertracing/jaeger/internal/storage/cassandra/metrics" "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/spanstore/dbmodel" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" ) const ( // latestVersion of operation_names table // increase the version if your table schema changes require code change latestVersion = schemaVersion("v2") // previous version of operation_names table // if latest version does not work, will fail back to use previous version previousVersion = schemaVersion("v1") // tableCheckStmt the query statement used to check if a table exists or not tableCheckStmt = "SELECT * from %s limit 1" ) type schemaVersion string type tableMeta struct { tableName string insertStmt string queryByKindStmt string queryStmt string createWriteQuery func(query cassandra.Query, service, kind, opName string) cassandra.Query getOperations func( s *OperationNamesStorage, query tracestore.OperationQueryParams, ) ([]tracestore.Operation, error) } func (t *tableMeta) materialize() { t.insertStmt = fmt.Sprintf(t.insertStmt, t.tableName) t.queryByKindStmt = fmt.Sprintf(t.queryByKindStmt, t.tableName) t.queryStmt = fmt.Sprintf(t.queryStmt, t.tableName) } var schemas = map[schemaVersion]tableMeta{ previousVersion: { tableName: "operation_names", insertStmt: "INSERT INTO %s(service_name, operation_name) VALUES (?, ?)", queryByKindStmt: "SELECT operation_name FROM %s WHERE service_name = ?", queryStmt: "SELECT operation_name FROM %s WHERE service_name = ?", getOperations: getOperationsV1, createWriteQuery: func(query cassandra.Query, service, _ /* kind*/, opName string) cassandra.Query { return query.Bind(service, opName) }, }, latestVersion: { tableName: "operation_names_v2", insertStmt: "INSERT INTO %s(service_name, span_kind, operation_name) VALUES (?, ?, ?)", queryByKindStmt: "SELECT span_kind, operation_name FROM %s WHERE service_name = ? AND span_kind = ?", queryStmt: "SELECT span_kind, operation_name FROM %s WHERE service_name = ?", getOperations: getOperationsV2, createWriteQuery: func(query cassandra.Query, service, kind, opName string) cassandra.Query { return query.Bind(service, kind, opName) }, }, } // OperationNamesStorage stores known operation names by service. type OperationNamesStorage struct { // CQL statements are public so that Cassandra2 storage can override them schemaVersion schemaVersion table tableMeta session cassandra.Session writeCacheTTL time.Duration metrics *casmetrics.Table operationNames cache.Cache logger *zap.Logger } // NewOperationNamesStorage returns a new OperationNamesStorage func NewOperationNamesStorage( session cassandra.Session, writeCacheTTL time.Duration, metricsFactory metrics.Factory, logger *zap.Logger, ) (*OperationNamesStorage, error) { schemaVersion := latestVersion if !tableExist(session, schemas[schemaVersion].tableName) { if !tableExist(session, schemas[previousVersion].tableName) { return nil, fmt.Errorf("neither table %s nor %s exist", schemas[schemaVersion].tableName, schemas[previousVersion].tableName) } schemaVersion = previousVersion } table := schemas[schemaVersion] table.materialize() return &OperationNamesStorage{ session: session, schemaVersion: schemaVersion, table: table, metrics: casmetrics.NewTable(metricsFactory, schemas[schemaVersion].tableName), writeCacheTTL: writeCacheTTL, logger: logger, operationNames: cache.NewLRUWithOptions( 100000, &cache.Options{ TTL: writeCacheTTL, InitialCapacity: 10000, }), }, nil } // Write saves Operation and Service name tuples func (s *OperationNamesStorage) Write(operation dbmodel.Operation) error { key := fmt.Sprintf("%s|%s|%s", operation.ServiceName, operation.SpanKind, operation.OperationName, ) if inCache := checkWriteCache(key, s.operationNames, s.writeCacheTTL); !inCache { q := s.table.createWriteQuery( s.session.Query(s.table.insertStmt), operation.ServiceName, operation.SpanKind, operation.OperationName, ) err := s.metrics.Exec(q, s.logger) if err != nil { return err } } return nil } // GetOperations returns all operations for a specific service traced by Jaeger func (s *OperationNamesStorage) GetOperations( query tracestore.OperationQueryParams, ) ([]tracestore.Operation, error) { return s.table.getOperations(s, query) } func tableExist(session cassandra.Session, tableName string) bool { query := session.Query(fmt.Sprintf(tableCheckStmt, tableName)) err := query.Exec() return err == nil } func getOperationsV1( s *OperationNamesStorage, query tracestore.OperationQueryParams, ) ([]tracestore.Operation, error) { iter := s.session.Query(s.table.queryStmt, query.ServiceName).Iter() var operation string var operations []tracestore.Operation for iter.Scan(&operation) { operations = append(operations, tracestore.Operation{ Name: operation, }) } if err := iter.Close(); err != nil { err = fmt.Errorf("error reading operation_names from storage: %w", err) return nil, err } return operations, nil } func getOperationsV2( s *OperationNamesStorage, query tracestore.OperationQueryParams, ) ([]tracestore.Operation, error) { var casQuery cassandra.Query if query.SpanKind == "" { // Get operations for all spanKind casQuery = s.session.Query(s.table.queryStmt, query.ServiceName) } else { // Get operations for given spanKind casQuery = s.session.Query(s.table.queryByKindStmt, query.ServiceName, query.SpanKind) } iter := casQuery.Iter() var operationName string var spanKind string var operations []tracestore.Operation for iter.Scan(&spanKind, &operationName) { operations = append(operations, tracestore.Operation{ Name: operationName, SpanKind: spanKind, }) } if err := iter.Close(); err != nil { err = fmt.Errorf("error reading %s from storage: %w", s.table.tableName, err) return nil, err } return operations, nil } ================================================ FILE: internal/storage/v1/cassandra/spanstore/operation_names_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "errors" "fmt" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/metricstest" "github.com/jaegertracing/jaeger/internal/storage/cassandra/mocks" "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/spanstore/dbmodel" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" "github.com/jaegertracing/jaeger/internal/testutils" ) type operationNameStorageTest struct { session *mocks.Session writeCacheTTL time.Duration metricsFactory *metricstest.Factory logger *zap.Logger logBuffer *testutils.Buffer storage *OperationNamesStorage } func withOperationNamesStorage(t *testing.T, writeCacheTTL time.Duration, schemaVersion schemaVersion, fn func(s *operationNameStorageTest), ) { session := &mocks.Session{} logger, logBuffer := testutils.NewLogger() metricsFactory := metricstest.NewFactory(0) latestTableCheckquery := &mocks.Query{} session.On("Query", fmt.Sprintf(tableCheckStmt, schemas[latestVersion].tableName), mock.Anything).Return(latestTableCheckquery) if schemaVersion == latestVersion { latestTableCheckquery.On("Exec").Return(nil) } else { previousTableCheckquery := &mocks.Query{} session.On("Query", fmt.Sprintf(tableCheckStmt, schemas[previousVersion].tableName), mock.Anything).Return(previousTableCheckquery) latestTableCheckquery.On("Exec").Return(errors.New("table not found")) previousTableCheckquery.On("Exec").Return(nil) } storage, err := NewOperationNamesStorage(session, writeCacheTTL, metricsFactory, logger) require.NoError(t, err) s := &operationNameStorageTest{ session: session, writeCacheTTL: writeCacheTTL, metricsFactory: metricsFactory, logger: logger, logBuffer: logBuffer, storage: storage, } fn(s) } func TestNewOperationNamesStorage(t *testing.T) { t.Run("test operation names storage creation with old schema", func(t *testing.T) { withOperationNamesStorage(t, 0, previousVersion, func(s *operationNameStorageTest) { assert.NotNil(t, s.storage) }) }) t.Run("test operation names storage creation with new schema", func(t *testing.T) { withOperationNamesStorage(t, 0, latestVersion, func(s *operationNameStorageTest) { assert.NotNil(t, s.storage) }) }) t.Run("test operation names storage creation error", func(t *testing.T) { session := &mocks.Session{} logger, _ := testutils.NewLogger() metricsFactory := metricstest.NewFactory(0) query := &mocks.Query{} session.On("Query", fmt.Sprintf(tableCheckStmt, schemas[latestVersion].tableName), mock.Anything).Return(query) session.On("Query", fmt.Sprintf(tableCheckStmt, schemas[previousVersion].tableName), mock.Anything).Return(query) query.On("Exec").Return(errors.New("table does not exist")) _, err := NewOperationNamesStorage(session, 0, metricsFactory, logger) require.EqualError(t, err, "neither table operation_names_v2 nor operation_names exist") }) } func TestOperationNamesStorageWrite(t *testing.T) { for _, test := range []struct { name string ttl time.Duration schemaVersion schemaVersion }{ {name: "test old schema with 0 ttl", ttl: 0, schemaVersion: previousVersion}, {name: "test old schema with 1min ttl", ttl: time.Minute, schemaVersion: previousVersion}, {name: "test new schema with 0 ttl", ttl: 0, schemaVersion: latestVersion}, {name: "test new schema with 1min ttl", ttl: time.Minute, schemaVersion: latestVersion}, } { t.Run(test.name, func(t *testing.T) { withOperationNamesStorage(t, test.ttl, test.schemaVersion, func(s *operationNameStorageTest) { execError := errors.New("exec error") query := &mocks.Query{} query1 := &mocks.Query{} query2 := &mocks.Query{} if test.schemaVersion == previousVersion { query.On("Bind", []any{"service-a", "Operation-b"}).Return(query1) query.On("Bind", []any{"service-c", "operation-d"}).Return(query2) } else { query.On("Bind", []any{"service-a", "", "Operation-b"}).Return(query1) query.On("Bind", []any{"service-c", "", "operation-d"}).Return(query2) } query1.On("Exec").Return(nil) query2.On("Exec").Return(execError) query2.On("String").Return("select from " + schemas[test.schemaVersion].tableName) s.session.On("Query", mock.AnythingOfType("string"), mock.Anything).Return(query) err := s.storage.Write(dbmodel.Operation{ ServiceName: "service-a", OperationName: "Operation-b", }) require.NoError(t, err) err = s.storage.Write(dbmodel.Operation{ ServiceName: "service-c", OperationName: "operation-d", }) require.EqualError(t, err, "failed to Exec query 'select from "+ schemas[test.schemaVersion].tableName+ "': exec error") assert.Equal(t, map[string]string{ "level": "error", "msg": "Failed to exec query", "query": "select from " + schemas[test.schemaVersion].tableName, "error": "exec error", }, s.logBuffer.JSONLine(0)) counts, _ := s.metricsFactory.Snapshot() assert.Equal(t, map[string]int64{ "attempts|table=" + schemas[test.schemaVersion].tableName: 2, "inserts|table=" + schemas[test.schemaVersion].tableName: 1, "errors|table=" + schemas[test.schemaVersion].tableName: 1, }, counts, "after first two writes") // write again err = s.storage.Write(dbmodel.Operation{ ServiceName: "service-a", OperationName: "Operation-b", }) require.NoError(t, err) counts2, _ := s.metricsFactory.Snapshot() expCounts := counts if test.ttl == 0 { // without write cache, the second write must succeed expCounts["attempts|table="+schemas[test.schemaVersion].tableName]++ expCounts["inserts|table="+schemas[test.schemaVersion].tableName]++ } assert.Equal(t, expCounts, counts2) }) }) } } func TestOperationNamesStorageGetServices(t *testing.T) { scanError := errors.New("scan error") for _, test := range []struct { name string schemaVersion schemaVersion expErr error expRes []tracestore.Operation }{ { name: "test old schema without error", schemaVersion: previousVersion, expRes: []tracestore.Operation{{Name: "foo"}}, }, { name: "test new schema without error", schemaVersion: latestVersion, expRes: []tracestore.Operation{{SpanKind: "foo", Name: "bar"}}, }, {name: "test old schema with scan error", schemaVersion: previousVersion, expErr: scanError}, {name: "test new schema with scan error", schemaVersion: latestVersion, expErr: scanError}, } { t.Run(test.name, func(t *testing.T) { withOperationNamesStorage(t, 0, test.schemaVersion, func(s *operationNameStorageTest) { assignPtr := func(vals ...string) any { return mock.MatchedBy(func(args []any) bool { if len(args) != len(vals) { return false } for i, arg := range args { ptr, ok := arg.(*string) if !ok { return false } *ptr = vals[i] } return true }) } iter := &mocks.Iterator{} if test.schemaVersion == previousVersion { iter.On("Scan", assignPtr("foo")).Return(true).Once() } else { iter.On("Scan", assignPtr("foo", "bar")).Return(true).Once() } iter.On("Scan", mock.Anything).Return(false) // false to stop the loop iter.On("Close").Return(test.expErr) query := &mocks.Query{} query.On("Iter").Return(iter) s.session.On("Query", mock.AnythingOfType("string"), mock.Anything).Return(query) services, err := s.storage.GetOperations(tracestore.OperationQueryParams{ ServiceName: "service-a", }) if test.expErr == nil { require.NoError(t, err) assert.Equal(t, test.expRes, services) } else { require.EqualError( t, err, fmt.Sprintf("error reading %s from storage: %s", schemas[test.schemaVersion].tableName, test.expErr.Error()), ) } }) }) } } ================================================ FILE: internal/storage/v1/cassandra/spanstore/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/cassandra/spanstore/reader.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "context" "errors" "fmt" "time" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/storage/cassandra" casmetrics "github.com/jaegertracing/jaeger/internal/storage/cassandra/metrics" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/spanstore/dbmodel" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) const ( bucketRange = `(0,1,2,3,4,5,6,7,8,9)` querySpanByTraceID = ` SELECT trace_id, span_id, parent_id, operation_name, flags, start_time, duration, tags, logs, refs, process FROM traces WHERE trace_id = ?` queryByTag = ` SELECT trace_id FROM tag_index WHERE service_name = ? AND tag_key = ? AND tag_value = ? and start_time > ? and start_time < ? ORDER BY start_time DESC LIMIT ?` queryByServiceName = ` SELECT trace_id FROM service_name_index WHERE bucket IN ` + bucketRange + ` AND service_name = ? AND start_time > ? AND start_time < ? ORDER BY start_time DESC LIMIT ?` queryByServiceAndOperationName = ` SELECT trace_id FROM service_operation_index WHERE service_name = ? AND operation_name = ? AND start_time > ? AND start_time < ? ORDER BY start_time DESC LIMIT ?` queryByDuration = ` SELECT trace_id FROM duration_index WHERE bucket = ? AND service_name = ? AND operation_name = ? AND duration > ? AND duration < ? LIMIT ?` defaultNumTraces = 100 // limitMultiple exists because many spans that are returned from indices can have the same trace, limitMultiple increases // the number of responses from the index, so we can respect the user's limit value they provided. limitMultiple = 3 ) var ( // ErrServiceNameNotSet occurs when attempting to query with an empty service name ErrServiceNameNotSet = errors.New("service Name must be set") // ErrStartTimeMinGreaterThanMax occurs when start time min is above start time max ErrStartTimeMinGreaterThanMax = errors.New("start Time Minimum is above Maximum") // ErrDurationMinGreaterThanMax occurs when duration min is above duration max ErrDurationMinGreaterThanMax = errors.New("duration Minimum is above Maximum") // ErrMalformedRequestObject occurs when a request object is nil ErrMalformedRequestObject = errors.New("malformed request object") // ErrDurationAndTagQueryNotSupported occurs when duration and tags are both set ErrDurationAndTagQueryNotSupported = errors.New("cannot query for duration and tags simultaneously") // ErrStartAndEndTimeNotSet occurs when start time and end time are not set ErrStartAndEndTimeNotSet = errors.New("start and End Time must be set") ) type serviceNamesReader func() ([]string, error) type operationNamesReader func(query tracestore.OperationQueryParams) ([]tracestore.Operation, error) type spanReaderMetrics struct { readTraces *casmetrics.Table queryTrace *casmetrics.Table queryTagIndex *casmetrics.Table queryDurationIndex *casmetrics.Table queryServiceOperationIndex *casmetrics.Table queryServiceNameIndex *casmetrics.Table } type CoreSpanReader interface { GetServices(ctx context.Context) ([]string, error) GetOperations(ctx context.Context, query spanstore.OperationQueryParameters) ([]spanstore.Operation, error) GetOperationsV2(ctx context.Context, query tracestore.OperationQueryParams) ([]tracestore.Operation, error) GetTrace(ctx context.Context, query spanstore.GetTraceParameters) (*model.Trace, error) FindTraces(ctx context.Context, traceQuery *spanstore.TraceQueryParameters) ([]*model.Trace, error) FindTraceIDs(ctx context.Context, traceQuery *spanstore.TraceQueryParameters) ([]model.TraceID, error) } // SpanReader can query for and load traces from Cassandra. type SpanReader struct { session cassandra.Session serviceNamesReader serviceNamesReader operationNamesReader operationNamesReader metrics spanReaderMetrics logger *zap.Logger tracer trace.Tracer } // NewSpanReader returns a new SpanReader. func NewSpanReader( session cassandra.Session, metricsFactory metrics.Factory, logger *zap.Logger, tracer trace.Tracer, ) (*SpanReader, error) { readFactory := metricsFactory.Namespace(metrics.NSOptions{Name: "read", Tags: nil}) serviceNamesStorage := NewServiceNamesStorage(session, 0, metricsFactory, logger) operationNamesStorage, err := NewOperationNamesStorage(session, 0, metricsFactory, logger) if err != nil { return nil, err } return &SpanReader{ session: session, serviceNamesReader: serviceNamesStorage.GetServices, operationNamesReader: operationNamesStorage.GetOperations, metrics: spanReaderMetrics{ readTraces: casmetrics.NewTable(readFactory, "read_traces"), queryTrace: casmetrics.NewTable(readFactory, "query_traces"), queryTagIndex: casmetrics.NewTable(readFactory, "tag_index"), queryDurationIndex: casmetrics.NewTable(readFactory, "duration_index"), queryServiceOperationIndex: casmetrics.NewTable(readFactory, "service_operation_index"), queryServiceNameIndex: casmetrics.NewTable(readFactory, "service_name_index"), }, logger: logger, tracer: tracer, }, nil } // GetServices returns all services traced by Jaeger func (s *SpanReader) GetServices(context.Context) ([]string, error) { return s.serviceNamesReader() } // GetOperations returns all operations for a specific service traced by Jaeger func (*SpanReader) GetOperations( _ context.Context, _ spanstore.OperationQueryParameters, ) ([]spanstore.Operation, error) { return nil, errors.New("not implemented") } func (s *SpanReader) GetOperationsV2( _ context.Context, query tracestore.OperationQueryParams, ) ([]tracestore.Operation, error) { return s.operationNamesReader(query) } func (s *SpanReader) readTrace(ctx context.Context, traceID dbmodel.TraceID) (*model.Trace, error) { ctx, span := s.startSpanForQuery(ctx, "readTrace", querySpanByTraceID) defer span.End() span.SetAttributes(attribute.Key("trace_id").String(traceID.String())) trc, err := s.readTraceInSpan(ctx, traceID) logErrorToSpan(span, err) return trc, err } func (s *SpanReader) readTraceInSpan(_ context.Context, traceID dbmodel.TraceID) (*model.Trace, error) { start := time.Now() q := s.session.Query(querySpanByTraceID, traceID) i := q.Iter() var traceIDFromSpan dbmodel.TraceID var startTime, spanID, duration, parentID int64 var flags int32 var operationName string var dbProcess dbmodel.Process var refs []dbmodel.SpanRef var tags []dbmodel.KeyValue var logs []dbmodel.Log retMe := &model.Trace{} for i.Scan(&traceIDFromSpan, &spanID, &parentID, &operationName, &flags, &startTime, &duration, &tags, &logs, &refs, &dbProcess) { dbSpan := dbmodel.Span{ TraceID: traceIDFromSpan, SpanID: spanID, ParentID: parentID, OperationName: operationName, Flags: flags, StartTime: startTime, Duration: duration, Tags: tags, Logs: logs, Refs: refs, Process: dbProcess, ServiceName: dbProcess.ServiceName, } span, err := dbmodel.ToDomain(&dbSpan) if err != nil { s.metrics.readTraces.Emit(err, time.Since(start)) return nil, err } retMe.Spans = append(retMe.Spans, span) } err := i.Close() s.metrics.readTraces.Emit(err, time.Since(start)) if err != nil { return nil, fmt.Errorf("error reading traces from storage: %w", err) } if len(retMe.Spans) == 0 { return nil, spanstore.ErrTraceNotFound } return retMe, nil } // GetTrace takes a traceID and returns a Trace associated with that traceID func (s *SpanReader) GetTrace(ctx context.Context, query spanstore.GetTraceParameters) (*model.Trace, error) { return s.readTrace(ctx, dbmodel.TraceIDFromDomain(query.TraceID)) } func validateQuery(p *spanstore.TraceQueryParameters) error { if p == nil { return ErrMalformedRequestObject } if p.ServiceName == "" && len(p.Tags) > 0 { return ErrServiceNameNotSet } if p.StartTimeMin.IsZero() || p.StartTimeMax.IsZero() { return ErrStartAndEndTimeNotSet } if !p.StartTimeMin.IsZero() && !p.StartTimeMax.IsZero() && p.StartTimeMax.Before(p.StartTimeMin) { return ErrStartTimeMinGreaterThanMax } if p.DurationMin != 0 && p.DurationMax != 0 && p.DurationMin > p.DurationMax { return ErrDurationMinGreaterThanMax } if (p.DurationMin != 0 || p.DurationMax != 0) && len(p.Tags) > 0 { return ErrDurationAndTagQueryNotSupported } return nil } // FindTraces retrieves traces that match the traceQuery func (s *SpanReader) FindTraces(ctx context.Context, traceQuery *spanstore.TraceQueryParameters) ([]*model.Trace, error) { uniqueTraceIDs, err := s.FindTraceIDs(ctx, traceQuery) if err != nil { return nil, err } var retMe []*model.Trace for _, traceID := range uniqueTraceIDs { jTrace, err := s.GetTrace(ctx, spanstore.GetTraceParameters{TraceID: traceID}) if err != nil { s.logger.Error("Failure to read trace", zap.String("trace_id", traceID.String()), zap.Error(err)) continue } retMe = append(retMe, jTrace) } return retMe, nil } // FindTraceIDs retrieve traceIDs that match the traceQuery func (s *SpanReader) FindTraceIDs(ctx context.Context, traceQuery *spanstore.TraceQueryParameters) ([]model.TraceID, error) { if err := validateQuery(traceQuery); err != nil { return nil, err } if traceQuery.NumTraces == 0 { traceQuery.NumTraces = defaultNumTraces } dbTraceIDs, err := s.findTraceIDsFromQuery(ctx, traceQuery) if err != nil { return nil, err } var traceIDs []model.TraceID for t := range dbTraceIDs { if len(traceIDs) >= traceQuery.NumTraces { break } traceIDs = append(traceIDs, t.ToDomain()) } return traceIDs, nil } func (s *SpanReader) findTraceIDsFromQuery(ctx context.Context, traceQuery *spanstore.TraceQueryParameters) (dbmodel.UniqueTraceIDs, error) { // See docs/adr/cassandra-find-traces-duration.md for rationale: duration queries use the duration_index // and are handled as a separate path. Other query parameters (like tags) are ignored when duration is specified. if traceQuery.DurationMin != 0 || traceQuery.DurationMax != 0 { return s.queryByDuration(ctx, traceQuery) } if traceQuery.OperationName != "" { traceIds, err := s.queryByServiceNameAndOperation(ctx, traceQuery) if err != nil { return nil, err } if len(traceQuery.Tags) > 0 { tagTraceIds, err := s.queryByTagsAndLogs(ctx, traceQuery) if err != nil { return nil, err } return dbmodel.IntersectTraceIDs([]dbmodel.UniqueTraceIDs{ traceIds, tagTraceIds, }), nil } return traceIds, nil } if len(traceQuery.Tags) > 0 { return s.queryByTagsAndLogs(ctx, traceQuery) } return s.queryByService(ctx, traceQuery) } func (s *SpanReader) queryByTagsAndLogs(ctx context.Context, tq *spanstore.TraceQueryParameters) (dbmodel.UniqueTraceIDs, error) { ctx, span := s.startSpanForQuery(ctx, "queryByTagsAndLogs", queryByTag) defer span.End() results := make([]dbmodel.UniqueTraceIDs, 0, len(tq.Tags)) for k, v := range tq.Tags { _, childSpan := s.tracer.Start(ctx, "queryByTag") childSpan.SetAttributes( attribute.Key("tag.key").String(k), attribute.Key("tag.value").String(v), ) query := s.session.Query( queryByTag, tq.ServiceName, k, v, model.TimeAsEpochMicroseconds(tq.StartTimeMin), model.TimeAsEpochMicroseconds(tq.StartTimeMax), tq.NumTraces*limitMultiple, ).PageSize(0) t, err := s.executeQuery(childSpan, query, s.metrics.queryTagIndex) childSpan.End() if err != nil { return nil, err } results = append(results, t) } return dbmodel.IntersectTraceIDs(results), nil } func (s *SpanReader) queryByDuration(ctx context.Context, traceQuery *spanstore.TraceQueryParameters) (dbmodel.UniqueTraceIDs, error) { ctx, span := s.startSpanForQuery(ctx, "queryByDuration", queryByDuration) defer span.End() results := dbmodel.UniqueTraceIDs{} minDurationMicros := traceQuery.DurationMin.Nanoseconds() / int64(time.Microsecond/time.Nanosecond) maxDurationMicros := (time.Hour * 24).Nanoseconds() / int64(time.Microsecond/time.Nanosecond) if traceQuery.DurationMax != 0 { maxDurationMicros = traceQuery.DurationMax.Nanoseconds() / int64(time.Microsecond/time.Nanosecond) } // See writer.go:indexByDuration for how this is indexed // This is indexed in hours since epoch startTimeByHour := traceQuery.StartTimeMin.Round(durationBucketSize) endTimeByHour := traceQuery.StartTimeMax.Round(durationBucketSize) for timeBucket := endTimeByHour; timeBucket.After(startTimeByHour) || timeBucket.Equal(startTimeByHour); timeBucket = timeBucket.Add(-1 * durationBucketSize) { _, childSpan := s.tracer.Start(ctx, "queryForTimeBucket") childSpan.SetAttributes(attribute.Key("timeBucket").String(timeBucket.String())) query := s.session.Query( queryByDuration, timeBucket, traceQuery.ServiceName, traceQuery.OperationName, minDurationMicros, maxDurationMicros, traceQuery.NumTraces*limitMultiple) t, err := s.executeQuery(childSpan, query, s.metrics.queryDurationIndex) childSpan.End() if err != nil { return nil, err } for traceID := range t { results.Add(traceID) if len(results) == traceQuery.NumTraces { break } } } return results, nil } func (s *SpanReader) queryByServiceNameAndOperation(ctx context.Context, tq *spanstore.TraceQueryParameters) (dbmodel.UniqueTraceIDs, error) { _, span := s.startSpanForQuery(ctx, "queryByServiceNameAndOperation", queryByServiceAndOperationName) defer span.End() query := s.session.Query( queryByServiceAndOperationName, tq.ServiceName, tq.OperationName, model.TimeAsEpochMicroseconds(tq.StartTimeMin), model.TimeAsEpochMicroseconds(tq.StartTimeMax), tq.NumTraces*limitMultiple, ).PageSize(0) return s.executeQuery(span, query, s.metrics.queryServiceOperationIndex) } func (s *SpanReader) queryByService(ctx context.Context, tq *spanstore.TraceQueryParameters) (dbmodel.UniqueTraceIDs, error) { _, span := s.startSpanForQuery(ctx, "queryByService", queryByServiceAndOperationName) defer span.End() query := s.session.Query( queryByServiceName, tq.ServiceName, model.TimeAsEpochMicroseconds(tq.StartTimeMin), model.TimeAsEpochMicroseconds(tq.StartTimeMax), tq.NumTraces*limitMultiple, ).PageSize(0) return s.executeQuery(span, query, s.metrics.queryServiceNameIndex) } func (s *SpanReader) executeQuery(span trace.Span, query cassandra.Query, tableMetrics *casmetrics.Table) (dbmodel.UniqueTraceIDs, error) { start := time.Now() i := query.Iter() retMe := dbmodel.UniqueTraceIDs{} var traceID dbmodel.TraceID for i.Scan(&traceID) { retMe.Add(traceID) } err := i.Close() tableMetrics.Emit(err, time.Since(start)) if err != nil { logErrorToSpan(span, err) s.logger.Error("Failed to exec query", zap.Error(err), zap.String("query", query.String())) return nil, err } return retMe, nil } func (s *SpanReader) startSpanForQuery(ctx context.Context, name, query string) (context.Context, trace.Span) { ctx, span := s.tracer.Start(ctx, name) span.SetAttributes( attribute.Key(otelsemconv.DBQueryTextKey).String(query), attribute.Key(otelsemconv.DBSystemKey).String("cassandra"), attribute.Key("component").String("gocql"), ) return ctx, span } func logErrorToSpan(span trace.Span, err error) { if err == nil { return } span.RecordError(err) span.SetStatus(codes.Error, err.Error()) } ================================================ FILE: internal/storage/v1/cassandra/spanstore/reader_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "context" "errors" "fmt" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/metricstest" "github.com/jaegertracing/jaeger/internal/storage/cassandra" "github.com/jaegertracing/jaeger/internal/storage/cassandra/mocks" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/spanstore/dbmodel" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" "github.com/jaegertracing/jaeger/internal/testutils" ) type spanReaderTest struct { session *mocks.Session logger *zap.Logger logBuffer *testutils.Buffer traceBuffer *tracetest.InMemoryExporter reader *SpanReader } func tracerProvider(t *testing.T) (trace.TracerProvider, *tracetest.InMemoryExporter, func()) { exporter := tracetest.NewInMemoryExporter() tp := sdktrace.NewTracerProvider( sdktrace.WithSampler(sdktrace.AlwaysSample()), sdktrace.WithSyncer(exporter), ) closer := func() { require.NoError(t, tp.Shutdown(context.Background())) } return tp, exporter, closer } func withSpanReader(t *testing.T, fn func(r *spanReaderTest)) { session := &mocks.Session{} query := &mocks.Query{} session.On("Query", fmt.Sprintf(tableCheckStmt, schemas[latestVersion].tableName), mock.Anything).Return(query) query.On("Exec").Return(nil) logger, logBuffer := testutils.NewLogger() metricsFactory := metricstest.NewFactory(0) tracer, exp, closer := tracerProvider(t) defer closer() reader, err := NewSpanReader(session, metricsFactory, logger, tracer.Tracer("test")) require.NoError(t, err) r := &spanReaderTest{ session: session, logger: logger, logBuffer: logBuffer, traceBuffer: exp, reader: reader, } fn(r) } func TestNewSpanReader(t *testing.T) { t.Run("test span reader creation", func(t *testing.T) { withSpanReader(t, func(r *spanReaderTest) { assert.NotNil(t, r.reader) }) }) t.Run("test span reader creation error", func(t *testing.T) { session := &mocks.Session{} query := &mocks.Query{} session.On("Query", fmt.Sprintf(tableCheckStmt, schemas[latestVersion].tableName), mock.Anything).Return(query) session.On("Query", fmt.Sprintf(tableCheckStmt, schemas[previousVersion].tableName), mock.Anything).Return(query) query.On("Exec").Return(errors.New("table does not exist")) logger, _ := testutils.NewLogger() metricsFactory := metricstest.NewFactory(0) tracer, _, closer := tracerProvider(t) defer closer() _, err := NewSpanReader(session, metricsFactory, logger, tracer.Tracer("test")) require.EqualError(t, err, "neither table operation_names_v2 nor operation_names exist") }) } func TestSpanReaderGetServices(t *testing.T) { withSpanReader(t, func(r *spanReaderTest) { r.reader.serviceNamesReader = func() ([]string, error) { return []string{"service-a"}, nil } s, err := r.reader.GetServices(context.Background()) require.NoError(t, err) assert.Equal(t, []string{"service-a"}, s) }) } func TestSpanReaderGetOperations(t *testing.T) { withSpanReader(t, func(r *spanReaderTest) { expectedOperations := []tracestore.Operation{ { Name: "operation-a", SpanKind: "server", }, } r.reader.operationNamesReader = func(_ tracestore.OperationQueryParams) ([]tracestore.Operation, error) { return expectedOperations, nil } s, err := r.reader.GetOperationsV2(context.Background(), tracestore.OperationQueryParams{ServiceName: "service-x", SpanKind: "server"}) require.NoError(t, err) assert.Equal(t, expectedOperations, s) }) } func TestSpanReaderGetTrace(t *testing.T) { badScan := func() any { return matchOnceWithSideEffect(func(args []any) { for _, arg := range args { if v, ok := arg.(*[]dbmodel.KeyValue); ok { *v = []dbmodel.KeyValue{ { ValueType: "bad", }, } } } }) } testCases := []struct { scanner any closeErr error expectedErr string }{ {scanner: matchOnce()}, {scanner: badScan(), expectedErr: "invalid ValueType in"}, { scanner: matchOnce(), closeErr: errors.New("error on close()"), expectedErr: "error reading traces from storage: error on close()", }, } for _, tc := range testCases { testCase := tc // capture loop var t.Run("expected err="+testCase.expectedErr, func(t *testing.T) { withSpanReader(t, func(r *spanReaderTest) { iter := &mocks.Iterator{} iter.On("Scan", testCase.scanner).Return(true) iter.On("Scan", mock.Anything).Return(false) iter.On("Close").Return(testCase.closeErr) query := &mocks.Query{} query.On("Consistency", cassandra.One).Return(query) query.On("Iter").Return(iter) r.session.On("Query", mock.AnythingOfType("string"), mock.Anything).Return(query) trace, err := r.reader.GetTrace(context.Background(), spanstore.GetTraceParameters{}) if testCase.expectedErr == "" { require.NotEmpty(t, r.traceBuffer.GetSpans(), "Spans recorded") require.NoError(t, err) assert.NotNil(t, trace) } else { require.ErrorContains(t, err, testCase.expectedErr) assert.Nil(t, trace) } }) }) } } func TestSpanReaderGetTrace_TraceNotFound(t *testing.T) { withSpanReader(t, func(r *spanReaderTest) { iter := &mocks.Iterator{} iter.On("Scan", mock.Anything).Return(false) iter.On("Close").Return(nil) query := &mocks.Query{} query.On("Consistency", cassandra.One).Return(query) query.On("Iter").Return(iter) r.session.On("Query", mock.AnythingOfType("string"), mock.Anything).Return(query) trace, err := r.reader.GetTrace(context.Background(), spanstore.GetTraceParameters{}) require.NotEmpty(t, r.traceBuffer.GetSpans(), "Spans recorded") assert.Nil(t, trace) require.EqualError(t, err, "trace not found") }) } func TestSpanReaderFindTracesBadRequest(t *testing.T) { withSpanReader(t, func(r *spanReaderTest) { _, err := r.reader.FindTraces(context.Background(), nil) require.Empty(t, r.traceBuffer.GetSpans(), "Spans Not recorded") require.Error(t, err) }) } func TestSpanReaderFindTraces(t *testing.T) { testCases := []struct { caption string numTraces int queryTags bool queryOperation bool queryDuration bool mainQueryError error tagsQueryError error serviceNameAndOperationQueryError error durationQueryError error loadQueryError error expectedCount int expectedError string expectedLogs []string }{ { caption: "main query", expectedCount: 2, }, { caption: "tag query", expectedCount: 2, queryTags: true, }, { caption: "with limit", numTraces: 1, expectedCount: 1, }, { caption: "main query error", mainQueryError: errors.New("main query error"), expectedError: "main query error", expectedLogs: []string{ "Failed to exec query", "main query error", }, }, { caption: "tags query error", queryTags: true, tagsQueryError: errors.New("tags query error"), expectedError: "tags query error", expectedLogs: []string{ "Failed to exec query", "tags query error", }, }, { caption: "operation name query", queryOperation: true, numTraces: 0, expectedCount: 2, }, { caption: "operation name and tag query", queryTags: true, queryOperation: true, expectedCount: 2, }, { caption: "operation name and tag error on operation query", queryTags: true, queryOperation: true, serviceNameAndOperationQueryError: errors.New("operation query error"), expectedError: "operation query error", expectedLogs: []string{ "Failed to exec query", "operation query error", }, }, { caption: "operation name and tag error on tag query", queryTags: true, queryOperation: true, tagsQueryError: errors.New("tags query error"), expectedError: "tags query error", expectedLogs: []string{ "Failed to exec query", "tags query error", }, }, { caption: "duration query", queryDuration: true, numTraces: 1, expectedCount: 1, }, { caption: "duration query error", queryDuration: true, durationQueryError: errors.New("duration query error"), expectedError: "duration query error", expectedLogs: []string{ "Failed to exec query", "duration query error", }, }, { caption: "load trace error", loadQueryError: errors.New("load query error"), expectedCount: 0, expectedLogs: []string{ "Failure to read trace", "error reading traces from storage: load query error", `"trace_id":"0000000000000001"`, `"trace_id":"0000000000000002"`, }, }, } for _, tc := range testCases { testCase := tc // capture loop var t.Run(testCase.caption, func(t *testing.T) { withSpanReader(t, func(r *spanReaderTest) { // scanMatcher can match Iter.Scan() parameters and set trace ID fields scanMatcher := func(_ /* name */ string) any { traceIDs := []dbmodel.TraceID{ dbmodel.TraceIDFromDomain(model.NewTraceID(0, 1)), dbmodel.TraceIDFromDomain(model.NewTraceID(0, 2)), } scanFunc := func(args []any) bool { if len(traceIDs) == 0 { return false } for _, arg := range args { if ptr, ok := arg.(*dbmodel.TraceID); ok { *ptr = traceIDs[0] break } } traceIDs = traceIDs[1:] return true } return mock.MatchedBy(scanFunc) } mockQuery := func(queryErr error) *mocks.Query { iter := &mocks.Iterator{} iter.On("Scan", scanMatcher("queryIter")).Return(true) iter.On("Scan", mock.Anything).Return(false) iter.On("Close").Return(queryErr) query := &mocks.Query{} query.On("Bind", mock.Anything).Return(query) query.On("Consistency", cassandra.One).Return(query) query.On("PageSize", 0).Return(query) query.On("Iter").Return(iter) query.On("String").Return("queryString") return query } mainQuery := mockQuery(testCase.mainQueryError) tagsQuery := mockQuery(testCase.tagsQueryError) operationQuery := mockQuery(testCase.serviceNameAndOperationQueryError) durationQuery := mockQuery(testCase.durationQueryError) makeLoadQuery := func() *mocks.Query { loadQueryIter := &mocks.Iterator{} loadQueryIter.On("Scan", scanMatcher("loadIter")).Return(true) loadQueryIter.On("Scan", mock.Anything).Return(false) loadQueryIter.On("Close").Return(testCase.loadQueryError) loadQuery := &mocks.Query{} loadQuery.On("Consistency", cassandra.One).Return(loadQuery) loadQuery.On("Iter").Return(loadQueryIter) loadQuery.On("PageSize", mock.Anything).Return(loadQuery) return loadQuery } r.session.On("Query", queryByServiceName, mock.Anything).Return(mainQuery) r.session.On("Query", queryByTag, mock.Anything).Return(tagsQuery) r.session.On("Query", queryByServiceAndOperationName, mock.Anything).Return(operationQuery) r.session.On("Query", queryByDuration, mock.Anything).Return(durationQuery) r.session.On("Query", stringMatcher("SELECT trace_id"), matchOnce()).Return(makeLoadQuery()) r.session.On("Query", stringMatcher("SELECT trace_id"), mock.Anything).Return(makeLoadQuery()) queryParams := &spanstore.TraceQueryParameters{ ServiceName: "service-a", NumTraces: 100, StartTimeMax: time.Now(), StartTimeMin: time.Now().Add(-1 * time.Minute * 30), } queryParams.NumTraces = testCase.numTraces if testCase.queryTags { queryParams.Tags = make(map[string]string) queryParams.Tags["x"] = "y" } if testCase.queryOperation { queryParams.OperationName = "operation-b" } if testCase.queryDuration { queryParams.DurationMin = time.Minute queryParams.DurationMax = time.Minute * 3 } res, err := r.reader.FindTraces(context.Background(), queryParams) if testCase.expectedError == "" { require.NotEmpty(t, r.traceBuffer.GetSpans(), "Spans recorded") require.NoError(t, err) assert.Len(t, res, testCase.expectedCount, "expecting certain number of traces") } else { require.EqualError(t, err, testCase.expectedError) } for _, expectedLog := range testCase.expectedLogs { assert.Contains(t, r.logBuffer.String(), expectedLog) } if len(testCase.expectedLogs) == 0 { assert.Empty(t, r.logBuffer.String()) } }) }) } } func TestTraceQueryParameterValidation(t *testing.T) { tsp := &spanstore.TraceQueryParameters{ ServiceName: "", Tags: map[string]string{ "michael": "jackson", }, } err := validateQuery(tsp) require.EqualError(t, err, ErrServiceNameNotSet.Error()) tsp.ServiceName = "serviceName" tsp.StartTimeMin = time.Now() tsp.StartTimeMax = time.Now().Add(-1 * time.Hour) err = validateQuery(tsp) require.EqualError(t, err, ErrStartTimeMinGreaterThanMax.Error()) tsp.StartTimeMin = time.Now().Add(-12 * time.Hour) tsp.DurationMin = time.Hour tsp.DurationMax = time.Minute err = validateQuery(tsp) require.EqualError(t, err, ErrDurationMinGreaterThanMax.Error()) tsp.DurationMin = time.Minute tsp.DurationMax = time.Hour err = validateQuery(tsp) require.EqualError(t, err, ErrDurationAndTagQueryNotSupported.Error()) tsp.StartTimeMin = time.Time{} // time.Unix(0,0) doesn't work because timezones tsp.StartTimeMax = time.Time{} err = validateQuery(tsp) require.EqualError(t, err, ErrStartAndEndTimeNotSet.Error()) } func TestGetOperations(t *testing.T) { reader := SpanReader{} _, err := reader.GetOperations(context.Background(), spanstore.OperationQueryParameters{}) require.ErrorContains(t, err, "not implemented") } ================================================ FILE: internal/storage/v1/cassandra/spanstore/service_names.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "fmt" "time" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/cache" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/storage/cassandra" casmetrics "github.com/jaegertracing/jaeger/internal/storage/cassandra/metrics" ) const ( insertServiceName = `INSERT INTO service_names(service_name) VALUES (?)` queryServiceNames = `SELECT service_name FROM service_names` ) // ServiceNamesStorage stores known service names. type ServiceNamesStorage struct { session cassandra.Session writeCacheTTL time.Duration InsertStmt string QueryStmt string metrics *casmetrics.Table serviceNames cache.Cache logger *zap.Logger } // NewServiceNamesStorage returns a new ServiceNamesStorage func NewServiceNamesStorage( session cassandra.Session, writeCacheTTL time.Duration, metricsFactory metrics.Factory, logger *zap.Logger, ) *ServiceNamesStorage { return &ServiceNamesStorage{ session: session, InsertStmt: insertServiceName, QueryStmt: queryServiceNames, metrics: casmetrics.NewTable(metricsFactory, "service_names"), writeCacheTTL: writeCacheTTL, logger: logger, serviceNames: cache.NewLRUWithOptions( 10000, &cache.Options{ TTL: writeCacheTTL, InitialCapacity: 1000, }), } } // Write saves a single service name func (s *ServiceNamesStorage) Write(serviceName string) error { var err error query := s.session.Query(s.InsertStmt) if inCache := checkWriteCache(serviceName, s.serviceNames, s.writeCacheTTL); !inCache { q := query.Bind(serviceName) err2 := s.metrics.Exec(q, s.logger) if err2 != nil { err = err2 } } return err } // checks if the key is in cache; returns true if it is, otherwise puts it there and returns false func checkWriteCache(key string, c cache.Cache, writeCacheTTL time.Duration) bool { if writeCacheTTL == 0 { return false } // even though there is a race condition between Get and Put, it's not a problem for storage, // it simply means we might write the same service name twice. inCache := c.Get(key) if inCache == nil { c.Put(key, key) } return inCache != nil } // GetServices returns all services traced by Jaeger func (s *ServiceNamesStorage) GetServices() ([]string, error) { iter := s.session.Query(s.QueryStmt).Iter() var service string var services []string for iter.Scan(&service) { services = append(services, service) } if err := iter.Close(); err != nil { err = fmt.Errorf("error reading service_names from storage: %w", err) return nil, err } return services, nil } ================================================ FILE: internal/storage/v1/cassandra/spanstore/service_names_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "errors" "fmt" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/metricstest" "github.com/jaegertracing/jaeger/internal/storage/cassandra/mocks" "github.com/jaegertracing/jaeger/internal/testutils" ) type serviceNameStorageTest struct { session *mocks.Session writeCacheTTL time.Duration metricsFactory *metricstest.Factory logger *zap.Logger logBuffer *testutils.Buffer storage *ServiceNamesStorage } func withServiceNamesStorage(writeCacheTTL time.Duration, fn func(s *serviceNameStorageTest)) { session := &mocks.Session{} logger, logBuffer := testutils.NewLogger() metricsFactory := metricstest.NewFactory(time.Second) defer metricsFactory.Stop() s := &serviceNameStorageTest{ session: session, writeCacheTTL: writeCacheTTL, metricsFactory: metricsFactory, logger: logger, logBuffer: logBuffer, storage: NewServiceNamesStorage(session, writeCacheTTL, metricsFactory, logger), } fn(s) } func TestServiceNamesStorageWrite(t *testing.T) { for _, ttl := range []time.Duration{0, time.Minute} { writeCacheTTL := ttl // capture loop var t.Run(fmt.Sprintf("writeCacheTTL=%v", writeCacheTTL), func(t *testing.T) { withServiceNamesStorage(writeCacheTTL, func(s *serviceNameStorageTest) { execError := errors.New("exec error") query := &mocks.Query{} query1 := &mocks.Query{} query2 := &mocks.Query{} query.On("Bind", []any{"service-a"}).Return(query1) query.On("Bind", []any{"service-b"}).Return(query2) query1.On("Exec").Return(nil) query2.On("Exec").Return(execError) query2.On("String").Return("select from service_names") s.session.On("Query", mock.AnythingOfType("string")).Return(query) err := s.storage.Write("service-a") require.NoError(t, err) err = s.storage.Write("service-b") require.EqualError(t, err, "failed to Exec query 'select from service_names': exec error") assert.Equal(t, map[string]string{ "level": "error", "msg": "Failed to exec query", "query": "select from service_names", "error": "exec error", }, s.logBuffer.JSONLine(0)) counts, _ := s.metricsFactory.Snapshot() assert.Equal(t, map[string]int64{ "attempts|table=service_names": 2, "inserts|table=service_names": 1, "errors|table=service_names": 1, }, counts) // write again err = s.storage.Write("service-a") require.NoError(t, err) counts2, _ := s.metricsFactory.Snapshot() expCounts := counts if writeCacheTTL == 0 { // without write cache, the second write must succeed expCounts["attempts|table=service_names"]++ expCounts["inserts|table=service_names"]++ } assert.Equal(t, expCounts, counts2) }) }) } } func TestServiceNamesStorageGetServices(t *testing.T) { scanError := errors.New("scan error") var writeCacheTTL time.Duration var matched bool matchOnce := mock.MatchedBy(func(_ []any) bool { if matched { return false } matched = true return true }) matchEverything := mock.MatchedBy(func(_ []any) bool { return true }) for _, expErr := range []error{nil, scanError} { withServiceNamesStorage(writeCacheTTL, func(s *serviceNameStorageTest) { iter := &mocks.Iterator{} iter.On("Scan", matchOnce).Return(true) iter.On("Scan", matchEverything).Return(false) // false to stop the loop iter.On("Close").Return(expErr) query := &mocks.Query{} query.On("Iter").Return(iter) s.session.On("Query", mock.AnythingOfType("string")).Return(query) services, err := s.storage.GetServices() if expErr == nil { require.NoError(t, err) // expect empty string because mock iter.Scan(&placeholder) does not write to `placeholder` assert.Equal(t, []string{""}, services) } else { require.EqualError(t, err, "error reading service_names from storage: "+expErr.Error()) } }) } } ================================================ FILE: internal/storage/v1/cassandra/spanstore/writer.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "context" "encoding/json" "fmt" "strings" "time" "unicode/utf8" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/storage/cassandra" casmetrics "github.com/jaegertracing/jaeger/internal/storage/cassandra/metrics" "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/spanstore/dbmodel" ) const ( insertSpan = ` INSERT INTO traces(trace_id, span_id, span_hash, parent_id, operation_name, flags, start_time, duration, tags, logs, refs, process) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` serviceNameIndex = ` INSERT INTO service_name_index(service_name, bucket, start_time, trace_id) VALUES (?, ?, ?, ?)` serviceOperationIndex = ` INSERT INTO service_operation_index(service_name, operation_name, start_time, trace_id) VALUES (?, ?, ?, ?)` tagIndex = ` INSERT INTO tag_index(trace_id, span_id, service_name, start_time, tag_key, tag_value) VALUES (?, ?, ?, ?, ?, ?)` durationIndex = ` INSERT INTO duration_index(service_name, operation_name, bucket, duration, start_time, trace_id) VALUES (?, ?, ?, ?, ?, ?)` maximumTagKeyOrValueSize = 256 // DefaultNumBuckets Number of buckets for bucketed keys defaultNumBuckets = 10 durationBucketSize = time.Hour ) const ( storeFlag = storageMode(1 << iota) indexFlag ) type ( storageMode uint8 serviceNamesWriter func(serviceName string) error operationNamesWriter func(operation dbmodel.Operation) error ) type spanWriterMetrics struct { traces *casmetrics.Table tagIndex *casmetrics.Table serviceNameIndex *casmetrics.Table serviceOperationIndex *casmetrics.Table durationIndex *casmetrics.Table } // SpanWriter handles all writes to Cassandra for the Jaeger data model type SpanWriter struct { session cassandra.Session serviceNamesWriter serviceNamesWriter operationNamesWriter operationNamesWriter writerMetrics spanWriterMetrics logger *zap.Logger tagIndexSkipped metrics.Counter tagFilter dbmodel.TagFilter storageMode storageMode indexFilter dbmodel.IndexFilter } // NewSpanWriter returns a SpanWriter func NewSpanWriter( session cassandra.Session, writeCacheTTL time.Duration, metricsFactory metrics.Factory, logger *zap.Logger, options ...Option, ) (*SpanWriter, error) { serviceNamesStorage := NewServiceNamesStorage(session, writeCacheTTL, metricsFactory, logger) operationNamesStorage, err := NewOperationNamesStorage(session, writeCacheTTL, metricsFactory, logger) if err != nil { return nil, err } tagIndexSkipped := metricsFactory.Counter(metrics.Options{Name: "tag_index_skipped", Tags: nil}) opts := applyOptions(options...) return &SpanWriter{ session: session, serviceNamesWriter: serviceNamesStorage.Write, operationNamesWriter: operationNamesStorage.Write, writerMetrics: spanWriterMetrics{ traces: casmetrics.NewTable(metricsFactory, "traces"), tagIndex: casmetrics.NewTable(metricsFactory, "tag_index"), serviceNameIndex: casmetrics.NewTable(metricsFactory, "service_name_index"), serviceOperationIndex: casmetrics.NewTable(metricsFactory, "service_operation_index"), durationIndex: casmetrics.NewTable(metricsFactory, "duration_index"), }, logger: logger, tagIndexSkipped: tagIndexSkipped, tagFilter: opts.tagFilter, storageMode: opts.storageMode, indexFilter: opts.indexFilter, }, nil } // Close closes SpanWriter func (s *SpanWriter) Close() error { s.session.Close() return nil } // WriteSpan saves the span into Cassandra func (s *SpanWriter) WriteSpan(_ context.Context, span *model.Span) error { ds := dbmodel.FromDomain(span) if s.storageMode&storeFlag == storeFlag { if err := s.writeSpanToDB(span, ds); err != nil { return err } } if s.storageMode&indexFlag == indexFlag { if err := s.writeIndexes(span, ds); err != nil { return err } } return nil } func (s *SpanWriter) writeSpanToDB(_ *model.Span, ds *dbmodel.Span) error { mainQuery := s.session.Query( insertSpan, ds.TraceID, ds.SpanID, ds.SpanHash, ds.ParentID, ds.OperationName, ds.Flags, ds.StartTime, ds.Duration, ds.Tags, ds.Logs, ds.Refs, ds.Process, ) if err := s.writerMetrics.traces.Exec(mainQuery, s.logger); err != nil { return s.logError(ds, err, "Failed to insert span", s.logger) } return nil } func (s *SpanWriter) writeIndexes(span *model.Span, ds *dbmodel.Span) error { spanKind, _ := span.GetSpanKind() // if not found it returns "" if err := s.saveServiceNameAndOperationName(dbmodel.Operation{ ServiceName: ds.ServiceName, SpanKind: string(spanKind), OperationName: ds.OperationName, }); err != nil { // should this be a soft failure? return s.logError(ds, err, "Failed to insert service name and operation name", s.logger) } if s.indexFilter(ds, dbmodel.ServiceIndex) { if err := s.indexByService(ds); err != nil { return s.logError(ds, err, "Failed to index service name", s.logger) } } if s.indexFilter(ds, dbmodel.OperationIndex) { if err := s.indexByOperation(ds); err != nil { return s.logError(ds, err, "Failed to index operation name", s.logger) } } if span.Flags.IsFirehoseEnabled() { return nil // skipping expensive indexing } if err := s.indexByTags(ds); err != nil { return s.logError(ds, err, "Failed to index tags", s.logger) } if s.indexFilter(ds, dbmodel.DurationIndex) { if err := s.indexByDuration(ds, span.StartTime); err != nil { return s.logError(ds, err, "Failed to index duration", s.logger) } } return nil } func (s *SpanWriter) indexByTags(ds *dbmodel.Span) error { for _, v := range dbmodel.GetAllUniqueTags(ds, s.tagFilter) { // we should introduce retries or just ignore failures imo, retrying each individual tag insertion might be better // we should consider bucketing. if s.shouldIndexTag(v) { insertTagQuery := s.session.Query(tagIndex, ds.TraceID, ds.SpanID, v.ServiceName, ds.StartTime, v.TagKey, v.TagValue) if err := s.writerMetrics.tagIndex.Exec(insertTagQuery, s.logger); err != nil { withTagInfo := s.logger. With(zap.String("tag_key", v.TagKey)). With(zap.String("tag_value", v.TagValue)). With(zap.String("service_name", v.ServiceName)) return s.logError(ds, err, "Failed to index tag", withTagInfo) } } else { s.tagIndexSkipped.Inc(1) } } return nil } func (s *SpanWriter) indexByDuration(span *dbmodel.Span, startTime time.Time) error { query := s.session.Query(durationIndex) timeBucket := startTime.Round(durationBucketSize) var err error indexByOperationName := func(operationName string) { q1 := query.Bind(span.Process.ServiceName, operationName, timeBucket, span.Duration, span.StartTime, span.TraceID) if err2 := s.writerMetrics.durationIndex.Exec(q1, s.logger); err2 != nil { _ = s.logError(span, err2, "Cannot index duration", s.logger) err = err2 } } indexByOperationName("") // index by service name alone indexByOperationName(span.OperationName) // index by service name and operation name return err } func (s *SpanWriter) indexByService(span *dbmodel.Span) error { //nolint:gosec // G115 bucketNo := uint64(span.SpanHash) % defaultNumBuckets query := s.session.Query(serviceNameIndex) q := query.Bind(span.Process.ServiceName, bucketNo, span.StartTime, span.TraceID) return s.writerMetrics.serviceNameIndex.Exec(q, s.logger) } func (s *SpanWriter) indexByOperation(span *dbmodel.Span) error { query := s.session.Query(serviceOperationIndex) q := query.Bind(span.Process.ServiceName, span.OperationName, span.StartTime, span.TraceID) return s.writerMetrics.serviceOperationIndex.Exec(q, s.logger) } // shouldIndexTag checks to see if the tag is json or not, if it's UTF8 valid and it's not too large func (*SpanWriter) shouldIndexTag(tag dbmodel.TagInsertion) bool { isJSON := func(s string) bool { var js json.RawMessage // poor man's string-is-a-json check shortcircuits full unmarshalling return strings.HasPrefix(s, "{") && json.Unmarshal([]byte(s), &js) == nil } return len(tag.TagKey) < maximumTagKeyOrValueSize && len(tag.TagValue) < maximumTagKeyOrValueSize && utf8.ValidString(tag.TagValue) && utf8.ValidString(tag.TagKey) && !isJSON(tag.TagValue) } func (*SpanWriter) logError(span *dbmodel.Span, err error, msg string, logger *zap.Logger) error { logger. With(zap.String("trace_id", span.TraceID.String())). With(zap.Int64("span_id", span.SpanID)). With(zap.Error(err)). Error(msg) return fmt.Errorf("%s: %w", msg, err) } func (s *SpanWriter) saveServiceNameAndOperationName(operation dbmodel.Operation) error { if err := s.serviceNamesWriter(operation.ServiceName); err != nil { return err } return s.operationNamesWriter(operation) } ================================================ FILE: internal/storage/v1/cassandra/spanstore/writer_options.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/spanstore/dbmodel" ) // Option is a function that sets some option on the writer. type Option func(c *Options) // Options control behavior of the writer. type Options struct { tagFilter dbmodel.TagFilter storageMode storageMode indexFilter dbmodel.IndexFilter } // TagFilter can be provided to filter any tags that should not be indexed. func TagFilter(tagFilter dbmodel.TagFilter) Option { return func(o *Options) { o.tagFilter = tagFilter } } // StoreIndexesOnly can be provided to skip storing spans, and only store span indexes. func StoreIndexesOnly() Option { return func(o *Options) { o.storageMode = indexFlag } } // StoreWithoutIndexing can be provided to store spans without indexing them. func StoreWithoutIndexing() Option { return func(o *Options) { o.storageMode = storeFlag } } // IndexFilter can be provided to filter certain spans that should not be indexed. func IndexFilter(indexFilter dbmodel.IndexFilter) Option { return func(o *Options) { o.indexFilter = indexFilter } } func applyOptions(opts ...Option) Options { o := Options{} for _, opt := range opts { opt(&o) } if o.tagFilter == nil { o.tagFilter = dbmodel.DefaultTagFilter } if o.storageMode == 0 { o.storageMode = storeFlag | indexFlag } if o.indexFilter == nil { o.indexFilter = dbmodel.DefaultIndexFilter } return o } ================================================ FILE: internal/storage/v1/cassandra/spanstore/writer_options_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "testing" "github.com/stretchr/testify/assert" "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/spanstore/dbmodel" ) func TestWriterOptions(t *testing.T) { opts := applyOptions(TagFilter(dbmodel.DefaultTagFilter), IndexFilter(dbmodel.DefaultIndexFilter)) assert.Equal(t, dbmodel.DefaultTagFilter, opts.tagFilter) assert.ObjectsAreEqual(dbmodel.DefaultIndexFilter, opts.indexFilter) } func TestWriterOptions_StorageMode(t *testing.T) { tests := []struct { name string expected storageMode opts Options }{ { name: "Default", expected: indexFlag | storeFlag, opts: applyOptions(), }, { name: "Index Only", expected: indexFlag, opts: applyOptions(StoreIndexesOnly()), }, { name: "Store Only", expected: storeFlag, opts: applyOptions(StoreWithoutIndexing()), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, tt.opts.storageMode) }) } } ================================================ FILE: internal/storage/v1/cassandra/spanstore/writer_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "context" "errors" "fmt" "strings" "sync/atomic" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/metricstest" "github.com/jaegertracing/jaeger/internal/storage/cassandra/mocks" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/spanstore/dbmodel" "github.com/jaegertracing/jaeger/internal/testutils" ) type spanWriterTest struct { session *mocks.Session logger *zap.Logger logBuffer *testutils.Buffer writer *SpanWriter } func withSpanWriter(t *testing.T, writeCacheTTL time.Duration, fn func(w *spanWriterTest), options ...Option, ) { session := &mocks.Session{} query := &mocks.Query{} session.On("Query", fmt.Sprintf(tableCheckStmt, schemas[latestVersion].tableName), mock.Anything).Return(query) query.On("Exec").Return(nil) logger, logBuffer := testutils.NewLogger() metricsFactory := metricstest.NewFactory(0) writer, err := NewSpanWriter(session, writeCacheTTL, metricsFactory, logger, options...) require.NoError(t, err) w := &spanWriterTest{ session: session, logger: logger, logBuffer: logBuffer, writer: writer, } fn(w) } var _ spanstore.Writer = &SpanWriter{} // check API conformance func TestNewSpanWriter(t *testing.T) { t.Run("test span writer creation", func(t *testing.T) { withSpanWriter(t, 0, func(w *spanWriterTest) { assert.NotNil(t, w.writer) }) }) t.Run("test span writer creation error", func(t *testing.T) { session := &mocks.Session{} query := &mocks.Query{} session.On("Query", fmt.Sprintf(tableCheckStmt, schemas[latestVersion].tableName), mock.Anything).Return(query) session.On("Query", fmt.Sprintf(tableCheckStmt, schemas[previousVersion].tableName), mock.Anything).Return(query) query.On("Exec").Return(errors.New("table does not exist")) logger, _ := testutils.NewLogger() metricsFactory := metricstest.NewFactory(0) _, err := NewSpanWriter(session, 0, metricsFactory, logger) assert.EqualError(t, err, "neither table operation_names_v2 nor operation_names exist") }) } func TestClientClose(t *testing.T) { withSpanWriter(t, 0, func(w *spanWriterTest) { w.session.On("Close").Return(nil) w.writer.Close() w.session.AssertNumberOfCalls(t, "Close", 1) }) } func TestSpanWriter(t *testing.T) { testCases := []struct { caption string firehose bool mainQueryError error tagsQueryError error serviceNameQueryError error serviceOperationNameQueryError error durationNoOperationQueryError error serviceNameError error expectedError string expectedLogs []string }{ { caption: "main query", }, { caption: "main firehose query", firehose: true, }, { caption: "main query error", mainQueryError: errors.New("main query error"), expectedError: "Failed to insert span: failed to Exec query 'select from traces': main query error", expectedLogs: []string{ `"msg":"Failed to exec query"`, `"query":"select from traces"`, `"error":"main query error"`, "Failed to insert span", `"trace_id":"0000000000000001"`, `"span_id":0`, }, }, { caption: "tags query error", tagsQueryError: errors.New("tags query error"), expectedError: "Failed to index tags: Failed to index tag: failed to Exec query 'select from tags': tags query error", expectedLogs: []string{ `"msg":"Failed to exec query"`, `"query":"select from tags"`, `"error":"tags query error"`, "Failed to index tags", `"tag_key":"x"`, `"tag_value":"y"`, }, }, { caption: "save service name query error", serviceNameError: errors.New("serviceNameError"), expectedError: "Failed to insert service name and operation name: serviceNameError", expectedLogs: []string{ "Failed to insert service name and operation name", }, }, { caption: "add span to service name index", serviceNameQueryError: errors.New("serviceNameQueryError"), expectedError: "Failed to index service name: failed to Exec query 'select from service_name_index': serviceNameQueryError", expectedLogs: []string{ `"msg":"Failed to exec query"`, `"query":"select from service_name_index"`, `"error":"serviceNameQueryError"`, }, }, { caption: "add span to operation name index", serviceOperationNameQueryError: errors.New("serviceOperationNameQueryError"), expectedError: "Failed to index operation name: failed to Exec query 'select from service_operation_index': serviceOperationNameQueryError", expectedLogs: []string{ `"msg":"Failed to exec query"`, `"query":"select from service_operation_index"`, `"error":"serviceOperationNameQueryError"`, }, }, { caption: "add duration with no operation name", durationNoOperationQueryError: errors.New("durationNoOperationError"), expectedError: "Failed to index duration: failed to Exec query 'select from duration_index': durationNoOperationError", expectedLogs: []string{ `"msg":"Failed to exec query"`, `"query":"select from duration_index"`, `"error":"durationNoOperationError"`, }, }, } for _, tc := range testCases { testCase := tc // capture loop var t.Run(testCase.caption, func(t *testing.T) { withSpanWriter(t, 0, func(w *spanWriterTest) { span := &model.Span{ TraceID: model.NewTraceID(0, 1), OperationName: "operation-a", Tags: model.KeyValues{ model.String("x", "y"), model.String("json", `{"x":"y"}`), // string tag with json value will not be inserted }, Process: &model.Process{ ServiceName: "service-a", }, } if testCase.firehose { span.Flags = model.FirehoseFlag } spanQuery := &mocks.Query{} spanQuery.On("Bind", mock.Anything).Return(spanQuery) spanQuery.On("Exec").Return(testCase.mainQueryError) spanQuery.On("String").Return("select from traces") serviceNameQuery := &mocks.Query{} serviceNameQuery.On("Bind", mock.Anything).Return(serviceNameQuery) serviceNameQuery.On("Exec").Return(testCase.serviceNameQueryError) serviceNameQuery.On("String").Return("select from service_name_index") serviceOperationNameQuery := &mocks.Query{} serviceOperationNameQuery.On("Bind", mock.Anything).Return(serviceOperationNameQuery) serviceOperationNameQuery.On("Exec").Return(testCase.serviceOperationNameQueryError) serviceOperationNameQuery.On("String").Return("select from service_operation_index") tagsQuery := &mocks.Query{} tagsQuery.On("Exec").Return(testCase.tagsQueryError) tagsQuery.On("String").Return("select from tags") durationNoOperationQuery := &mocks.Query{} durationNoOperationQuery.On("Bind", mock.Anything).Return(durationNoOperationQuery) durationNoOperationQuery.On("Exec").Return(testCase.durationNoOperationQueryError) durationNoOperationQuery.On("String").Return("select from duration_index") // Define expected queries w.session.On("Query", insertSpan, mock.Anything).Return(spanQuery) w.session.On("Query", serviceNameIndex).Return(serviceNameQuery) w.session.On("Query", serviceOperationIndex, mock.Anything).Return(serviceOperationNameQuery) // note: using Once() below because we only want one tag to be inserted w.session.On("Query", tagIndex, mock.Anything).Return(tagsQuery).Once() w.session.On("Query", durationIndex).Return(durationNoOperationQuery).Once() w.writer.serviceNamesWriter = func(_ /* serviceName */ string) error { return testCase.serviceNameError } w.writer.operationNamesWriter = func(_ dbmodel.Operation) error { return testCase.serviceNameError } err := w.writer.WriteSpan(context.Background(), span) if testCase.expectedError == "" { require.NoError(t, err) } else { require.EqualError(t, err, testCase.expectedError) } for _, expectedLog := range testCase.expectedLogs { assert.Contains(t, w.logBuffer.String(), expectedLog) } if len(testCase.expectedLogs) == 0 { assert.Empty(t, w.logBuffer.String()) } }) }) } } func TestSpanWriterSaveServiceNameAndOperationName(t *testing.T) { expectedErr := errors.New("some error") testCases := []struct { serviceNamesWriter serviceNamesWriter operationNamesWriter operationNamesWriter expectedError string }{ { serviceNamesWriter: func(_ /* serviceName */ string) error { return nil }, operationNamesWriter: func(_ dbmodel.Operation) error { return nil }, }, { serviceNamesWriter: func(_ /* serviceName */ string) error { return expectedErr }, operationNamesWriter: func(_ dbmodel.Operation) error { return nil }, expectedError: "some error", }, { serviceNamesWriter: func(_ /* serviceName */ string) error { return nil }, operationNamesWriter: func(_ dbmodel.Operation) error { return expectedErr }, expectedError: "some error", }, } for _, tc := range testCases { testCase := tc // capture loop var withSpanWriter(t, 0, func(w *spanWriterTest) { w.writer.serviceNamesWriter = testCase.serviceNamesWriter w.writer.operationNamesWriter = testCase.operationNamesWriter err := w.writer.saveServiceNameAndOperationName( dbmodel.Operation{ ServiceName: "service", OperationName: "operation", }) if testCase.expectedError == "" { require.NoError(t, err) } else { require.EqualError(t, err, testCase.expectedError) } }) } } func TestSpanWriterSkippingTags(t *testing.T) { longString := strings.Repeat("x", 300) testCases := []struct { key string value string insert bool }{ {key: "x", value: "y", insert: true}, {key: longString, value: "y", insert: false}, {key: "x", value: longString, insert: false}, {key: "x", value: `{"x":"y"}`, insert: false}, // value is a JSON {key: "x", value: `{"x":`, insert: true}, // value is not a JSON } for _, tc := range testCases { testCase := tc // capture loop var withSpanWriter(t, 0, func(w *spanWriterTest) { db := dbmodel.TagInsertion{ ServiceName: "service-a", TagKey: testCase.key, TagValue: testCase.value, } ok := w.writer.shouldIndexTag(db) assert.Equal(t, testCase.insert, ok) }) } } func TestStorageMode_IndexOnly(t *testing.T) { withSpanWriter(t, 0, func(w *spanWriterTest) { w.writer.serviceNamesWriter = func(_ /* serviceName */ string) error { return nil } w.writer.operationNamesWriter = func(_ dbmodel.Operation) error { return nil } span := &model.Span{ TraceID: model.NewTraceID(0, 1), Process: &model.Process{ ServiceName: "service-a", }, } serviceNameQuery := &mocks.Query{} serviceNameQuery.On("Bind", mock.Anything).Return(serviceNameQuery) serviceNameQuery.On("Exec").Return(nil) serviceOperationNameQuery := &mocks.Query{} serviceOperationNameQuery.On("Bind", mock.Anything).Return(serviceOperationNameQuery) serviceOperationNameQuery.On("Exec").Return(nil) durationNoOperationQuery := &mocks.Query{} durationNoOperationQuery.On("Bind", mock.Anything).Return(durationNoOperationQuery) durationNoOperationQuery.On("Exec").Return(nil) w.session.On("Query", serviceNameIndex).Return(serviceNameQuery) w.session.On("Query", serviceOperationIndex).Return(serviceOperationNameQuery) w.session.On("Query", durationIndex).Return(durationNoOperationQuery).Once() err := w.writer.WriteSpan(context.Background(), span) require.NoError(t, err) serviceNameQuery.AssertExpectations(t) serviceOperationNameQuery.AssertExpectations(t) durationNoOperationQuery.AssertExpectations(t) w.session.AssertExpectations(t) w.session.AssertNotCalled(t, "Query", insertSpan, mock.Anything) }, StoreIndexesOnly()) } var filterEverything = func(*dbmodel.Span, int) bool { return false } func TestStorageMode_IndexOnly_WithFilter(t *testing.T) { withSpanWriter(t, 0, func(w *spanWriterTest) { w.writer.indexFilter = filterEverything w.writer.serviceNamesWriter = func(_ /* serviceName */ string) error { return nil } w.writer.operationNamesWriter = func(_ dbmodel.Operation) error { return nil } span := &model.Span{ TraceID: model.NewTraceID(0, 1), Process: &model.Process{ ServiceName: "service-a", }, } err := w.writer.WriteSpan(context.Background(), span) require.NoError(t, err) w.session.AssertExpectations(t) w.session.AssertNotCalled(t, "Query", serviceOperationIndex, mock.Anything) w.session.AssertNotCalled(t, "Query", serviceNameIndex, mock.Anything) w.session.AssertNotCalled(t, "Query", durationIndex, mock.Anything) }, StoreIndexesOnly()) } func TestStorageMode_IndexOnly_FirehoseSpan(t *testing.T) { withSpanWriter(t, 0, func(w *spanWriterTest) { var serviceWritten atomic.Pointer[string] var operationWritten atomic.Pointer[dbmodel.Operation] empty := "" serviceWritten.Store(&empty) operationWritten.Store(&dbmodel.Operation{}) w.writer.serviceNamesWriter = func(serviceName string) error { serviceWritten.Store(&serviceName) return nil } w.writer.operationNamesWriter = func(operation dbmodel.Operation) error { operationWritten.Store(&operation) return nil } span := &model.Span{ TraceID: model.NewTraceID(0, 1), OperationName: "package-delivery", Process: &model.Process{ ServiceName: "planet-express", }, Flags: model.Flags(8), } serviceNameQuery := &mocks.Query{} serviceNameQuery.On("Bind", mock.Anything).Return(serviceNameQuery) serviceNameQuery.On("Exec").Return(nil) serviceNameQuery.On("String").Return("select from service_name_index") serviceOperationNameQuery := &mocks.Query{} serviceOperationNameQuery.On("Bind", mock.Anything).Return(serviceOperationNameQuery) serviceOperationNameQuery.On("Exec").Return(nil) serviceOperationNameQuery.On("String").Return("select from service_operation_index") // Define expected queries w.session.On("Query", serviceNameIndex).Return(serviceNameQuery) w.session.On("Query", serviceOperationIndex).Return(serviceOperationNameQuery) err := w.writer.WriteSpan(context.Background(), span) require.NoError(t, err) w.session.AssertExpectations(t) w.session.AssertNotCalled(t, "Query", tagIndex, mock.Anything) w.session.AssertNotCalled(t, "Query", durationIndex, mock.Anything) assert.Equal(t, "planet-express", *serviceWritten.Load()) assert.Equal(t, dbmodel.Operation{ ServiceName: "planet-express", SpanKind: "", OperationName: "package-delivery", }, *operationWritten.Load()) }, StoreIndexesOnly()) } func TestStorageMode_StoreWithoutIndexing(t *testing.T) { withSpanWriter(t, 0, func(w *spanWriterTest) { w.writer.serviceNamesWriter = func(_ /* serviceName */ string) error { assert.Fail(t, "Non indexing store shouldn't index") return nil } span := &model.Span{ TraceID: model.NewTraceID(0, 1), Process: &model.Process{ ServiceName: "service-a", }, } spanQuery := &mocks.Query{} spanQuery.On("Exec").Return(nil) w.session.On("Query", insertSpan, mock.Anything).Return(spanQuery) err := w.writer.WriteSpan(context.Background(), span) require.NoError(t, err) spanQuery.AssertExpectations(t) w.session.AssertExpectations(t) w.session.AssertNotCalled(t, "Query", serviceNameIndex, mock.Anything) }, StoreWithoutIndexing()) } ================================================ FILE: internal/storage/v1/configurable.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package storage import ( "flag" "github.com/spf13/viper" "go.uber.org/zap" ) // Configurable interface can be implemented by plugins that require external configuration, // such as CLI flags, config files, or environment variables. type Configurable interface { // AddFlags adds CLI flags for configuring this component. AddFlags(flagSet *flag.FlagSet) // InitFromViper initializes this component with properties from spf13/viper. InitFromViper(v *viper.Viper, logger *zap.Logger) } ================================================ FILE: internal/storage/v1/elasticsearch/.gitignore ================================================ esmapping-generator-*-* ================================================ FILE: internal/storage/v1/elasticsearch/README.md ================================================ # ElasticSearch Support This provides a storage backend for Jaeger using [Elasticsearch](https://www.elastic.co). More information is available on the [Jaeger documentation website](https://www.jaegertracing.io/docs/latest/deployment/#elasticsearch). ## Indices Indices will be created depending on the spans timestamp. i.e., a span with a timestamp on 2017/04/21 will be stored in an index named `jaeger-2017-04-21`. It is common to only keep observability data for a limited time. However, Elasticsearch does not support expiring of old data via TTL. To purge old Jaeger indices, use [jaeger-es-index-cleaner](../../../cmd/es-index-cleaner/). ### Timestamps Because ElasticSearch's `Date` datatype has only millisecond granularity and Jaeger requires microsecond granularity, Jaeger spans' `StartTime` is saved as a long type. The conversion is done automatically. ### Nested fields (tags) `Tags` are [nested](https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html) fields in the ElasticSearch schema used for Jaeger. This allows for better search capabilities and data retention. However, because ElasticSearch creates a new document for every nested field, there is currently a limit of 50 nested fields per document. ### Shards and Replicas Number of shards and replicas per index can be specified as parameters to the writer and/or through configs under `./internal/storage/elasticsearch/config/config.go`. If not specified, it defaults to ElasticSearch defaults: 5 shards and 1 replica. [This article](https://www.elastic.co/blog/how-many-shards-should-i-have-in-my-elasticsearch-cluster) goes into more information about choosing how many shards should be chosen for optimization. ## Limitations ### Tag query over multiple spans This plugin queries against spans. This means that all tags in a query must lie under the same span for a query to successfully return a trace. ### Case-sensitivity Queries are case-sensitive. For example, if a document with service name `ABC` is searched using a query `abc`, the document will not be retrieved. ## Testing To locally test the ElasticSearch storage plugin, * have [ElasticSearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/setup.html) running on port 9200 * run `STORAGE=es make storage-integration-test` in the top folder. All integration tests also run on pull request via GitHub Actions. This integration test is against ElasticSearch v7.x and v8.x. * The script used in GitHub Actions can be found under `scripts/e2e/elasticsearch.sh`, and that script be run from the top folder to integration test ElasticSearch as well. This script requires Docker to be running. ### Adding tests Integration test framework for storage lie under `../integration`. Add to `../integration/fixtures/traces/*.json` and `../integration/fixtures/queries.json` to add more trace cases. ================================================ FILE: internal/storage/v1/elasticsearch/dependencystore/package_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dependencystore import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/elasticsearch/dependencystore/storagev1.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dependencystore import ( "context" "time" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/api/dependencystore" "github.com/jaegertracing/jaeger/internal/storage/v2/elasticsearch/depstore" "github.com/jaegertracing/jaeger/internal/storage/v2/elasticsearch/depstore/dbmodel" ) var ( _ dependencystore.Reader = &StoreV1{} // check API conformance _ dependencystore.Writer = &StoreV1{} // check API conformance ) type StoreV1 struct { depStore depstore.CoreDependencyStore } // NewDependencyStoreV1 returns a StoreV1 func NewDependencyStoreV1(p depstore.Params) *StoreV1 { return &StoreV1{ depStore: depstore.NewDependencyStore(p), } } // WriteDependencies implements dependencystore.Writer#WriteDependencies. func (s *StoreV1) WriteDependencies(ts time.Time, dependencies []model.DependencyLink) error { dbDependencies := dbmodel.FromDomainDependencies(dependencies) return s.depStore.WriteDependencies(ts, dbDependencies) } // CreateTemplates creates index templates. func (s *StoreV1) CreateTemplates(dependenciesTemplate string) error { return s.depStore.CreateTemplates(dependenciesTemplate) } // GetDependencies returns all interservice dependencies func (s *StoreV1) GetDependencies(ctx context.Context, endTs time.Time, lookback time.Duration) ([]model.DependencyLink, error) { dbDependencies, err := s.depStore.GetDependencies(ctx, endTs, lookback) if err != nil { return nil, err } return dbmodel.ToDomainDependencies(dbDependencies), nil } ================================================ FILE: internal/storage/v1/elasticsearch/dependencystore/storagev1_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dependencystore import ( "context" "errors" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v2/elasticsearch/depstore/dbmodel" "github.com/jaegertracing/jaeger/internal/storage/v2/elasticsearch/depstore/mocks" ) func TestV1WriteDependencies(t *testing.T) { coreDependencyStore := &mocks.CoreDependencyStore{} depStore := StoreV1{depStore: coreDependencyStore} dependencies := []model.DependencyLink{ { Parent: "hello", Child: "world", CallCount: 12, }, } dbDependencies := dbmodel.FromDomainDependencies(dependencies) ts := time.Now() coreDependencyStore.On("WriteDependencies", ts, dbDependencies).Return(nil) err := depStore.WriteDependencies(ts, dependencies) require.NoError(t, err) } func TestV1CreateTemplates(t *testing.T) { coreDependencyStore := &mocks.CoreDependencyStore{} depStore := StoreV1{depStore: coreDependencyStore} templateName := "testing-template" coreDependencyStore.On("CreateTemplates", templateName).Return(nil) err := depStore.CreateTemplates(templateName) require.NoError(t, err) } func TestV1GetDependencies(t *testing.T) { tests := []struct { name string returningDependencies []dbmodel.DependencyLink returningErr error expectedDependencies []model.DependencyLink expectedErr string }{ { name: "no error", returningDependencies: []dbmodel.DependencyLink{ { Parent: "hello", Child: "world", CallCount: 12, }, }, expectedDependencies: []model.DependencyLink{ { Parent: "hello", Child: "world", CallCount: 12, }, }, }, { name: "error", returningErr: errors.New("some error"), expectedErr: "some error", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { coreDependencyStore := &mocks.CoreDependencyStore{} depStore := StoreV1{depStore: coreDependencyStore} ts := time.Now() drn := 24 * time.Hour coreDependencyStore.On("GetDependencies", mock.Anything, ts, drn).Return(test.returningDependencies, test.returningErr) deps, err := depStore.GetDependencies(context.Background(), ts, drn) if test.expectedErr != "" { require.ErrorContains(t, err, test.expectedErr) } else { require.NoError(t, err) assert.Equal(t, test.expectedDependencies, deps) } }) } } ================================================ FILE: internal/storage/v1/elasticsearch/factory.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "context" "errors" "fmt" "io" "os" "path/filepath" "strings" "sync/atomic" "go.opentelemetry.io/collector/config/configoptional" "go.opentelemetry.io/collector/extension/extensionauth" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/fswatcher" "github.com/jaegertracing/jaeger/internal/metrics" es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore" "github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch/mappings" essamplestore "github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch/samplingstore" esspanstore "github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch/spanstore" esdepstorev2 "github.com/jaegertracing/jaeger/internal/storage/v2/elasticsearch/depstore" ) var _ io.Closer = (*FactoryBase)(nil) // FactoryBase for Elasticsearch backend. type FactoryBase struct { metricsFactory metrics.Factory logger *zap.Logger tracer trace.TracerProvider newClientFn func(ctx context.Context, c *config.Configuration, logger *zap.Logger, metricsFactory metrics.Factory, httpAuth extensionauth.HTTPClient) (es.Client, error) config *config.Configuration client atomic.Pointer[es.Client] pwdFileWatcher *fswatcher.FSWatcher templateBuilder es.TemplateBuilder tags []string } func NewFactoryBase( ctx context.Context, cfg config.Configuration, metricsFactory metrics.Factory, logger *zap.Logger, httpAuth extensionauth.HTTPClient, ) (*FactoryBase, error) { f := &FactoryBase{ config: &cfg, newClientFn: config.NewClient, tracer: otel.GetTracerProvider(), } f.metricsFactory = metricsFactory f.logger = logger f.templateBuilder = es.TextTemplateBuilder{} tags, err := f.config.TagKeysAsFields() if err != nil { return nil, err } f.tags = tags client, err := f.newClientFn(ctx, f.config, logger, metricsFactory, httpAuth) if err != nil { return nil, fmt.Errorf("failed to create Elasticsearch client: %w", err) } f.client.Store(&client) if f.config.Authentication.BasicAuthentication.HasValue() { if file := f.config.Authentication.BasicAuthentication.Get().PasswordFilePath; file != "" { watcher, err := fswatcher.New([]string{file}, f.onPasswordChange, f.logger) if err != nil { return nil, fmt.Errorf("failed to create watcher for ES client's password: %w", err) } f.pwdFileWatcher = watcher } } err = f.createTemplates(ctx) if err != nil { return nil, err } return f, nil } func (f *FactoryBase) getClient() es.Client { if c := f.client.Load(); c != nil { return *c } return nil } // GetSpanReaderParams returns the SpanReaderParams which can be used to initialize the v1 and v2 readers. func (f *FactoryBase) GetSpanReaderParams() esspanstore.SpanReaderParams { return esspanstore.SpanReaderParams{ Client: f.getClient, MaxDocCount: f.config.MaxDocCount, MaxSpanAge: f.config.MaxSpanAge, IndexPrefix: f.config.Indices.IndexPrefix, SpanIndex: f.config.Indices.Spans, ServiceIndex: f.config.Indices.Services, TagDotReplacement: f.config.Tags.DotReplacement, UseReadWriteAliases: f.config.UseReadWriteAliases, ReadAliasSuffix: f.config.ReadAliasSuffix, RemoteReadClusters: f.config.RemoteReadClusters, SpanReadAlias: f.config.SpanReadAlias, ServiceReadAlias: f.config.ServiceReadAlias, Logger: f.logger, Tracer: f.tracer.Tracer("esspanstore.SpanReader"), } } // GetSpanWriterParams returns the SpanWriterParams which can be used to initialize the v1 and v2 writers. func (f *FactoryBase) GetSpanWriterParams() esspanstore.SpanWriterParams { return esspanstore.SpanWriterParams{ Client: f.getClient, IndexPrefix: f.config.Indices.IndexPrefix, SpanIndex: f.config.Indices.Spans, ServiceIndex: f.config.Indices.Services, AllTagsAsFields: f.config.Tags.AllAsFields, TagKeysAsFields: f.tags, TagDotReplacement: f.config.Tags.DotReplacement, UseReadWriteAliases: f.config.UseReadWriteAliases, WriteAliasSuffix: f.config.WriteAliasSuffix, SpanWriteAlias: f.config.SpanWriteAlias, ServiceWriteAlias: f.config.ServiceWriteAlias, Logger: f.logger, MetricsFactory: f.metricsFactory, ServiceCacheTTL: f.config.ServiceCacheTTL, } } // GetDependencyStoreParams returns the esdepstorev2.Params which can be used to initialize the v1 and v2 dependency stores. func (f *FactoryBase) GetDependencyStoreParams() esdepstorev2.Params { return esdepstorev2.Params{ Client: f.getClient, Logger: f.logger, IndexPrefix: f.config.Indices.IndexPrefix, IndexDateLayout: f.config.Indices.Dependencies.DateLayout, MaxDocCount: f.config.MaxDocCount, UseReadWriteAliases: f.config.UseReadWriteAliases, } } func (f *FactoryBase) CreateSamplingStore(int /* maxBuckets */) (samplingstore.Store, error) { params := essamplestore.Params{ Client: f.getClient, Logger: f.logger, IndexPrefix: f.config.Indices.IndexPrefix, IndexDateLayout: f.config.Indices.Sampling.DateLayout, IndexRolloverFrequency: config.RolloverFrequencyAsNegativeDuration(f.config.Indices.Sampling.RolloverFrequency), Lookback: f.config.AdaptiveSamplingLookback, MaxDocCount: f.config.MaxDocCount, } store := essamplestore.NewSamplingStore(params) if f.config.CreateIndexTemplates { mappingBuilder := f.mappingBuilderFromConfig(f.config) samplingMapping, err := mappingBuilder.GetSamplingMappings() if err != nil { return nil, err } if _, err := f.getClient().CreateTemplate(params.PrefixedIndexName()).Body(samplingMapping).Do(context.Background()); err != nil { return nil, fmt.Errorf("failed to create template: %w", err) } } return store, nil } func (f *FactoryBase) mappingBuilderFromConfig(cfg *config.Configuration) mappings.MappingBuilder { return mappings.MappingBuilder{ TemplateBuilder: f.templateBuilder, Indices: cfg.Indices, EsVersion: cfg.Version, UseILM: cfg.UseILM, } } // Close closes the resources held by the factory func (f *FactoryBase) Close() error { var errs []error if f.pwdFileWatcher != nil { errs = append(errs, f.pwdFileWatcher.Close()) } errs = append(errs, f.getClient().Close()) return errors.Join(errs...) } func (f *FactoryBase) onPasswordChange() { f.onClientPasswordChange(f.config, &f.client, f.metricsFactory) } func (f *FactoryBase) onClientPasswordChange(cfg *config.Configuration, client *atomic.Pointer[es.Client], mf metrics.Factory) { basicAuth := cfg.Authentication.BasicAuthentication.Get() newPassword, err := loadTokenFromFile(basicAuth.PasswordFilePath) if err != nil { f.logger.Error("failed to reload password for Elasticsearch client", zap.Error(err)) return } f.logger.Sugar().Infof("loaded new password of length %d from file", len(newPassword)) newCfg := *cfg // copy by value newCfg.Authentication.BasicAuthentication = configoptional.Some(config.BasicAuthentication{ Username: basicAuth.Username, Password: newPassword, PasswordFilePath: "", // avoid error that both are set }) newClient, err := f.newClientFn(context.Background(), &newCfg, f.logger, mf, nil) if err != nil { f.logger.Error("failed to recreate Elasticsearch client with new password", zap.Error(err)) return } if oldClient := *client.Swap(&newClient); oldClient != nil { if err := oldClient.Close(); err != nil { f.logger.Error("failed to close Elasticsearch client", zap.Error(err)) } } } func (f *FactoryBase) Purge(ctx context.Context) error { esClient := f.getClient() _, err := esClient.DeleteIndex("*").Do(ctx) return err } func loadTokenFromFile(path string) (string, error) { b, err := os.ReadFile(filepath.Clean(path)) if err != nil { return "", err } return strings.TrimRight(string(b), "\r\n"), nil } func (f *FactoryBase) createTemplates(ctx context.Context) error { if f.config.CreateIndexTemplates { mappingBuilder := f.mappingBuilderFromConfig(f.config) spanMapping, serviceMapping, err := mappingBuilder.GetSpanServiceMappings() if err != nil { return err } jaegerSpanIdx := f.config.Indices.IndexPrefix.Apply("jaeger-span") jaegerServiceIdx := f.config.Indices.IndexPrefix.Apply("jaeger-service") _, err = f.getClient().CreateTemplate(jaegerSpanIdx).Body(spanMapping).Do(ctx) if err != nil { return fmt.Errorf("failed to create template %q: %w", jaegerSpanIdx, err) } _, err = f.getClient().CreateTemplate(jaegerServiceIdx).Body(serviceMapping).Do(ctx) if err != nil { return fmt.Errorf("failed to create template %q: %w", jaegerServiceIdx, err) } } return nil } ================================================ FILE: internal/storage/v1/elasticsearch/factory_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "context" "encoding/base64" "errors" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "sync" "sync/atomic" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/config/configoptional" "go.opentelemetry.io/collector/extension/extensionauth" "go.opentelemetry.io/otel" "go.uber.org/zap" "go.uber.org/zap/zaptest" "github.com/jaegertracing/jaeger/internal/metrics" es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" escfg "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/mocks" "github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch/spanstore" esdepstorev2 "github.com/jaegertracing/jaeger/internal/storage/v2/elasticsearch/depstore" "github.com/jaegertracing/jaeger/internal/testutils" ) var mockEsServerResponse = []byte(` { "Version": { "Number": "6" } } `) func TestElasticsearchFactoryBase(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Write(mockEsServerResponse) })) t.Cleanup(server.Close) cfg := escfg.Configuration{ Servers: []string{server.URL}, LogLevel: "debug", } f, err := NewFactoryBase(context.Background(), cfg, metrics.NullFactory, zaptest.NewLogger(t), nil) require.NoError(t, err) readerParams := f.GetSpanReaderParams() assert.IsType(t, spanstore.SpanReaderParams{}, readerParams) writerParams := f.GetSpanWriterParams() assert.IsType(t, spanstore.SpanWriterParams{}, writerParams) depParams := f.GetDependencyStoreParams() assert.IsType(t, esdepstorev2.Params{}, depParams) _, err = f.CreateSamplingStore(1) require.NoError(t, err) require.NoError(t, f.Close()) } func TestFactoryBase_Purge(t *testing.T) { tests := []struct { name string setupMock func(*mocks.IndicesDeleteService) expectedErr bool }{ { name: "successful purge", setupMock: func(mockDelete *mocks.IndicesDeleteService) { mockDelete.On("Do", mock.Anything).Return(nil, nil) }, expectedErr: false, }, { name: "purge error", setupMock: func(mockDelete *mocks.IndicesDeleteService) { mockDelete.On("Do", mock.Anything).Return(nil, errors.New("delete error")) }, expectedErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create a real factory with a mock ES client mockClient := &mocks.Client{} mockDelete := &mocks.IndicesDeleteService{} mockClient.On("DeleteIndex", "*").Return(mockDelete) tt.setupMock(mockDelete) // Create a mock client that will be stored in the atomic.Pointer f := &FactoryBase{ client: atomic.Pointer[es.Client]{}, } // Create a concrete type that implements es.Client var client es.Client = mockClient // Store the client in the atomic.Pointer f.client.Store(&client) err := f.Purge(context.Background()) if tt.expectedErr { require.Error(t, err) } else { require.NoError(t, err) } // Verify the mock was called as expected mockClient.AssertExpectations(t) mockDelete.AssertExpectations(t) }) } } func TestElasticsearchTagsFileDoNotExist(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Write(mockEsServerResponse) })) t.Cleanup(server.Close) cfg := escfg.Configuration{ Servers: []string{server.URL}, Tags: escfg.TagsAsFields{ File: "fixtures/file-does-not-exist.txt", }, LogLevel: "debug", } f, err := NewFactoryBase(context.Background(), cfg, metrics.NullFactory, zaptest.NewLogger(t), nil) require.ErrorContains(t, err, "open fixtures/file-does-not-exist.txt: no such file or directory") assert.Nil(t, f) } func TestTagKeysAsFields(t *testing.T) { tests := []struct { path string include string expected []string errorExpected bool }{ { path: "fixtures/do_not_exists.txt", errorExpected: true, }, { path: "fixtures/tags_01.txt", expected: []string{"foo", "bar", "space"}, }, { path: "fixtures/tags_02.txt", expected: nil, }, { include: "televators,eriatarka,thewidow", expected: []string{"televators", "eriatarka", "thewidow"}, }, { expected: nil, }, { path: "fixtures/tags_01.txt", include: "televators,eriatarka,thewidow", expected: []string{"foo", "bar", "space", "televators", "eriatarka", "thewidow"}, }, { path: "fixtures/tags_02.txt", include: "televators,eriatarka,thewidow", expected: []string{"televators", "eriatarka", "thewidow"}, }, } for _, test := range tests { cfg := escfg.Configuration{ Tags: escfg.TagsAsFields{ File: test.path, Include: test.include, }, } tags, err := cfg.TagKeysAsFields() if test.errorExpected { require.Error(t, err) assert.Nil(t, tags) } else { require.NoError(t, err) assert.Equal(t, test.expected, tags) } } } func TestCreateTemplates(t *testing.T) { tests := []struct { err string spanTemplateService func() *mocks.TemplateCreateService serviceTemplateService func() *mocks.TemplateCreateService indexPrefix escfg.IndexPrefix }{ { spanTemplateService: func() *mocks.TemplateCreateService { tService := &mocks.TemplateCreateService{} tService.On("Body", mock.Anything).Return(tService) tService.On("Do", context.Background()).Return(nil, nil) return tService }, serviceTemplateService: func() *mocks.TemplateCreateService { tService := &mocks.TemplateCreateService{} tService.On("Body", mock.Anything).Return(tService) tService.On("Do", context.Background()).Return(nil, nil) return tService }, }, { spanTemplateService: func() *mocks.TemplateCreateService { tService := &mocks.TemplateCreateService{} tService.On("Body", mock.Anything).Return(tService) tService.On("Do", context.Background()).Return(nil, nil) return tService }, serviceTemplateService: func() *mocks.TemplateCreateService { tService := &mocks.TemplateCreateService{} tService.On("Body", mock.Anything).Return(tService) tService.On("Do", context.Background()).Return(nil, nil) return tService }, indexPrefix: "test", }, { err: "span-template-error", spanTemplateService: func() *mocks.TemplateCreateService { tService := new(mocks.TemplateCreateService) tService.On("Body", mock.Anything).Return(tService) tService.On("Do", context.Background()).Return(nil, errors.New("span-template-error")) return tService }, serviceTemplateService: func() *mocks.TemplateCreateService { tService := new(mocks.TemplateCreateService) tService.On("Body", mock.Anything).Return(tService) tService.On("Do", context.Background()).Return(nil, nil) return tService }, }, { err: "service-template-error", spanTemplateService: func() *mocks.TemplateCreateService { tService := new(mocks.TemplateCreateService) tService.On("Body", mock.Anything).Return(tService) tService.On("Do", context.Background()).Return(nil, nil) return tService }, serviceTemplateService: func() *mocks.TemplateCreateService { tService := new(mocks.TemplateCreateService) tService.On("Body", mock.Anything).Return(tService) tService.On("Do", context.Background()).Return(nil, errors.New("service-template-error")) return tService }, }, } for _, test := range tests { f := FactoryBase{} mockClient := &mocks.Client{} f.newClientFn = func(_ context.Context, _ *escfg.Configuration, _ *zap.Logger, _ metrics.Factory, _ extensionauth.HTTPClient) (es.Client, error) { return mockClient, nil } f.logger = zaptest.NewLogger(t) f.metricsFactory = metrics.NullFactory f.config = &escfg.Configuration{CreateIndexTemplates: true, Indices: escfg.Indices{ IndexPrefix: test.indexPrefix, Spans: escfg.IndexOptions{ Shards: 3, Replicas: new(int64(1)), Priority: 10, }, Services: escfg.IndexOptions{ Shards: 3, Replicas: new(int64(1)), Priority: 10, }, }} f.tracer = otel.GetTracerProvider() client, err := f.newClientFn(context.Background(), &escfg.Configuration{}, zaptest.NewLogger(t), metrics.NullFactory, nil) require.NoError(t, err) f.client.Store(&client) f.templateBuilder = es.TextTemplateBuilder{} jaegerSpanId := test.indexPrefix.Apply("jaeger-span") jaegerServiceId := test.indexPrefix.Apply("jaeger-service") mockClient.On("CreateTemplate", jaegerSpanId).Return(test.spanTemplateService()) mockClient.On("CreateTemplate", jaegerServiceId).Return(test.serviceTemplateService()) err = f.createTemplates(context.Background()) if test.err != "" { require.ErrorContains(t, err, test.err) } else { require.NoError(t, err) } } } func TestESStorageFactoryWithConfig(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Write(mockEsServerResponse) })) defer server.Close() cfg := escfg.Configuration{ Servers: []string{server.URL}, LogLevel: "error", } factory, err := NewFactoryBase(context.Background(), cfg, metrics.NullFactory, zap.NewNop(), nil) require.NoError(t, err) factory.Close() } func TestESStorageFactoryWithConfigError(t *testing.T) { t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { w.WriteHeader(http.StatusInternalServerError) return } })) defer server.Close() cfg := escfg.Configuration{ Servers: []string{server.URL}, DisableHealthCheck: true, LogLevel: "error", } _, err := NewFactoryBase(context.Background(), cfg, metrics.NullFactory, zap.NewNop(), nil) require.ErrorContains(t, err, "failed to create Elasticsearch client") } func TestPasswordFromFile(t *testing.T) { t.Cleanup(func() { testutils.VerifyGoLeaksOnce(t) }) t.Run("primary client", func(t *testing.T) { runPasswordFromFileTest(t) }) t.Run("load token error", func(t *testing.T) { file := filepath.Join(t.TempDir(), "does not exist") token, err := loadTokenFromFile(file) require.Error(t, err) assert.Empty(t, token) }) } func runPasswordFromFileTest(t *testing.T) { const ( pwd1 = "first password" pwd2 = "second password" // and with user name upwd1 = "user:" + pwd1 upwd2 = "user:" + pwd2 ) var authReceived sync.Map server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Logf("request to fake ES server: %v", r) // epecting header in the form Authorization:[Basic OmZpcnN0IHBhc3N3b3Jk] h := strings.Split(r.Header.Get("Authorization"), " ") if !assert.Len(t, h, 2) { return } assert.Equal(t, "Basic", h[0]) authBytes, err := base64.StdEncoding.DecodeString(h[1]) assert.NoError(t, err, "header: %s", h) auth := string(authBytes) authReceived.Store(auth, auth) t.Logf("request to fake ES server contained auth=%s", auth) w.Write(mockEsServerResponse) })) t.Cleanup(server.Close) pwdFile := filepath.Join(t.TempDir(), "pwd") require.NoError(t, os.WriteFile(pwdFile, []byte(pwd1), 0o600)) cfg := escfg.Configuration{ Servers: []string{server.URL}, LogLevel: "debug", Authentication: escfg.Authentication{ BasicAuthentication: configoptional.Some(escfg.BasicAuthentication{ Username: "user", PasswordFilePath: pwdFile, }), }, BulkProcessing: escfg.BulkProcessing{ MaxBytes: -1, // disable bulk MaxActions: -1, // disable bulk; the test only validates auth headers }, } f, err := NewFactoryBase(context.Background(), cfg, metrics.NullFactory, zap.NewNop(), nil) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, f.Close()) }) writer := spanstore.NewSpanWriter(f.GetSpanWriterParams()) span1 := &dbmodel.Span{ Process: dbmodel.Process{ServiceName: "foo"}, } writer.WriteSpan(time.Now(), span1) assert.Eventually(t, func() bool { pwd, ok := authReceived.Load(upwd1) return ok && pwd == upwd1 }, 5*time.Second, time.Millisecond, "expecting es.Client to send the first password", ) t.Log("replace password in the file") client1 := f.getClient() newPwdFile := filepath.Join(t.TempDir(), "pwd2") require.NoError(t, os.WriteFile(newPwdFile, []byte(pwd2), 0o600)) require.NoError(t, os.Rename(newPwdFile, pwdFile)) assert.Eventually(t, func() bool { client2 := f.getClient() return client1 != client2 }, 5*time.Second, time.Millisecond, "expecting es.Client to change for the new password", ) span2 := &dbmodel.Span{ Process: dbmodel.Process{ServiceName: "foo"}, } writer.WriteSpan(time.Now(), span2) assert.Eventually(t, func() bool { pwd, ok := authReceived.Load(upwd2) return ok && pwd == upwd2 }, 5*time.Second, time.Millisecond, "expecting es.Client to send the new password", ) } func TestFactoryESClientsAreNil(t *testing.T) { f := &FactoryBase{} assert.Nil(t, f.getClient()) } func TestPasswordFromFileErrors(t *testing.T) { defer testutils.VerifyGoLeaksOnce(t) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Write(mockEsServerResponse) })) defer server.Close() pwdFile := filepath.Join(t.TempDir(), "pwd") require.NoError(t, os.WriteFile(pwdFile, []byte("first password"), 0o600)) cfg := escfg.Configuration{ Servers: []string{server.URL}, LogLevel: "debug", Authentication: escfg.Authentication{ BasicAuthentication: configoptional.Some(escfg.BasicAuthentication{ PasswordFilePath: pwdFile, }), }, BulkProcessing: escfg.BulkProcessing{ MaxBytes: -1, // disable bulk MaxActions: -1, // disable bulk; the test only validates error paths }, } logger, buf := testutils.NewEchoLogger(t) f, err := NewFactoryBase(context.Background(), cfg, metrics.NullFactory, logger, nil) require.NoError(t, err) defer f.Close() f.config.Servers = []string{} f.onPasswordChange() assert.Contains(t, buf.String(), "no servers specified") require.NoError(t, os.Remove(pwdFile)) f.onPasswordChange() } func TestFactoryBase_NewClient_WatcherError(t *testing.T) { cfg := escfg.Configuration{ Servers: []string{"http://localhost:9200"}, LogLevel: "debug", Authentication: escfg.Authentication{ BasicAuthentication: configoptional.Some(escfg.BasicAuthentication{ Username: "testuser", PasswordFilePath: "/nonexistent/path/to/password.txt", }), }, } _, err := NewFactoryBase(context.Background(), cfg, metrics.NullFactory, zaptest.NewLogger(t), nil) require.Error(t, err) assert.Contains(t, err.Error(), "failed to initialize basic authentication") assert.Contains(t, err.Error(), "failed to get token from file") } func TestElasticsearchFactoryBaseWithAuthenticator(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Write(mockEsServerResponse) })) t.Cleanup(server.Close) cfg := escfg.Configuration{ Servers: []string{server.URL}, LogLevel: "debug", BulkProcessing: escfg.BulkProcessing{ MaxBytes: -1, // disable bulk MaxActions: -1, // disable bulk; the test only validates authenticator setup }, } // Mock authenticator mockAuth := &mockHTTPAuthenticator{} f, err := NewFactoryBase(context.Background(), cfg, metrics.NullFactory, zaptest.NewLogger(t), mockAuth) require.NoError(t, err) require.NotNil(t, f) defer require.NoError(t, f.Close()) // Verify factory is properly initialized with authenticator readerParams := f.GetSpanReaderParams() assert.IsType(t, spanstore.SpanReaderParams{}, readerParams) } // mockHTTPAuthenticator implements extensionauth.HTTPClient for testing type mockHTTPAuthenticator struct{} func (*mockHTTPAuthenticator) RoundTripper(base http.RoundTripper) (http.RoundTripper, error) { return &mockRoundTripper{base: base}, nil } // mockRoundTripper wraps the base RoundTripper type mockRoundTripper struct { base http.RoundTripper } func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { req.Header.Set("Authorization", "Bearer mock-token") if m.base != nil { return m.base.RoundTrip(req) } return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil } ================================================ FILE: internal/storage/v1/elasticsearch/fixtures/tags_01.txt ================================================ foo bar space ================================================ FILE: internal/storage/v1/elasticsearch/fixtures/tags_02.txt ================================================ ================================================ FILE: internal/storage/v1/elasticsearch/mappings/command.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package mappings import ( "fmt" "strconv" "github.com/spf13/cobra" es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" ) func Command() *cobra.Command { options := Options{} command := &cobra.Command{ Use: "elasticsearch-mappings", Short: "Jaeger esmapping-generator prints rendered mappings as string", Long: "Jaeger esmapping-generator renders passed templates with provided values and prints rendered output to stdout", RunE: func(_ *cobra.Command, _ /* args */ []string) error { result, err := generateMappings(options) if err != nil { return fmt.Errorf("error generating mappings: %w", err) } fmt.Println(result) return nil }, } options.AddFlags(command) return command } func generateMappings(options Options) (string, error) { if _, err := MappingTypeFromString(options.Mapping); err != nil { return "", fmt.Errorf("invalid mapping type '%s': please pass either 'jaeger-service' or 'jaeger-span' as the mapping type %w", options.Mapping, err) } parsedMapping, err := getMappingAsString(es.TextTemplateBuilder{}, options) if err != nil { return "", fmt.Errorf("failed to render mapping to string: %w", err) } return parsedMapping, nil } // getMappingAsString returns rendered index templates as string func getMappingAsString(builder es.TemplateBuilder, opt Options) (string, error) { enableILM, err := strconv.ParseBool(opt.UseILM) if err != nil { return "", err } indexOpts := config.IndexOptions{ Shards: opt.Shards, Replicas: opt.Replicas, } mappingBuilder := MappingBuilder{ TemplateBuilder: builder, Indices: config.Indices{ IndexPrefix: config.IndexPrefix(opt.IndexPrefix), Spans: indexOpts, Services: indexOpts, Dependencies: indexOpts, Sampling: indexOpts, }, EsVersion: opt.EsVersion, UseILM: enableILM, ILMPolicyName: opt.ILMPolicyName, } mappingType, err := MappingTypeFromString(opt.Mapping) if err != nil { return "", err } return mappingBuilder.GetMapping(mappingType) } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/command_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package mappings import ( "encoding/json" "errors" "io" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/mocks" ) func TestCommandExecute(t *testing.T) { cmd := Command() // TempFile to capture output tempFile, err := os.Create(t.TempDir() + "command-output-*.txt") require.NoError(t, err) // Redirect stdout to the TempFile oldStdout := os.Stdout os.Stdout = tempFile defer func() { os.Stdout = oldStdout }() err = cmd.ParseFlags([]string{ "--mapping=jaeger-span", "--es-version=7", "--shards=5", "--replicas=1", "--index-prefix=jaeger-index", "--use-ilm=false", "--ilm-policy-name=jaeger-ilm-policy", }) require.NoError(t, err) require.NoError(t, cmd.Execute()) output, err := os.ReadFile(tempFile.Name()) require.NoError(t, err) var jsonOutput map[string]any err = json.Unmarshal(output, &jsonOutput) require.NoError(t, err, "Output should be valid JSON") } func TestCommandExecuteError(t *testing.T) { cmd := Command() require.NoError(t, cmd.ParseFlags([]string{"--mapping=foobar"})) require.ErrorContains(t, cmd.Execute(), "foobar") } func TestIsValidOption(t *testing.T) { tests := []struct { name string arg string expectedValue bool }{ {name: "span mapping", arg: "jaeger-span", expectedValue: true}, {name: "service mapping", arg: "jaeger-service", expectedValue: true}, {name: "Invalid mapping", arg: "dependency-service", expectedValue: false}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { _, err := MappingTypeFromString(test.arg) if test.expectedValue { assert.NoError(t, err) } else { assert.Error(t, err) } }) } } func Test_getMappingAsString(t *testing.T) { tests := []struct { name string args Options want string wantErr error }{ { name: "ES version 7", args: Options{Mapping: "jaeger-span", EsVersion: 7, Shards: 5, Replicas: new(int64(1)), IndexPrefix: "test", UseILM: "true", ILMPolicyName: "jaeger-test-policy"}, want: "ES version 7", }, { name: "Parse Error version 7", args: Options{Mapping: "jaeger-span", EsVersion: 7, Shards: 5, Replicas: new(int64(1)), IndexPrefix: "test", UseILM: "true", ILMPolicyName: "jaeger-test-policy"}, wantErr: errors.New("parse error"), }, { name: "Parse bool error", args: Options{Mapping: "jaeger-span", EsVersion: 7, Shards: 5, Replicas: new(int64(1)), IndexPrefix: "test", UseILM: "foo", ILMPolicyName: "jaeger-test-policy"}, wantErr: errors.New("strconv.ParseBool: parsing \"foo\": invalid syntax"), }, { name: "Invalid Mapping type", args: Options{Mapping: "invalid-mapping", EsVersion: 7, Shards: 5, Replicas: new(int64(1)), IndexPrefix: "test", UseILM: "true", ILMPolicyName: "jaeger-test-policy"}, wantErr: errors.New("invalid mapping type: invalid-mapping"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Prepare mockTemplateApplier := &mocks.TemplateApplier{} mockTemplateApplier.On("Execute", mock.Anything, mock.Anything).Return( func(wr io.Writer, _ any) error { wr.Write([]byte(tt.want)) return nil }, ) mockTemplateBuilder := &mocks.TemplateBuilder{} mockTemplateBuilder.On("Parse", mock.Anything).Return(mockTemplateApplier, tt.wantErr) // Test got, err := getMappingAsString(mockTemplateBuilder, tt.args) // Validate if tt.wantErr != nil { require.EqualError(t, err, tt.wantErr.Error()) } else { require.NoError(t, err) } assert.Equal(t, tt.want, got) }) } } func TestGenerateMappings(t *testing.T) { tests := []struct { name string options Options expectErr string }{ { name: "bad ILM setting", options: Options{ Mapping: "jaeger-span", UseILM: "foobar", }, expectErr: "foobar", }, { name: "valid jaeger-span mapping", options: Options{ Mapping: "jaeger-span", EsVersion: 7, Shards: 5, Replicas: new(int64(1)), IndexPrefix: "jaeger-index", UseILM: "false", ILMPolicyName: "jaeger-ilm-policy", }, expectErr: "", }, { name: "valid jaeger-service mapping", options: Options{ Mapping: "jaeger-service", EsVersion: 7, Shards: 5, Replicas: new(int64(1)), IndexPrefix: "jaeger-service-index", UseILM: "true", ILMPolicyName: "service-ilm-policy", }, expectErr: "", }, { name: "invalid mapping type", options: Options{ Mapping: "invalid-mapping", }, expectErr: "invalid-mapping", }, { name: "missing mapping flag", options: Options{ Mapping: "", }, expectErr: "invalid mapping type ''", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := generateMappings(tt.options) if tt.expectErr != "" { require.ErrorContains(t, err, tt.expectErr) } else { require.NoError(t, err, "Did not expect an error") var parsed map[string]any err = json.Unmarshal([]byte(result), &parsed) require.NoError(t, err, "Expected valid JSON output") assert.NotEmpty(t, parsed["index_patterns"], "Expected index_patterns to be present") assert.NotEmpty(t, parsed["mappings"], "Expected mappings to be present") assert.NotEmpty(t, parsed["settings"], "Expected settings to be present") } }) } } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/fixtures/jaeger-dependencies-6.json ================================================ { "template": "*jaeger-dependencies-*", "settings":{ "index.number_of_shards": 3, "index.number_of_replicas": 3, "index.mapping.nested_fields.limit":50, "index.requests.cache.enable":true }, "mappings":{} } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/fixtures/jaeger-dependencies-7.json ================================================ { "index_patterns": "*jaeger-dependencies-*", "aliases": { "test-jaeger-dependencies-read" : {} }, "settings":{ "index.number_of_shards": 3, "index.number_of_replicas": 3, "index.mapping.nested_fields.limit":50, "index.requests.cache.enable":true ,"lifecycle": { "name": "jaeger-test-policy", "rollover_alias": "test-jaeger-dependencies-write" } }, "mappings":{} } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/fixtures/jaeger-dependencies-8.json ================================================ { "priority": 502, "index_patterns": "test-jaeger-dependencies-*", "template": { "aliases": { "test-jaeger-dependencies-read": {} }, "settings": { "index.number_of_shards": 3, "index.number_of_replicas": 3, "index.mapping.nested_fields.limit": 50, "index.requests.cache.enable": true, "lifecycle": { "name": "jaeger-test-policy", "rollover_alias": "test-jaeger-dependencies-write" } }, "mappings": {} } } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/fixtures/jaeger-sampling-6.json ================================================ { "template": "*jaeger-sampling-*", "settings":{ "index.number_of_shards": 3, "index.number_of_replicas": 3, "index.mapping.nested_fields.limit":50, "index.requests.cache.enable":true }, "mappings":{} } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/fixtures/jaeger-sampling-7.json ================================================ { "index_patterns": "*jaeger-sampling-*", "aliases": { "test-jaeger-sampling-read" : {} }, "settings":{ "index.number_of_shards": 3, "index.number_of_replicas": 3, "index.mapping.nested_fields.limit":50, "index.requests.cache.enable":true ,"lifecycle": { "name": "jaeger-test-policy", "rollover_alias": "test-jaeger-sampling-write" } }, "mappings":{} } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/fixtures/jaeger-sampling-8.json ================================================ { "priority": 503, "index_patterns": "test-jaeger-sampling-*", "template": { "aliases": { "test-jaeger-sampling-read": {} }, "settings": { "index.number_of_shards": 3, "index.number_of_replicas": 3, "index.mapping.nested_fields.limit": 50, "index.requests.cache.enable": true, "lifecycle": { "name": "jaeger-test-policy", "rollover_alias": "test-jaeger-sampling-write" } }, "mappings": {} } } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/fixtures/jaeger-service-6.json ================================================ { "template": "*jaeger-service-*", "settings":{ "index.number_of_shards": 3, "index.number_of_replicas": 3, "index.mapping.nested_fields.limit":50, "index.requests.cache.enable":true, "index.mapper.dynamic":false }, "mappings":{ "_default_":{ "_all":{ "enabled":false }, "dynamic_templates":[ { "span_tags_map":{ "mapping":{ "type":"keyword", "ignore_above":256 }, "path_match":"tag.*" } }, { "process_tags_map":{ "mapping":{ "type":"keyword", "ignore_above":256 }, "path_match":"process.tag.*" } } ] }, "service":{ "properties":{ "serviceName":{ "type":"keyword", "ignore_above":256 }, "operationName":{ "type":"keyword", "ignore_above":256 } } } } } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/fixtures/jaeger-service-7.json ================================================ { "index_patterns": "*test-jaeger-service-*", "aliases": { "test-jaeger-service-read" : {} }, "settings":{ "index.number_of_shards": 3, "index.number_of_replicas": 3, "index.mapping.nested_fields.limit":50, "index.requests.cache.enable":true ,"lifecycle": { "name": "jaeger-test-policy", "rollover_alias": "test-jaeger-service-write" } }, "mappings":{ "dynamic_templates":[ { "span_tags_map":{ "mapping":{ "type":"keyword", "ignore_above":256 }, "path_match":"tag.*" } }, { "process_tags_map":{ "mapping":{ "type":"keyword", "ignore_above":256 }, "path_match":"process.tag.*" } } ], "properties":{ "serviceName":{ "type":"keyword", "ignore_above":256 }, "operationName":{ "type":"keyword", "ignore_above":256 } } } } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/fixtures/jaeger-service-8.json ================================================ { "priority": 501, "index_patterns": "test-jaeger-service-*", "template": { "aliases": { "test-jaeger-service-read": {} }, "settings": { "index.number_of_shards": 3, "index.number_of_replicas": 3, "index.mapping.nested_fields.limit": 50, "index.requests.cache.enable": true, "lifecycle": { "name": "jaeger-test-policy", "rollover_alias": "test-jaeger-service-write" } }, "mappings": { "dynamic_templates": [ { "span_tags_map": { "mapping": { "type": "keyword", "ignore_above": 256 }, "path_match": "tag.*" } }, { "process_tags_map": { "mapping": { "type": "keyword", "ignore_above": 256 }, "path_match": "process.tag.*" } } ], "properties": { "serviceName": { "type": "keyword", "ignore_above": 256 }, "operationName": { "type": "keyword", "ignore_above": 256 } } } } } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/fixtures/jaeger-span-6.json ================================================ { "template": "*jaeger-span-*", "settings":{ "index.number_of_shards": 3, "index.number_of_replicas": 3, "index.mapping.nested_fields.limit":50, "index.requests.cache.enable":true, "index.mapper.dynamic":false }, "mappings":{ "_default_":{ "_all":{ "enabled":false }, "dynamic_templates":[ { "span_tags_map":{ "mapping":{ "type":"keyword", "ignore_above":256 }, "path_match":"tag.*" } }, { "process_tags_map":{ "mapping":{ "type":"keyword", "ignore_above":256 }, "path_match":"process.tag.*" } } ] }, "span":{ "properties":{ "traceID":{ "type":"keyword", "ignore_above":256 }, "parentSpanID":{ "type":"keyword", "ignore_above":256 }, "spanID":{ "type":"keyword", "ignore_above":256 }, "operationName":{ "type":"keyword", "ignore_above":256 }, "startTime":{ "type":"long" }, "startTimeMillis":{ "type":"date", "format":"epoch_millis" }, "duration":{ "type":"long" }, "flags":{ "type":"integer" }, "logs":{ "type":"nested", "dynamic":false, "properties":{ "timestamp":{ "type":"long" }, "fields":{ "type":"nested", "dynamic":false, "properties":{ "key":{ "type":"keyword", "ignore_above":256 }, "value":{ "type":"keyword", "ignore_above":256 }, "type":{ "type":"keyword", "ignore_above":256 } } } } }, "process":{ "properties":{ "serviceName":{ "type":"keyword", "ignore_above":256 }, "tag":{ "type":"object" }, "tags":{ "type":"nested", "dynamic":false, "properties":{ "key":{ "type":"keyword", "ignore_above":256 }, "value":{ "type":"keyword", "ignore_above":256 }, "type":{ "type":"keyword", "ignore_above":256 } } } } }, "references":{ "type":"nested", "dynamic":false, "properties":{ "refType":{ "type":"keyword", "ignore_above":256 }, "traceID":{ "type":"keyword", "ignore_above":256 }, "spanID":{ "type":"keyword", "ignore_above":256 } } }, "tag":{ "type":"object" }, "tags":{ "type":"nested", "dynamic":false, "properties":{ "key":{ "type":"keyword", "ignore_above":256 }, "value":{ "type":"keyword", "ignore_above":256 }, "type":{ "type":"keyword", "ignore_above":256 } } } } } } } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/fixtures/jaeger-span-7.json ================================================ { "index_patterns": "*test-jaeger-span-*", "aliases": { "test-jaeger-span-read": {} }, "settings":{ "index.number_of_shards": 3, "index.number_of_replicas": 3, "index.mapping.nested_fields.limit":50, "index.requests.cache.enable":true ,"lifecycle": { "name": "jaeger-test-policy", "rollover_alias": "test-jaeger-span-write" } }, "mappings":{ "dynamic_templates":[ { "span_tags_map":{ "mapping":{ "type":"keyword", "ignore_above":256 }, "path_match":"tag.*" } }, { "process_tags_map":{ "mapping":{ "type":"keyword", "ignore_above":256 }, "path_match":"process.tag.*" } } ], "properties":{ "traceID":{ "type":"keyword", "ignore_above":256 }, "parentSpanID":{ "type":"keyword", "ignore_above":256 }, "spanID":{ "type":"keyword", "ignore_above":256 }, "operationName":{ "type":"keyword", "ignore_above":256 }, "startTime":{ "type":"long" }, "startTimeMillis":{ "type":"date", "format":"epoch_millis" }, "duration":{ "type":"long" }, "flags":{ "type":"integer" }, "logs":{ "type":"nested", "dynamic":false, "properties":{ "timestamp":{ "type":"long" }, "fields":{ "type":"nested", "dynamic":false, "properties":{ "key":{ "type":"keyword", "ignore_above":256 }, "value":{ "type":"keyword", "ignore_above":256 }, "type":{ "type":"keyword", "ignore_above":256 } } } } }, "process":{ "properties":{ "serviceName":{ "type":"keyword", "ignore_above":256 }, "tag":{ "type":"object" }, "tags":{ "type":"nested", "dynamic":false, "properties":{ "key":{ "type":"keyword", "ignore_above":256 }, "value":{ "type":"keyword", "ignore_above":256 }, "type":{ "type":"keyword", "ignore_above":256 } } } } }, "references":{ "type":"nested", "dynamic":false, "properties":{ "refType":{ "type":"keyword", "ignore_above":256 }, "traceID":{ "type":"keyword", "ignore_above":256 }, "spanID":{ "type":"keyword", "ignore_above":256 } } }, "tag":{ "type":"object" }, "tags":{ "type":"nested", "dynamic":false, "properties":{ "key":{ "type":"keyword", "ignore_above":256 }, "value":{ "type":"keyword", "ignore_above":256 }, "type":{ "type":"keyword", "ignore_above":256 } } } } } } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/fixtures/jaeger-span-8.json ================================================ { "priority": 500, "index_patterns": "test-jaeger-span-*", "template": { "aliases": { "test-jaeger-span-read": {} }, "settings": { "index.number_of_shards": 3, "index.number_of_replicas": 3, "index.mapping.nested_fields.limit": 50, "index.requests.cache.enable": true, "lifecycle": { "name": "jaeger-test-policy", "rollover_alias": "test-jaeger-span-write" } }, "mappings": { "dynamic_templates": [ { "span_tags_map": { "mapping": { "type": "keyword", "ignore_above": 256 }, "path_match": "tag.*" } }, { "process_tags_map": { "mapping": { "type": "keyword", "ignore_above": 256 }, "path_match": "process.tag.*" } } ], "properties": { "traceID": { "type": "keyword", "ignore_above": 256 }, "parentSpanID": { "type": "keyword", "ignore_above": 256 }, "spanID": { "type": "keyword", "ignore_above": 256 }, "operationName": { "type": "keyword", "ignore_above": 256 }, "startTime": { "type": "long" }, "startTimeMillis": { "type": "date", "format": "epoch_millis" }, "duration": { "type": "long" }, "flags": { "type": "integer" }, "logs": { "type": "nested", "dynamic": false, "properties": { "timestamp": { "type": "long" }, "fields": { "type": "nested", "dynamic": false, "properties": { "key": { "type": "keyword", "ignore_above": 256 }, "value": { "type": "keyword", "ignore_above": 256 }, "type": { "type": "keyword", "ignore_above": 256 } } } } }, "process": { "properties": { "serviceName": { "type": "keyword", "ignore_above": 256 }, "tag": { "type": "object" }, "tags": { "type": "nested", "dynamic": false, "properties": { "key": { "type": "keyword", "ignore_above": 256 }, "value": { "type": "keyword", "ignore_above": 256 }, "type": { "type": "keyword", "ignore_above": 256 } } } } }, "references": { "type": "nested", "dynamic": false, "properties": { "refType": { "type": "keyword", "ignore_above": 256 }, "traceID": { "type": "keyword", "ignore_above": 256 }, "spanID": { "type": "keyword", "ignore_above": 256 } } }, "tag": { "type": "object" }, "tags": { "type": "nested", "dynamic": false, "properties": { "key": { "type": "keyword", "ignore_above": 256 }, "value": { "type": "keyword", "ignore_above": 256 }, "type": { "type": "keyword", "ignore_above": 256 } } } } } } } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/flags.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package mappings import ( "github.com/spf13/cobra" ) // Options represent configurable parameters for jaeger-esmapping-generator type Options struct { Mapping string EsVersion uint Shards int64 Replicas *int64 IndexPrefix string UseILM string // using string as util is being used in python and using bool leads to type issues. ILMPolicyName string } const ( mappingFlag = "mapping" esVersionFlag = "es-version" shardsFlag = "shards" replicasFlag = "replicas" indexPrefixFlag = "index-prefix" useILMFlag = "use-ilm" ilmPolicyNameFlag = "ilm-policy-name" ) // AddFlags adds flags for esmapping-generator main program func (o *Options) AddFlags(command *cobra.Command) { command.Flags().StringVar( &o.Mapping, mappingFlag, "", "The index mapping the template will be applied to. Pass either jaeger-span or jaeger-service") command.Flags().UintVar( &o.EsVersion, esVersionFlag, 7, "The major Elasticsearch version") command.Flags().Int64Var( &o.Shards, shardsFlag, 5, "The number of shards per index in Elasticsearch") // Allocate storage for Replicas so Int64Var can write into it. o.Replicas = new(int64) command.Flags().Int64Var( o.Replicas, replicasFlag, 1, "The number of replicas per index in Elasticsearch") command.Flags().StringVar( &o.IndexPrefix, indexPrefixFlag, "", "Specifies index prefix") command.Flags().StringVar( &o.UseILM, useILMFlag, "false", "Set to true to use ILM for managing lifecycle of jaeger indices") command.Flags().StringVar( &o.ILMPolicyName, ilmPolicyNameFlag, "jaeger-ilm-policy", "The name of the ILM policy to use if ILM is active") // mark mapping flag as mandatory command.MarkFlagRequired(mappingFlag) } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/flags_test.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package mappings import ( "testing" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestOptionsWithDefaultFlags(t *testing.T) { o := Options{} c := cobra.Command{} o.AddFlags(&c) assert.Empty(t, o.Mapping) assert.Equal(t, uint(7), o.EsVersion) assert.EqualValues(t, 5, o.Shards) assert.EqualValues(t, 1, *o.Replicas) assert.Empty(t, o.IndexPrefix) assert.Equal(t, "false", o.UseILM) assert.Equal(t, "jaeger-ilm-policy", o.ILMPolicyName) } func TestOptionsWithFlags(t *testing.T) { o := Options{} c := cobra.Command{} o.AddFlags(&c) err := c.ParseFlags([]string{ "--mapping=jaeger-span", "--es-version=7", "--shards=5", "--replicas=1", "--index-prefix=test", "--use-ilm=true", "--ilm-policy-name=jaeger-test-policy", }) require.NoError(t, err) assert.Equal(t, "jaeger-span", o.Mapping) assert.Equal(t, uint(7), o.EsVersion) assert.Equal(t, int64(5), o.Shards) assert.Equal(t, int64(1), *o.Replicas) assert.Equal(t, "test", o.IndexPrefix) assert.Equal(t, "true", o.UseILM) assert.Equal(t, "jaeger-test-policy", o.ILMPolicyName) } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/jaeger-dependencies-6.json ================================================ { "template": "*jaeger-dependencies-*", "settings":{ "index.number_of_shards": {{ .Shards }}, "index.number_of_replicas": {{ .Replicas }}, "index.mapping.nested_fields.limit":50, "index.requests.cache.enable":true }, "mappings":{} } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/jaeger-dependencies-7.json ================================================ { "index_patterns": "*jaeger-dependencies-*", {{- if .UseILM }} "aliases": { "{{ .IndexPrefix }}jaeger-dependencies-read" : {} }, {{- end }} "settings":{ "index.number_of_shards": {{ .Shards }}, "index.number_of_replicas": {{ .Replicas }}, "index.mapping.nested_fields.limit":50, "index.requests.cache.enable":true {{- if .UseILM }} ,"lifecycle": { "name": "{{ .ILMPolicyName }}", "rollover_alias": "{{ .IndexPrefix }}jaeger-dependencies-write" } {{- end }} }, "mappings":{} } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/jaeger-dependencies-8.json ================================================ { "priority": {{ .Priority }}, "index_patterns": "{{ .IndexPrefix }}jaeger-dependencies-*", "template": { {{- if .UseILM }} "aliases": { "{{ .IndexPrefix }}jaeger-dependencies-read": {} }, {{- end }} "settings": { "index.number_of_shards": {{ .Shards }}, "index.number_of_replicas": {{ .Replicas }}, "index.mapping.nested_fields.limit": 50, "index.requests.cache.enable": true {{- if .UseILM }}, "lifecycle": { "name": "{{ .ILMPolicyName }}", "rollover_alias": "{{ .IndexPrefix }}jaeger-dependencies-write" } {{- end }} }, "mappings": {} } } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/jaeger-sampling-6.json ================================================ { "template": "*jaeger-sampling-*", "settings":{ "index.number_of_shards": {{ .Shards }}, "index.number_of_replicas": {{ .Replicas }}, "index.mapping.nested_fields.limit":50, "index.requests.cache.enable":false }, "mappings":{} } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/jaeger-sampling-7.json ================================================ { "index_patterns": "*jaeger-sampling-*", {{- if .UseILM }} "aliases": { "{{ .IndexPrefix }}jaeger-sampling-read" : {} }, {{- end }} "settings":{ "index.number_of_shards": {{ .Shards }}, "index.number_of_replicas": {{ .Replicas }}, "index.mapping.nested_fields.limit":50, "index.requests.cache.enable":false {{- if .UseILM }} ,"lifecycle": { "name": "{{ .ILMPolicyName }}", "rollover_alias": "{{ .IndexPrefix }}jaeger-sampling-write" } {{- end }} }, "mappings":{} } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/jaeger-sampling-8.json ================================================ { "priority": {{ .Priority }}, "index_patterns": "{{ .IndexPrefix }}jaeger-sampling-*", "template": { {{- if .UseILM }} "aliases": { "{{ .IndexPrefix }}jaeger-sampling-read": {} }, {{- end }} "settings": { "index.number_of_shards": {{ .Shards }}, "index.number_of_replicas": {{ .Replicas }}, "index.mapping.nested_fields.limit": 50, "index.requests.cache.enable": false {{- if .UseILM }}, "lifecycle": { "name": "{{ .ILMPolicyName }}", "rollover_alias": "{{ .IndexPrefix }}jaeger-sampling-write" } {{- end }} }, "mappings": {} } } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/jaeger-service-6.json ================================================ { "template": "*jaeger-service-*", "settings":{ "index.number_of_shards": {{ .Shards }}, "index.number_of_replicas": {{ .Replicas }}, "index.mapping.nested_fields.limit":50, "index.requests.cache.enable":true, "index.mapper.dynamic":false }, "mappings":{ "_default_":{ "_all":{ "enabled":false }, "dynamic_templates":[ { "span_tags_map":{ "mapping":{ "type":"keyword", "ignore_above":256 }, "path_match":"tag.*" } }, { "process_tags_map":{ "mapping":{ "type":"keyword", "ignore_above":256 }, "path_match":"process.tag.*" } } ] }, "service":{ "properties":{ "serviceName":{ "type":"keyword", "ignore_above":256 }, "operationName":{ "type":"keyword", "ignore_above":256 } } } } } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/jaeger-service-7.json ================================================ { "index_patterns": "*{{ .IndexPrefix }}jaeger-service-*", {{- if .UseILM }} "aliases": { "{{ .IndexPrefix }}jaeger-service-read" : {} }, {{- end }} "settings":{ "index.number_of_shards": {{ .Shards }}, "index.number_of_replicas": {{ .Replicas }}, "index.mapping.nested_fields.limit":50, "index.requests.cache.enable":true {{- if .UseILM }} ,"lifecycle": { "name": "{{ .ILMPolicyName }}", "rollover_alias": "{{ .IndexPrefix }}jaeger-service-write" } {{- end }} }, "mappings":{ "dynamic_templates":[ { "span_tags_map":{ "mapping":{ "type":"keyword", "ignore_above":256 }, "path_match":"tag.*" } }, { "process_tags_map":{ "mapping":{ "type":"keyword", "ignore_above":256 }, "path_match":"process.tag.*" } } ], "properties":{ "serviceName":{ "type":"keyword", "ignore_above":256 }, "operationName":{ "type":"keyword", "ignore_above":256 } } } } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/jaeger-service-8.json ================================================ { "priority": {{ .Priority }}, "index_patterns": "{{ .IndexPrefix }}jaeger-service-*", "template": { {{- if .UseILM }} "aliases": { "{{ .IndexPrefix }}jaeger-service-read": {} }, {{- end }} "settings": { "index.number_of_shards": {{ .Shards }}, "index.number_of_replicas": {{ .Replicas }}, "index.mapping.nested_fields.limit": 50, "index.requests.cache.enable": true {{- if .UseILM }}, "lifecycle": { "name": "{{ .ILMPolicyName }}", "rollover_alias": "{{ .IndexPrefix }}jaeger-service-write" } {{- end }} }, "mappings": { "dynamic_templates": [ { "span_tags_map": { "mapping": { "type": "keyword", "ignore_above": 256 }, "path_match": "tag.*" } }, { "process_tags_map": { "mapping": { "type": "keyword", "ignore_above": 256 }, "path_match": "process.tag.*" } } ], "properties": { "serviceName": { "type": "keyword", "ignore_above": 256 }, "operationName": { "type": "keyword", "ignore_above": 256 } } } } } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/jaeger-span-6.json ================================================ { "template": "*jaeger-span-*", "settings":{ "index.number_of_shards": {{ .Shards }}, "index.number_of_replicas": {{ .Replicas }}, "index.mapping.nested_fields.limit":50, "index.requests.cache.enable":true, "index.mapper.dynamic":false }, "mappings":{ "_default_":{ "_all":{ "enabled":false }, "dynamic_templates":[ { "span_tags_map":{ "mapping":{ "type":"keyword", "ignore_above":256 }, "path_match":"tag.*" } }, { "process_tags_map":{ "mapping":{ "type":"keyword", "ignore_above":256 }, "path_match":"process.tag.*" } } ] }, "span":{ "properties":{ "traceID":{ "type":"keyword", "ignore_above":256 }, "parentSpanID":{ "type":"keyword", "ignore_above":256 }, "spanID":{ "type":"keyword", "ignore_above":256 }, "operationName":{ "type":"keyword", "ignore_above":256 }, "startTime":{ "type":"long" }, "startTimeMillis":{ "type":"date", "format":"epoch_millis" }, "duration":{ "type":"long" }, "flags":{ "type":"integer" }, "logs":{ "type":"nested", "dynamic":false, "properties":{ "timestamp":{ "type":"long" }, "fields":{ "type":"nested", "dynamic":false, "properties":{ "key":{ "type":"keyword", "ignore_above":256 }, "value":{ "type":"keyword", "ignore_above":256 }, "type":{ "type":"keyword", "ignore_above":256 } } } } }, "process":{ "properties":{ "serviceName":{ "type":"keyword", "ignore_above":256 }, "tag":{ "type":"object" }, "tags":{ "type":"nested", "dynamic":false, "properties":{ "key":{ "type":"keyword", "ignore_above":256 }, "value":{ "type":"keyword", "ignore_above":256 }, "type":{ "type":"keyword", "ignore_above":256 } } } } }, "references":{ "type":"nested", "dynamic":false, "properties":{ "refType":{ "type":"keyword", "ignore_above":256 }, "traceID":{ "type":"keyword", "ignore_above":256 }, "spanID":{ "type":"keyword", "ignore_above":256 } } }, "tag":{ "type":"object" }, "tags":{ "type":"nested", "dynamic":false, "properties":{ "key":{ "type":"keyword", "ignore_above":256 }, "value":{ "type":"keyword", "ignore_above":256 }, "type":{ "type":"keyword", "ignore_above":256 } } } } } } } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/jaeger-span-7.json ================================================ { "index_patterns": "*{{ .IndexPrefix }}jaeger-span-*", {{- if .UseILM }} "aliases": { "{{ .IndexPrefix }}jaeger-span-read": {} }, {{- end }} "settings":{ "index.number_of_shards": {{ .Shards }}, "index.number_of_replicas": {{ .Replicas }}, "index.mapping.nested_fields.limit":50, "index.requests.cache.enable":true {{- if .UseILM }} ,"lifecycle": { "name": "{{ .ILMPolicyName }}", "rollover_alias": "{{ .IndexPrefix }}jaeger-span-write" } {{- end }} }, "mappings":{ "dynamic_templates":[ { "span_tags_map":{ "mapping":{ "type":"keyword", "ignore_above":256 }, "path_match":"tag.*" } }, { "process_tags_map":{ "mapping":{ "type":"keyword", "ignore_above":256 }, "path_match":"process.tag.*" } } ], "properties":{ "traceID":{ "type":"keyword", "ignore_above":256 }, "parentSpanID":{ "type":"keyword", "ignore_above":256 }, "spanID":{ "type":"keyword", "ignore_above":256 }, "operationName":{ "type":"keyword", "ignore_above":256 }, "startTime":{ "type":"long" }, "startTimeMillis":{ "type":"date", "format":"epoch_millis" }, "duration":{ "type":"long" }, "flags":{ "type":"integer" }, "logs":{ "type":"nested", "dynamic":false, "properties":{ "timestamp":{ "type":"long" }, "fields":{ "type":"nested", "dynamic":false, "properties":{ "key":{ "type":"keyword", "ignore_above":256 }, "value":{ "type":"keyword", "ignore_above":256 }, "type":{ "type":"keyword", "ignore_above":256 } } } } }, "process":{ "properties":{ "serviceName":{ "type":"keyword", "ignore_above":256 }, "tag":{ "type":"object" }, "tags":{ "type":"nested", "dynamic":false, "properties":{ "key":{ "type":"keyword", "ignore_above":256 }, "value":{ "type":"keyword", "ignore_above":256 }, "type":{ "type":"keyword", "ignore_above":256 } } } } }, "references":{ "type":"nested", "dynamic":false, "properties":{ "refType":{ "type":"keyword", "ignore_above":256 }, "traceID":{ "type":"keyword", "ignore_above":256 }, "spanID":{ "type":"keyword", "ignore_above":256 } } }, "tag":{ "type":"object" }, "tags":{ "type":"nested", "dynamic":false, "properties":{ "key":{ "type":"keyword", "ignore_above":256 }, "value":{ "type":"keyword", "ignore_above":256 }, "type":{ "type":"keyword", "ignore_above":256 } } } } } } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/jaeger-span-8.json ================================================ { "priority": {{ .Priority }}, "index_patterns": "{{ .IndexPrefix }}jaeger-span-*", "template": { {{- if .UseILM}} "aliases": { "{{ .IndexPrefix }}jaeger-span-read": {} }, {{- end}} "settings": { "index.number_of_shards": {{ .Shards }}, "index.number_of_replicas": {{ .Replicas }}, "index.mapping.nested_fields.limit": 50, "index.requests.cache.enable": true {{- if .UseILM }}, "lifecycle": { "name": "{{ .ILMPolicyName }}", "rollover_alias": "{{ .IndexPrefix }}jaeger-span-write" } {{- end }} }, "mappings": { "dynamic_templates": [ { "span_tags_map": { "mapping": { "type": "keyword", "ignore_above": 256 }, "path_match": "tag.*" } }, { "process_tags_map": { "mapping": { "type": "keyword", "ignore_above": 256 }, "path_match": "process.tag.*" } } ], "properties": { "traceID": { "type": "keyword", "ignore_above": 256 }, "parentSpanID": { "type": "keyword", "ignore_above": 256 }, "spanID": { "type": "keyword", "ignore_above": 256 }, "operationName": { "type": "keyword", "ignore_above": 256 }, "startTime": { "type": "long" }, "startTimeMillis": { "type": "date", "format": "epoch_millis" }, "duration": { "type": "long" }, "flags": { "type": "integer" }, "logs": { "type": "nested", "dynamic": false, "properties": { "timestamp": { "type": "long" }, "fields": { "type": "nested", "dynamic": false, "properties": { "key": { "type": "keyword", "ignore_above": 256 }, "value": { "type": "keyword", "ignore_above": 256 }, "type": { "type": "keyword", "ignore_above": 256 } } } } }, "process": { "properties": { "serviceName": { "type": "keyword", "ignore_above": 256 }, "tag": { "type": "object" }, "tags": { "type": "nested", "dynamic": false, "properties": { "key": { "type": "keyword", "ignore_above": 256 }, "value": { "type": "keyword", "ignore_above": 256 }, "type": { "type": "keyword", "ignore_above": 256 } } } } }, "references": { "type": "nested", "dynamic": false, "properties": { "refType": { "type": "keyword", "ignore_above": 256 }, "traceID": { "type": "keyword", "ignore_above": 256 }, "spanID": { "type": "keyword", "ignore_above": 256 } } }, "tag": { "type": "object" }, "tags": { "type": "nested", "dynamic": false, "properties": { "key": { "type": "keyword", "ignore_above": 256 }, "value": { "type": "keyword", "ignore_above": 256 }, "type": { "type": "keyword", "ignore_above": 256 } } } } } } } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/mapping.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package mappings import ( "bytes" "embed" "fmt" es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" ) // MAPPINGS contains embedded index templates. // //go:embed *.json var MAPPINGS embed.FS // MappingType represents the type of Elasticsearch mapping type MappingType int const ( SpanMapping MappingType = iota ServiceMapping DependenciesMapping SamplingMapping ) // MappingBuilder holds common parameters required to render an elasticsearch index template type MappingBuilder struct { TemplateBuilder es.TemplateBuilder Indices config.Indices EsVersion uint UseILM bool ILMPolicyName string } // templateParams holds parameters required to render an elasticsearch index template type templateParams struct { UseILM bool ILMPolicyName string IndexPrefix string Shards int64 Replicas int64 Priority int64 } func (mb MappingBuilder) getMappingTemplateOptions(mappingType MappingType) templateParams { mappingOpts := templateParams{} mappingOpts.UseILM = mb.UseILM mappingOpts.ILMPolicyName = mb.ILMPolicyName switch mappingType { case SpanMapping: mappingOpts.Shards = mb.Indices.Spans.Shards mappingOpts.Replicas = *mb.Indices.Spans.Replicas mappingOpts.Priority = mb.Indices.Spans.Priority case ServiceMapping: mappingOpts.Shards = mb.Indices.Services.Shards mappingOpts.Replicas = *mb.Indices.Services.Replicas mappingOpts.Priority = mb.Indices.Services.Priority case DependenciesMapping: mappingOpts.Shards = mb.Indices.Dependencies.Shards mappingOpts.Replicas = *mb.Indices.Dependencies.Replicas mappingOpts.Priority = mb.Indices.Dependencies.Priority case SamplingMapping: mappingOpts.Shards = mb.Indices.Sampling.Shards mappingOpts.Replicas = *mb.Indices.Sampling.Replicas mappingOpts.Priority = mb.Indices.Sampling.Priority default: // Using default values as fallback to avoid breaking functionality. mappingOpts.Shards = 5 mappingOpts.Replicas = 1 mappingOpts.Priority = 0 } return mappingOpts } func (mt MappingType) String() string { switch mt { case SpanMapping: return "jaeger-span" case ServiceMapping: return "jaeger-service" case DependenciesMapping: return "jaeger-dependencies" case SamplingMapping: return "jaeger-sampling" default: return "unknown" } } // MappingTypeFromString converts a string to a MappingType func MappingTypeFromString(val string) (MappingType, error) { switch val { case "jaeger-span": return SpanMapping, nil case "jaeger-service": return ServiceMapping, nil case "jaeger-dependencies": return DependenciesMapping, nil case "jaeger-sampling": return SamplingMapping, nil default: return -1, fmt.Errorf("invalid mapping type: %s", val) } } // GetMapping returns the rendered mapping based on elasticsearch version func (mb *MappingBuilder) GetMapping(mappingType MappingType) (string, error) { templateOpts := mb.getMappingTemplateOptions(mappingType) esVersion := min(mb.EsVersion, 8) // Elasticsearch v9 uses the same template as v8 return mb.renderMapping(fmt.Sprintf("%s-%d.json", mappingType.String(), esVersion), templateOpts) } // GetSpanServiceMappings returns span and service mappings func (mb *MappingBuilder) GetSpanServiceMappings() (spanMapping string, serviceMapping string, err error) { spanMapping, err = mb.GetMapping(SpanMapping) if err != nil { return "", "", err } serviceMapping, err = mb.GetMapping(ServiceMapping) if err != nil { return "", "", err } return spanMapping, serviceMapping, nil } // GetDependenciesMappings returns dependencies mappings func (mb *MappingBuilder) GetDependenciesMappings() (string, error) { return mb.GetMapping(DependenciesMapping) } // GetSamplingMappings returns sampling mappings func (mb *MappingBuilder) GetSamplingMappings() (string, error) { return mb.GetMapping(SamplingMapping) } func loadMapping(name string) string { s, _ := MAPPINGS.ReadFile(name) return string(s) } func (mb *MappingBuilder) renderMapping(mapping string, options templateParams) (string, error) { tmpl, err := mb.TemplateBuilder.Parse(loadMapping(mapping)) if err != nil { return "", err } writer := new(bytes.Buffer) options.IndexPrefix = mb.Indices.IndexPrefix.Apply("") if err := tmpl.Execute(writer, options); err != nil { return "", err } return writer.String(), nil } ================================================ FILE: internal/storage/v1/elasticsearch/mappings/mapping_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package mappings import ( "embed" "errors" "fmt" "io" "os" "testing" "text/template" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/mocks" "github.com/jaegertracing/jaeger/internal/testutils" ) //go:embed fixtures/*.json var FIXTURES embed.FS func TestMappingBuilderGetMapping(t *testing.T) { tests := []struct { mapping MappingType esVersion uint }{ {mapping: SpanMapping, esVersion: 8}, {mapping: SpanMapping, esVersion: 7}, {mapping: SpanMapping, esVersion: 6}, {mapping: ServiceMapping, esVersion: 8}, {mapping: ServiceMapping, esVersion: 7}, {mapping: ServiceMapping, esVersion: 6}, {mapping: DependenciesMapping, esVersion: 8}, {mapping: DependenciesMapping, esVersion: 7}, {mapping: DependenciesMapping, esVersion: 6}, } for _, tt := range tests { templateName := tt.mapping.String() t.Run(templateName, func(t *testing.T) { defaultOpts := func(p int64) config.IndexOptions { return config.IndexOptions{ Shards: 3, Replicas: new(int64(3)), Priority: p, } } serviceOps := defaultOpts(501) dependenciesOps := defaultOpts(502) samplingOps := defaultOpts(503) mb := &MappingBuilder{ TemplateBuilder: es.TextTemplateBuilder{}, Indices: config.Indices{ IndexPrefix: "test-", Spans: defaultOpts(500), Services: serviceOps, Dependencies: dependenciesOps, Sampling: samplingOps, }, EsVersion: tt.esVersion, UseILM: true, ILMPolicyName: "jaeger-test-policy", } got, err := mb.GetMapping(tt.mapping) require.NoError(t, err) var wantbytes []byte fileSuffix := fmt.Sprintf("-%d", tt.esVersion) wantbytes, err = FIXTURES.ReadFile("fixtures/" + templateName + fileSuffix + ".json") require.NoError(t, err) want := string(wantbytes) assert.Equal(t, want, got) }) } } func TestMappingTypeFromString(t *testing.T) { tests := []struct { input string expected MappingType hasError bool }{ {"jaeger-span", SpanMapping, false}, {"jaeger-service", ServiceMapping, false}, {"jaeger-dependencies", DependenciesMapping, false}, {"jaeger-sampling", SamplingMapping, false}, {"invalid", MappingType(-1), true}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { result, err := MappingTypeFromString(tt.input) if tt.hasError { require.Error(t, err) assert.Equal(t, "unknown", result.String()) } else { require.NoError(t, err) assert.Equal(t, tt.expected, result) } }) } } func TestMappingBuilderLoadMapping(t *testing.T) { tests := []struct { name string }{ {name: "jaeger-span-6.json"}, {name: "jaeger-span-7.json"}, {name: "jaeger-span-8.json"}, {name: "jaeger-service-6.json"}, {name: "jaeger-service-7.json"}, {name: "jaeger-service-8.json"}, {name: "jaeger-dependencies-6.json"}, {name: "jaeger-dependencies-7.json"}, {name: "jaeger-dependencies-8.json"}, } for _, test := range tests { mapping := loadMapping(test.name) f, err := os.Open("./" + test.name) require.NoError(t, err) b, err := io.ReadAll(f) require.NoError(t, err) assert.Equal(t, string(b), mapping) _, err = template.New("mapping").Parse(mapping) require.NoError(t, err) } } func TestMappingBuilderFixMapping(t *testing.T) { tests := []struct { name string templateBuilderMockFunc func() *mocks.TemplateBuilder err string }{ { name: "templateRenderSuccess", templateBuilderMockFunc: func() *mocks.TemplateBuilder { tb := mocks.TemplateBuilder{} ta := mocks.TemplateApplier{} ta.On("Execute", mock.Anything, mock.Anything).Return(nil) tb.On("Parse", mock.Anything).Return(&ta, nil) return &tb }, err: "", }, { name: "templateRenderFailure", templateBuilderMockFunc: func() *mocks.TemplateBuilder { tb := mocks.TemplateBuilder{} ta := mocks.TemplateApplier{} ta.On("Execute", mock.Anything, mock.Anything).Return(errors.New("template exec error")) tb.On("Parse", mock.Anything).Return(&ta, nil) return &tb }, err: "template exec error", }, { name: "templateLoadError", templateBuilderMockFunc: func() *mocks.TemplateBuilder { tb := mocks.TemplateBuilder{} tb.On("Parse", mock.Anything).Return(nil, errors.New("template load error")) return &tb }, err: "template load error", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { indexTemOps := config.IndexOptions{ Shards: 3, Replicas: new(int64(5)), Priority: 500, } mappingBuilder := MappingBuilder{ TemplateBuilder: test.templateBuilderMockFunc(), Indices: config.Indices{ Spans: indexTemOps, Services: indexTemOps, Dependencies: indexTemOps, Sampling: indexTemOps, }, EsVersion: 7, UseILM: true, ILMPolicyName: "jaeger-test-policy", } _, err := mappingBuilder.renderMapping("test", mappingBuilder.getMappingTemplateOptions(SpanMapping)) if test.err != "" { require.EqualError(t, err, test.err) } else { require.NoError(t, err) } }) } } func TestMappingBuilderGetSpanServiceMappings(t *testing.T) { type args struct { esVersion uint indexPrefix string useILM bool ilmPolicyName string } tests := []struct { name string args args mockNewTextTemplateBuilder func() es.TemplateBuilder err string }{ { name: "ES Version 7", args: args{ esVersion: 7, indexPrefix: "test", useILM: true, ilmPolicyName: "jaeger-test-policy", }, mockNewTextTemplateBuilder: func() es.TemplateBuilder { tb := mocks.TemplateBuilder{} ta := mocks.TemplateApplier{} ta.On("Execute", mock.Anything, mock.Anything).Return(nil) tb.On("Parse", mock.Anything).Return(&ta, nil) return &tb }, err: "", }, { name: "ES Version 7 Service Error", args: args{ esVersion: 7, indexPrefix: "test", useILM: true, ilmPolicyName: "jaeger-test-policy", }, mockNewTextTemplateBuilder: func() es.TemplateBuilder { tb := mocks.TemplateBuilder{} ta := mocks.TemplateApplier{} ta.On("Execute", mock.Anything, mock.Anything).Return(nil).Once() ta.On("Execute", mock.Anything, mock.Anything).Return(errors.New("template load error")).Once() tb.On("Parse", mock.Anything).Return(&ta, nil) return &tb }, err: "template load error", }, { name: "ES Version < 7", args: args{ esVersion: 6, indexPrefix: "test", useILM: true, ilmPolicyName: "jaeger-test-policy", }, mockNewTextTemplateBuilder: func() es.TemplateBuilder { tb := mocks.TemplateBuilder{} ta := mocks.TemplateApplier{} ta.On("Execute", mock.Anything, mock.Anything).Return(nil) tb.On("Parse", mock.Anything).Return(&ta, nil) return &tb }, err: "", }, { name: "ES Version < 7 Service Error", args: args{ esVersion: 6, indexPrefix: "test", useILM: true, ilmPolicyName: "jaeger-test-policy", }, mockNewTextTemplateBuilder: func() es.TemplateBuilder { tb := mocks.TemplateBuilder{} ta := mocks.TemplateApplier{} ta.On("Execute", mock.Anything, mock.Anything).Return(nil).Once() ta.On("Execute", mock.Anything, mock.Anything).Return(errors.New("template load error")).Once() tb.On("Parse", mock.Anything).Return(&ta, nil) return &tb }, err: "template load error", }, { name: "ES Version < 7 Span Error", args: args{ esVersion: 6, indexPrefix: "test", useILM: true, ilmPolicyName: "jaeger-test-policy", }, mockNewTextTemplateBuilder: func() es.TemplateBuilder { tb := mocks.TemplateBuilder{} ta := mocks.TemplateApplier{} ta.On("Execute", mock.Anything, mock.Anything).Return(errors.New("template load error")) tb.On("Parse", mock.Anything).Return(&ta, nil) return &tb }, err: "template load error", }, { name: "ES Version 7 Span Error", args: args{ esVersion: 7, indexPrefix: "test", useILM: true, ilmPolicyName: "jaeger-test-policy", }, mockNewTextTemplateBuilder: func() es.TemplateBuilder { tb := mocks.TemplateBuilder{} ta := mocks.TemplateApplier{} ta.On("Execute", mock.Anything, mock.Anything).Return(errors.New("template load error")).Once() tb.On("Parse", mock.Anything).Return(&ta, nil) return &tb }, err: "template load error", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { indexTemOps := config.IndexOptions{ Shards: 3, Replicas: new(int64(3)), } mappingBuilder := MappingBuilder{ TemplateBuilder: test.mockNewTextTemplateBuilder(), Indices: config.Indices{ Spans: indexTemOps, Services: indexTemOps, Dependencies: indexTemOps, Sampling: indexTemOps, }, EsVersion: test.args.esVersion, UseILM: test.args.useILM, ILMPolicyName: test.args.ilmPolicyName, } _, _, err := mappingBuilder.GetSpanServiceMappings() if test.err != "" { require.EqualError(t, err, test.err) } else { require.NoError(t, err) } }) } } func TestMappingBuilderGetDependenciesMappings(t *testing.T) { tb := mocks.TemplateBuilder{} ta := mocks.TemplateApplier{} ta.On("Execute", mock.Anything, mock.Anything).Return(errors.New("template load error")) tb.On("Parse", mock.Anything).Return(&ta, nil) mappingBuilder := MappingBuilder{ TemplateBuilder: &tb, Indices: config.Indices{ Dependencies: config.IndexOptions{ Replicas: new(int64(1)), Shards: 3, Priority: 10, }, }, } _, err := mappingBuilder.GetDependenciesMappings() require.EqualError(t, err, "template load error") } func TestMappingBuilderGetSamplingMappings(t *testing.T) { tb := mocks.TemplateBuilder{} ta := mocks.TemplateApplier{} ta.On("Execute", mock.Anything, mock.Anything).Return(errors.New("template load error")) tb.On("Parse", mock.Anything).Return(&ta, nil) mappingBuilder := MappingBuilder{ TemplateBuilder: &tb, Indices: config.Indices{ Sampling: config.IndexOptions{ Replicas: new(int64(1)), Shards: 3, Priority: 10, }, }, } _, err := mappingBuilder.GetSamplingMappings() require.EqualError(t, err, "template load error") } func TestGetMappingTemplateOptions_DefaultCase(t *testing.T) { mappingBuilder := &MappingBuilder{ Indices: config.Indices{ Spans: config.IndexOptions{ Shards: 2, Replicas: new(int64(1)), Priority: 10, }, }, UseILM: true, ILMPolicyName: "test-policy", } opts := mappingBuilder.getMappingTemplateOptions(MappingType(-1)) assert.Equal(t, int64(5), opts.Shards) assert.Equal(t, int64(1), opts.Replicas) assert.Equal(t, int64(0), opts.Priority) assert.True(t, opts.UseILM) assert.Equal(t, "test-policy", opts.ILMPolicyName) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/elasticsearch/options.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "time" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" ) var defaultIndexOptions = config.IndexOptions{ DateLayout: initDateLayout("day", "-"), RolloverFrequency: "day", Shards: 5, Replicas: new(int64(1)), Priority: 0, } // TODO this should be moved next to config.Configuration struct (maybe ./flags package) // Options contains various type of Elasticsearch configs and provides the ability // to bind them to command line flag and apply overlays, so that some configurations // (e.g. archive) may be underspecified and infer the rest of its parameters from primary. type Options struct { Config config.Configuration `mapstructure:",squash"` } func initDateLayout(rolloverFreq, sep string) string { // default to daily format indexLayout := "2006" + sep + "01" + sep + "02" if rolloverFreq == "hour" { indexLayout = indexLayout + sep + "15" } return indexLayout } func DefaultConfig() config.Configuration { return config.Configuration{ Authentication: config.Authentication{}, Sniffing: config.Sniffing{ Enabled: false, }, DisableHealthCheck: false, MaxSpanAge: 72 * time.Hour, AdaptiveSamplingLookback: 72 * time.Hour, BulkProcessing: config.BulkProcessing{ MaxBytes: 5 * 1000 * 1000, Workers: 1, MaxActions: 1000, FlushInterval: time.Millisecond * 200, }, Tags: config.TagsAsFields{ DotReplacement: "@", }, Enabled: true, CreateIndexTemplates: true, Version: 0, UseReadWriteAliases: false, UseILM: false, Servers: []string{"http://127.0.0.1:9200"}, RemoteReadClusters: []string{}, MaxDocCount: 10_000, LogLevel: "error", SendGetBodyAs: "", HTTPCompression: true, Indices: config.Indices{ Spans: defaultIndexOptions, Services: defaultIndexOptions, Dependencies: defaultIndexOptions, Sampling: defaultIndexOptions, }, } } ================================================ FILE: internal/storage/v1/elasticsearch/options_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/config/configoptional" "go.opentelemetry.io/collector/config/configtls" escfg "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" ) func getBasicAuthField(opt configoptional.Optional[escfg.BasicAuthentication], field string) any { if !opt.HasValue() { return "" } ba := opt.Get() switch field { case "Username": return ba.Username case "Password": return ba.Password case "PasswordFilePath": return ba.PasswordFilePath case "ReloadInterval": return ba.ReloadInterval default: return "" } } func getBearerTokenField(opt configoptional.Optional[escfg.TokenAuthentication], field string) any { if !opt.HasValue() { if field == "AllowFromContext" { return false } return "" } ba := opt.Get() switch field { case "FilePath": return ba.FilePath case "AllowFromContext": return ba.AllowFromContext case "ReloadInterval": return ba.ReloadInterval default: return "" } } func getAPIKeyField(opt configoptional.Optional[escfg.TokenAuthentication], field string) any { if !opt.HasValue() { if field == "AllowFromContext" { return false } return "" } ba := opt.Get() switch field { case "FilePath": return ba.FilePath case "AllowFromContext": return ba.AllowFromContext case "ReloadInterval": return ba.ReloadInterval default: return "" } } func TestOptions(t *testing.T) { primary := DefaultConfig() // Authentication should not be present when no values are provided assert.False(t, primary.Authentication.BasicAuthentication.HasValue()) assert.False(t, primary.Authentication.BearerTokenAuth.HasValue()) assert.False(t, primary.Authentication.APIKeyAuth.HasValue()) assert.NotEmpty(t, primary.Servers) assert.Empty(t, primary.RemoteReadClusters) assert.EqualValues(t, 5, primary.Indices.Spans.Shards) assert.EqualValues(t, 5, primary.Indices.Services.Shards) assert.EqualValues(t, 5, primary.Indices.Sampling.Shards) assert.EqualValues(t, 5, primary.Indices.Dependencies.Shards) require.NotNil(t, primary.Indices.Spans.Replicas) assert.EqualValues(t, 1, *primary.Indices.Spans.Replicas) require.NotNil(t, primary.Indices.Services.Replicas) assert.EqualValues(t, 1, *primary.Indices.Services.Replicas) require.NotNil(t, primary.Indices.Sampling.Replicas) assert.EqualValues(t, 1, *primary.Indices.Sampling.Replicas) require.NotNil(t, primary.Indices.Dependencies.Replicas) assert.EqualValues(t, 1, *primary.Indices.Dependencies.Replicas) assert.Equal(t, 72*time.Hour, primary.MaxSpanAge) assert.False(t, primary.Sniffing.Enabled) assert.False(t, primary.Sniffing.UseHTTPS) assert.False(t, primary.DisableHealthCheck) } func TestOptionsWithFlags(t *testing.T) { primary := escfg.Configuration{ Servers: []string{"1.1.1.1", "2.2.2.2"}, Authentication: escfg.Authentication{ BasicAuthentication: configoptional.Some(escfg.BasicAuthentication{ Username: "hello", Password: "world", PasswordFilePath: "/foo/bar/baz", ReloadInterval: 35 * time.Second, }), BearerTokenAuth: configoptional.Some(escfg.TokenAuthentication{ FilePath: "/foo/bar", AllowFromContext: true, ReloadInterval: 50 * time.Second, }), APIKeyAuth: configoptional.Some(escfg.TokenAuthentication{ FilePath: "/foo/api-key", AllowFromContext: true, ReloadInterval: 30 * time.Second, }), }, RemoteReadClusters: []string{"cluster_one", "cluster_two"}, MaxSpanAge: 48 * time.Hour, Sniffing: escfg.Sniffing{ Enabled: true, UseHTTPS: true, }, DisableHealthCheck: true, TLS: configtls.ClientConfig{ Insecure: false, InsecureSkipVerify: true, }, Tags: escfg.TagsAsFields{ AllAsFields: true, Include: "test,tags", File: "./file.txt", DotReplacement: "!", }, Indices: escfg.Indices{ Spans: escfg.IndexOptions{ DateLayout: "2006010215", // Go reference time formatted for hourly rollover (yyyy-MM-dd-HH) }, Services: escfg.IndexOptions{ DateLayout: "20060102", // Go reference time formatted for daily rollover (yyyy-MM-dd) }, }, UseILM: true, HTTPCompression: true, } // Now authentication should be present since values were provided assert.True(t, primary.Authentication.BasicAuthentication.HasValue()) assert.True(t, primary.Authentication.BearerTokenAuth.HasValue()) assert.True(t, primary.Authentication.APIKeyAuth.HasValue()) // Basic Authentication assert.Equal(t, "hello", getBasicAuthField(primary.Authentication.BasicAuthentication, "Username")) assert.Equal(t, "world", getBasicAuthField(primary.Authentication.BasicAuthentication, "Password")) assert.Equal(t, "/foo/bar/baz", getBasicAuthField(primary.Authentication.BasicAuthentication, "PasswordFilePath")) assert.Equal(t, 35*time.Second, getBasicAuthField(primary.Authentication.BasicAuthentication, "ReloadInterval")) // Bearer Token Authentication assert.Equal(t, "/foo/bar", getBearerTokenField(primary.Authentication.BearerTokenAuth, "FilePath")) assert.Equal(t, true, getBearerTokenField(primary.Authentication.BearerTokenAuth, "AllowFromContext")) assert.Equal(t, 50*time.Second, getBearerTokenField(primary.Authentication.BearerTokenAuth, "ReloadInterval")) // API Key Authentication assert.Equal(t, "/foo/api-key", getAPIKeyField(primary.Authentication.APIKeyAuth, "FilePath")) assert.Equal(t, true, getAPIKeyField(primary.Authentication.APIKeyAuth, "AllowFromContext")) assert.Equal(t, 30*time.Second, getAPIKeyField(primary.Authentication.APIKeyAuth, "ReloadInterval")) // Server URLs assert.Equal(t, []string{"1.1.1.1", "2.2.2.2"}, primary.Servers) // Remote Read Clusters assert.Equal(t, []string{"cluster_one", "cluster_two"}, primary.RemoteReadClusters) // Max Span Age assert.Equal(t, 48*time.Hour, primary.MaxSpanAge) // Sniffing assert.True(t, primary.Sniffing.Enabled) assert.True(t, primary.Sniffing.UseHTTPS) assert.True(t, primary.DisableHealthCheck) // TLS assert.False(t, primary.TLS.Insecure) assert.True(t, primary.TLS.InsecureSkipVerify) // Tags assert.True(t, primary.Tags.AllAsFields) assert.Equal(t, "!", primary.Tags.DotReplacement) assert.Equal(t, "./file.txt", primary.Tags.File) assert.Equal(t, "test,tags", primary.Tags.Include) // Indices assert.Equal(t, "20060102", primary.Indices.Services.DateLayout) assert.Equal(t, "2006010215", primary.Indices.Spans.DateLayout) // Use ILM assert.True(t, primary.UseILM) // HTTP Compression assert.True(t, primary.HTTPCompression) } func TestAuthenticationConditionalCreation(t *testing.T) { testCases := []struct { name string config escfg.Configuration expectBasicAuth bool expectBearerAuth bool expectAPIKeyAuth bool expectedUsername string expectedPassword string expectedPasswordFilePath string expectedPasswordReloadInterval time.Duration expectedTokenPath string expectedBearerFromContext bool expectedBearerReloadInterval time.Duration expectedAPIKeyFilePath string expectedAPIKeyFromContext bool expectedAPIKeyReloadInterval time.Duration }{ { name: "no authentication flags", config: escfg.Configuration{ Authentication: escfg.Authentication{}, }, expectBasicAuth: false, expectBearerAuth: false, expectAPIKeyAuth: false, }, { name: "only username provided", config: escfg.Configuration{ Authentication: escfg.Authentication{ BasicAuthentication: configoptional.Some(escfg.BasicAuthentication{ Username: "testuser", ReloadInterval: 10 * time.Second, }), }, }, expectBasicAuth: true, expectBearerAuth: false, expectAPIKeyAuth: false, expectedUsername: "testuser", expectedPasswordReloadInterval: 10 * time.Second, }, { name: "only password provided", config: escfg.Configuration{ Authentication: escfg.Authentication{ BasicAuthentication: configoptional.Some(escfg.BasicAuthentication{ Password: "testpass", ReloadInterval: 10 * time.Second, }), }, }, expectBasicAuth: true, expectBearerAuth: false, expectAPIKeyAuth: false, expectedPassword: "testpass", expectedPasswordReloadInterval: 10 * time.Second, }, { name: "only token file provided", config: escfg.Configuration{ Authentication: escfg.Authentication{ BearerTokenAuth: configoptional.Some(escfg.TokenAuthentication{ FilePath: "/path/to/token", AllowFromContext: false, ReloadInterval: 10 * time.Second, }), }, }, expectBasicAuth: false, expectBearerAuth: true, expectAPIKeyAuth: false, expectedTokenPath: "/path/to/token", expectedBearerFromContext: false, expectedBearerReloadInterval: 10 * time.Second, }, { name: "username and password provided", config: escfg.Configuration{ Authentication: escfg.Authentication{ BasicAuthentication: configoptional.Some(escfg.BasicAuthentication{ Username: "testuser", Password: "testpass", ReloadInterval: 10 * time.Second, }), }, }, expectBasicAuth: true, expectBearerAuth: false, expectAPIKeyAuth: false, expectedUsername: "testuser", expectedPassword: "testpass", expectedPasswordReloadInterval: 10 * time.Second, }, { name: "only bearer token context propagation enabled", config: escfg.Configuration{ Authentication: escfg.Authentication{ BearerTokenAuth: configoptional.Some(escfg.TokenAuthentication{ AllowFromContext: true, ReloadInterval: 10 * time.Second, }), }, }, expectBasicAuth: false, expectBearerAuth: true, expectAPIKeyAuth: false, expectedBearerFromContext: true, expectedBearerReloadInterval: 10 * time.Second, }, { name: "both token file and context propagation enabled", config: escfg.Configuration{ Authentication: escfg.Authentication{ BearerTokenAuth: configoptional.Some(escfg.TokenAuthentication{ FilePath: "/path/to/token", AllowFromContext: true, ReloadInterval: 10 * time.Second, }), }, }, expectBasicAuth: false, expectBearerAuth: true, expectAPIKeyAuth: false, expectedTokenPath: "/path/to/token", expectedBearerFromContext: true, expectedBearerReloadInterval: 10 * time.Second, }, { name: "bearer token with custom reload interval", config: escfg.Configuration{ Authentication: escfg.Authentication{ BearerTokenAuth: configoptional.Some(escfg.TokenAuthentication{ FilePath: "/path/to/token", AllowFromContext: true, ReloadInterval: 45 * time.Second, }), }, }, expectBasicAuth: false, expectBearerAuth: true, expectAPIKeyAuth: false, expectedTokenPath: "/path/to/token", expectedBearerFromContext: true, expectedBearerReloadInterval: 45 * time.Second, }, { name: "API key all options with zero reload interval", config: escfg.Configuration{ Authentication: escfg.Authentication{ APIKeyAuth: configoptional.Some(escfg.TokenAuthentication{ FilePath: "/path/to/keyfile", AllowFromContext: true, ReloadInterval: 0 * time.Second, }), }, }, expectBasicAuth: false, expectBearerAuth: false, expectAPIKeyAuth: true, expectedAPIKeyFilePath: "/path/to/keyfile", expectedAPIKeyFromContext: true, expectedAPIKeyReloadInterval: 0 * time.Second, }, { name: "API key with non-zero reload interval", config: escfg.Configuration{ Authentication: escfg.Authentication{ APIKeyAuth: configoptional.Some(escfg.TokenAuthentication{ FilePath: "/path/to/keyfile", AllowFromContext: true, ReloadInterval: 30 * time.Second, }), }, }, expectBasicAuth: false, expectBearerAuth: false, expectAPIKeyAuth: true, expectedAPIKeyFilePath: "/path/to/keyfile", expectedAPIKeyFromContext: true, expectedAPIKeyReloadInterval: 30 * time.Second, }, { name: "only API key file provided", config: escfg.Configuration{ Authentication: escfg.Authentication{ APIKeyAuth: configoptional.Some(escfg.TokenAuthentication{ FilePath: "/path/to/key", AllowFromContext: false, ReloadInterval: 10 * time.Second, }), }, }, expectBasicAuth: false, expectBearerAuth: false, expectAPIKeyAuth: true, expectedAPIKeyFilePath: "/path/to/key", expectedAPIKeyFromContext: false, expectedAPIKeyReloadInterval: 10 * time.Second, }, { name: "only API key context propagation enabled", config: escfg.Configuration{ Authentication: escfg.Authentication{ APIKeyAuth: configoptional.Some(escfg.TokenAuthentication{ AllowFromContext: true, ReloadInterval: 10 * time.Second, }), }, }, expectBasicAuth: false, expectBearerAuth: false, expectAPIKeyAuth: true, expectedAPIKeyFromContext: true, expectedAPIKeyReloadInterval: 10 * time.Second, }, { name: "both API key file and context enabled", config: escfg.Configuration{ Authentication: escfg.Authentication{ APIKeyAuth: configoptional.Some(escfg.TokenAuthentication{ FilePath: "/path/to/key", AllowFromContext: true, ReloadInterval: 10 * time.Second, }), }, }, expectBasicAuth: false, expectBearerAuth: false, expectAPIKeyAuth: true, expectedAPIKeyFilePath: "/path/to/key", expectedAPIKeyFromContext: true, expectedAPIKeyReloadInterval: 10 * time.Second, }, { name: "all API key options provided", config: escfg.Configuration{ Authentication: escfg.Authentication{ APIKeyAuth: configoptional.Some(escfg.TokenAuthentication{ FilePath: "/path/to/key", AllowFromContext: true, ReloadInterval: 60 * time.Second, }), }, }, expectBasicAuth: false, expectBearerAuth: false, expectAPIKeyAuth: true, expectedAPIKeyFilePath: "/path/to/key", expectedAPIKeyFromContext: true, expectedAPIKeyReloadInterval: 60 * time.Second, }, { name: "basic auth and API key both enabled", config: escfg.Configuration{ Authentication: escfg.Authentication{ BasicAuthentication: configoptional.Some(escfg.BasicAuthentication{ Username: "testuser", Password: "testpass", ReloadInterval: 10 * time.Second, }), APIKeyAuth: configoptional.Some(escfg.TokenAuthentication{ FilePath: "/path/to/key", ReloadInterval: 10 * time.Second, }), }, }, expectBasicAuth: true, expectBearerAuth: false, expectAPIKeyAuth: true, expectedUsername: "testuser", expectedPassword: "testpass", expectedPasswordReloadInterval: 10 * time.Second, expectedAPIKeyFilePath: "/path/to/key", expectedAPIKeyReloadInterval: 10 * time.Second, }, { name: "bearer token and API key both enabled", config: escfg.Configuration{ Authentication: escfg.Authentication{ BearerTokenAuth: configoptional.Some(escfg.TokenAuthentication{ FilePath: "/path/to/token", AllowFromContext: false, ReloadInterval: 10 * time.Second, }), APIKeyAuth: configoptional.Some(escfg.TokenAuthentication{ AllowFromContext: true, ReloadInterval: 10 * time.Second, }), }, }, expectBasicAuth: false, expectBearerAuth: true, expectAPIKeyAuth: true, expectedTokenPath: "/path/to/token", expectedBearerFromContext: false, expectedBearerReloadInterval: 10 * time.Second, expectedAPIKeyFromContext: true, expectedAPIKeyReloadInterval: 10 * time.Second, }, { name: "basic auth password reload interval disabled", config: escfg.Configuration{ Authentication: escfg.Authentication{ BasicAuthentication: configoptional.Some(escfg.BasicAuthentication{ Username: "testuser", PasswordFilePath: "/path/to/password", ReloadInterval: 0 * time.Second, }), }, }, expectBasicAuth: true, expectBearerAuth: false, expectAPIKeyAuth: false, expectedUsername: "testuser", expectedPasswordFilePath: "/path/to/password", expectedPasswordReloadInterval: 0 * time.Second, }, { name: "bearer token reload interval disabled", config: escfg.Configuration{ Authentication: escfg.Authentication{ BearerTokenAuth: configoptional.Some(escfg.TokenAuthentication{ FilePath: "/path/to/token", ReloadInterval: 0 * time.Second, }), }, }, expectBasicAuth: false, expectBearerAuth: true, expectAPIKeyAuth: false, expectedTokenPath: "/path/to/token", expectedBearerReloadInterval: 0 * time.Second, }, { name: "all three authentication methods enabled", config: escfg.Configuration{ Authentication: escfg.Authentication{ BasicAuthentication: configoptional.Some(escfg.BasicAuthentication{ Username: "testuser", Password: "testpass", ReloadInterval: 10 * time.Second, }), BearerTokenAuth: configoptional.Some(escfg.TokenAuthentication{ FilePath: "/path/to/token", AllowFromContext: true, ReloadInterval: 25 * time.Second, }), APIKeyAuth: configoptional.Some(escfg.TokenAuthentication{ FilePath: "/path/to/key", AllowFromContext: true, ReloadInterval: 30 * time.Second, }), }, }, expectBasicAuth: true, expectBearerAuth: true, expectAPIKeyAuth: true, expectedUsername: "testuser", expectedPassword: "testpass", expectedPasswordReloadInterval: 10 * time.Second, expectedTokenPath: "/path/to/token", expectedBearerFromContext: true, expectedBearerReloadInterval: 25 * time.Second, expectedAPIKeyFilePath: "/path/to/key", expectedAPIKeyFromContext: true, expectedAPIKeyReloadInterval: 30 * time.Second, }, { name: "basic auth with custom reload interval (non-zero)", config: escfg.Configuration{ Authentication: escfg.Authentication{ BasicAuthentication: configoptional.Some(escfg.BasicAuthentication{ Username: "testuser", PasswordFilePath: "/path/to/password", ReloadInterval: 15 * time.Second, }), }, }, expectBasicAuth: true, expectBearerAuth: false, expectAPIKeyAuth: false, expectedUsername: "testuser", expectedPasswordFilePath: "/path/to/password", expectedPasswordReloadInterval: 15 * time.Second, }, { name: "bearer token with custom reload interval (non-zero)", config: escfg.Configuration{ Authentication: escfg.Authentication{ BearerTokenAuth: configoptional.Some(escfg.TokenAuthentication{ FilePath: "/path/to/token", ReloadInterval: 20 * time.Second, }), }, }, expectBasicAuth: false, expectBearerAuth: true, expectAPIKeyAuth: false, expectedTokenPath: "/path/to/token", expectedBearerReloadInterval: 20 * time.Second, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { primary := tc.config // Assert authentication method presence assert.Equal(t, tc.expectBasicAuth, primary.Authentication.BasicAuthentication.HasValue()) assert.Equal(t, tc.expectBearerAuth, primary.Authentication.BearerTokenAuth.HasValue()) assert.Equal(t, tc.expectAPIKeyAuth, primary.Authentication.APIKeyAuth.HasValue()) // Assert basic authentication details if tc.expectBasicAuth { basicAuth := primary.Authentication.BasicAuthentication.Get() assert.Equal(t, tc.expectedUsername, basicAuth.Username) assert.Equal(t, tc.expectedPassword, basicAuth.Password) assert.Equal(t, tc.expectedPasswordFilePath, basicAuth.PasswordFilePath) assert.Equal(t, tc.expectedPasswordReloadInterval, basicAuth.ReloadInterval) } // Assert bearer token authentication details if tc.expectBearerAuth { bearerAuth := primary.Authentication.BearerTokenAuth.Get() assert.Equal(t, tc.expectedTokenPath, bearerAuth.FilePath) assert.Equal(t, tc.expectedBearerFromContext, bearerAuth.AllowFromContext) assert.Equal(t, tc.expectedBearerReloadInterval, bearerAuth.ReloadInterval) } // Assert API key authentication details if tc.expectAPIKeyAuth { apiKeyAuth := primary.Authentication.APIKeyAuth.Get() assert.Equal(t, tc.expectedAPIKeyFilePath, apiKeyAuth.FilePath) assert.Equal(t, tc.expectedAPIKeyFromContext, apiKeyAuth.AllowFromContext) assert.Equal(t, tc.expectedAPIKeyReloadInterval, apiKeyAuth.ReloadInterval) } }) } } func TestGetBasicAuthField_DefaultCase(t *testing.T) { basicAuth := escfg.BasicAuthentication{ Username: "test-user", Password: "test-pass", PasswordFilePath: "/path/to/file", } opt := configoptional.Some(basicAuth) result := getBasicAuthField(opt, "UnknownField") assert.Empty(t, result) } func TestEmptyRemoteReadClusters(t *testing.T) { primary := escfg.Configuration{ RemoteReadClusters: []string{}, } assert.Equal(t, []string{}, primary.RemoteReadClusters) } func TestMaxSpanAgeSetErrorInArchiveMode(t *testing.T) { // This test verifies that max-span-age flag is not available in archive mode // Since we're not testing flags anymore, we just verify that the behavior is documented // In archive mode, MaxSpanAge should not be used (traces are searched with no look-back limit) t.Skip("Test for flag parsing behavior - no longer applicable with direct config initialization") } func TestMaxDocCount(t *testing.T) { testCases := []struct { name string config escfg.Configuration wantMaxDocCount int }{ { name: "default value", config: DefaultConfig(), wantMaxDocCount: 10_000, }, { name: "custom value", config: escfg.Configuration{ MaxDocCount: 1000, }, wantMaxDocCount: 1000, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { assert.Equal(t, tc.wantMaxDocCount, tc.config.MaxDocCount) }) } } func TestIndexDateSeparator(t *testing.T) { testCases := []struct { name string config escfg.Configuration wantDateLayout string }{ { name: "default separator", config: DefaultConfig(), wantDateLayout: "2006-01-02", }, { name: "empty separator", config: escfg.Configuration{ Indices: escfg.Indices{ Spans: escfg.IndexOptions{ DateLayout: "20060102", }, }, }, wantDateLayout: "20060102", }, { name: "dot separator", config: escfg.Configuration{ Indices: escfg.Indices{ Spans: escfg.IndexOptions{ DateLayout: "2006.01.02", }, }, }, wantDateLayout: "2006.01.02", }, { name: "dash separator", config: escfg.Configuration{ Indices: escfg.Indices{ Spans: escfg.IndexOptions{ DateLayout: "2006-01-02", }, }, }, wantDateLayout: "2006-01-02", }, { name: "slash separator", config: escfg.Configuration{ Indices: escfg.Indices{ Spans: escfg.IndexOptions{ DateLayout: "2006/01/02", }, }, }, wantDateLayout: "2006/01/02", }, { name: "single quote separator", config: escfg.Configuration{ Indices: escfg.Indices{ Spans: escfg.IndexOptions{ DateLayout: "2006''01''02", }, }, }, wantDateLayout: "2006''01''02", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { assert.Equal(t, tc.wantDateLayout, tc.config.Indices.Spans.DateLayout) }) } } func TestIndexRollover(t *testing.T) { testCases := []struct { name string config escfg.Configuration wantSpanDateLayout string wantServiceDateLayout string wantSpanIndexRolloverFrequency time.Duration wantServiceIndexRolloverFrequency time.Duration }{ { name: "default", config: DefaultConfig(), wantSpanDateLayout: "2006-01-02", wantServiceDateLayout: "2006-01-02", wantSpanIndexRolloverFrequency: -24 * time.Hour, wantServiceIndexRolloverFrequency: -24 * time.Hour, }, { name: "hourly spans, daily services", config: escfg.Configuration{ Indices: escfg.Indices{ Spans: escfg.IndexOptions{ DateLayout: "2006-01-02-15", RolloverFrequency: "hour", }, Services: escfg.IndexOptions{ DateLayout: "2006-01-02", RolloverFrequency: "day", }, }, }, wantSpanDateLayout: "2006-01-02-15", wantServiceDateLayout: "2006-01-02", wantSpanIndexRolloverFrequency: -1 * time.Hour, wantServiceIndexRolloverFrequency: -24 * time.Hour, }, { name: "daily spans, hourly services", config: escfg.Configuration{ Indices: escfg.Indices{ Spans: escfg.IndexOptions{ DateLayout: "2006-01-02", RolloverFrequency: "day", }, Services: escfg.IndexOptions{ DateLayout: "2006-01-02-15", RolloverFrequency: "hour", }, }, }, wantSpanDateLayout: "2006-01-02", wantServiceDateLayout: "2006-01-02-15", wantSpanIndexRolloverFrequency: -24 * time.Hour, wantServiceIndexRolloverFrequency: -1 * time.Hour, }, { name: "invalid rollover frequency defaults to day", config: escfg.Configuration{ Indices: escfg.Indices{ Spans: escfg.IndexOptions{ DateLayout: "2006-01-02", RolloverFrequency: "hours", }, Services: escfg.IndexOptions{ DateLayout: "2006-01-02", RolloverFrequency: "hours", }, }, }, wantSpanDateLayout: "2006-01-02", wantServiceDateLayout: "2006-01-02", wantSpanIndexRolloverFrequency: -24 * time.Hour, wantServiceIndexRolloverFrequency: -24 * time.Hour, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { assert.Equal(t, tc.wantSpanDateLayout, tc.config.Indices.Spans.DateLayout) assert.Equal(t, tc.wantServiceDateLayout, tc.config.Indices.Services.DateLayout) assert.Equal(t, tc.wantSpanIndexRolloverFrequency, escfg.RolloverFrequencyAsNegativeDuration(tc.config.Indices.Spans.RolloverFrequency)) assert.Equal(t, tc.wantServiceIndexRolloverFrequency, escfg.RolloverFrequencyAsNegativeDuration(tc.config.Indices.Services.RolloverFrequency)) }) } } // TestAddFlags and TestAddFlagsWithPreExistingAuth were removed as they tested // flag registration behavior which is no longer relevant after moving to direct config initialization ================================================ FILE: internal/storage/v1/elasticsearch/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/elasticsearch/samplingstore/dbmodel/converter.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore/model" ) func FromThroughputs(throughputs []*model.Throughput) []Throughput { if throughputs == nil { return nil } ret := make([]Throughput, len(throughputs)) for i, d := range throughputs { ret[i] = Throughput{ Service: d.Service, Operation: d.Operation, Count: d.Count, Probabilities: d.Probabilities, } } return ret } func ToThroughputs(throughputs []Throughput) []*model.Throughput { if throughputs == nil { return nil } ret := make([]*model.Throughput, len(throughputs)) for i, d := range throughputs { ret[i] = &model.Throughput{ Service: d.Service, Operation: d.Operation, Count: d.Count, Probabilities: d.Probabilities, } } return ret } ================================================ FILE: internal/storage/v1/elasticsearch/samplingstore/dbmodel/converter_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "strconv" "testing" "github.com/stretchr/testify/assert" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore/model" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestConvertDependencies(t *testing.T) { tests := []struct { throughputs []*model.Throughput }{ { throughputs: []*model.Throughput{{Service: "service1", Operation: "operation1", Count: 10, Probabilities: map[string]struct{}{"new-srv": {}}}}, }, { throughputs: []*model.Throughput{}, }, { throughputs: nil, }, } for i, test := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { got := FromThroughputs(test.throughputs) a := ToThroughputs(got) assert.Equal(t, test.throughputs, a) }) } } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/elasticsearch/samplingstore/dbmodel/model.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "time" ) type Throughput struct { Service string Operation string Count int64 Probabilities map[string]struct{} } type TimeThroughput struct { Timestamp time.Time `json:"timestamp"` Throughput Throughput `json:"throughputs"` } type ProbabilitiesAndQPS struct { Hostname string Probabilities map[string]map[string]float64 QPS map[string]map[string]float64 } type TimeProbabilitiesAndQPS struct { Timestamp time.Time `json:"timestamp"` ProbabilitiesAndQPS ProbabilitiesAndQPS `json:"probabilitiesandqps"` } ================================================ FILE: internal/storage/v1/elasticsearch/samplingstore/storage.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package samplingstore import ( "context" "encoding/json" "errors" "fmt" "time" "github.com/olivere/elastic/v7" "go.uber.org/zap" es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" esquery "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/query" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore/model" "github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch/samplingstore/dbmodel" ) const ( samplingIndexBaseName = "jaeger-sampling" throughputType = "throughput-sampling" probabilitiesType = "probabilities-sampling" ) type SamplingStore struct { client func() es.Client logger *zap.Logger samplingIndexPrefix string indexDateLayout string maxDocCount int indexRolloverFrequency time.Duration lookback time.Duration } type Params struct { Client func() es.Client Logger *zap.Logger IndexPrefix config.IndexPrefix IndexDateLayout string IndexRolloverFrequency time.Duration Lookback time.Duration MaxDocCount int } func NewSamplingStore(p Params) *SamplingStore { return &SamplingStore{ client: p.Client, logger: p.Logger, samplingIndexPrefix: p.PrefixedIndexName() + config.IndexPrefixSeparator, indexDateLayout: p.IndexDateLayout, maxDocCount: p.MaxDocCount, indexRolloverFrequency: p.IndexRolloverFrequency, lookback: p.Lookback, } } func (s *SamplingStore) InsertThroughput(throughput []*model.Throughput) error { ts := time.Now() indexName := indexWithDate(s.samplingIndexPrefix, s.indexDateLayout, ts) for _, eachThroughput := range dbmodel.FromThroughputs(throughput) { s.client().Index().Index(indexName).Type(throughputType). BodyJson(&dbmodel.TimeThroughput{ Timestamp: ts, Throughput: eachThroughput, }).Add() } return nil } func (s *SamplingStore) GetThroughput(start, end time.Time) ([]*model.Throughput, error) { ctx := context.Background() indices := getReadIndices(s.samplingIndexPrefix, s.indexDateLayout, start, end, s.indexRolloverFrequency) searchResult, err := s.client().Search(indices...). Size(s.maxDocCount). Query(buildTSQuery(start, end)). IgnoreUnavailable(true). Do(ctx) if err != nil { return nil, fmt.Errorf("failed to search for throughputs: %w", err) } output := make([]dbmodel.TimeThroughput, len(searchResult.Hits.Hits)) for i, hit := range searchResult.Hits.Hits { if err := json.Unmarshal(hit.Source, &output[i]); err != nil { return nil, fmt.Errorf("unmarshalling documents failed: %w", err) } } outThroughputs := make([]dbmodel.Throughput, len(output)) for i, out := range output { outThroughputs[i] = out.Throughput } return dbmodel.ToThroughputs(outThroughputs), nil } func (s *SamplingStore) InsertProbabilitiesAndQPS(hostname string, probabilities model.ServiceOperationProbabilities, qps model.ServiceOperationQPS, ) error { ts := time.Now() writeIndexName := indexWithDate(s.samplingIndexPrefix, s.indexDateLayout, ts) val := dbmodel.ProbabilitiesAndQPS{ Hostname: hostname, Probabilities: probabilities, QPS: qps, } s.writeProbabilitiesAndQPS(writeIndexName, ts, val) return nil } func (s *SamplingStore) GetLatestProbabilities() (model.ServiceOperationProbabilities, error) { ctx := context.Background() clientFn := s.client() indices, err := getLatestIndices(s.samplingIndexPrefix, s.indexDateLayout, clientFn, s.indexRolloverFrequency, s.lookback) if err != nil { return nil, fmt.Errorf("failed to get latest indices: %w", err) } searchResult, err := clientFn.Search(indices...). Size(s.maxDocCount). IgnoreUnavailable(true). Do(ctx) if err != nil { return nil, fmt.Errorf("failed to search for Latest Probabilities: %w", err) } lengthOfSearchResult := len(searchResult.Hits.Hits) if lengthOfSearchResult == 0 { return nil, nil } var latestProbabilities dbmodel.TimeProbabilitiesAndQPS latestTime := time.Time{} for _, hit := range searchResult.Hits.Hits { var data dbmodel.TimeProbabilitiesAndQPS if err = json.Unmarshal(hit.Source, &data); err != nil { return nil, fmt.Errorf("unmarshalling documents failed: %w", err) } if data.Timestamp.After(latestTime) { latestTime = data.Timestamp latestProbabilities = data } } return latestProbabilities.ProbabilitiesAndQPS.Probabilities, nil } func (s *SamplingStore) writeProbabilitiesAndQPS(indexName string, ts time.Time, pandqps dbmodel.ProbabilitiesAndQPS) { s.client().Index().Index(indexName).Type(probabilitiesType). BodyJson(&dbmodel.TimeProbabilitiesAndQPS{ Timestamp: ts, ProbabilitiesAndQPS: pandqps, }).Add() } func getLatestIndices(indexPrefix, indexDateLayout string, clientFn es.Client, rollover time.Duration, maxDuration time.Duration) ([]string, error) { ctx := context.Background() now := time.Now().UTC() earliest := now.Add(-maxDuration) earliestIndex := indexWithDate(indexPrefix, indexDateLayout, earliest) for { currentIndex := indexWithDate(indexPrefix, indexDateLayout, now) exists, err := clientFn.IndexExists(currentIndex).Do(ctx) if err != nil { return nil, fmt.Errorf("failed to check index existence: %w", err) } if exists { return []string{currentIndex}, nil } if currentIndex == earliestIndex { return nil, errors.New("falied to find latest index") } now = now.Add(rollover) // rollover is negative } } func getReadIndices(indexName, indexDateLayout string, startTime time.Time, endTime time.Time, rollover time.Duration) []string { var indices []string firstIndex := indexWithDate(indexName, indexDateLayout, startTime) currentIndex := indexWithDate(indexName, indexDateLayout, endTime) for currentIndex != firstIndex { indices = append(indices, currentIndex) endTime = endTime.Add(rollover) // rollover is negative currentIndex = indexWithDate(indexName, indexDateLayout, endTime) } indices = append(indices, firstIndex) return indices } func (p *Params) PrefixedIndexName() string { return p.IndexPrefix.Apply(samplingIndexBaseName) } func buildTSQuery(start, end time.Time) elastic.Query { return esquery.NewRangeQuery("timestamp").Gte(start).Lte(end) } func indexWithDate(indexNamePrefix, indexDateLayout string, date time.Time) string { return indexNamePrefix + date.UTC().Format(indexDateLayout) } ================================================ FILE: internal/storage/v1/elasticsearch/samplingstore/storage_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package samplingstore import ( "errors" "strings" "testing" "time" "github.com/olivere/elastic/v7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.uber.org/zap" es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/mocks" samplemodel "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore/model" "github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch/samplingstore/dbmodel" "github.com/jaegertracing/jaeger/internal/testutils" ) const defaultMaxDocCount = 10_000 type samplingStorageTest struct { client *mocks.Client logger *zap.Logger logBuffer *testutils.Buffer storage *SamplingStore } func withEsSampling(indexPrefix config.IndexPrefix, indexDateLayout string, maxDocCount int, fn func(w *samplingStorageTest)) { client := &mocks.Client{} logger, logBuffer := testutils.NewLogger() w := &samplingStorageTest{ client: client, logger: logger, logBuffer: logBuffer, storage: NewSamplingStore(Params{ Client: func() es.Client { return client }, Logger: logger, IndexPrefix: indexPrefix, IndexDateLayout: indexDateLayout, MaxDocCount: maxDocCount, }), } fn(w) } func TestNewIndexPrefix(t *testing.T) { tests := []struct { name string prefix config.IndexPrefix expected string }{ { name: "without prefix", prefix: "", expected: "", }, { name: "with prefix", prefix: "foo", expected: "foo-", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { client := &mocks.Client{} r := NewSamplingStore(Params{ Client: func() es.Client { return client }, Logger: zap.NewNop(), IndexPrefix: test.prefix, IndexDateLayout: "2006-01-02", MaxDocCount: defaultMaxDocCount, }) assert.Equal(t, test.expected+samplingIndexBaseName+config.IndexPrefixSeparator, r.samplingIndexPrefix) }) } } func TestGetReadIndices(t *testing.T) { test := struct { name string start time.Time end time.Time }{ name: "", start: time.Date(2024, time.February, 10, 0, 0, 0, 0, time.UTC), end: time.Date(2024, time.February, 12, 0, 0, 0, 0, time.UTC), } t.Run(test.name, func(t *testing.T) { expectedIndices := []string{ "prefix-jaeger-sampling-2024-02-12", "prefix-jaeger-sampling-2024-02-11", "prefix-jaeger-sampling-2024-02-10", } rollover := -time.Hour * 24 indices := getReadIndices("prefix-jaeger-sampling-", "2006-01-02", test.start, test.end, rollover) assert.Equal(t, expectedIndices, indices) }) } func TestGetLatestIndices(t *testing.T) { tests := []struct { name string indexDateLayout string maxDuration time.Duration expectedIndices []string expectedError string IndexExistError error indexExist bool }{ { name: "with index", indexDateLayout: "2006-01-02", maxDuration: 24 * time.Hour, expectedIndices: []string{indexWithDate("", "2006-01-02", time.Now().UTC())}, expectedError: "", indexExist: true, }, { name: "without index", indexDateLayout: "2006-01-02", maxDuration: 72 * time.Hour, expectedError: "falied to find latest index", indexExist: false, }, { name: "check index existence", indexDateLayout: "2006-01-02", maxDuration: 24 * time.Hour, expectedError: "failed to check index existence: fail", indexExist: true, IndexExistError: errors.New("fail"), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { withEsSampling("", test.indexDateLayout, defaultMaxDocCount, func(w *samplingStorageTest) { indexService := &mocks.IndicesExistsService{} w.client.On("IndexExists", mock.Anything).Return(indexService) indexService.On("Do", mock.Anything).Return(test.indexExist, test.IndexExistError) clientFnMock := w.storage.client() actualIndices, err := getLatestIndices("", test.indexDateLayout, clientFnMock, -24*time.Hour, test.maxDuration) if test.expectedError != "" { require.EqualError(t, err, test.expectedError) assert.Nil(t, actualIndices) } else { require.NoError(t, err) require.Equal(t, test.expectedIndices, actualIndices) } }) }) } } func TestInsertThroughput(t *testing.T) { test := struct { name string expectedError string }{ name: "insert throughput", expectedError: "", } t.Run(test.name, func(t *testing.T) { withEsSampling("", "2006-01-02", defaultMaxDocCount, func(w *samplingStorageTest) { throughputs := []*samplemodel.Throughput{ {Service: "my-svc", Operation: "op"}, {Service: "our-svc", Operation: "op2"}, } fixedTime := time.Now() indexName := indexWithDate("", "2006-01-02", fixedTime) writeService := &mocks.IndexService{} w.client.On("Index").Return(writeService) writeService.On("Index", stringMatcher(indexName)).Return(writeService) writeService.On("Type", stringMatcher(throughputType)).Return(writeService) writeService.On("BodyJson", mock.Anything).Return(writeService) writeService.On("Add", mock.Anything) err := w.storage.InsertThroughput(throughputs) if test.expectedError != "" { require.EqualError(t, err, test.expectedError) } else { require.NoError(t, err) } }) }) } func TestInsertProbabilitiesAndQPS(t *testing.T) { test := struct { name string expectedError string }{ name: "insert probabilities and qps", expectedError: "", } t.Run(test.name, func(t *testing.T) { withEsSampling("", "2006-01-02", defaultMaxDocCount, func(w *samplingStorageTest) { pAQ := dbmodel.ProbabilitiesAndQPS{ Hostname: "dell11eg843d", Probabilities: samplemodel.ServiceOperationProbabilities{"new-srv": {"op": 0.1}}, QPS: samplemodel.ServiceOperationQPS{"new-srv": {"op": 4}}, } fixedTime := time.Now() indexName := indexWithDate("", "2006-01-02", fixedTime) writeService := &mocks.IndexService{} w.client.On("Index").Return(writeService) writeService.On("Index", stringMatcher(indexName)).Return(writeService) writeService.On("Type", stringMatcher(probabilitiesType)).Return(writeService) writeService.On("BodyJson", mock.Anything).Return(writeService) writeService.On("Add", mock.Anything) err := w.storage.InsertProbabilitiesAndQPS(pAQ.Hostname, pAQ.Probabilities, pAQ.QPS) if test.expectedError != "" { require.EqualError(t, err, test.expectedError) } else { require.NoError(t, err) } }) }) } func TestGetThroughput(t *testing.T) { mockIndex := "jaeger-sampling-" + time.Now().UTC().Format("2006-01-02") goodThroughputs := `{ "timestamp": "2024-02-08T12:00:00Z", "throughputs": { "Service": "my-svc", "Operation": "op", "Count": 10 } }` tests := []struct { name string searchResult *elastic.SearchResult searchError error expectedError string expectedOutput []*samplemodel.Throughput indexPrefix config.IndexPrefix maxDocCount int index string }{ { name: "good throughputs without prefix", searchResult: createSearchResult(goodThroughputs), expectedOutput: []*samplemodel.Throughput{ { Service: "my-svc", Operation: "op", Count: 10, }, }, index: mockIndex, maxDocCount: 1000, }, { name: "good throughputs without prefix", searchResult: createSearchResult(goodThroughputs), expectedOutput: []*samplemodel.Throughput{ { Service: "my-svc", Operation: "op", Count: 10, }, }, index: mockIndex, maxDocCount: 1000, }, { name: "good throughputs with prefix", searchResult: createSearchResult(goodThroughputs), expectedOutput: []*samplemodel.Throughput{ { Service: "my-svc", Operation: "op", Count: 10, }, }, index: mockIndex, indexPrefix: "foo", maxDocCount: 1000, }, { name: "bad throughputs", searchResult: createSearchResult(`badJson{hello}world`), expectedError: "unmarshalling documents failed: invalid character 'b' looking for beginning of value", index: mockIndex, }, { name: "search fails", searchError: errors.New("search failure"), expectedError: "failed to search for throughputs: search failure", index: mockIndex, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { withEsSampling(test.indexPrefix, "2006-01-02", defaultMaxDocCount, func(w *samplingStorageTest) { searchService := &mocks.SearchService{} if test.indexPrefix != "" { test.indexPrefix += "-" } index := test.indexPrefix.Apply(test.index) w.client.On("Search", []string{index}).Return(searchService) searchService.On("Size", mock.Anything).Return(searchService) searchService.On("Query", mock.Anything).Return(searchService) searchService.On("IgnoreUnavailable", true).Return(searchService) searchService.On("Do", mock.Anything).Return(test.searchResult, test.searchError) actual, err := w.storage.GetThroughput(time.Now().Add(-time.Minute), time.Now()) if test.expectedError != "" { require.EqualError(t, err, test.expectedError) assert.Nil(t, actual) } else { require.NoError(t, err) assert.Equal(t, test.expectedOutput, actual) } }) }) } } func TestGetLatestProbabilities(t *testing.T) { mockIndex := "jaeger-sampling-" + time.Now().UTC().Format("2006-01-02") goodProbabilities := `{ "timestamp": "2024-02-08T12:00:00Z", "probabilitiesandqps": { "Hostname": "dell11eg843d", "Probabilities": { "new-srv": {"op": 0.1} }, "QPS": { "new-srv": {"op": 4} } } }` tests := []struct { name string searchResult *elastic.SearchResult searchError error expectedOutput samplemodel.ServiceOperationProbabilities expectedError string maxDocCount int index string indexPresent bool indexError error indexPrefix config.IndexPrefix }{ { name: "good probabilities without prefix", searchResult: createSearchResult(goodProbabilities), expectedOutput: samplemodel.ServiceOperationProbabilities{ "new-srv": { "op": 0.1, }, }, index: mockIndex, maxDocCount: 1000, indexPresent: true, }, { name: "good probabilities with prefix", searchResult: createSearchResult(goodProbabilities), expectedOutput: samplemodel.ServiceOperationProbabilities{ "new-srv": { "op": 0.1, }, }, index: mockIndex, maxDocCount: 1000, indexPresent: true, indexPrefix: "foo", }, { name: "bad probabilities", searchResult: createSearchResult(`badJson{hello}world`), expectedError: "unmarshalling documents failed: invalid character 'b' looking for beginning of value", index: mockIndex, indexPresent: true, }, { name: "search fail", searchError: errors.New("search failure"), expectedError: "failed to search for Latest Probabilities: search failure", index: mockIndex, indexPresent: true, }, { name: "index check fail", indexError: errors.New("index check failure"), expectedError: "failed to get latest indices: failed to check index existence: index check failure", index: mockIndex, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { withEsSampling(test.indexPrefix, "2006-01-02", defaultMaxDocCount, func(w *samplingStorageTest) { searchService := &mocks.SearchService{} index := test.indexPrefix.Apply(test.index) w.client.On("Search", []string{index}).Return(searchService) searchService.On("Size", mock.Anything).Return(searchService) searchService.On("IgnoreUnavailable", true).Return(searchService) searchService.On("Do", mock.Anything).Return(test.searchResult, test.searchError) indicesexistsservice := &mocks.IndicesExistsService{} w.client.On("IndexExists", index).Return(indicesexistsservice) indicesexistsservice.On("Do", mock.Anything).Return(test.indexPresent, test.indexError) actual, err := w.storage.GetLatestProbabilities() if test.expectedError != "" { require.EqualError(t, err, test.expectedError) assert.Nil(t, actual) } else { require.NoError(t, err) assert.Equal(t, test.expectedOutput, actual) } }) }) } } func createSearchResult(rawJsonStr string) *elastic.SearchResult { rawJsonArr := []byte(rawJsonStr) hits := make([]*elastic.SearchHit, 1) hits[0] = &elastic.SearchHit{ Source: rawJsonArr, } searchResult := &elastic.SearchResult{Hits: &elastic.SearchHits{Hits: hits}} return searchResult } func stringMatcher(q string) any { matchFunc := func(s string) bool { return strings.Contains(s, q) } return mock.MatchedBy(matchFunc) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/core_span_reader.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "context" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" ) // CoreSpanReader is a DB-Level abstraction which directly deals with database level operations type CoreSpanReader interface { // FindTraceIDs retrieves traces IDs that match the traceQuery FindTraceIDs(ctx context.Context, traceQuery dbmodel.TraceQueryParameters) ([]dbmodel.TraceID, error) // FindTraces retrieves traces that match the traceQuery FindTraces(ctx context.Context, traceQuery dbmodel.TraceQueryParameters) ([]dbmodel.Trace, error) // GetOperations returns all operations for a specific service traced by Jaeger GetOperations(ctx context.Context, query dbmodel.OperationQueryParameters) ([]dbmodel.Operation, error) // GetServices returns all services traced by Jaeger, ordered by frequency GetServices(ctx context.Context) ([]string, error) // GetTraces takes a traceID and returns a Trace associated with that traceID GetTraces(ctx context.Context, query []dbmodel.TraceID) ([]dbmodel.Trace, error) } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/fixtures/domain_01.json ================================================ { "traceId": "AAAAAAAAAAAAAAAAAAAAAQ==", "spanId": "AAAAAAAAAAI=", "operationName": "test-general-conversion", "references": [ { "refType": "CHILD_OF", "traceId": "AAAAAAAAAAAAAAAAAAAAAQ==", "spanId": "AAAAAAAAAAM=" }, { "refType": "FOLLOWS_FROM", "traceId": "AAAAAAAAAAAAAAAAAAAAAQ==", "spanId": "AAAAAAAAAAQ=" }, { "refType": "CHILD_OF", "traceId": "AAAAAAAAAAAAAAAAAAAA/w==", "spanId": "AAAAAAAAAP8=" } ], "flags": 1, "startTime": "2017-01-26T16:46:31.639875-05:00", "duration": "5000ns", "tags": [ { "key": "peer.service", "vType": "STRING", "vStr": "service-y" }, { "key": "peer.ipv4", "vType": "INT64", "vInt64": 23456 }, { "key": "error", "vType": "BOOL", "vBool": true }, { "key": "temperature", "vType": "FLOAT64", "vFloat64": 72.5 }, { "key": "blob", "vType": "BINARY", "vBinary": "AAAwOQ==" } ], "logs": [ { "timestamp": "2017-01-26T16:46:31.639875-05:00", "fields": [ { "key": "event", "vType": "INT64", "vInt64": 123415 } ] }, { "timestamp": "2017-01-26T16:46:31.639875-05:00", "fields": [ { "key": "x", "vType": "STRING", "vStr": "y" } ] } ], "process": { "serviceName": "service-x", "tags": [ { "key": "peer.ipv4", "vType": "INT64", "vInt64": 23456 }, { "key": "error", "vType": "BOOL", "vBool": true } ] } } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/fixtures/es_01.json ================================================ { "traceID": "0000000000000001", "spanID": "0000000000000002", "flags": 1, "operationName": "test-general-conversion", "references": [ { "refType": "CHILD_OF", "traceID": "0000000000000001", "spanID": "0000000000000003" }, { "refType": "FOLLOWS_FROM", "traceID": "0000000000000001", "spanID": "0000000000000004" }, { "refType": "CHILD_OF", "traceID": "00000000000000ff", "spanID": "00000000000000ff" } ], "startTime": 1485467191639875, "startTimeMillis": 1485467191639, "duration": 5, "tags": [ { "key": "peer.service", "type": "string", "value": "service-y" }, { "key": "peer.ipv4", "type": "int64", "value": 23456 }, { "key": "error", "type": "bool", "value": true }, { "key": "temperature", "type": "float64", "value": 72.5 }, { "key": "blob", "type": "binary", "value": "00003039" } ], "logs": [ { "timestamp": 1485467191639875, "fields": [ { "key": "event", "type": "int64", "value": 123415 } ] }, { "timestamp": 1485467191639875, "fields": [ { "key": "x", "type": "string", "value": "y" } ] } ], "process": { "serviceName": "service-x", "tags": [ { "key": "peer.ipv4", "type": "int64", "value": 23456 }, { "key": "error", "type": "bool", "value": true } ] } } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/fixtures/query_01.json ================================================ { "bool":{ "should":[ { "bool":{ "must":{ "regexp":{ "tag.bat@foo":{ "value":"spook" } } } } }, { "bool":{ "must":{ "regexp":{ "process.tag.bat@foo":{ "value":"spook" } } } } }, { "nested":{ "path":"tags", "query":{ "bool":{ "must":[ { "match":{ "tags.key":{ "query":"bat.foo" } } }, { "regexp":{ "tags.value":{ "value":"spook" } } } ] } } } }, { "nested":{ "path":"process.tags", "query":{ "bool":{ "must":[ { "match":{ "process.tags.key":{ "query":"bat.foo" } } }, { "regexp":{ "process.tags.value":{ "value":"spook" } } } ] } } } }, { "nested":{ "path":"logs.fields", "query":{ "bool":{ "must":[ { "match":{ "logs.fields.key":{ "query":"bat.foo" } } }, { "regexp":{ "logs.fields.value":{ "value":"spook" } } } ] } } } } ] } } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/fixtures/query_02.json ================================================ { "bool":{ "should":[ { "bool":{ "must":{ "regexp":{ "tag.bat@foo":{ "value":"spo.*" } } } } }, { "bool":{ "must":{ "regexp":{ "process.tag.bat@foo":{ "value":"spo.*" } } } } }, { "nested":{ "path":"tags", "query":{ "bool":{ "must":[ { "match":{ "tags.key":{ "query":"bat.foo" } } }, { "regexp":{ "tags.value":{ "value":"spo.*" } } } ] } } } }, { "nested":{ "path":"process.tags", "query":{ "bool":{ "must":[ { "match":{ "process.tags.key":{ "query":"bat.foo" } } }, { "regexp":{ "process.tags.value":{ "value":"spo.*" } } } ] } } } }, { "nested":{ "path":"logs.fields", "query":{ "bool":{ "must":[ { "match":{ "logs.fields.key":{ "query":"bat.foo" } } }, { "regexp":{ "logs.fields.value":{ "value":"spo.*" } } } ] } } } } ] } } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/fixtures/query_03.json ================================================ { "bool":{ "should":[ { "bool":{ "must":{ "regexp":{ "tag.bat@foo":{ "value":"spo\\*" } } } } }, { "bool":{ "must":{ "regexp":{ "process.tag.bat@foo":{ "value":"spo\\*" } } } } }, { "nested":{ "path":"tags", "query":{ "bool":{ "must":[ { "match":{ "tags.key":{ "query":"bat.foo" } } }, { "regexp":{ "tags.value":{ "value":"spo\\*" } } } ] } } } }, { "nested":{ "path":"process.tags", "query":{ "bool":{ "must":[ { "match":{ "process.tags.key":{ "query":"bat.foo" } } }, { "regexp":{ "process.tags.value":{ "value":"spo\\*" } } } ] } } } }, { "nested":{ "path":"logs.fields", "query":{ "bool":{ "must":[ { "match":{ "logs.fields.key":{ "query":"bat.foo" } } }, { "regexp":{ "logs.fields.value":{ "value":"spo\\*" } } } ] } } } } ] } } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/from_domain.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2018 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "strings" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" ) // NewFromDomain creates FromDomain used to convert model span to db span func NewFromDomain() FromDomain { return FromDomain{} } // FromDomain is used to convert model span to db span type FromDomain struct{} // FromDomainEmbedProcess converts model.Span into json.Span format. // This format includes a ParentSpanID and an embedded Process. func (fd FromDomain) FromDomainEmbedProcess(span *model.Span) *dbmodel.Span { s := fd.convertSpanInternal(span) s.Process = fd.convertProcess(span.Process) s.References = fd.convertReferences(span) return &s } func (fd FromDomain) convertSpanInternal(span *model.Span) dbmodel.Span { tags := fd.convertKeyValues(span.Tags) return dbmodel.Span{ TraceID: dbmodel.TraceID(span.TraceID.String()), SpanID: dbmodel.SpanID(span.SpanID.String()), Flags: uint32(span.Flags), OperationName: span.OperationName, StartTime: model.TimeAsEpochMicroseconds(span.StartTime), StartTimeMillis: model.TimeAsEpochMicroseconds(span.StartTime) / 1000, Duration: model.DurationAsMicroseconds(span.Duration), Tags: tags, Logs: fd.convertLogs(span.Logs), } } func (fd FromDomain) convertReferences(span *model.Span) []dbmodel.Reference { out := make([]dbmodel.Reference, 0, len(span.References)) for _, ref := range span.References { out = append(out, dbmodel.Reference{ RefType: fd.convertRefType(ref.RefType), TraceID: dbmodel.TraceID(ref.TraceID.String()), SpanID: dbmodel.SpanID(ref.SpanID.String()), }) } return out } func (FromDomain) convertRefType(refType model.SpanRefType) dbmodel.ReferenceType { if refType == model.FollowsFrom { return dbmodel.FollowsFrom } return dbmodel.ChildOf } func (fd FromDomain) convertKeyValues(keyValues model.KeyValues) []dbmodel.KeyValue { kvs := make([]dbmodel.KeyValue, 0, len(keyValues)) for i := range keyValues { kvs = append(kvs, fd.convertKeyValue(keyValues[i])) } if kvs == nil { kvs = make([]dbmodel.KeyValue, 0) } return kvs } func (fd FromDomain) convertLogs(logs []model.Log) []dbmodel.Log { out := make([]dbmodel.Log, len(logs)) for i, log := range logs { var kvs []dbmodel.KeyValue for _, kv := range log.Fields { kvs = append(kvs, fd.convertKeyValue(kv)) } out[i] = dbmodel.Log{ Timestamp: model.TimeAsEpochMicroseconds(log.Timestamp), Fields: kvs, } } return out } func (fd FromDomain) convertProcess(process *model.Process) dbmodel.Process { tags := fd.convertKeyValues(process.Tags) return dbmodel.Process{ ServiceName: process.ServiceName, Tags: tags, } } func (FromDomain) convertKeyValue(kv model.KeyValue) dbmodel.KeyValue { outKv := dbmodel.KeyValue{ Key: kv.Key, Type: dbmodel.ValueType(strings.ToLower(kv.VType.String())), } if kv.GetVType() == model.BinaryType { outKv.Value = kv.AsString() } else { outKv.Value = kv.Value() } return outKv } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/from_domain_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2018 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "bytes" "encoding/hex" "encoding/json" "fmt" "os" "testing" "github.com/gogo/protobuf/jsonpb" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" ) const NumberOfFixtures = 1 func TestFromDomainEmbedProcess(t *testing.T) { for i := 1; i <= NumberOfFixtures; i++ { t.Run(fmt.Sprintf("fixture_%d", i), func(t *testing.T) { domainStr, jsonStr := loadFixtures(t, i) var span model.Span require.NoError(t, jsonpb.Unmarshal(bytes.NewReader(domainStr), &span)) converter := NewFromDomain() embeddedSpan := converter.FromDomainEmbedProcess(&span) testJSONEncoding(t, i, jsonStr, embeddedSpan) CompareJSONSpans(t, jsonStr, embeddedSpan) }) } } // Loads and returns domain model and JSON model fixtures with given number i. func loadFixtures(t *testing.T, i int) (inStr []byte, outStr []byte) { var err error in := fmt.Sprintf("fixtures/domain_%02d.json", i) inStr, err = os.ReadFile(in) require.NoError(t, err) out := fmt.Sprintf("fixtures/es_%02d.json", i) outStr, err = os.ReadFile(out) require.NoError(t, err) return inStr, outStr } func testJSONEncoding(t *testing.T, i int, expectedStr []byte, object any) { buf := &bytes.Buffer{} enc := json.NewEncoder(buf) enc.SetIndent("", " ") outFile := fmt.Sprintf("fixtures/es_%02d", i) require.NoError(t, enc.Encode(object)) if !assert.Equal(t, string(expectedStr), buf.String()) { err := os.WriteFile(outFile+"-actual.json", buf.Bytes(), 0o644) require.NoError(t, err) } } func TestEmptyTags(t *testing.T) { tags := make([]model.KeyValue, 0) span := model.Span{Tags: tags, Process: &model.Process{Tags: tags}} converter := NewFromDomain() dbSpan := converter.FromDomainEmbedProcess(&span) assert.Empty(t, dbSpan.Tags) assert.Empty(t, dbSpan.Tag) } func TestConvertKeyValueValue(t *testing.T) { longString := `Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues ` key := "key" tests := []struct { kv model.KeyValue expected dbmodel.KeyValue }{ { kv: model.Bool(key, true), expected: dbmodel.KeyValue{Key: key, Value: true, Type: "bool"}, }, { kv: model.Bool(key, false), expected: dbmodel.KeyValue{Key: key, Value: false, Type: "bool"}, }, { kv: model.Int64(key, int64(1499)), expected: dbmodel.KeyValue{Key: key, Value: int64(1499), Type: "int64"}, }, { kv: model.Float64(key, float64(15.66)), expected: dbmodel.KeyValue{Key: key, Value: 15.66, Type: "float64"}, }, { kv: model.String(key, longString), expected: dbmodel.KeyValue{Key: key, Value: longString, Type: "string"}, }, { kv: model.Binary(key, []byte(longString)), expected: dbmodel.KeyValue{Key: key, Value: hex.EncodeToString([]byte(longString)), Type: "binary"}, }, } for _, test := range tests { t.Run(fmt.Sprintf("%s:%s", test.expected.Type, test.expected.Key), func(t *testing.T) { converter := NewFromDomain() actual := converter.convertKeyValue(test.kv) assert.Equal(t, test.expected, actual) }) } } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/index_utils.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "time" ) // returns index name with date func indexWithDate(indexPrefix, indexDateLayout string, date time.Time) string { spanDate := date.UTC().Format(indexDateLayout) return indexPrefix + spanDate } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/json_span_compare_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2018 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "bytes" "encoding/json" "testing" "github.com/kr/pretty" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" ) func CompareJSONSpans(t *testing.T, expected []byte, actual *dbmodel.Span) { buf := &bytes.Buffer{} enc := json.NewEncoder(buf) enc.SetIndent("", " ") require.NoError(t, enc.Encode(actual)) if !assert.Equal(t, string(expected), buf.String()) { for _, err := range pretty.Diff(expected, actual) { t.Log(err) } out, err := json.Marshal(actual) require.NoError(t, err) t.Logf("Actual trace: %s", string(out)) } } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/mocks/mocks.go ================================================ // Copyright (c) The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // // Run 'make generate-mocks' to regenerate. // Code generated by mockery; DO NOT EDIT. // github.com/vektra/mockery // template: testify package mocks import ( "context" "time" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" mock "github.com/stretchr/testify/mock" ) // NewCoreSpanReader creates a new instance of CoreSpanReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewCoreSpanReader(t interface { mock.TestingT Cleanup(func()) }) *CoreSpanReader { mock := &CoreSpanReader{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // CoreSpanReader is an autogenerated mock type for the CoreSpanReader type type CoreSpanReader struct { mock.Mock } type CoreSpanReader_Expecter struct { mock *mock.Mock } func (_m *CoreSpanReader) EXPECT() *CoreSpanReader_Expecter { return &CoreSpanReader_Expecter{mock: &_m.Mock} } // FindTraceIDs provides a mock function for the type CoreSpanReader func (_mock *CoreSpanReader) FindTraceIDs(ctx context.Context, traceQuery dbmodel.TraceQueryParameters) ([]dbmodel.TraceID, error) { ret := _mock.Called(ctx, traceQuery) if len(ret) == 0 { panic("no return value specified for FindTraceIDs") } var r0 []dbmodel.TraceID var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, dbmodel.TraceQueryParameters) ([]dbmodel.TraceID, error)); ok { return returnFunc(ctx, traceQuery) } if returnFunc, ok := ret.Get(0).(func(context.Context, dbmodel.TraceQueryParameters) []dbmodel.TraceID); ok { r0 = returnFunc(ctx, traceQuery) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]dbmodel.TraceID) } } if returnFunc, ok := ret.Get(1).(func(context.Context, dbmodel.TraceQueryParameters) error); ok { r1 = returnFunc(ctx, traceQuery) } else { r1 = ret.Error(1) } return r0, r1 } // CoreSpanReader_FindTraceIDs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindTraceIDs' type CoreSpanReader_FindTraceIDs_Call struct { *mock.Call } // FindTraceIDs is a helper method to define mock.On call // - ctx context.Context // - traceQuery dbmodel.TraceQueryParameters func (_e *CoreSpanReader_Expecter) FindTraceIDs(ctx interface{}, traceQuery interface{}) *CoreSpanReader_FindTraceIDs_Call { return &CoreSpanReader_FindTraceIDs_Call{Call: _e.mock.On("FindTraceIDs", ctx, traceQuery)} } func (_c *CoreSpanReader_FindTraceIDs_Call) Run(run func(ctx context.Context, traceQuery dbmodel.TraceQueryParameters)) *CoreSpanReader_FindTraceIDs_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 dbmodel.TraceQueryParameters if args[1] != nil { arg1 = args[1].(dbmodel.TraceQueryParameters) } run( arg0, arg1, ) }) return _c } func (_c *CoreSpanReader_FindTraceIDs_Call) Return(traceIDs []dbmodel.TraceID, err error) *CoreSpanReader_FindTraceIDs_Call { _c.Call.Return(traceIDs, err) return _c } func (_c *CoreSpanReader_FindTraceIDs_Call) RunAndReturn(run func(ctx context.Context, traceQuery dbmodel.TraceQueryParameters) ([]dbmodel.TraceID, error)) *CoreSpanReader_FindTraceIDs_Call { _c.Call.Return(run) return _c } // FindTraces provides a mock function for the type CoreSpanReader func (_mock *CoreSpanReader) FindTraces(ctx context.Context, traceQuery dbmodel.TraceQueryParameters) ([]dbmodel.Trace, error) { ret := _mock.Called(ctx, traceQuery) if len(ret) == 0 { panic("no return value specified for FindTraces") } var r0 []dbmodel.Trace var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, dbmodel.TraceQueryParameters) ([]dbmodel.Trace, error)); ok { return returnFunc(ctx, traceQuery) } if returnFunc, ok := ret.Get(0).(func(context.Context, dbmodel.TraceQueryParameters) []dbmodel.Trace); ok { r0 = returnFunc(ctx, traceQuery) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]dbmodel.Trace) } } if returnFunc, ok := ret.Get(1).(func(context.Context, dbmodel.TraceQueryParameters) error); ok { r1 = returnFunc(ctx, traceQuery) } else { r1 = ret.Error(1) } return r0, r1 } // CoreSpanReader_FindTraces_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindTraces' type CoreSpanReader_FindTraces_Call struct { *mock.Call } // FindTraces is a helper method to define mock.On call // - ctx context.Context // - traceQuery dbmodel.TraceQueryParameters func (_e *CoreSpanReader_Expecter) FindTraces(ctx interface{}, traceQuery interface{}) *CoreSpanReader_FindTraces_Call { return &CoreSpanReader_FindTraces_Call{Call: _e.mock.On("FindTraces", ctx, traceQuery)} } func (_c *CoreSpanReader_FindTraces_Call) Run(run func(ctx context.Context, traceQuery dbmodel.TraceQueryParameters)) *CoreSpanReader_FindTraces_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 dbmodel.TraceQueryParameters if args[1] != nil { arg1 = args[1].(dbmodel.TraceQueryParameters) } run( arg0, arg1, ) }) return _c } func (_c *CoreSpanReader_FindTraces_Call) Return(traces []dbmodel.Trace, err error) *CoreSpanReader_FindTraces_Call { _c.Call.Return(traces, err) return _c } func (_c *CoreSpanReader_FindTraces_Call) RunAndReturn(run func(ctx context.Context, traceQuery dbmodel.TraceQueryParameters) ([]dbmodel.Trace, error)) *CoreSpanReader_FindTraces_Call { _c.Call.Return(run) return _c } // GetOperations provides a mock function for the type CoreSpanReader func (_mock *CoreSpanReader) GetOperations(ctx context.Context, query dbmodel.OperationQueryParameters) ([]dbmodel.Operation, error) { ret := _mock.Called(ctx, query) if len(ret) == 0 { panic("no return value specified for GetOperations") } var r0 []dbmodel.Operation var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, dbmodel.OperationQueryParameters) ([]dbmodel.Operation, error)); ok { return returnFunc(ctx, query) } if returnFunc, ok := ret.Get(0).(func(context.Context, dbmodel.OperationQueryParameters) []dbmodel.Operation); ok { r0 = returnFunc(ctx, query) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]dbmodel.Operation) } } if returnFunc, ok := ret.Get(1).(func(context.Context, dbmodel.OperationQueryParameters) error); ok { r1 = returnFunc(ctx, query) } else { r1 = ret.Error(1) } return r0, r1 } // CoreSpanReader_GetOperations_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetOperations' type CoreSpanReader_GetOperations_Call struct { *mock.Call } // GetOperations is a helper method to define mock.On call // - ctx context.Context // - query dbmodel.OperationQueryParameters func (_e *CoreSpanReader_Expecter) GetOperations(ctx interface{}, query interface{}) *CoreSpanReader_GetOperations_Call { return &CoreSpanReader_GetOperations_Call{Call: _e.mock.On("GetOperations", ctx, query)} } func (_c *CoreSpanReader_GetOperations_Call) Run(run func(ctx context.Context, query dbmodel.OperationQueryParameters)) *CoreSpanReader_GetOperations_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 dbmodel.OperationQueryParameters if args[1] != nil { arg1 = args[1].(dbmodel.OperationQueryParameters) } run( arg0, arg1, ) }) return _c } func (_c *CoreSpanReader_GetOperations_Call) Return(operations []dbmodel.Operation, err error) *CoreSpanReader_GetOperations_Call { _c.Call.Return(operations, err) return _c } func (_c *CoreSpanReader_GetOperations_Call) RunAndReturn(run func(ctx context.Context, query dbmodel.OperationQueryParameters) ([]dbmodel.Operation, error)) *CoreSpanReader_GetOperations_Call { _c.Call.Return(run) return _c } // GetServices provides a mock function for the type CoreSpanReader func (_mock *CoreSpanReader) GetServices(ctx context.Context) ([]string, error) { ret := _mock.Called(ctx) if len(ret) == 0 { panic("no return value specified for GetServices") } var r0 []string var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context) ([]string, error)); ok { return returnFunc(ctx) } if returnFunc, ok := ret.Get(0).(func(context.Context) []string); ok { r0 = returnFunc(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]string) } } if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { r1 = returnFunc(ctx) } else { r1 = ret.Error(1) } return r0, r1 } // CoreSpanReader_GetServices_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetServices' type CoreSpanReader_GetServices_Call struct { *mock.Call } // GetServices is a helper method to define mock.On call // - ctx context.Context func (_e *CoreSpanReader_Expecter) GetServices(ctx interface{}) *CoreSpanReader_GetServices_Call { return &CoreSpanReader_GetServices_Call{Call: _e.mock.On("GetServices", ctx)} } func (_c *CoreSpanReader_GetServices_Call) Run(run func(ctx context.Context)) *CoreSpanReader_GetServices_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } run( arg0, ) }) return _c } func (_c *CoreSpanReader_GetServices_Call) Return(strings []string, err error) *CoreSpanReader_GetServices_Call { _c.Call.Return(strings, err) return _c } func (_c *CoreSpanReader_GetServices_Call) RunAndReturn(run func(ctx context.Context) ([]string, error)) *CoreSpanReader_GetServices_Call { _c.Call.Return(run) return _c } // GetTraces provides a mock function for the type CoreSpanReader func (_mock *CoreSpanReader) GetTraces(ctx context.Context, query []dbmodel.TraceID) ([]dbmodel.Trace, error) { ret := _mock.Called(ctx, query) if len(ret) == 0 { panic("no return value specified for GetTraces") } var r0 []dbmodel.Trace var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, []dbmodel.TraceID) ([]dbmodel.Trace, error)); ok { return returnFunc(ctx, query) } if returnFunc, ok := ret.Get(0).(func(context.Context, []dbmodel.TraceID) []dbmodel.Trace); ok { r0 = returnFunc(ctx, query) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]dbmodel.Trace) } } if returnFunc, ok := ret.Get(1).(func(context.Context, []dbmodel.TraceID) error); ok { r1 = returnFunc(ctx, query) } else { r1 = ret.Error(1) } return r0, r1 } // CoreSpanReader_GetTraces_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTraces' type CoreSpanReader_GetTraces_Call struct { *mock.Call } // GetTraces is a helper method to define mock.On call // - ctx context.Context // - query []dbmodel.TraceID func (_e *CoreSpanReader_Expecter) GetTraces(ctx interface{}, query interface{}) *CoreSpanReader_GetTraces_Call { return &CoreSpanReader_GetTraces_Call{Call: _e.mock.On("GetTraces", ctx, query)} } func (_c *CoreSpanReader_GetTraces_Call) Run(run func(ctx context.Context, query []dbmodel.TraceID)) *CoreSpanReader_GetTraces_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 []dbmodel.TraceID if args[1] != nil { arg1 = args[1].([]dbmodel.TraceID) } run( arg0, arg1, ) }) return _c } func (_c *CoreSpanReader_GetTraces_Call) Return(traces []dbmodel.Trace, err error) *CoreSpanReader_GetTraces_Call { _c.Call.Return(traces, err) return _c } func (_c *CoreSpanReader_GetTraces_Call) RunAndReturn(run func(ctx context.Context, query []dbmodel.TraceID) ([]dbmodel.Trace, error)) *CoreSpanReader_GetTraces_Call { _c.Call.Return(run) return _c } // NewCoreSpanWriter creates a new instance of CoreSpanWriter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewCoreSpanWriter(t interface { mock.TestingT Cleanup(func()) }) *CoreSpanWriter { mock := &CoreSpanWriter{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // CoreSpanWriter is an autogenerated mock type for the CoreSpanWriter type type CoreSpanWriter struct { mock.Mock } type CoreSpanWriter_Expecter struct { mock *mock.Mock } func (_m *CoreSpanWriter) EXPECT() *CoreSpanWriter_Expecter { return &CoreSpanWriter_Expecter{mock: &_m.Mock} } // Close provides a mock function for the type CoreSpanWriter func (_mock *CoreSpanWriter) Close() error { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Close") } var r0 error if returnFunc, ok := ret.Get(0).(func() error); ok { r0 = returnFunc() } else { r0 = ret.Error(0) } return r0 } // CoreSpanWriter_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' type CoreSpanWriter_Close_Call struct { *mock.Call } // Close is a helper method to define mock.On call func (_e *CoreSpanWriter_Expecter) Close() *CoreSpanWriter_Close_Call { return &CoreSpanWriter_Close_Call{Call: _e.mock.On("Close")} } func (_c *CoreSpanWriter_Close_Call) Run(run func()) *CoreSpanWriter_Close_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *CoreSpanWriter_Close_Call) Return(err error) *CoreSpanWriter_Close_Call { _c.Call.Return(err) return _c } func (_c *CoreSpanWriter_Close_Call) RunAndReturn(run func() error) *CoreSpanWriter_Close_Call { _c.Call.Return(run) return _c } // WriteSpan provides a mock function for the type CoreSpanWriter func (_mock *CoreSpanWriter) WriteSpan(spanStartTime time.Time, span *dbmodel.Span) { _mock.Called(spanStartTime, span) return } // CoreSpanWriter_WriteSpan_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteSpan' type CoreSpanWriter_WriteSpan_Call struct { *mock.Call } // WriteSpan is a helper method to define mock.On call // - spanStartTime time.Time // - span *dbmodel.Span func (_e *CoreSpanWriter_Expecter) WriteSpan(spanStartTime interface{}, span interface{}) *CoreSpanWriter_WriteSpan_Call { return &CoreSpanWriter_WriteSpan_Call{Call: _e.mock.On("WriteSpan", spanStartTime, span)} } func (_c *CoreSpanWriter_WriteSpan_Call) Run(run func(spanStartTime time.Time, span *dbmodel.Span)) *CoreSpanWriter_WriteSpan_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 time.Time if args[0] != nil { arg0 = args[0].(time.Time) } var arg1 *dbmodel.Span if args[1] != nil { arg1 = args[1].(*dbmodel.Span) } run( arg0, arg1, ) }) return _c } func (_c *CoreSpanWriter_WriteSpan_Call) Return() *CoreSpanWriter_WriteSpan_Call { _c.Call.Return() return _c } func (_c *CoreSpanWriter_WriteSpan_Call) RunAndReturn(run func(spanStartTime time.Time, span *dbmodel.Span)) *CoreSpanWriter_WriteSpan_Call { _c.Run(run) return _c } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/reader.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "bytes" "context" "encoding/json" "errors" "fmt" "strings" "time" "github.com/olivere/elastic/v7" "go.opentelemetry.io/collector/featuregate" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" cfg "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" esquery "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/query" ) const ( spanIndexBaseName = "jaeger-span-" serviceIndexBaseName = "jaeger-service-" traceIDAggregation = "traceIDs" indexPrefixSeparator = "-" traceIDField = "traceID" durationField = "duration" startTimeField = "startTime" startTimeMillisField = "startTimeMillis" serviceNameField = "process.serviceName" operationNameField = "operationName" objectTagsField = "tag" objectProcessTagsField = "process.tag" nestedTagsField = "tags" nestedProcessTagsField = "process.tags" nestedLogFieldsField = "logs.fields" tagKeyField = "key" tagValueField = "value" defaultNumTraces = 100 dawnOfTimeSpanAge = time.Hour * 24 * 365 * 50 ) var ( // ErrServiceNameNotSet occurs when attempting to query with an empty service name ErrServiceNameNotSet = errors.New("service Name must be set") // ErrStartTimeMinGreaterThanMax occurs when start time min is above start time max ErrStartTimeMinGreaterThanMax = errors.New("start Time Minimum is above Maximum") // ErrDurationMinGreaterThanMax occurs when duration min is above duration max ErrDurationMinGreaterThanMax = errors.New("duration Minimum is above Maximum") // ErrMalformedRequestObject occurs when a request object is nil ErrMalformedRequestObject = errors.New("malformed request object") // ErrStartAndEndTimeNotSet occurs when start time and end time are not set ErrStartAndEndTimeNotSet = errors.New("start and End Time must be set") // ErrUnableToFindTraceIDAggregation occurs when an aggregation query for TraceIDs fail. ErrUnableToFindTraceIDAggregation = errors.New("could not find aggregation of traceIDs") defaultMaxDuration = model.DurationAsMicroseconds(time.Hour * 24) objectTagFieldList = []string{objectTagsField, objectProcessTagsField} nestedTagFieldList = []string{nestedTagsField, nestedProcessTagsField, nestedLogFieldsField} _ CoreSpanReader = (*SpanReader)(nil) // check API conformance disableLegacyIDs *featuregate.Gate ) func init() { disableLegacyIDs = featuregate.GlobalRegistry().MustRegister( "jaeger.es.disableLegacyId", featuregate.StageStable, // enabled by default and cannot be disabled featuregate.WithRegisterFromVersion("v2.5.0"), featuregate.WithRegisterToVersion("v2.8.0"), featuregate.WithRegisterDescription("Legacy trace ids are the ids that used to be rendered with leading 0s omitted. Setting this gate to false will force the reader to search for the spans with trace ids having leading zeroes"), featuregate.WithRegisterReferenceURL("https://github.com/jaegertracing/jaeger/issues/1578")) } // SpanReader can query for and load traces from ElasticSearch type SpanReader struct { client func() es.Client // The age of the oldest service/operation we will look for. Because indices in ElasticSearch are by day, // this will be rounded down to UTC 00:00 of that day. maxSpanAge time.Duration serviceOperationStorage *ServiceOperationStorage spanIndexPrefix string serviceIndexPrefix string spanIndex cfg.IndexOptions serviceIndex cfg.IndexOptions timeRangeIndices TimeRangeIndexFn sourceFn sourceFn maxDocCount int useReadWriteAliases bool logger *zap.Logger tracer trace.Tracer dotReplacer dbmodel.DotReplacer } // SpanReaderParams holds constructor params for NewSpanReader type SpanReaderParams struct { Client func() es.Client MaxSpanAge time.Duration MaxDocCount int IndexPrefix cfg.IndexPrefix SpanIndex cfg.IndexOptions ServiceIndex cfg.IndexOptions TagDotReplacement string ReadAliasSuffix string UseReadWriteAliases bool RemoteReadClusters []string SpanReadAlias string ServiceReadAlias string Logger *zap.Logger Tracer trace.Tracer } // NewSpanReader returns a new SpanReader with a metrics. func NewSpanReader(p SpanReaderParams) *SpanReader { spanIndexPrefix := p.SpanReadAlias serviceIndexPrefix := p.ServiceReadAlias if spanIndexPrefix == "" { spanIndexPrefix = p.IndexPrefix.Apply(spanIndexBaseName) } if serviceIndexPrefix == "" { serviceIndexPrefix = p.IndexPrefix.Apply(serviceIndexBaseName) } maxSpanAge := p.MaxSpanAge // Setting the maxSpanAge to a large duration will ensure all spans in the "read" alias are accessible by queries (query window = [now - maxSpanAge, now]). if p.UseReadWriteAliases { maxSpanAge = dawnOfTimeSpanAge } var timeRangeFn TimeRangeIndexFn if p.SpanReadAlias != "" && p.ServiceReadAlias != "" { // When using explicit aliases, return them directly without any date logic timeRangeFn = func(indexPrefix string, _ string, _ time.Time, _ time.Time, _ time.Duration) []string { return []string{indexPrefix} } } else { timeRangeFn = TimeRangeIndicesFn(p.UseReadWriteAliases, p.ReadAliasSuffix, p.RemoteReadClusters) } return &SpanReader{ client: p.Client, maxSpanAge: maxSpanAge, serviceOperationStorage: NewServiceOperationStorage(p.Client, p.Logger, 0), // the decorator takes care of metrics spanIndexPrefix: spanIndexPrefix, serviceIndexPrefix: serviceIndexPrefix, spanIndex: p.SpanIndex, serviceIndex: p.ServiceIndex, timeRangeIndices: LoggingTimeRangeIndexFn(p.Logger, timeRangeFn), sourceFn: getSourceFn(p.MaxDocCount), maxDocCount: p.MaxDocCount, useReadWriteAliases: p.UseReadWriteAliases, logger: p.Logger, tracer: p.Tracer, dotReplacer: dbmodel.NewDotReplacer(p.TagDotReplacement), } } type TimeRangeIndexFn func(indexName string, indexDateLayout string, startTime time.Time, endTime time.Time, reduceDuration time.Duration) []string type sourceFn func(query elastic.Query, nextTime uint64) *elastic.SearchSource func LoggingTimeRangeIndexFn(logger *zap.Logger, fn TimeRangeIndexFn) TimeRangeIndexFn { if !logger.Core().Enabled(zap.DebugLevel) { return fn } return func(indexName string, indexDateLayout string, startTime time.Time, endTime time.Time, reduceDuration time.Duration) []string { indices := fn(indexName, indexDateLayout, startTime, endTime, reduceDuration) logger.Debug("Reading from ES indices", zap.Strings("index", indices)) return indices } } func TimeRangeIndicesFn(useReadWriteAliases bool, readAliasSuffix string, remoteReadClusters []string) TimeRangeIndexFn { suffix := "" if useReadWriteAliases { if readAliasSuffix != "" { suffix = readAliasSuffix } else { suffix = "read" } } return addRemoteReadClusters( getTimeRangeIndexFn(useReadWriteAliases, suffix), remoteReadClusters, ) } func getTimeRangeIndexFn(useReadWriteAliases bool, readAlias string) TimeRangeIndexFn { if useReadWriteAliases { return func(indexPrefix, _ /* indexDateLayout */ string, _ /* startTime */ time.Time, _ /* endTime */ time.Time, _ /* reduceDuration */ time.Duration) []string { return []string{indexPrefix + readAlias} } } return timeRangeIndices } // Add a remote cluster prefix for each cluster and for each index and add it to the list of original indices. // Elasticsearch cross cluster api example GET /twitter,cluster_one:twitter,cluster_two:twitter/_search. func addRemoteReadClusters(fn TimeRangeIndexFn, remoteReadClusters []string) TimeRangeIndexFn { return func(indexPrefix string, indexDateLayout string, startTime time.Time, endTime time.Time, reduceDuration time.Duration) []string { jaegerIndices := fn(indexPrefix, indexDateLayout, startTime, endTime, reduceDuration) if len(remoteReadClusters) == 0 { return jaegerIndices } for _, jaegerIndex := range jaegerIndices { for _, remoteCluster := range remoteReadClusters { remoteIndex := remoteCluster + ":" + jaegerIndex jaegerIndices = append(jaegerIndices, remoteIndex) } } return jaegerIndices } } func getSourceFn(maxDocCount int) sourceFn { return func(query elastic.Query, nextTime uint64) *elastic.SearchSource { return elastic.NewSearchSource(). Query(query). Size(maxDocCount). Sort("startTime", true). SearchAfter(nextTime) } } // timeRangeIndices returns the array of indices that we need to query, based on query params func timeRangeIndices(indexName, indexDateLayout string, startTime time.Time, endTime time.Time, reduceDuration time.Duration) []string { var indices []string firstIndex := indexWithDate(indexName, indexDateLayout, startTime) currentIndex := indexWithDate(indexName, indexDateLayout, endTime) for currentIndex != firstIndex && endTime.After(startTime) { if len(indices) == 0 || indices[len(indices)-1] != currentIndex { indices = append(indices, currentIndex) } endTime = endTime.Add(reduceDuration) currentIndex = indexWithDate(indexName, indexDateLayout, endTime) } indices = append(indices, firstIndex) return indices } // GetTraces takes a traceID and returns a Trace associated with that traceID func (s *SpanReader) GetTraces(ctx context.Context, query []dbmodel.TraceID) ([]dbmodel.Trace, error) { ctx, span := s.tracer.Start(ctx, "GetTrace") defer span.End() currentTime := time.Now() // TODO: use start time & end time in "query" struct return s.multiRead(ctx, query, currentTime.Add(-s.maxSpanAge), currentTime) } func (s *SpanReader) collectSpans(esSpansRaw []*elastic.SearchHit) ([]dbmodel.Span, error) { spans := make([]dbmodel.Span, len(esSpansRaw)) for i, esSpanRaw := range esSpansRaw { dbSpan, err := s.unmarshalJSONSpan(esSpanRaw) if err != nil { return nil, fmt.Errorf("marshalling JSON to span object failed: %w", err) } s.mergeAllNestedAndElevatedTagsOfSpan(&dbSpan) spans[i] = dbSpan } return spans, nil } func (*SpanReader) unmarshalJSONSpan(esSpanRaw *elastic.SearchHit) (dbmodel.Span, error) { esSpanInByteArray := esSpanRaw.Source var jsonSpan dbmodel.Span d := json.NewDecoder(bytes.NewReader(esSpanInByteArray)) d.UseNumber() if err := d.Decode(&jsonSpan); err != nil { return dbmodel.Span{}, err } return jsonSpan, nil } // GetServices returns all services traced by Jaeger, ordered by frequency func (s *SpanReader) GetServices(ctx context.Context) ([]string, error) { ctx, span := s.tracer.Start(ctx, "GetService") defer span.End() currentTime := time.Now() jaegerIndices := s.timeRangeIndices( s.serviceIndexPrefix, s.serviceIndex.DateLayout, currentTime.Add(-s.maxSpanAge), currentTime, cfg.RolloverFrequencyAsNegativeDuration(s.serviceIndex.RolloverFrequency), ) return s.serviceOperationStorage.getServices(ctx, jaegerIndices, s.maxDocCount) } // GetOperations returns all operations for a specific service traced by Jaeger func (s *SpanReader) GetOperations( ctx context.Context, query dbmodel.OperationQueryParameters, ) ([]dbmodel.Operation, error) { ctx, span := s.tracer.Start(ctx, "GetOperations") defer span.End() currentTime := time.Now() jaegerIndices := s.timeRangeIndices( s.serviceIndexPrefix, s.serviceIndex.DateLayout, currentTime.Add(-s.maxSpanAge), currentTime, cfg.RolloverFrequencyAsNegativeDuration(s.serviceIndex.RolloverFrequency), ) operations, err := s.serviceOperationStorage.getOperations(ctx, jaegerIndices, query.ServiceName, s.maxDocCount) if err != nil { return nil, err } // TODO: https://github.com/jaegertracing/jaeger/issues/1923 // - return the operations with actual span kind that meet requirement var result []dbmodel.Operation for _, operation := range operations { result = append(result, dbmodel.Operation{ Name: operation, }) } return result, err } func bucketToStringArray[T ~string](buckets []*elastic.AggregationBucketKeyItem) ([]T, error) { stringSlice := make([]T, len(buckets)) for i, keyitem := range buckets { str, ok := keyitem.Key.(string) if !ok { return nil, errors.New("non-string key found in aggregation") } stringSlice[i] = T(str) } return stringSlice, nil } // FindTraces retrieves traces that match the traceQuery func (s *SpanReader) FindTraces(ctx context.Context, traceQuery dbmodel.TraceQueryParameters) ([]dbmodel.Trace, error) { ctx, span := s.tracer.Start(ctx, "FindTraces") defer span.End() uniqueTraceIDs, err := s.FindTraceIDs(ctx, traceQuery) if err != nil { return nil, es.DetailedError(err) } return s.multiRead(ctx, uniqueTraceIDs, traceQuery.StartTimeMin, traceQuery.StartTimeMax) } // FindTraceIDs retrieves traces IDs that match the traceQuery func (s *SpanReader) FindTraceIDs(ctx context.Context, traceQuery dbmodel.TraceQueryParameters) ([]dbmodel.TraceID, error) { ctx, span := s.tracer.Start(ctx, "FindTraceIDs") defer span.End() if err := validateQuery(traceQuery); err != nil { return nil, err } if traceQuery.NumTraces == 0 { traceQuery.NumTraces = defaultNumTraces } esTraceIDs, err := s.findTraceIDsFromQuery(ctx, traceQuery) if err != nil { return nil, err } return esTraceIDs, nil } func (s *SpanReader) multiRead(ctx context.Context, traceIDs []dbmodel.TraceID, startTime, endTime time.Time) ([]dbmodel.Trace, error) { ctx, childSpan := s.tracer.Start(ctx, "multiRead") defer childSpan.End() if childSpan.IsRecording() { tracesIDs := make([]string, len(traceIDs)) for i, traceID := range traceIDs { tracesIDs[i] = string(traceID) } childSpan.SetAttributes(attribute.Key("trace_ids").StringSlice(tracesIDs)) } traces := make([]dbmodel.Trace, 0, len(traceIDs)) if len(traceIDs) == 0 { return traces, nil } // Add an hour in both directions so that traces that straddle two indexes are retrieved. // i.e starts in one and ends in another. indices := s.timeRangeIndices( s.spanIndexPrefix, s.spanIndex.DateLayout, startTime.Add(-time.Hour), endTime.Add(time.Hour), cfg.RolloverFrequencyAsNegativeDuration(s.spanIndex.RolloverFrequency), ) nextTime := model.TimeAsEpochMicroseconds(startTime.Add(-time.Hour)) searchAfterTime := make(map[dbmodel.TraceID]uint64) totalDocumentsFetched := make(map[dbmodel.TraceID]int) tracesMap := make(map[dbmodel.TraceID]*dbmodel.Trace) for len(traceIDs) != 0 { searchRequests := make([]*elastic.SearchRequest, len(traceIDs)) for i, traceID := range traceIDs { traceQuery := buildTraceByIDQuery(traceID) query := elastic.NewBoolQuery(). Must(traceQuery) if s.useReadWriteAliases { startTimeRangeQuery := s.buildStartTimeQuery(startTime.Add(-time.Hour*24), endTime.Add(time.Hour*24)) query = query.Must(startTimeRangeQuery) } if val, ok := searchAfterTime[traceID]; ok { nextTime = val } s := s.sourceFn(query, nextTime). TrackTotalHits(true) searchRequests[i] = elastic.NewSearchRequest(). IgnoreUnavailable(true). Source(s) } // set traceIDs to empty traceIDs = nil results, err := s.client().MultiSearch().Add(searchRequests...).Index(indices...).Do(ctx) if err != nil { err = es.DetailedError(err) logErrorToSpan(childSpan, err) return nil, err } if len(results.Responses) == 0 { break } for _, result := range results.Responses { if result.Hits == nil || len(result.Hits.Hits) == 0 { continue } spans, err := s.collectSpans(result.Hits.Hits) if err != nil { err = es.DetailedError(err) logErrorToSpan(childSpan, err) return nil, err } lastSpan := spans[len(spans)-1] if traceSpan, ok := tracesMap[lastSpan.TraceID]; ok { traceSpan.Spans = append(traceSpan.Spans, spans...) } else { traces = append(traces, dbmodel.Trace{Spans: spans}) tracesMap[lastSpan.TraceID] = &traces[len(traces)-1] } totalDocumentsFetched[lastSpan.TraceID] += len(result.Hits.Hits) if totalDocumentsFetched[lastSpan.TraceID] < int(result.TotalHits()) { traceIDs = append(traceIDs, lastSpan.TraceID) searchAfterTime[lastSpan.TraceID] = lastSpan.StartTime } } } return traces, nil } func buildTraceByIDQuery(traceID dbmodel.TraceID) elastic.Query { traceIDStr := string(traceID) if traceIDStr[0] != '0' || disableLegacyIDs.IsEnabled() { return elastic.NewTermQuery(traceIDField, traceIDStr) } // https://github.com/jaegertracing/jaeger/pull/1956 added leading zeros to IDs // So we need to also read IDs without leading zeros for compatibility with previously saved data. legacyTraceID := strings.TrimLeft(traceIDStr, "0") return elastic.NewBoolQuery().Should( elastic.NewTermQuery(traceIDField, traceIDStr).Boost(2), elastic.NewTermQuery(traceIDField, legacyTraceID)) } func validateQuery(p dbmodel.TraceQueryParameters) error { if p.ServiceName == "" && len(p.Tags) > 0 { return ErrServiceNameNotSet } if p.StartTimeMin.IsZero() || p.StartTimeMax.IsZero() { return ErrStartAndEndTimeNotSet } if p.StartTimeMax.Before(p.StartTimeMin) { return ErrStartTimeMinGreaterThanMax } if p.DurationMin != 0 && p.DurationMax != 0 && p.DurationMin > p.DurationMax { return ErrDurationMinGreaterThanMax } return nil } func (s *SpanReader) findTraceIDsFromQuery(ctx context.Context, traceQuery dbmodel.TraceQueryParameters) ([]dbmodel.TraceID, error) { ctx, childSpan := s.tracer.Start(ctx, "findTraceIDs") defer childSpan.End() // Below is the JSON body to our HTTP GET request to ElasticSearch. This function creates this. // { // "size": 0, // "query": { // "bool": { // "must": [ // { "match": { "operationName": "op1" }}, // { "match": { "process.serviceName": "service1" }}, // { "range": { "startTime": { "gte": 0, "lte": 90000000000000000 }}}, // { "range": { "duration": { "gte": 0, "lte": 90000000000000000 }}}, // { "should": [ // { "nested" : { // "path" : "tags", // "query" : { // "bool" : { // "must" : [ // { "match" : {"tags.key" : "tag3"} }, // { "match" : {"tags.value" : "xyz"} } // ] // }}}}, // { "nested" : { // "path" : "process.tags", // "query" : { // "bool" : { // "must" : [ // { "match" : {"tags.key" : "tag3"} }, // { "match" : {"tags.value" : "xyz"} } // ] // }}}}, // { "nested" : { // "path" : "logs.fields", // "query" : { // "bool" : { // "must" : [ // { "match" : {"tags.key" : "tag3"} }, // { "match" : {"tags.value" : "xyz"} } // ] // }}}}, // { "bool":{ // "must": { // "match":{ "tags.bat":{ "query":"spook" }} // }}}, // { "bool":{ // "must": { // "match":{ "tag.bat":{ "query":"spook" }} // }}} // ] // } // ] // } // }, // "aggs": { "traceIDs" : { "terms" : {"size": 100,"field": "traceID" }}} // } aggregation := s.buildTraceIDAggregation(traceQuery.NumTraces) boolQuery := s.buildFindTraceIDsQuery(traceQuery) jaegerIndices := s.timeRangeIndices( s.spanIndexPrefix, s.spanIndex.DateLayout, traceQuery.StartTimeMin, traceQuery.StartTimeMax, cfg.RolloverFrequencyAsNegativeDuration(s.spanIndex.RolloverFrequency), ) searchService := s.client().Search(jaegerIndices...). Size(0). // set to 0 because we don't want actual documents. Aggregation(traceIDAggregation, aggregation). IgnoreUnavailable(true). Query(boolQuery) searchResult, err := searchService.Do(ctx) if err != nil { err = es.DetailedError(err) s.logger.Info("es search services failed", zap.Any("traceQuery", traceQuery), zap.Error(err)) return nil, fmt.Errorf("search services failed: %w", err) } if searchResult.Aggregations == nil { return []dbmodel.TraceID{}, nil } bucket, found := searchResult.Aggregations.Terms(traceIDAggregation) if !found { return nil, ErrUnableToFindTraceIDAggregation } traceIDBuckets := bucket.Buckets return bucketToStringArray[dbmodel.TraceID](traceIDBuckets) } func (s *SpanReader) buildTraceIDAggregation(numOfTraces int) elastic.Aggregation { return elastic.NewTermsAggregation(). Size(numOfTraces). Field(traceIDField). Order(startTimeField, false). SubAggregation(startTimeField, s.buildTraceIDSubAggregation()) } func (*SpanReader) buildTraceIDSubAggregation() elastic.Aggregation { return elastic.NewMaxAggregation(). Field(startTimeField) } func (s *SpanReader) buildFindTraceIDsQuery(traceQuery dbmodel.TraceQueryParameters) elastic.Query { boolQuery := elastic.NewBoolQuery() // add duration query if traceQuery.DurationMax != 0 || traceQuery.DurationMin != 0 { durationQuery := s.buildDurationQuery(traceQuery.DurationMin, traceQuery.DurationMax) boolQuery.Must(durationQuery) } // add startTime query startTimeQuery := s.buildStartTimeQuery(traceQuery.StartTimeMin, traceQuery.StartTimeMax) boolQuery.Must(startTimeQuery) // add process.serviceName query if traceQuery.ServiceName != "" { serviceNameQuery := s.buildServiceNameQuery(traceQuery.ServiceName) boolQuery.Must(serviceNameQuery) } // add operationName query if traceQuery.OperationName != "" { operationNameQuery := s.buildOperationNameQuery(traceQuery.OperationName) boolQuery.Must(operationNameQuery) } for k, v := range traceQuery.Tags { tagQuery := s.buildTagQuery(k, v) boolQuery.Must(tagQuery) } return boolQuery } func (*SpanReader) buildDurationQuery(durationMin time.Duration, durationMax time.Duration) elastic.Query { minDurationMicros := model.DurationAsMicroseconds(durationMin) maxDurationMicros := defaultMaxDuration if durationMax != 0 { maxDurationMicros = model.DurationAsMicroseconds(durationMax) } return esquery.NewRangeQuery(durationField).Gte(minDurationMicros).Lte(maxDurationMicros) } func (*SpanReader) buildStartTimeQuery(startTimeMin time.Time, startTimeMax time.Time) elastic.Query { minStartTimeMicros := model.TimeAsEpochMicroseconds(startTimeMin) maxStartTimeMicros := model.TimeAsEpochMicroseconds(startTimeMax) // startTimeMillisField is date field in ES mapping. // Using date field in range queries helps to skip search on unnecessary shards at Elasticsearch side. // https://discuss.elastic.co/t/timeline-query-on-timestamped-indices/129328/2 return esquery.NewRangeQuery(startTimeMillisField).Gte(minStartTimeMicros / 1000).Lte(maxStartTimeMicros / 1000) } func (*SpanReader) buildServiceNameQuery(serviceName string) elastic.Query { return elastic.NewMatchQuery(serviceNameField, serviceName) } func (*SpanReader) buildOperationNameQuery(operationName string) elastic.Query { return elastic.NewMatchQuery(operationNameField, operationName) } func (s *SpanReader) buildTagQuery(k string, v string) elastic.Query { objectTagListLen := len(objectTagFieldList) queries := make([]elastic.Query, len(nestedTagFieldList)+objectTagListLen) kd := s.dotReplacer.ReplaceDot(k) for i := range objectTagFieldList { queries[i] = s.buildObjectQuery(objectTagFieldList[i], kd, v) } for i := range nestedTagFieldList { queries[i+objectTagListLen] = s.buildNestedQuery(nestedTagFieldList[i], k, v) } // but configuration can change over time return elastic.NewBoolQuery().Should(queries...) } func (*SpanReader) buildNestedQuery(field string, k string, v string) elastic.Query { keyField := fmt.Sprintf("%s.%s", field, tagKeyField) valueField := fmt.Sprintf("%s.%s", field, tagValueField) keyQuery := elastic.NewMatchQuery(keyField, k) valueQuery := elastic.NewRegexpQuery(valueField, v) tagBoolQuery := elastic.NewBoolQuery().Must(keyQuery, valueQuery) return elastic.NewNestedQuery(field, tagBoolQuery) } func (*SpanReader) buildObjectQuery(field string, k string, v string) elastic.Query { keyField := fmt.Sprintf("%s.%s", field, k) keyQuery := elastic.NewRegexpQuery(keyField, v) return elastic.NewBoolQuery().Must(keyQuery) } func (s *SpanReader) mergeAllNestedAndElevatedTagsOfSpan(span *dbmodel.Span) { processTags := s.mergeNestedAndElevatedTags(span.Process.Tags, span.Process.Tag) span.Process.Tags = processTags spanTags := s.mergeNestedAndElevatedTags(span.Tags, span.Tag) span.Tags = spanTags } func (s *SpanReader) mergeNestedAndElevatedTags(nestedTags []dbmodel.KeyValue, elevatedTags map[string]any) []dbmodel.KeyValue { mergedTags := make([]dbmodel.KeyValue, 0, len(nestedTags)+len(elevatedTags)) mergedTags = append(mergedTags, nestedTags...) for k, v := range elevatedTags { kv := s.convertTagField(k, v) mergedTags = append(mergedTags, kv) delete(elevatedTags, k) } return mergedTags } func (s *SpanReader) convertTagField(k string, v any) dbmodel.KeyValue { dKey := s.dotReplacer.ReplaceDotReplacement(k) kv := dbmodel.KeyValue{ Key: dKey, Value: v, } switch val := v.(type) { case int64: kv.Type = dbmodel.Int64Type case float64: kv.Type = dbmodel.Float64Type case bool: kv.Type = dbmodel.BoolType case string: kv.Type = dbmodel.StringType // the binary is never returned, ES returns it as string with base64 encoding case []byte: kv.Type = dbmodel.BinaryType // in spans are decoded using json.UseNumber() to preserve the type // however note that float(1) will be parsed as int as ES does not store decimal point case json.Number: n, err := val.Int64() if err == nil { kv.Value = n kv.Type = dbmodel.Int64Type } else { f, err := val.Float64() if err != nil { return dbmodel.KeyValue{ Key: dKey, Value: fmt.Sprintf("invalid tag type in %+v: %s", v, err.Error()), Type: dbmodel.StringType, } } kv.Value = f kv.Type = dbmodel.Float64Type } default: return dbmodel.KeyValue{ Key: dKey, Value: fmt.Sprintf("invalid tag type in %+v", v), Type: dbmodel.StringType, } } return kv } func logErrorToSpan(span trace.Span, err error) { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/reader_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "context" "encoding/json" "errors" "fmt" "maps" "os" "reflect" "testing" "time" "github.com/olivere/elastic/v7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "go.uber.org/zap/zaptest" "github.com/jaegertracing/jaeger-idl/model/v1" es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/mocks" "github.com/jaegertracing/jaeger/internal/testutils" ) const ( defaultMaxDocCount = 10_000 testingTraceId = "testing-id" ) var exampleESSpan = []byte( `{ "traceID": "1", "parentSpanID": "2", "spanID": "3", "flags": 0, "operationName": "op", "references": [], "startTime": 812965625, "duration": 3290114992, "tags": [ { "key": "tag", "value": "1965806585", "type": "int64" } ], "logs": [ { "timestamp": 812966073, "fields": [ { "key": "logtag", "value": "helloworld", "type": "string" } ] } ], "process": { "serviceName": "serv", "tags": [ { "key": "processtag", "value": "false", "type": "bool" } ] } }`) type spanReaderTest struct { client *mocks.Client logger *zap.Logger logBuffer *testutils.Buffer traceBuffer *tracetest.InMemoryExporter reader *SpanReader } func tracerProvider(t *testing.T) (trace.TracerProvider, *tracetest.InMemoryExporter, func()) { exporter := tracetest.NewInMemoryExporter() tp := sdktrace.NewTracerProvider( sdktrace.WithSampler(sdktrace.AlwaysSample()), sdktrace.WithSyncer(exporter), ) closer := func() { require.NoError(t, tp.Shutdown(context.Background())) } return tp, exporter, closer } func withSpanReader(t *testing.T, fn func(r *spanReaderTest)) { client := &mocks.Client{} tracer, exp, closer := tracerProvider(t) defer closer() logger, logBuffer := testutils.NewLogger() r := &spanReaderTest{ client: client, logger: logger, logBuffer: logBuffer, traceBuffer: exp, reader: NewSpanReader(SpanReaderParams{ Client: func() es.Client { return client }, Logger: zap.NewNop(), Tracer: tracer.Tracer("test"), MaxSpanAge: 0, TagDotReplacement: "@", MaxDocCount: defaultMaxDocCount, }), } fn(r) } func withArchiveSpanReader(t *testing.T, readAlias bool, readAliasSuffix string, fn func(r *spanReaderTest)) { client := &mocks.Client{} tracer, exp, closer := tracerProvider(t) defer closer() logger, logBuffer := testutils.NewLogger() r := &spanReaderTest{ client: client, logger: logger, logBuffer: logBuffer, traceBuffer: exp, reader: NewSpanReader(SpanReaderParams{ Client: func() es.Client { return client }, Logger: zap.NewNop(), Tracer: tracer.Tracer("test"), MaxSpanAge: 0, TagDotReplacement: "@", ReadAliasSuffix: readAliasSuffix, UseReadWriteAliases: readAlias, }), } fn(r) } func TestNewSpanReader(t *testing.T) { tests := []struct { name string params SpanReaderParams maxSpanAge time.Duration }{ { name: "no rollover", params: SpanReaderParams{ MaxSpanAge: time.Hour * 72, }, maxSpanAge: time.Hour * 72, }, { name: "rollover enabled", params: SpanReaderParams{ MaxSpanAge: time.Hour * 72, UseReadWriteAliases: true, }, maxSpanAge: time.Hour * 24 * 365 * 50, }, { name: "explicit read aliases with UseReadWriteAliases", params: SpanReaderParams{ MaxSpanAge: time.Hour * 72, UseReadWriteAliases: true, SpanReadAlias: "production-traces-read", ServiceReadAlias: "production-services-read", }, maxSpanAge: time.Hour * 24 * 365 * 50, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { params := test.params params.Logger = zaptest.NewLogger(t) reader := NewSpanReader(params) require.NotNil(t, reader) assert.Equal(t, test.maxSpanAge, reader.maxSpanAge) }) } } func TestSpanReaderIndices(t *testing.T) { client := &mocks.Client{} clientFn := func() es.Client { return client } date := time.Date(2019, 10, 10, 5, 0, 0, 0, time.UTC) spanDataLayout := "2006-01-02-15" serviceDataLayout := "2006-01-02" spanDataLayoutFormat := date.UTC().Format(spanDataLayout) serviceDataLayoutFormat := date.UTC().Format(serviceDataLayout) logger, _ := testutils.NewLogger() tracer, _, closer := tracerProvider(t) defer closer() spanIndexOpts := config.IndexOptions{DateLayout: spanDataLayout} serviceIndexOpts := config.IndexOptions{DateLayout: serviceDataLayout} testCases := []struct { indices []string params SpanReaderParams }{ { params: SpanReaderParams{ SpanIndex: spanIndexOpts, ServiceIndex: serviceIndexOpts, }, indices: []string{spanIndexBaseName + spanDataLayoutFormat, serviceIndexBaseName + serviceDataLayoutFormat}, }, { params: SpanReaderParams{ UseReadWriteAliases: true, }, indices: []string{spanIndexBaseName + "read", serviceIndexBaseName + "read"}, }, { params: SpanReaderParams{ ReadAliasSuffix: "archive", // ignored because ReadWriteAliases is false }, indices: []string{spanIndexBaseName, serviceIndexBaseName}, }, { params: SpanReaderParams{ SpanIndex: spanIndexOpts, ServiceIndex: serviceIndexOpts, IndexPrefix: "foo:", }, indices: []string{"foo:" + config.IndexPrefixSeparator + spanIndexBaseName + spanDataLayoutFormat, "foo:" + config.IndexPrefixSeparator + serviceIndexBaseName + serviceDataLayoutFormat}, }, { params: SpanReaderParams{ SpanIndex: spanIndexOpts, ServiceIndex: serviceIndexOpts, IndexPrefix: "foo:", UseReadWriteAliases: true, }, indices: []string{"foo:-" + spanIndexBaseName + "read", "foo:-" + serviceIndexBaseName + "read"}, }, { params: SpanReaderParams{ ReadAliasSuffix: "archive", UseReadWriteAliases: true, }, indices: []string{spanIndexBaseName + "archive", serviceIndexBaseName + "archive"}, }, { params: SpanReaderParams{ SpanIndex: spanIndexOpts, ServiceIndex: serviceIndexOpts, IndexPrefix: "foo:", UseReadWriteAliases: true, ReadAliasSuffix: "archive", }, indices: []string{"foo:" + config.IndexPrefixSeparator + spanIndexBaseName + "archive", "foo:" + config.IndexPrefixSeparator + serviceIndexBaseName + "archive"}, }, { params: SpanReaderParams{ SpanIndex: spanIndexOpts, ServiceIndex: serviceIndexOpts, SpanReadAlias: "custom-span-read-alias", ServiceReadAlias: "custom-service-read-alias", }, indices: []string{"custom-span-read-alias", "custom-service-read-alias"}, }, { params: SpanReaderParams{ SpanIndex: spanIndexOpts, ServiceIndex: serviceIndexOpts, IndexPrefix: "foo:", UseReadWriteAliases: true, SpanReadAlias: "production-traces-read", ServiceReadAlias: "production-services-read", }, indices: []string{"production-traces-read", "production-services-read"}, }, { params: SpanReaderParams{ SpanIndex: spanIndexOpts, ServiceIndex: serviceIndexOpts, RemoteReadClusters: []string{"cluster_one", "cluster_two"}, }, indices: []string{ spanIndexBaseName + spanDataLayoutFormat, "cluster_one:" + spanIndexBaseName + spanDataLayoutFormat, "cluster_two:" + spanIndexBaseName + spanDataLayoutFormat, serviceIndexBaseName + serviceDataLayoutFormat, "cluster_one:" + serviceIndexBaseName + serviceDataLayoutFormat, "cluster_two:" + serviceIndexBaseName + serviceDataLayoutFormat, }, }, { params: SpanReaderParams{ UseReadWriteAliases: true, ReadAliasSuffix: "archive", RemoteReadClusters: []string{"cluster_one", "cluster_two"}, }, indices: []string{ spanIndexBaseName + "archive", "cluster_one:" + spanIndexBaseName + "archive", "cluster_two:" + spanIndexBaseName + "archive", serviceIndexBaseName + "archive", "cluster_one:" + serviceIndexBaseName + "archive", "cluster_two:" + serviceIndexBaseName + "archive", }, }, { params: SpanReaderParams{ UseReadWriteAliases: true, RemoteReadClusters: []string{"cluster_one", "cluster_two"}, }, indices: []string{ spanIndexBaseName + "read", "cluster_one:" + spanIndexBaseName + "read", "cluster_two:" + spanIndexBaseName + "read", serviceIndexBaseName + "read", "cluster_one:" + serviceIndexBaseName + "read", "cluster_two:" + serviceIndexBaseName + "read", }, }, } for _, testCase := range testCases { testCase.params.Client = clientFn testCase.params.Logger = logger testCase.params.Tracer = tracer.Tracer("test") r := NewSpanReader(testCase.params) actualSpan := r.timeRangeIndices(r.spanIndexPrefix, r.spanIndex.DateLayout, date, date, -1*time.Hour) actualService := r.timeRangeIndices(r.serviceIndexPrefix, r.serviceIndex.DateLayout, date, date, -24*time.Hour) assert.Equal(t, testCase.indices, append(actualSpan, actualService...)) } } func TestSpanReader_GetTrace(t *testing.T) { withSpanReader(t, func(r *spanReaderTest) { hits := make([]*elastic.SearchHit, 1) hits[0] = &elastic.SearchHit{ Source: exampleESSpan, } searchHits := &elastic.SearchHits{Hits: hits} mockSearchService(r).Return(&elastic.SearchResult{Hits: searchHits}, nil) mockMultiSearchService(r). Return(&elastic.MultiSearchResult{ Responses: []*elastic.SearchResult{ {Hits: searchHits}, }, }, nil) query := []dbmodel.TraceID{dbmodel.TraceID(testingTraceId)} trace, err := r.reader.GetTraces(context.Background(), query) require.NotEmpty(t, r.traceBuffer.GetSpans(), "Spans recorded") require.NoError(t, err) require.NotNil(t, trace) assert.Len(t, trace, 1) expectedSpans, err := r.reader.collectSpans(hits) require.NoError(t, err) require.Len(t, trace[0].Spans, 1) assert.Equal(t, trace[0].Spans[0], expectedSpans[0]) }) } func newSearchRequest(fn *elastic.SearchSource) *elastic.SearchRequest { return elastic.NewSearchRequest(). IgnoreUnavailable(true). Source(fn) } func TestSpanReader_multiRead_followUp_query(t *testing.T) { withSpanReader(t, func(r *spanReaderTest) { traceID1 := dbmodel.TraceID(testingTraceId + "1") traceID2 := dbmodel.TraceID(testingTraceId + "2") date := time.Date(2019, 10, 10, 5, 0, 0, 0, time.UTC) spanID1 := dbmodel.Span{ SpanID: "0", TraceID: traceID1, StartTime: model.TimeAsEpochMicroseconds(date), Tags: []dbmodel.KeyValue{}, Process: dbmodel.Process{ Tags: []dbmodel.KeyValue{}, }, } spanBytesID1, err := json.Marshal(spanID1) require.NoError(t, err) spanID2 := dbmodel.Span{ SpanID: "0", TraceID: traceID2, StartTime: model.TimeAsEpochMicroseconds(date), Tags: []dbmodel.KeyValue{}, Process: dbmodel.Process{ Tags: []dbmodel.KeyValue{}, }, } spanBytesID2, err := json.Marshal(spanID2) require.NoError(t, err) traceID1Query := elastic.NewTermQuery(traceIDField, string(traceID1)) id1Query := elastic.NewBoolQuery().Must(traceID1Query) id1Search := newSearchRequest(r.reader.sourceFn(id1Query, model.TimeAsEpochMicroseconds(date.Add(-time.Hour))).TrackTotalHits(true)) traceID2Query := elastic.NewTermQuery(traceIDField, string(traceID2)) id2Query := elastic.NewBoolQuery().Must(traceID2Query) id2Search := newSearchRequest(r.reader.sourceFn(id2Query, model.TimeAsEpochMicroseconds(date.Add(-time.Hour))).TrackTotalHits(true)) id1SearchSpanTime := newSearchRequest(r.reader.sourceFn(id1Query, spanID1.StartTime).TrackTotalHits(true)) multiSearchService := &mocks.MultiSearchService{} firstMultiSearch := &mocks.MultiSearchService{} secondMultiSearch := &mocks.MultiSearchService{} multiSearchService.On("Add", mock.MatchedBy(func(searches []*elastic.SearchRequest) bool { return len(searches) == 2 && reflect.DeepEqual(searches[0], id1Search) && reflect.DeepEqual(searches[1], id2Search) })).Return(firstMultiSearch).Once() multiSearchService.On("Add", mock.MatchedBy(func(searches []*elastic.SearchRequest) bool { return len(searches) == 1 && reflect.DeepEqual(searches[0], id1SearchSpanTime) })).Return(secondMultiSearch).Once() firstMultiSearch.On("Index", mock.AnythingOfType("[]string")).Return(firstMultiSearch) secondMultiSearch.On("Index", mock.AnythingOfType("[]string")).Return(secondMultiSearch) r.client.On("MultiSearch").Return(multiSearchService) fistMultiSearchMock := firstMultiSearch.On("Do", mock.Anything) secondMultiSearchMock := secondMultiSearch.On("Do", mock.Anything) // set TotalHits to two to trigger the follow up query // the client will return only one span therefore the implementation // triggers follow up query for the same traceID with the timestamp of the last span searchHitsID1 := &elastic.SearchHits{Hits: []*elastic.SearchHit{ {Source: spanBytesID1}, }, TotalHits: &elastic.TotalHits{ Value: 2, Relation: "eq", }} fistMultiSearchMock. Return(&elastic.MultiSearchResult{ Responses: []*elastic.SearchResult{ {Hits: searchHitsID1}, }, }, nil) searchHitsID2 := &elastic.SearchHits{Hits: []*elastic.SearchHit{ {Source: spanBytesID2}, }, TotalHits: &elastic.TotalHits{ Value: 1, Relation: "eq", }} secondMultiSearchMock. Return(&elastic.MultiSearchResult{ Responses: []*elastic.SearchResult{ {Hits: searchHitsID2}, }, }, nil) traces, err := r.reader.multiRead(context.Background(), []dbmodel.TraceID{traceID1, traceID2}, date, date) require.NotEmpty(t, r.traceBuffer.GetSpans(), "Spans recorded") require.NoError(t, err) require.NotNil(t, traces) require.Len(t, traces, 2) for i, s := range []dbmodel.Span{spanID1, spanID2} { actual := traces[i].Spans[0] actualData, err := json.Marshal(actual) require.NoError(t, err) expectedData, err := json.Marshal(s) require.NoError(t, err) assert.Equal(t, string(expectedData), string(actualData)) } }) } func TestSpanReader_SearchAfter(t *testing.T) { withSpanReader(t, func(r *spanReaderTest) { var hits []*elastic.SearchHit for range 10000 { hit := &elastic.SearchHit{Source: exampleESSpan} hits = append(hits, hit) } totalHits := &elastic.TotalHits{ Value: int64(10040), Relation: "eq", } searchHits := &elastic.SearchHits{Hits: hits, TotalHits: totalHits} mockSearchService(r).Return(&elastic.SearchResult{Hits: searchHits}, nil) mockMultiSearchService(r). Return(&elastic.MultiSearchResult{ Responses: []*elastic.SearchResult{ {Hits: searchHits}, }, }, nil).Times(2) query := []dbmodel.TraceID{dbmodel.TraceID("testing-id")} trace, err := r.reader.GetTraces(context.Background(), query) require.NotEmpty(t, r.traceBuffer.GetSpans(), "Spans recorded") require.NoError(t, err) require.NotNil(t, trace) assert.Len(t, trace, 1) expectedSpans, err := r.reader.collectSpans(hits) require.NoError(t, err) assert.Equal(t, trace[0].Spans[0], expectedSpans[0]) }) } func TestSpanReader_GetTraceQueryError(t *testing.T) { withSpanReader(t, func(r *spanReaderTest) { mockSearchService(r). Return(nil, errors.New("query error occurred")) mockMultiSearchService(r). Return(&elastic.MultiSearchResult{ Responses: []*elastic.SearchResult{}, }, nil) query := []dbmodel.TraceID{dbmodel.TraceID("testing-id")} trace, err := r.reader.GetTraces(context.Background(), query) require.NoError(t, err) require.NotEmpty(t, r.traceBuffer.GetSpans(), "Spans recorded") require.Empty(t, trace) }) } func TestSpanReader_GetTraceNilHits(t *testing.T) { withSpanReader(t, func(r *spanReaderTest) { var hits []*elastic.SearchHit searchHits := &elastic.SearchHits{Hits: hits} mockSearchService(r).Return(&elastic.SearchResult{Hits: searchHits}, nil) mockMultiSearchService(r). Return(&elastic.MultiSearchResult{ Responses: []*elastic.SearchResult{ {Hits: nil}, }, }, nil) query := []dbmodel.TraceID{dbmodel.TraceID(testingTraceId)} trace, err := r.reader.GetTraces(context.Background(), query) require.NoError(t, err) require.NotEmpty(t, r.traceBuffer.GetSpans(), "Spans recorded") require.Empty(t, trace) }) } func TestSpanReader_GetTraceInvalidSpanError(t *testing.T) { withSpanReader(t, func(r *spanReaderTest) { data := []byte(`{"TraceID": "123"asdf fadsg}`) hits := make([]*elastic.SearchHit, 1) hits[0] = &elastic.SearchHit{ Source: data, } searchHits := &elastic.SearchHits{Hits: hits} mockSearchService(r).Return(&elastic.SearchResult{Hits: searchHits}, nil) mockMultiSearchService(r). Return(&elastic.MultiSearchResult{ Responses: []*elastic.SearchResult{ {Hits: searchHits}, }, }, nil) query := []dbmodel.TraceID{dbmodel.TraceID(testingTraceId)} trace, err := r.reader.GetTraces(context.Background(), query) require.NotEmpty(t, r.traceBuffer.GetSpans(), "Spans recorded") require.Error(t, err, "invalid span") require.Nil(t, trace) }) } func TestSpanReader_esJSONtoJSONSpanModel(t *testing.T) { withSpanReader(t, func(r *spanReaderTest) { jsonPayload := exampleESSpan esSpanRaw := &elastic.SearchHit{ Source: jsonPayload, } span, err := r.reader.unmarshalJSONSpan(esSpanRaw) require.NoError(t, err) var expectedSpan dbmodel.Span require.NoError(t, json.Unmarshal(exampleESSpan, &expectedSpan)) assert.Equal(t, expectedSpan, span) }) } func TestSpanReader_esJSONtoJSONSpanModelError(t *testing.T) { withSpanReader(t, func(r *spanReaderTest) { data := []byte(`{"TraceID": "123"asdf fadsg}`) jsonPayload := data esSpanRaw := &elastic.SearchHit{ Source: jsonPayload, } _, err := r.reader.unmarshalJSONSpan(esSpanRaw) require.Error(t, err) }) } func TestSpanReaderFindIndices(t *testing.T) { today := time.Date(1995, time.April, 21, 4, 12, 19, 95, time.UTC) yesterday := today.AddDate(0, 0, -1) twoDaysAgo := today.AddDate(0, 0, -2) dateLayout := "2006-01-02" testCases := []struct { startTime time.Time endTime time.Time expected []string }{ { startTime: today.Add(-time.Millisecond), endTime: today, expected: []string{ indexWithDate(spanIndexBaseName, dateLayout, today), }, }, { startTime: today.Add(-13 * time.Hour), endTime: today, expected: []string{ indexWithDate(spanIndexBaseName, dateLayout, today), indexWithDate(spanIndexBaseName, dateLayout, yesterday), }, }, { startTime: today.Add(-48 * time.Hour), endTime: today, expected: []string{ indexWithDate(spanIndexBaseName, dateLayout, today), indexWithDate(spanIndexBaseName, dateLayout, yesterday), indexWithDate(spanIndexBaseName, dateLayout, twoDaysAgo), }, }, } withSpanReader(t, func(r *spanReaderTest) { for _, testCase := range testCases { actual := r.reader.timeRangeIndices(spanIndexBaseName, dateLayout, testCase.startTime, testCase.endTime, -24*time.Hour) assert.Equal(t, testCase.expected, actual) } }) } func TestSpanReader_indexWithDate(t *testing.T) { withSpanReader(t, func(_ *spanReaderTest) { actual := indexWithDate(spanIndexBaseName, "2006-01-02", time.Date(1995, time.April, 21, 4, 21, 19, 95, time.UTC)) assert.Equal(t, "jaeger-span-1995-04-21", actual) }) } func testGet(typ string, t *testing.T) { goodAggregations := make(map[string]json.RawMessage) rawMessage := []byte(`{"buckets": [{"key": "123","doc_count": 16}]}`) goodAggregations[typ] = rawMessage badAggregations := make(map[string]json.RawMessage) badRawMessage := []byte(`{"buckets": [{bad json]}asdf`) badAggregations[typ] = badRawMessage testCases := []struct { caption string searchResult *elastic.SearchResult searchError error expectedError func() string expectedOutput map[string]any }{ { caption: typ + " full behavior", searchResult: &elastic.SearchResult{Aggregations: elastic.Aggregations(goodAggregations)}, expectedOutput: map[string]any{ operationsAggregation: []dbmodel.Operation{{Name: "123"}}, traceIDAggregation: []dbmodel.TraceID{"123"}, "default": []string{"123"}, }, expectedError: func() string { return "" }, }, { caption: typ + " search error", searchError: errors.New("Search failure"), expectedError: func() string { if typ == operationsAggregation { return "search operations failed: Search failure" } return "search services failed: Search failure" }, }, { caption: typ + " search error", searchResult: &elastic.SearchResult{Aggregations: elastic.Aggregations(badAggregations)}, expectedError: func() string { return "could not find aggregation of " + typ }, }, } for _, tc := range testCases { testCase := tc t.Run(testCase.caption, func(t *testing.T) { withSpanReader(t, func(r *spanReaderTest) { mockSearchService(r).Return(testCase.searchResult, testCase.searchError) actual, err := returnSearchFunc(typ, r) if testCase.expectedError() != "" { require.EqualError(t, err, testCase.expectedError()) assert.Nil(t, actual) } else if expectedOutput, ok := testCase.expectedOutput[typ]; ok { assert.Equal(t, expectedOutput, actual) } else { assert.Equal(t, testCase.expectedOutput["default"], actual) } }) }) } } func returnSearchFunc(typ string, r *spanReaderTest) (any, error) { switch typ { case servicesAggregation: return r.reader.GetServices(context.Background()) case operationsAggregation: return r.reader.GetOperations( context.Background(), dbmodel.OperationQueryParameters{ServiceName: "someService"}, ) case traceIDAggregation: return r.reader.findTraceIDsFromQuery(context.Background(), dbmodel.TraceQueryParameters{}) default: return nil, errors.New("Specify services, operations, traceIDs only") } } func TestSpanReader_bucketToStringArray(t *testing.T) { withSpanReader(t, func(_ *spanReaderTest) { buckets := make([]*elastic.AggregationBucketKeyItem, 3) buckets[0] = &elastic.AggregationBucketKeyItem{Key: "hello"} buckets[1] = &elastic.AggregationBucketKeyItem{Key: "world"} buckets[2] = &elastic.AggregationBucketKeyItem{Key: "2"} actual, err := bucketToStringArray[string](buckets) require.NoError(t, err) assert.Equal(t, []string{"hello", "world", "2"}, actual) }) } func TestSpanReader_bucketToStringArrayError(t *testing.T) { withSpanReader(t, func(_ *spanReaderTest) { buckets := make([]*elastic.AggregationBucketKeyItem, 3) buckets[0] = &elastic.AggregationBucketKeyItem{Key: "hello"} buckets[1] = &elastic.AggregationBucketKeyItem{Key: "world"} buckets[2] = &elastic.AggregationBucketKeyItem{Key: 2} _, err := bucketToStringArray[string](buckets) require.EqualError(t, err, "non-string key found in aggregation") }) } func TestSpanReader_FindTraces(t *testing.T) { goodAggregations := make(map[string]json.RawMessage) rawMessage := []byte(`{"buckets": [{"key": "1","doc_count": 16},{"key": "2","doc_count": 16},{"key": "3","doc_count": 16}]}`) goodAggregations[traceIDAggregation] = rawMessage hits := make([]*elastic.SearchHit, 1) hits[0] = &elastic.SearchHit{ Source: exampleESSpan, } searchHits := &elastic.SearchHits{Hits: hits} withSpanReader(t, func(r *spanReaderTest) { // find trace IDs mockSearchService(r). Return(&elastic.SearchResult{Aggregations: elastic.Aggregations(goodAggregations), Hits: searchHits}, nil) // bulk read traces mockMultiSearchService(r). Return(&elastic.MultiSearchResult{ Responses: []*elastic.SearchResult{ {Hits: searchHits}, {Hits: searchHits}, }, }, nil) traceQuery := dbmodel.TraceQueryParameters{ ServiceName: serviceName, Tags: map[string]string{ "hello": "world", }, StartTimeMin: time.Now().Add(-1 * time.Hour), StartTimeMax: time.Now(), NumTraces: 1, } traces, err := r.reader.FindTraces(context.Background(), traceQuery) require.NotEmpty(t, r.traceBuffer.GetSpans(), "Spans recorded") require.NoError(t, err) assert.Len(t, traces, 1) trace := traces[0] expectedSpans, err := r.reader.collectSpans(hits) require.NoError(t, err) require.Len(t, trace.Spans, 2) assert.Equal(t, trace.Spans[0], expectedSpans[0]) }) } func TestSpanReader_FindTracesInvalidQuery(t *testing.T) { goodAggregations := make(map[string]json.RawMessage) rawMessage := []byte(`{"buckets": [{"key": "1","doc_count": 16},{"key": "2","doc_count": 16},{"key": "3","doc_count": 16}]}`) goodAggregations[traceIDAggregation] = rawMessage hits := make([]*elastic.SearchHit, 1) hits[0] = &elastic.SearchHit{ Source: exampleESSpan, } searchHits := &elastic.SearchHits{Hits: hits} withSpanReader(t, func(r *spanReaderTest) { mockSearchService(r). Return(&elastic.SearchResult{Aggregations: elastic.Aggregations(goodAggregations), Hits: searchHits}, nil) mockMultiSearchService(r). Return(&elastic.MultiSearchResult{ Responses: []*elastic.SearchResult{ {Hits: searchHits}, {Hits: searchHits}, }, }, nil) traceQuery := dbmodel.TraceQueryParameters{ ServiceName: "", Tags: map[string]string{ "hello": "world", }, StartTimeMin: time.Now().Add(-1 * time.Hour), StartTimeMax: time.Now(), } traces, err := r.reader.FindTraces(context.Background(), traceQuery) require.NotEmpty(t, r.traceBuffer.GetSpans(), "Spans recorded") require.Error(t, err) assert.Nil(t, traces) }) } func TestSpanReader_FindTracesAggregationFailure(t *testing.T) { goodAggregations := make(map[string]json.RawMessage) hits := make([]*elastic.SearchHit, 1) hits[0] = &elastic.SearchHit{ Source: exampleESSpan, } searchHits := &elastic.SearchHits{Hits: hits} withSpanReader(t, func(r *spanReaderTest) { mockSearchService(r). Return(&elastic.SearchResult{Aggregations: elastic.Aggregations(goodAggregations), Hits: searchHits}, nil) mockMultiSearchService(r). Return(&elastic.MultiSearchResult{ Responses: []*elastic.SearchResult{}, }, nil) traceQuery := dbmodel.TraceQueryParameters{ ServiceName: serviceName, Tags: map[string]string{ "hello": "world", }, StartTimeMin: time.Now().Add(-1 * time.Hour), StartTimeMax: time.Now(), } traces, err := r.reader.FindTraces(context.Background(), traceQuery) require.NotEmpty(t, r.traceBuffer.GetSpans(), "Spans recorded") require.Error(t, err) assert.Nil(t, traces) }) } func TestSpanReader_FindTracesNoTraceIDs(t *testing.T) { goodAggregations := make(map[string]json.RawMessage) rawMessage := []byte(`{"buckets": []}`) goodAggregations[traceIDAggregation] = rawMessage hits := make([]*elastic.SearchHit, 1) hits[0] = &elastic.SearchHit{ Source: exampleESSpan, } searchHits := &elastic.SearchHits{Hits: hits} withSpanReader(t, func(r *spanReaderTest) { mockSearchService(r). Return(&elastic.SearchResult{Aggregations: elastic.Aggregations(goodAggregations), Hits: searchHits}, nil) mockMultiSearchService(r). Return(&elastic.MultiSearchResult{ Responses: []*elastic.SearchResult{}, }, nil) traceQuery := dbmodel.TraceQueryParameters{ ServiceName: serviceName, Tags: map[string]string{ "hello": "world", }, StartTimeMin: time.Now().Add(-1 * time.Hour), StartTimeMax: time.Now(), } traces, err := r.reader.FindTraces(context.Background(), traceQuery) require.NotEmpty(t, r.traceBuffer.GetSpans(), "Spans recorded") require.NoError(t, err) assert.Empty(t, traces) }) } func TestSpanReader_FindTracesReadTraceFailure(t *testing.T) { goodAggregations := make(map[string]json.RawMessage) rawMessage := []byte(`{"buckets": [{"key": "1","doc_count": 16},{"key": "2","doc_count": 16}]}`) goodAggregations[traceIDAggregation] = rawMessage badSpan := []byte(`{"TraceID": "123"asjlgajdfhilqghi[adfvca} bad json`) hits := make([]*elastic.SearchHit, 1) hits[0] = &elastic.SearchHit{ Source: badSpan, } searchHits := &elastic.SearchHits{Hits: hits} withSpanReader(t, func(r *spanReaderTest) { mockSearchService(r). Return(&elastic.SearchResult{Aggregations: elastic.Aggregations(goodAggregations), Hits: searchHits}, nil) mockMultiSearchService(r). Return(nil, errors.New("read error")) traceQuery := dbmodel.TraceQueryParameters{ ServiceName: serviceName, Tags: map[string]string{ "hello": "world", }, StartTimeMin: time.Now().Add(-1 * time.Hour), StartTimeMax: time.Now(), } traces, err := r.reader.FindTraces(context.Background(), traceQuery) require.NotEmpty(t, r.traceBuffer.GetSpans(), "Spans recorded") require.EqualError(t, err, "read error") assert.Empty(t, traces) }) } func TestSpanReader_FindTracesSpanCollectionFailure(t *testing.T) { goodAggregations := make(map[string]json.RawMessage) rawMessage := []byte(`{"buckets": [{"key": "1","doc_count": 16},{"key": "2","doc_count": 16}]}`) goodAggregations[traceIDAggregation] = rawMessage badSpan := []byte(`{"TraceID": "123"asjlgajdfhilqghi[adfvca} bad json`) hits := make([]*elastic.SearchHit, 1) hits[0] = &elastic.SearchHit{ Source: badSpan, } searchHits := &elastic.SearchHits{Hits: hits} withSpanReader(t, func(r *spanReaderTest) { mockSearchService(r). Return(&elastic.SearchResult{Aggregations: elastic.Aggregations(goodAggregations), Hits: searchHits}, nil) mockMultiSearchService(r). Return(&elastic.MultiSearchResult{ Responses: []*elastic.SearchResult{ {Hits: searchHits}, {Hits: searchHits}, }, }, nil) traceQuery := dbmodel.TraceQueryParameters{ ServiceName: serviceName, Tags: map[string]string{ "hello": "world", }, StartTimeMin: time.Now().Add(-1 * time.Hour), StartTimeMax: time.Now(), } traces, err := r.reader.FindTraces(context.Background(), traceQuery) require.NotEmpty(t, r.traceBuffer.GetSpans(), "Spans recorded") require.Error(t, err) assert.Empty(t, traces) }) } func TestFindTraceIDs(t *testing.T) { testCases := []struct { aggregrationID string }{ {traceIDAggregation}, {servicesAggregation}, {operationsAggregation}, } for _, testCase := range testCases { t.Run(testCase.aggregrationID, func(t *testing.T) { testGet(testCase.aggregrationID, t) }) } } func TestReturnSearchFunc_DefaultCase(t *testing.T) { r := &spanReaderTest{} result, err := returnSearchFunc("unknownAggregationType", r) assert.Nil(t, result) require.Error(t, err) assert.Contains(t, err.Error(), "Specify services, operations, traceIDs only") } func mockMultiSearchService(r *spanReaderTest) *mock.Call { multiSearchService := &mocks.MultiSearchService{} multiSearchService.On("Add", mock.Anything, mock.Anything, mock.Anything).Return(multiSearchService) multiSearchService.On("Index", mock.AnythingOfType("[]string")).Return(multiSearchService) r.client.On("MultiSearch").Return(multiSearchService) return multiSearchService.On("Do", mock.Anything) } func mockArchiveMultiSearchService(r *spanReaderTest, indexName []string) *mock.Call { multiSearchService := &mocks.MultiSearchService{} multiSearchService.On("Add", mock.Anything, mock.Anything, mock.Anything).Return(multiSearchService) multiSearchService.On("Index", indexName).Return(multiSearchService) r.client.On("MultiSearch").Return(multiSearchService) return multiSearchService.On("Do", mock.Anything) } // matchTermsAggregation uses reflection to match the size attribute of the TermsAggregation; neither // attributes nor getters are exported by TermsAggregation. func matchTermsAggregation(termsAgg *elastic.TermsAggregation) bool { val := reflect.ValueOf(termsAgg).Elem() sizeVal := val.FieldByName("size").Elem().Int() return sizeVal == defaultMaxDocCount } func mockSearchService(r *spanReaderTest) *mock.Call { searchService := &mocks.SearchService{} searchService.On("Query", mock.Anything).Return(searchService) searchService.On("IgnoreUnavailable", mock.AnythingOfType("bool")).Return(searchService) searchService.On("Size", mock.MatchedBy(func(size int) bool { return size == 0 // Aggregations apply size (bucket) limits in their own query objects, and do not apply at the parent query level. })).Return(searchService) searchService.On("Aggregation", stringMatcher(servicesAggregation), mock.MatchedBy(matchTermsAggregation)).Return(searchService) searchService.On("Aggregation", stringMatcher(operationsAggregation), mock.MatchedBy(matchTermsAggregation)).Return(searchService) searchService.On("Aggregation", stringMatcher(traceIDAggregation), mock.AnythingOfType("*elastic.TermsAggregation")).Return(searchService) r.client.On("Search", mock.AnythingOfType("[]string")).Return(searchService) return searchService.On("Do", mock.Anything) } func TestTraceQueryParameterValidation(t *testing.T) { tqp := dbmodel.TraceQueryParameters{ ServiceName: "", Tags: map[string]string{ "hello": "world", }, } err := validateQuery(tqp) require.EqualError(t, err, ErrServiceNameNotSet.Error()) tqp.ServiceName = serviceName tqp.StartTimeMin = time.Time{} // time.Unix(0,0) doesn't work because timezones tqp.StartTimeMax = time.Time{} err = validateQuery(tqp) require.EqualError(t, err, ErrStartAndEndTimeNotSet.Error()) tqp.StartTimeMin = time.Now() tqp.StartTimeMax = time.Now().Add(-1 * time.Hour) err = validateQuery(tqp) require.EqualError(t, err, ErrStartTimeMinGreaterThanMax.Error()) tqp.StartTimeMin = time.Now().Add(-1 * time.Hour) tqp.StartTimeMax = time.Now() err = validateQuery(tqp) require.NoError(t, err) tqp.DurationMin = time.Hour tqp.DurationMax = time.Minute err = validateQuery(tqp) require.EqualError(t, err, ErrDurationMinGreaterThanMax.Error()) } func TestSpanReader_buildTraceIDAggregation(t *testing.T) { expectedStr := `{ "terms":{ "field":"traceID", "size":123, "order":{ "startTime":"desc" } }, "aggregations": { "startTime" : { "max": {"field": "startTime"}} }}` withSpanReader(t, func(r *spanReaderTest) { traceIDAggregation := r.reader.buildTraceIDAggregation(123) actual, err := traceIDAggregation.Source() require.NoError(t, err) expected := make(map[string]any) json.Unmarshal([]byte(expectedStr), &expected) expected["terms"].(map[string]any)["size"] = 123 expected["terms"].(map[string]any)["order"] = []any{map[string]string{"startTime": "desc"}} assert.EqualValues(t, expected, actual) }) } func TestSpanReader_buildFindTraceIDsQuery(t *testing.T) { withSpanReader(t, func(r *spanReaderTest) { traceQuery := dbmodel.TraceQueryParameters{ DurationMin: time.Second, DurationMax: time.Second * 2, StartTimeMin: time.Time{}, StartTimeMax: time.Time{}.Add(time.Second), ServiceName: "s", OperationName: "o", Tags: map[string]string{ "hello": "world", }, } actualQuery := r.reader.buildFindTraceIDsQuery(traceQuery) actual, err := actualQuery.Source() require.NoError(t, err) expectedQuery := elastic.NewBoolQuery(). Must( r.reader.buildDurationQuery(time.Second, time.Second*2), r.reader.buildStartTimeQuery(time.Time{}, time.Time{}.Add(time.Second)), r.reader.buildServiceNameQuery("s"), r.reader.buildOperationNameQuery("o"), r.reader.buildTagQuery("hello", "world"), ) expected, err := expectedQuery.Source() require.NoError(t, err) assert.Equal(t, expected, actual) }) } func TestSpanReader_buildDurationQuery(t *testing.T) { expectedStr := `{ "range": { "duration": { "gte": 1000000, "lte": 2000000 } } }` withSpanReader(t, func(r *spanReaderTest) { durationMin := time.Second durationMax := time.Second * 2 durationQuery := r.reader.buildDurationQuery(durationMin, durationMax) actual, err := durationQuery.Source() require.NoError(t, err) expected := make(map[string]any) json.Unmarshal([]byte(expectedStr), &expected) // We need to do this because we cannot process a json into uint64. expected["range"].(map[string]any)["duration"].(map[string]any)["gte"] = model.DurationAsMicroseconds(durationMin) expected["range"].(map[string]any)["duration"].(map[string]any)["lte"] = model.DurationAsMicroseconds(durationMax) assert.EqualValues(t, expected, actual) }) } func TestSpanReader_buildStartTimeQuery(t *testing.T) { expectedStr := `{ "range": { "startTimeMillis": { "gte": 1000000, "lte": 2000000 } } }` withSpanReader(t, func(r *spanReaderTest) { startTimeMin := time.Time{}.Add(time.Second) startTimeMax := time.Time{}.Add(2 * time.Second) durationQuery := r.reader.buildStartTimeQuery(startTimeMin, startTimeMax) actual, err := durationQuery.Source() require.NoError(t, err) expected := make(map[string]any) json.Unmarshal([]byte(expectedStr), &expected) // We need to do this because we cannot process a json into uint64. expected["range"].(map[string]any)["startTimeMillis"].(map[string]any)["gte"] = model.TimeAsEpochMicroseconds(startTimeMin) / 1000 expected["range"].(map[string]any)["startTimeMillis"].(map[string]any)["lte"] = model.TimeAsEpochMicroseconds(startTimeMax) / 1000 assert.EqualValues(t, expected, actual) }) } func TestSpanReader_buildServiceNameQuery(t *testing.T) { expectedStr := `{ "match": { "process.serviceName": { "query": "bat" }}}` withSpanReader(t, func(r *spanReaderTest) { serviceNameQuery := r.reader.buildServiceNameQuery("bat") actual, err := serviceNameQuery.Source() require.NoError(t, err) expected := make(map[string]any) json.Unmarshal([]byte(expectedStr), &expected) assert.EqualValues(t, expected, actual) }) } func TestSpanReader_buildOperationNameQuery(t *testing.T) { expectedStr := `{ "match": { "operationName": { "query": "spook" }}}` withSpanReader(t, func(r *spanReaderTest) { operationNameQuery := r.reader.buildOperationNameQuery("spook") actual, err := operationNameQuery.Source() require.NoError(t, err) expected := make(map[string]any) json.Unmarshal([]byte(expectedStr), &expected) assert.EqualValues(t, expected, actual) }) } func TestSpanReader_buildTagQuery(t *testing.T) { inStr, err := os.ReadFile("fixtures/query_01.json") require.NoError(t, err) withSpanReader(t, func(r *spanReaderTest) { tagQuery := r.reader.buildTagQuery("bat.foo", "spook") actual, err := tagQuery.Source() require.NoError(t, err) expected := make(map[string]any) json.Unmarshal(inStr, &expected) assert.EqualValues(t, expected, actual) }) } func TestSpanReader_buildTagRegexQuery(t *testing.T) { inStr, err := os.ReadFile("fixtures/query_02.json") require.NoError(t, err) withSpanReader(t, func(r *spanReaderTest) { tagQuery := r.reader.buildTagQuery("bat.foo", "spo.*") actual, err := tagQuery.Source() require.NoError(t, err) expected := make(map[string]any) json.Unmarshal(inStr, &expected) assert.EqualValues(t, expected, actual) }) } func TestSpanReader_buildTagRegexEscapedQuery(t *testing.T) { inStr, err := os.ReadFile("fixtures/query_03.json") require.NoError(t, err) withSpanReader(t, func(r *spanReaderTest) { tagQuery := r.reader.buildTagQuery("bat.foo", "spo\\*") actual, err := tagQuery.Source() require.NoError(t, err) expected := make(map[string]any) json.Unmarshal(inStr, &expected) assert.EqualValues(t, expected, actual) }) } func TestSpanReader_GetEmptyIndex(t *testing.T) { withSpanReader(t, func(r *spanReaderTest) { mockSearchService(r). Return(&elastic.SearchResult{}, nil) mockMultiSearchService(r). Return(&elastic.MultiSearchResult{ Responses: []*elastic.SearchResult{}, }, nil) traceQuery := dbmodel.TraceQueryParameters{ ServiceName: serviceName, Tags: map[string]string{ "hello": "world", }, StartTimeMin: time.Now().Add(-1 * time.Hour), StartTimeMax: time.Now(), NumTraces: 2, } services, err := r.reader.FindTraces(context.Background(), traceQuery) require.NotEmpty(t, r.traceBuffer.GetSpans(), "Spans recorded") require.NoError(t, err) assert.Empty(t, services) }) } func TestSpanReader_ArchiveTraces(t *testing.T) { testCases := []struct { useAliases bool suffix string expected string }{ {false, "", "jaeger-span-"}, {true, "", "jaeger-span-read"}, {false, "foobar", "jaeger-span-"}, {true, "foobar", "jaeger-span-foobar"}, } for _, tc := range testCases { t.Run(fmt.Sprintf("useAliases=%v suffix=%s", tc.useAliases, tc.suffix), func(t *testing.T) { withArchiveSpanReader(t, tc.useAliases, tc.suffix, func(r *spanReaderTest) { mockSearchService(r). Return(&elastic.SearchResult{}, nil) mockArchiveMultiSearchService(r, []string{tc.expected}). Return(&elastic.MultiSearchResult{ Responses: []*elastic.SearchResult{}, }, nil) query := []dbmodel.TraceID{} trace, err := r.reader.GetTraces(context.Background(), query) require.NoError(t, err) require.NotEmpty(t, r.traceBuffer.GetSpans(), "Spans recorded") require.Empty(t, trace) }) }) } } func TestBuildTraceByIDQuery(t *testing.T) { tests := []struct { traceID string query elastic.Query }{ { traceID: "0000000000000001", query: elastic.NewTermQuery(traceIDField, "0000000000000001"), }, { traceID: "00000000000000010000000000000001", query: elastic.NewTermQuery(traceIDField, "00000000000000010000000000000001"), }, { traceID: "ffffffffffffffffffffffffffffffff", query: elastic.NewTermQuery(traceIDField, "ffffffffffffffffffffffffffffffff"), }, { traceID: "0short-traceid", query: elastic.NewTermQuery(traceIDField, "0short-traceid"), }, } for _, test := range tests { t.Run(test.traceID, func(t *testing.T) { q := buildTraceByIDQuery(dbmodel.TraceID(test.traceID)) assert.Equal(t, test.query, q) }) } } func TestTerminateAfterNotSet(t *testing.T) { srcFn := getSourceFn(99) searchSource := srcFn(elastic.NewMatchAllQuery(), 1) sp, err := searchSource.Source() require.NoError(t, err) searchParams, ok := sp.(map[string]any) require.True(t, ok) termAfter, ok := searchParams["terminate_after"] require.False(t, ok) assert.Nil(t, termAfter) query, ok := searchParams["query"] require.True(t, ok) queryMap, ok := query.(map[string]any) require.True(t, ok) _, ok = queryMap["match_all"] require.True(t, ok) size, ok := searchParams["size"] require.True(t, ok) assert.Equal(t, 99, size) } func TestTagsMap(t *testing.T) { tests := []struct { fieldTags map[string]any expected dbmodel.KeyValue }{ {fieldTags: map[string]any{"bool:bool": true}, expected: dbmodel.KeyValue{Key: "bool.bool", Value: true, Type: dbmodel.BoolType}}, {fieldTags: map[string]any{"int.int": int64(1)}, expected: dbmodel.KeyValue{Key: "int.int", Value: int64(1), Type: dbmodel.Int64Type}}, {fieldTags: map[string]any{"int:int": int64(2)}, expected: dbmodel.KeyValue{Key: "int.int", Value: int64(2), Type: dbmodel.Int64Type}}, {fieldTags: map[string]any{"float": float64(1.1)}, expected: dbmodel.KeyValue{Key: "float", Value: float64(1.1), Type: dbmodel.Float64Type}}, {fieldTags: map[string]any{"float": float64(123)}, expected: dbmodel.KeyValue{Key: "float", Value: float64(123), Type: dbmodel.Float64Type}}, {fieldTags: map[string]any{"float": float64(123.0)}, expected: dbmodel.KeyValue{Key: "float", Value: float64(123.0), Type: dbmodel.Float64Type}}, {fieldTags: map[string]any{"float:float": float64(123)}, expected: dbmodel.KeyValue{Key: "float.float", Value: float64(123), Type: dbmodel.Float64Type}}, {fieldTags: map[string]any{"json_number:int": json.Number("123")}, expected: dbmodel.KeyValue{Key: "json_number.int", Value: int64(123), Type: dbmodel.Int64Type}}, {fieldTags: map[string]any{"json_number:float": json.Number("123.0")}, expected: dbmodel.KeyValue{Key: "json_number.float", Value: float64(123.0), Type: dbmodel.Float64Type}}, {fieldTags: map[string]any{"json_number:err": json.Number("foo")}, expected: dbmodel.KeyValue{Key: "json_number.err", Value: "invalid tag type in foo: strconv.ParseFloat: parsing \"foo\": invalid syntax", Type: dbmodel.StringType}}, {fieldTags: map[string]any{"str": "foo"}, expected: dbmodel.KeyValue{Key: "str", Value: "foo", Type: dbmodel.StringType}}, {fieldTags: map[string]any{"str:str": "foo"}, expected: dbmodel.KeyValue{Key: "str.str", Value: "foo", Type: dbmodel.StringType}}, {fieldTags: map[string]any{"binary": []byte("foo")}, expected: dbmodel.KeyValue{Key: "binary", Value: []byte("foo"), Type: dbmodel.BinaryType}}, {fieldTags: map[string]any{"binary:binary": []byte("foo")}, expected: dbmodel.KeyValue{Key: "binary.binary", Value: []byte("foo"), Type: dbmodel.BinaryType}}, {fieldTags: map[string]any{"unsupported": struct{}{}}, expected: dbmodel.KeyValue{Key: "unsupported", Value: fmt.Sprintf("invalid tag type in %+v", struct{}{}), Type: dbmodel.StringType}}, } reader := NewSpanReader(SpanReaderParams{ TagDotReplacement: ":", Logger: zap.NewNop(), }) for i, test := range tests { t.Run(fmt.Sprintf("%d, %s", i, test.fieldTags), func(t *testing.T) { tags := []dbmodel.KeyValue{ { Key: "testing-key", Type: dbmodel.StringType, Value: "testing-value", }, } spanTags := make(map[string]any) maps.Copy(spanTags, test.fieldTags) span := &dbmodel.Span{ Process: dbmodel.Process{ Tag: test.fieldTags, Tags: tags, }, Tag: spanTags, Tags: tags, } reader.mergeAllNestedAndElevatedTagsOfSpan(span) tags = append(tags, test.expected) assert.Empty(t, span.Tag) assert.Empty(t, span.Process.Tag) assert.Equal(t, tags, span.Tags) assert.Equal(t, tags, span.Process.Tags) }) } } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/readerv1.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "context" "fmt" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" ) var _ spanstore.Reader = (*SpanReaderV1)(nil) // check API conformance // SpanReaderV1 is a wrapper around SpanReader type SpanReaderV1 struct { spanReader CoreSpanReader spanConverter ToDomain } // NewSpanReaderV1 returns an instance of SpanReaderV1 func NewSpanReaderV1(p SpanReaderParams) *SpanReaderV1 { return &SpanReaderV1{ spanReader: NewSpanReader(p), spanConverter: NewToDomain(), } } // GetTrace takes a traceID and returns a Trace associated with that traceID func (s *SpanReaderV1) GetTrace(ctx context.Context, query spanstore.GetTraceParameters) (*model.Trace, error) { traces, err := s.spanReader.GetTraces(ctx, []dbmodel.TraceID{dbmodel.TraceID(query.TraceID.String())}) if err != nil { return nil, err } if len(traces) == 0 { return nil, spanstore.ErrTraceNotFound } spans, err := s.collectSpans(traces[0].Spans) if err != nil { return nil, err } return &model.Trace{Spans: spans}, nil } func (s *SpanReaderV1) collectSpans(dbSpans []dbmodel.Span) ([]*model.Span, error) { spans := make([]*model.Span, len(dbSpans)) for i := range dbSpans { span, err := s.spanConverter.SpanToDomain(&dbSpans[i]) if err != nil { return nil, fmt.Errorf("converting ES dbSpan to domain Span failed: %w", err) } spans[i] = span } return spans, nil } // GetOperations returns all operations for a specific service traced by Jaeger func (s *SpanReaderV1) GetOperations( ctx context.Context, query spanstore.OperationQueryParameters, ) ([]spanstore.Operation, error) { dbmodelQuery := dbmodel.OperationQueryParameters{ ServiceName: query.ServiceName, SpanKind: query.SpanKind, } operations, err := s.spanReader.GetOperations(ctx, dbmodelQuery) if err != nil { return nil, err } var result []spanstore.Operation for _, operation := range operations { result = append(result, spanstore.Operation{ Name: operation.Name, SpanKind: operation.SpanKind, }) } return result, nil } // GetServices returns all services traced by Jaeger, ordered by frequency func (s *SpanReaderV1) GetServices(ctx context.Context) ([]string, error) { return s.spanReader.GetServices(ctx) } // FindTraces retrieves traces that match the traceQuery func (s *SpanReaderV1) FindTraces(ctx context.Context, traceQuery *spanstore.TraceQueryParameters) ([]*model.Trace, error) { traces, err := s.spanReader.FindTraces(ctx, toDbQueryParams(traceQuery)) if err != nil { return nil, err } var result []*model.Trace for _, trace := range traces { spans, err := s.collectSpans(trace.Spans) if err != nil { return nil, err } result = append(result, &model.Trace{Spans: spans}) } return result, nil } // FindTraceIDs retrieves traces IDs that match the traceQuery func (s *SpanReaderV1) FindTraceIDs(ctx context.Context, traceQuery *spanstore.TraceQueryParameters) ([]model.TraceID, error) { ids, err := s.spanReader.FindTraceIDs(ctx, toDbQueryParams(traceQuery)) if err != nil { return nil, err } return toModelTraceIDs(ids) } func toDbQueryParams(p *spanstore.TraceQueryParameters) dbmodel.TraceQueryParameters { return dbmodel.TraceQueryParameters{ ServiceName: p.ServiceName, OperationName: p.OperationName, Tags: p.Tags, StartTimeMin: p.StartTimeMin, StartTimeMax: p.StartTimeMax, DurationMin: p.DurationMin, DurationMax: p.DurationMax, NumTraces: p.NumTraces, } } func toModelTraceIDs(traceIDs []dbmodel.TraceID) ([]model.TraceID, error) { traceIDsMap := map[model.TraceID]bool{} // https://github.com/jaegertracing/jaeger/pull/1956 added leading zeros to IDs // So we need to also read IDs without leading zeros for compatibility with previously saved data. // That means the input to this function may contain logically identical trace IDs but formatted // with or without padding, and we need to dedupe them. // TODO remove deduping in newer versions, added in Jaeger 1.16 traceIDsModels := make([]model.TraceID, 0, len(traceIDs)) for _, ID := range traceIDs { traceID, err := model.TraceIDFromString(string(ID)) if err != nil { return nil, fmt.Errorf("making traceID from string '%s' failed: %w", ID, err) } if _, ok := traceIDsMap[traceID]; !ok { traceIDsMap[traceID] = true traceIDsModels = append(traceIDsModels, traceID) } } return traceIDsModels, nil } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/readerv1_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "context" "errors" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch/spanstore/mocks" ) func withSpanReaderV1(fn func(r *SpanReaderV1, m *mocks.CoreSpanReader)) { spanReader := &mocks.CoreSpanReader{} r := &SpanReaderV1{ spanReader: spanReader, } fn(r, spanReader) } func getTestingTrace(traceID model.TraceID, spanId model.SpanID) dbmodel.Trace { return dbmodel.Trace{Spans: []dbmodel.Span{{ TraceID: dbmodel.TraceID(traceID.String()), SpanID: dbmodel.SpanID(spanId.String()), }}} } func TestSpanReaderV1_GetTrace(t *testing.T) { withSpanReaderV1(func(r *SpanReaderV1, m *mocks.CoreSpanReader) { traceID1 := model.NewTraceID(0, 1) spanID1 := model.NewSpanID(1) trace := getTestingTrace(traceID1, spanID1) m.On("GetTraces", mock.Anything, mock.Anything).Return([]dbmodel.Trace{trace}, nil) actual, err := r.GetTrace(context.Background(), spanstore.GetTraceParameters{}) require.NoError(t, err) assert.Len(t, actual.Spans, 1) assert.Equal(t, traceID1, actual.Spans[0].TraceID) }) } func TestSpanReaderV1_FindTraces(t *testing.T) { withSpanReaderV1(func(r *SpanReaderV1, m *mocks.CoreSpanReader) { traceID1 := model.NewTraceID(0, 1) spanID1 := model.NewSpanID(1) traceID2 := model.NewTraceID(0, 2) spanID2 := model.NewSpanID(2) trace1 := getTestingTrace(traceID1, spanID1) trace2 := getTestingTrace(traceID2, spanID2) traces := []dbmodel.Trace{trace1, trace2} m.On("FindTraces", mock.Anything, mock.Anything).Return(traces, nil) actual, err := r.FindTraces(context.Background(), &spanstore.TraceQueryParameters{}) require.NoError(t, err) assert.Len(t, actual, 2) assert.Len(t, actual[0].Spans, 1) assert.Len(t, actual[1].Spans, 1) assert.Equal(t, traceID1, actual[0].Spans[0].TraceID) assert.Equal(t, traceID2, actual[1].Spans[0].TraceID) }) } func TestSpanReaderV1_FindTraceIDs(t *testing.T) { withSpanReaderV1(func(r *SpanReaderV1, m *mocks.CoreSpanReader) { traceId1Model := model.NewTraceID(0, 1) traceId2Model := model.NewTraceID(0, 2) traceId1 := dbmodel.TraceID(traceId1Model.String()) traceId2 := dbmodel.TraceID(traceId2Model.String()) traceIds := []dbmodel.TraceID{traceId1, traceId2} m.On("FindTraceIDs", mock.Anything, mock.Anything).Return(traceIds, nil) actual, err := r.FindTraceIDs(context.Background(), &spanstore.TraceQueryParameters{}) require.NoError(t, err) expected := []model.TraceID{traceId1Model, traceId2Model} assert.Equal(t, expected, actual) }) } func TestSpanReaderV1_FindTraceIDs_Errors(t *testing.T) { tests := []struct { name string returningTraceIDs []dbmodel.TraceID returningErr error expectedError string }{ { name: "error from core span reader", returningErr: errors.New("error from core span reader"), expectedError: "error from core span reader", }, { name: "error from conversion", returningTraceIDs: []dbmodel.TraceID{dbmodel.TraceID("wrong-id")}, expectedError: "making traceID from string 'wrong-id' failed: strconv.ParseUint: parsing \"wrong-id\": invalid syntax", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { withSpanReaderV1(func(r *SpanReaderV1, m *mocks.CoreSpanReader) { m.On("FindTraceIDs", mock.Anything, mock.Anything).Return(tt.returningTraceIDs, tt.returningErr) actual, err := r.FindTraceIDs(context.Background(), &spanstore.TraceQueryParameters{}) assert.Nil(t, actual) assert.ErrorContains(t, err, tt.expectedError) }) }) } } func TestSpanReaderV1_GetServices(t *testing.T) { withSpanReaderV1(func(r *SpanReaderV1, m *mocks.CoreSpanReader) { services := []string{"service-1", "service-2"} m.On("GetServices", mock.Anything).Return(services, nil) actual, err := r.GetServices(context.Background()) require.NoError(t, err) assert.Equal(t, services, actual) }) } func TestSpanReaderV1_GetOperations(t *testing.T) { withSpanReaderV1(func(r *SpanReaderV1, m *mocks.CoreSpanReader) { operation := []dbmodel.Operation{{Name: "operation-1", SpanKind: "kind-1"}} input := dbmodel.OperationQueryParameters{ServiceName: "service", SpanKind: "kind-1"} m.On("GetOperations", mock.Anything, input).Return(operation, nil) actual, err := r.GetOperations(context.Background(), spanstore.OperationQueryParameters{ServiceName: "service", SpanKind: "kind-1"}) require.NoError(t, err) assert.Len(t, actual, 1) assert.Equal(t, operation[0].Name, actual[0].Name) assert.Equal(t, operation[0].SpanKind, actual[0].SpanKind) }) } func TestSpanReaderV1_GetOperations_Error(t *testing.T) { withSpanReaderV1(func(r *SpanReaderV1, m *mocks.CoreSpanReader) { input := dbmodel.OperationQueryParameters{ServiceName: "service", SpanKind: "kind-1"} m.On("GetOperations", mock.Anything, input).Return(nil, errors.New("error")) actual, err := r.GetOperations(context.Background(), spanstore.OperationQueryParameters{ServiceName: "service", SpanKind: "kind-1"}) require.Error(t, err, "error") assert.Nil(t, actual) }) } type traceError struct { name string returningErr error expectedError string returningTraces []dbmodel.Trace } func getTraceErrTests(includeTraceNotFound bool) []traceError { tests := []traceError{ { name: "conversion error", expectedError: "converting ES dbSpan to domain Span failed: strconv.ParseUint: parsing \"\": invalid syntax", returningTraces: []dbmodel.Trace{getBadTrace()}, }, { name: "generic error", returningErr: errors.New("error"), expectedError: "error", }, } if includeTraceNotFound { tests = append(tests, traceError{ name: "trace not found", returningTraces: []dbmodel.Trace{}, expectedError: "trace not found", }) } return tests } func TestSpanReaderV1_GetTraceError(t *testing.T) { tests := getTraceErrTests(true) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { withSpanReaderV1(func(r *SpanReaderV1, m *mocks.CoreSpanReader) { m.On("GetTraces", mock.Anything, mock.Anything).Return(tt.returningTraces, tt.returningErr) query := spanstore.GetTraceParameters{} trace, err := r.GetTrace(context.Background(), query) require.ErrorContains(t, err, tt.expectedError) require.Nil(t, trace) }) }) } } func TestSpanReaderV1_FindTracesError(t *testing.T) { tests := getTraceErrTests(false) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { withSpanReaderV1(func(r *SpanReaderV1, m *mocks.CoreSpanReader) { m.On("FindTraces", mock.Anything, mock.Anything).Return(tt.returningTraces, tt.returningErr) query := &spanstore.TraceQueryParameters{} trace, err := r.FindTraces(context.Background(), query) require.Error(t, err, tt.expectedError) require.Nil(t, trace) }) }) } } func getBadTrace() dbmodel.Trace { return dbmodel.Trace{ Spans: []dbmodel.Span{ { OperationName: "testing-operation", }, }, } } func TestTraceIDsStringsToModelsConversion(t *testing.T) { traceIDs, err := toModelTraceIDs([]dbmodel.TraceID{"1", "2", "3"}) require.NoError(t, err) assert.Len(t, traceIDs, 3) assert.Equal(t, model.NewTraceID(0, 1), traceIDs[0]) traceIDs, err = toModelTraceIDs([]dbmodel.TraceID{"dsfjsdklfjdsofdfsdbfkgbgoaemlrksdfbsdofgerjl"}) require.EqualError(t, err, "making traceID from string 'dsfjsdklfjdsofdfsdbfkgbgoaemlrksdfbsdofgerjl' failed: TraceID cannot be longer than 32 hex characters: dsfjsdklfjdsofdfsdbfkgbgoaemlrksdfbsdofgerjl") assert.Empty(t, traceIDs) } func TestConvertTraceIDsStringsToModels(t *testing.T) { ids, err := toModelTraceIDs([]dbmodel.TraceID{"1", "2", "01", "02", "001", "002"}) require.NoError(t, err) assert.Equal(t, []model.TraceID{model.NewTraceID(0, 1), model.NewTraceID(0, 2)}, ids) _, err = toModelTraceIDs([]dbmodel.TraceID{"blah"}) require.ErrorContains(t, err, "making traceID from string 'blah' failed: strconv.ParseUint: parsing \"blah\": invalid syntax") } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/service_operation.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "context" "errors" "fmt" "hash/fnv" "strconv" "time" "github.com/olivere/elastic/v7" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/cache" es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" ) const ( serviceName = "serviceName" operationsAggregation = "distinct_operations" servicesAggregation = "distinct_services" ) // ServiceOperationStorage stores service to operation pairs. type ServiceOperationStorage struct { client func() es.Client logger *zap.Logger serviceCache cache.Cache } // NewServiceOperationStorage returns a new ServiceOperationStorage. func NewServiceOperationStorage( client func() es.Client, logger *zap.Logger, cacheTTL time.Duration, ) *ServiceOperationStorage { return &ServiceOperationStorage{ client: client, logger: logger, serviceCache: cache.NewLRUWithOptions( 100000, &cache.Options{ TTL: cacheTTL, }, ), } } // Write saves a service to operation pair. func (s *ServiceOperationStorage) Write(indexName string, jsonSpan *dbmodel.Span) { // Insert serviceName:operationName document service := dbmodel.Service{ ServiceName: jsonSpan.Process.ServiceName, OperationName: jsonSpan.OperationName, } cacheKey := hashCode(service) if !keyInCache(cacheKey, s.serviceCache) { s.client().Index().Index(indexName).Type(serviceType).Id(cacheKey).BodyJson(service).Add() writeCache(cacheKey, s.serviceCache) } } func (s *ServiceOperationStorage) getServices(ctx context.Context, indices []string, maxDocCount int) ([]string, error) { serviceAggregation := getServicesAggregation(maxDocCount) searchService := s.client().Search(indices...). Size(0). // set to 0 because we don't want actual documents. IgnoreUnavailable(true). Aggregation(servicesAggregation, serviceAggregation) searchResult, err := searchService.Do(ctx) if err != nil { return nil, fmt.Errorf("search services failed: %w", es.DetailedError(err)) } if searchResult.Aggregations == nil { return []string{}, nil } bucket, found := searchResult.Aggregations.Terms(servicesAggregation) if !found { return nil, errors.New("could not find aggregation of " + servicesAggregation) } serviceNamesBucket := bucket.Buckets return bucketToStringArray[string](serviceNamesBucket) } func getServicesAggregation(maxDocCount int) elastic.Query { return elastic.NewTermsAggregation(). Field(serviceName). Size(maxDocCount) // ES deprecated size omission for aggregating all. https://github.com/elastic/elasticsearch/issues/18838 } func (s *ServiceOperationStorage) getOperations(ctx context.Context, indices []string, service string, maxDocCount int) ([]string, error) { serviceQuery := elastic.NewTermQuery(serviceName, service) serviceFilter := getOperationsAggregation(maxDocCount) searchService := s.client().Search(indices...). Size(0). Query(serviceQuery). IgnoreUnavailable(true). Aggregation(operationsAggregation, serviceFilter) searchResult, err := searchService.Do(ctx) if err != nil { return nil, fmt.Errorf("search operations failed: %w", es.DetailedError(err)) } if searchResult.Aggregations == nil { return []string{}, nil } bucket, found := searchResult.Aggregations.Terms(operationsAggregation) if !found { return nil, errors.New("could not find aggregation of " + operationsAggregation) } operationNamesBucket := bucket.Buckets return bucketToStringArray[string](operationNamesBucket) } func getOperationsAggregation(maxDocCount int) elastic.Query { return elastic.NewTermsAggregation(). Field(operationNameField). Size(maxDocCount) // ES deprecated size omission for aggregating all. https://github.com/elastic/elasticsearch/issues/18838 } func hashCode(s dbmodel.Service) string { h := fnv.New64a() h.Write([]byte(s.ServiceName)) h.Write([]byte(s.OperationName)) return strconv.FormatUint(h.Sum64(), 16) } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/service_operation_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "context" "testing" "github.com/olivere/elastic/v7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/mocks" ) func TestWriteService(t *testing.T) { withSpanWriter(func(w *spanWriterTest) { indexService := &mocks.IndexService{} indexName := "jaeger-1995-04-21" serviceHash := "de3b5a8f1a79989d" indexService.On("Index", stringMatcher(indexName)).Return(indexService) indexService.On("Type", stringMatcher(serviceType)).Return(indexService) indexService.On("Id", stringMatcher(serviceHash)).Return(indexService) indexService.On("BodyJson", mock.AnythingOfType("dbmodel.Service")).Return(indexService) indexService.On("Add") w.client.On("Index").Return(indexService) jsonSpan := &dbmodel.Span{ TraceID: dbmodel.TraceID("1"), SpanID: dbmodel.SpanID("0"), OperationName: "operation", Process: dbmodel.Process{ ServiceName: "service", }, } w.writer.writeService(indexName, jsonSpan) indexService.AssertNumberOfCalls(t, "Add", 1) assert.Empty(t, w.logBuffer.String()) // test that cache works, will call the index service only once. w.writer.writeService(indexName, jsonSpan) indexService.AssertNumberOfCalls(t, "Add", 1) }) } func TestWriteServiceError(*testing.T) { withSpanWriter(func(w *spanWriterTest) { indexService := &mocks.IndexService{} indexName := "jaeger-1995-04-21" serviceHash := "de3b5a8f1a79989d" indexService.On("Index", stringMatcher(indexName)).Return(indexService) indexService.On("Type", stringMatcher(serviceType)).Return(indexService) indexService.On("Id", stringMatcher(serviceHash)).Return(indexService) indexService.On("BodyJson", mock.AnythingOfType("dbmodel.Service")).Return(indexService) indexService.On("Add") w.client.On("Index").Return(indexService) jsonSpan := &dbmodel.Span{ TraceID: dbmodel.TraceID("1"), SpanID: dbmodel.SpanID("0"), OperationName: "operation", Process: dbmodel.Process{ ServiceName: "service", }, } w.writer.writeService(indexName, jsonSpan) }) } func TestSpanReader_GetServices(t *testing.T) { testGet(servicesAggregation, t) } func TestSpanReader_GetOperations(t *testing.T) { testGet(operationsAggregation, t) } func TestSpanReader_GetServicesEmptyIndex(t *testing.T) { withSpanReader(t, func(r *spanReaderTest) { mockSearchService(r). Return(&elastic.SearchResult{}, nil) mockMultiSearchService(r). Return(&elastic.MultiSearchResult{ Responses: []*elastic.SearchResult{}, }, nil) services, err := r.reader.GetServices(context.Background()) require.NoError(t, err) assert.Empty(t, services) }) } func TestSpanReader_GetOperationsEmptyIndex(t *testing.T) { withSpanReader(t, func(r *spanReaderTest) { mockSearchService(r). Return(&elastic.SearchResult{}, nil) mockMultiSearchService(r). Return(&elastic.MultiSearchResult{ Responses: []*elastic.SearchResult{}, }, nil) services, err := r.reader.GetOperations( context.Background(), dbmodel.OperationQueryParameters{ServiceName: "foo"}, ) require.NoError(t, err) assert.Empty(t, services) }) } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/to_domain.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2018 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "encoding/hex" "encoding/json" "fmt" "strconv" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" ) // NewToDomain creates ToDomain func NewToDomain() ToDomain { return ToDomain{} } // ToDomain is used to convert Span to model.Span type ToDomain struct{} // SpanToDomain converts db span into model Span func (td ToDomain) SpanToDomain(dbSpan *dbmodel.Span) (*model.Span, error) { tags, err := td.convertKeyValues(dbSpan.Tags) if err != nil { return nil, err } logs, err := td.convertLogs(dbSpan.Logs) if err != nil { return nil, err } refs, err := td.convertRefs(dbSpan.References) if err != nil { return nil, err } process, err := td.convertProcess(dbSpan.Process) if err != nil { return nil, err } traceID, err := model.TraceIDFromString(string(dbSpan.TraceID)) if err != nil { return nil, err } spanIDInt, err := model.SpanIDFromString(string(dbSpan.SpanID)) if err != nil { return nil, err } if dbSpan.ParentSpanID != "" { parentSpanID, err := model.SpanIDFromString(string(dbSpan.ParentSpanID)) if err != nil { return nil, err } refs = model.MaybeAddParentSpanID(traceID, parentSpanID, refs) } span := &model.Span{ TraceID: traceID, SpanID: model.NewSpanID(uint64(spanIDInt)), OperationName: dbSpan.OperationName, References: refs, Flags: model.Flags(uint32(dbSpan.Flags)), StartTime: model.EpochMicrosecondsAsTime(dbSpan.StartTime), Duration: model.MicrosecondsAsDuration(dbSpan.Duration), Tags: tags, Logs: logs, Process: process, } return span, nil } func (ToDomain) convertRefs(refs []dbmodel.Reference) ([]model.SpanRef, error) { retMe := make([]model.SpanRef, len(refs)) for i, r := range refs { // There are some inconsistencies with ReferenceTypes, hence the hacky fix. var refType model.SpanRefType switch r.RefType { case dbmodel.ChildOf: refType = model.ChildOf case dbmodel.FollowsFrom: refType = model.FollowsFrom default: return nil, fmt.Errorf("not a valid SpanRefType string %s", string(r.RefType)) } traceID, err := model.TraceIDFromString(string(r.TraceID)) if err != nil { return nil, err } spanID, err := strconv.ParseUint(string(r.SpanID), 16, 64) if err != nil { return nil, err } retMe[i] = model.SpanRef{ RefType: refType, TraceID: traceID, SpanID: model.NewSpanID(spanID), } } return retMe, nil } func (td ToDomain) convertKeyValues(tags []dbmodel.KeyValue) ([]model.KeyValue, error) { retMe := make([]model.KeyValue, len(tags)) for i := range tags { kv, err := td.convertKeyValue(&tags[i]) if err != nil { return nil, err } retMe[i] = kv } return retMe, nil } // convertKeyValue expects the Value field to be string, because it only works // as a reverse transformation after FromDomain() for ElasticSearch model. func (td ToDomain) convertKeyValue(tag *dbmodel.KeyValue) (model.KeyValue, error) { if tag.Value == nil { return model.KeyValue{}, fmt.Errorf("invalid nil Value in %v", tag) } tagValue, ok := tag.Value.(string) if !ok { switch tag.Type { case dbmodel.Int64Type, dbmodel.Float64Type: kv, err := td.fromDBNumber(tag) if err != nil { return model.KeyValue{}, err } return kv, nil case dbmodel.BoolType: if boolVal, ok := tag.Value.(bool); ok { return model.Bool(tag.Key, boolVal), nil } return model.KeyValue{}, invalidValueErr(tag) // string and binary values should always be of string type default: return model.KeyValue{}, invalidValueErr(tag) } } switch tag.Type { case dbmodel.StringType: return model.String(tag.Key, tagValue), nil case dbmodel.BoolType: value, err := strconv.ParseBool(tagValue) if err != nil { return model.KeyValue{}, err } return model.Bool(tag.Key, value), nil case dbmodel.Int64Type: value, err := strconv.ParseInt(tagValue, 10, 64) if err != nil { return model.KeyValue{}, err } return model.Int64(tag.Key, value), nil case dbmodel.Float64Type: value, err := strconv.ParseFloat(tagValue, 64) if err != nil { return model.KeyValue{}, err } return model.Float64(tag.Key, value), nil case dbmodel.BinaryType: value, err := hex.DecodeString(tagValue) if err != nil { return model.KeyValue{}, err } return model.Binary(tag.Key, value), nil default: return model.KeyValue{}, fmt.Errorf("not a valid ValueType string %s", string(tag.Type)) } } func (ToDomain) fromDBNumber(kv *dbmodel.KeyValue) (model.KeyValue, error) { switch kv.Type { case dbmodel.Int64Type: switch v := kv.Value.(type) { case int64: return model.Int64(kv.Key, v), nil // This case is very much possible as JSON converts every number to float64 case float64: return model.Int64(kv.Key, int64(v)), nil case json.Number: n, err := v.Int64() if err == nil { return model.Int64(kv.Key, n), nil } return model.KeyValue{}, fmt.Errorf("not a valid number ValueType %s", string(kv.Type)) default: return model.KeyValue{}, invalidValueErr(kv) } case dbmodel.Float64Type: switch v := kv.Value.(type) { case float64: return model.Float64(kv.Key, v), nil case json.Number: n, err := v.Float64() if err == nil { return model.Float64(kv.Key, n), nil } return model.KeyValue{}, fmt.Errorf("not a valid number ValueType %s", string(kv.Type)) default: return model.KeyValue{}, invalidValueErr(kv) } default: return model.KeyValue{}, fmt.Errorf("not a valid number ValueType %s", string(kv.Type)) } } func invalidValueErr(kv *dbmodel.KeyValue) error { return fmt.Errorf("invalid %s type in %+v", string(kv.Type), kv.Value) } func (td ToDomain) convertLogs(logs []dbmodel.Log) ([]model.Log, error) { retMe := make([]model.Log, len(logs)) for i, l := range logs { fields, err := td.convertKeyValues(l.Fields) if err != nil { return nil, err } retMe[i] = model.Log{ Timestamp: model.EpochMicrosecondsAsTime(l.Timestamp), Fields: fields, } } return retMe, nil } func (td ToDomain) convertProcess(process dbmodel.Process) (*model.Process, error) { tags, err := td.convertKeyValues(process.Tags) if err != nil { return nil, err } return &model.Process{ Tags: tags, ServiceName: process.ServiceName, }, nil } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/to_domain_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2018 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "bytes" "encoding/json" "fmt" "math" "os" "testing" gogojsonpb "github.com/gogo/protobuf/jsonpb" "github.com/kr/pretty" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" ) func TestToDomain(t *testing.T) { runToDomainTest(t, false) runToDomainTest(t, true) // this is just to confirm the uint64 representation of float64(72.5) used as a "temperature" tag assert.Equal(t, int64(4634802150889750528), int64(math.Float64bits(72.5))) } func runToDomainTest(t *testing.T, testParentSpanID bool) { for i := 1; i <= NumberOfFixtures; i++ { span, err := loadESSpanFixture(i) require.NoError(t, err) if testParentSpanID { span.ParentSpanID = "3" } actualSpan, err := NewToDomain().SpanToDomain(&span) require.NoError(t, err) out := fmt.Sprintf("fixtures/domain_%02d.json", i) outStr, err := os.ReadFile(out) require.NoError(t, err) var expectedSpan model.Span require.NoError(t, gogojsonpb.Unmarshal(bytes.NewReader(outStr), &expectedSpan)) CompareModelSpans(t, &expectedSpan, actualSpan) } } func loadESSpanFixture(i int) (dbmodel.Span, error) { in := fmt.Sprintf("fixtures/es_%02d.json", i) inStr, err := os.ReadFile(in) if err != nil { return dbmodel.Span{}, err } var span dbmodel.Span err = json.Unmarshal(inStr, &span) return span, err } func failingSpanTransform(t *testing.T, embeddedSpan *dbmodel.Span, errMsg string) { domainSpan, err := NewToDomain().SpanToDomain(embeddedSpan) assert.Nil(t, domainSpan) require.EqualError(t, err, errMsg) } func failingSpanTransformAnyMsg(t *testing.T, embeddedSpan *dbmodel.Span) { domainSpan, err := NewToDomain().SpanToDomain(embeddedSpan) assert.Nil(t, domainSpan) require.Error(t, err) } func TestFailureBadTypeTags(t *testing.T) { badTagESSpan, err := loadESSpanFixture(1) require.NoError(t, err) badTagESSpan.Tags = []dbmodel.KeyValue{ { Key: "meh", Type: "badType", Value: "", }, } failingSpanTransformAnyMsg(t, &badTagESSpan) } func TestFailureBadBoolTags(t *testing.T) { badTagESSpan, err := loadESSpanFixture(1) require.NoError(t, err) badTagESSpan.Tags = []dbmodel.KeyValue{ { Key: "meh", Value: "meh", Type: "bool", }, } failingSpanTransformAnyMsg(t, &badTagESSpan) } func TestFailureBadIntTags(t *testing.T) { badTagESSpan, err := loadESSpanFixture(1) require.NoError(t, err) badTagESSpan.Tags = []dbmodel.KeyValue{ { Key: "meh", Value: "meh", Type: "int64", }, } failingSpanTransformAnyMsg(t, &badTagESSpan) } func TestFailureBadFloatTags(t *testing.T) { badTagESSpan, err := loadESSpanFixture(1) require.NoError(t, err) badTagESSpan.Tags = []dbmodel.KeyValue{ { Key: "meh", Value: "meh", Type: "float64", }, } failingSpanTransformAnyMsg(t, &badTagESSpan) } func TestFailureBadBinaryTags(t *testing.T) { badTagESSpan, err := loadESSpanFixture(1) require.NoError(t, err) badTagESSpan.Tags = []dbmodel.KeyValue{ { Key: "zzzz", Value: "zzzz", Type: "binary", }, } failingSpanTransformAnyMsg(t, &badTagESSpan) } func TestFailureBadLogs(t *testing.T) { badLogsESSpan, err := loadESSpanFixture(1) require.NoError(t, err) badLogsESSpan.Logs = []dbmodel.Log{ { Timestamp: 0, Fields: []dbmodel.KeyValue{ { Key: "sneh", Value: "", Type: "badType", }, }, }, } failingSpanTransform(t, &badLogsESSpan, "not a valid ValueType string badType") } func TestRevertKeyValueOfType(t *testing.T) { tests := []struct { name string kv *dbmodel.KeyValue err string outKv model.KeyValue }{ { name: "not a valid ValueType string", kv: &dbmodel.KeyValue{ Key: "sneh", Type: "badType", Value: "someString", }, err: "not a valid ValueType string", }, { name: "invalid nil Value", kv: &dbmodel.KeyValue{}, err: "invalid nil Value", }, { name: "right int value", kv: &dbmodel.KeyValue{ Key: "int-val", Type: dbmodel.Int64Type, Value: int64(123), }, outKv: model.KeyValue{ Key: "int-val", VInt64: 123, VType: 2, }, }, { name: "right int float value", kv: &dbmodel.KeyValue{ Key: "int-val", Type: dbmodel.Int64Type, Value: float64(123), }, outKv: model.KeyValue{ Key: "int-val", VInt64: 123, VType: 2, }, }, { name: "right int json number", kv: &dbmodel.KeyValue{ Key: "int-val", Type: dbmodel.Int64Type, Value: json.Number("123"), }, outKv: model.KeyValue{ Key: "int-val", VInt64: 123, VType: 2, }, }, { name: "right float value", kv: &dbmodel.KeyValue{ Key: "float-val", Type: dbmodel.Float64Type, Value: 123.4, }, outKv: model.KeyValue{ Key: "float-val", VFloat64: 123.4, VType: 3, }, }, { name: "right float json number", kv: &dbmodel.KeyValue{ Key: "float-val", Type: dbmodel.Float64Type, Value: json.Number("123.4"), }, outKv: model.KeyValue{ Key: "float-val", VFloat64: 123.4, VType: 3, }, }, { name: "wrong int64 value", kv: &dbmodel.KeyValue{ Key: "int-val", Type: dbmodel.Int64Type, Value: true, }, err: "invalid int64 type in true", }, { name: "wrong float64 value", kv: &dbmodel.KeyValue{ Key: "float-val", Type: dbmodel.Float64Type, Value: true, }, err: "invalid float64 type in true", }, { name: "wrong bool value", kv: &dbmodel.KeyValue{ Key: "bool-val", Type: dbmodel.BoolType, Value: 1.23, }, err: "invalid bool type in 1.23", }, { name: "wrong string value", kv: &dbmodel.KeyValue{ Key: "string-val", Type: dbmodel.StringType, Value: 123, }, err: "invalid string type in 123", }, } td := ToDomain{} for _, test := range tests { t.Run(test.name, func(t *testing.T) { tag := test.kv out, err := td.convertKeyValue(tag) if test.err != "" { require.ErrorContains(t, err, test.err) } assert.Equal(t, test.outKv, out) }) } } func TestFromDBTag_DefaultCase(t *testing.T) { tag := &dbmodel.KeyValue{ Key: "test-key", Type: "unknown-type", Value: "test-value", } td := ToDomain{} result, err := td.convertKeyValue(tag) require.Error(t, err) assert.Contains(t, err.Error(), "not a valid ValueType string unknown-type") assert.Equal(t, model.KeyValue{}, result) } func TestFailureBadRefs(t *testing.T) { badRefsESSpan, err := loadESSpanFixture(1) require.NoError(t, err) badRefsESSpan.References = []dbmodel.Reference{ { RefType: "makeOurOwnCasino", TraceID: "1", }, } failingSpanTransform(t, &badRefsESSpan, "not a valid SpanRefType string makeOurOwnCasino") } func TestFailureBadTraceIDRefs(t *testing.T) { badRefsESSpan, err := loadESSpanFixture(1) require.NoError(t, err) badRefsESSpan.References = []dbmodel.Reference{ { RefType: "CHILD_OF", TraceID: "ZZBADZZ", SpanID: "1", }, } failingSpanTransformAnyMsg(t, &badRefsESSpan) } func TestFailureBadSpanIDRefs(t *testing.T) { badRefsESSpan, err := loadESSpanFixture(1) require.NoError(t, err) badRefsESSpan.References = []dbmodel.Reference{ { RefType: "CHILD_OF", TraceID: "1", SpanID: "ZZBADZZ", }, } failingSpanTransformAnyMsg(t, &badRefsESSpan) } func TestFailureBadProcess(t *testing.T) { badProcessESSpan, err := loadESSpanFixture(1) require.NoError(t, err) badTags := []dbmodel.KeyValue{ { Key: "meh", Value: "", Type: "badType", }, } badProcessESSpan.Process = dbmodel.Process{ ServiceName: "hello", Tags: badTags, } failingSpanTransform(t, &badProcessESSpan, "not a valid ValueType string badType") } func TestFailureBadTraceID(t *testing.T) { badTraceIDESSpan, err := loadESSpanFixture(1) require.NoError(t, err) badTraceIDESSpan.TraceID = "zz" failingSpanTransformAnyMsg(t, &badTraceIDESSpan) } func TestFailureBadSpanID(t *testing.T) { badSpanIDESSpan, err := loadESSpanFixture(1) require.NoError(t, err) badSpanIDESSpan.SpanID = "zz" failingSpanTransformAnyMsg(t, &badSpanIDESSpan) } func TestFailureBadParentSpanID(t *testing.T) { badParentSpanIDESSpan, err := loadESSpanFixture(1) require.NoError(t, err) badParentSpanIDESSpan.ParentSpanID = "zz" failingSpanTransformAnyMsg(t, &badParentSpanIDESSpan) } func CompareModelSpans(t *testing.T, expected *model.Span, actual *model.Span) { model.SortSpan(expected) model.SortSpan(actual) if !assert.Equal(t, expected, actual) { for _, err := range pretty.Diff(expected, actual) { t.Log(err) } out, err := json.Marshal(actual) require.NoError(t, err) t.Logf("Actual trace: %s", string(out)) } } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/writer.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "strings" "time" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/cache" "github.com/jaegertracing/jaeger/internal/metrics" es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" cfg "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore/spanstoremetrics" ) const ( spanType = "span" serviceType = "service" serviceCacheTTLDefault = 12 * time.Hour indexCacheTTLDefault = 48 * time.Hour ) type serviceWriter func(string, *dbmodel.Span) // SpanWriter is a wrapper around elastic.Client type SpanWriter struct { client func() es.Client logger *zap.Logger // indexCache cache.Cache writerMetrics *spanstoremetrics.WriteMetrics serviceWriter serviceWriter spanServiceIndex spanAndServiceIndexFn allTagsAsFields bool tagDotReplacement string tagKeysAsFields map[string]bool } // CoreSpanWriter is a DB-Level abstraction which directly deals with database level operations type CoreSpanWriter interface { // WriteSpan writes a span and its corresponding service:operation in ElasticSearch WriteSpan(spanStartTime time.Time, span *dbmodel.Span) // Close closes CoreSpanWriter Close() error } // SpanWriterParams holds constructor parameters for NewSpanWriter type SpanWriterParams struct { Client func() es.Client Logger *zap.Logger MetricsFactory metrics.Factory SpanIndex cfg.IndexOptions ServiceIndex cfg.IndexOptions IndexPrefix cfg.IndexPrefix AllTagsAsFields bool TagKeysAsFields []string TagDotReplacement string UseReadWriteAliases bool WriteAliasSuffix string SpanWriteAlias string ServiceWriteAlias string ServiceCacheTTL time.Duration } // NewSpanWriter creates a new SpanWriter for use func NewSpanWriter(p SpanWriterParams) *SpanWriter { serviceCacheTTL := p.ServiceCacheTTL if p.ServiceCacheTTL == 0 { serviceCacheTTL = serviceCacheTTLDefault } writeAliasSuffix := "" if p.UseReadWriteAliases { if p.WriteAliasSuffix != "" { writeAliasSuffix = p.WriteAliasSuffix } else { writeAliasSuffix = "write" } } tags := map[string]bool{} for _, k := range p.TagKeysAsFields { tags[k] = true } serviceOperationStorage := NewServiceOperationStorage(p.Client, p.Logger, serviceCacheTTL) return &SpanWriter{ client: p.Client, logger: p.Logger, writerMetrics: spanstoremetrics.NewWriter(p.MetricsFactory, "spans"), serviceWriter: serviceOperationStorage.Write, spanServiceIndex: getSpanAndServiceIndexFn(p, writeAliasSuffix), tagKeysAsFields: tags, allTagsAsFields: p.AllTagsAsFields, tagDotReplacement: p.TagDotReplacement, } } // spanAndServiceIndexFn returns names of span and service indices type spanAndServiceIndexFn func(spanTime time.Time) (string, string) func getSpanAndServiceIndexFn(p SpanWriterParams, writeAlias string) spanAndServiceIndexFn { // If explicit write aliases are provided, use them directly without modification if p.SpanWriteAlias != "" && p.ServiceWriteAlias != "" { return func(_ time.Time) (string, string) { return p.SpanWriteAlias, p.ServiceWriteAlias } } // Otherwise, use the standard prefix + suffix approach spanIndexPrefix := p.IndexPrefix.Apply(spanIndexBaseName) serviceIndexPrefix := p.IndexPrefix.Apply(serviceIndexBaseName) if p.UseReadWriteAliases { return func(_ time.Time) (string, string) { return spanIndexPrefix + writeAlias, serviceIndexPrefix + writeAlias } } return func(date time.Time) (string, string) { return indexWithDate(spanIndexPrefix, p.SpanIndex.DateLayout, date), indexWithDate(serviceIndexPrefix, p.ServiceIndex.DateLayout, date) } } // WriteSpan writes a span and its corresponding service:operation in ElasticSearch func (s *SpanWriter) WriteSpan(spanStartTime time.Time, span *dbmodel.Span) { s.writerMetrics.Attempts.Inc(1) s.convertNestedTagsToFieldTags(span) spanIndexName, serviceIndexName := s.spanServiceIndex(spanStartTime) if serviceIndexName != "" { s.writeService(serviceIndexName, span) } s.writeSpanToIndex(spanIndexName, span) s.logger.Debug("Wrote span to ES index", zap.String("index", spanIndexName)) } func (s *SpanWriter) convertNestedTagsToFieldTags(span *dbmodel.Span) { processNestedTags, processFieldTags := s.splitElevatedTags(span.Process.Tags) span.Process.Tags = processNestedTags span.Process.Tag = processFieldTags nestedTags, fieldTags := s.splitElevatedTags(span.Tags) span.Tags = nestedTags span.Tag = fieldTags } // Close closes SpanWriter func (s *SpanWriter) Close() error { return s.client().Close() } func keyInCache(key string, c cache.Cache) bool { return c.Get(key) != nil } func writeCache(key string, c cache.Cache) { c.Put(key, key) } func (s *SpanWriter) writeService(indexName string, jsonSpan *dbmodel.Span) { s.serviceWriter(indexName, jsonSpan) } func (s *SpanWriter) writeSpanToIndex(indexName string, jsonSpan *dbmodel.Span) { s.client().Index().Index(indexName).Type(spanType).BodyJson(&jsonSpan).Add() } func (s *SpanWriter) splitElevatedTags(keyValues []dbmodel.KeyValue) ([]dbmodel.KeyValue, map[string]any) { if !s.allTagsAsFields && len(s.tagKeysAsFields) == 0 { return keyValues, nil } var tagsMap map[string]any var kvs []dbmodel.KeyValue for _, kv := range keyValues { if kv.Type != dbmodel.BinaryType && (s.allTagsAsFields || s.tagKeysAsFields[kv.Key]) { if tagsMap == nil { tagsMap = map[string]any{} } tagsMap[strings.ReplaceAll(kv.Key, ".", s.tagDotReplacement)] = kv.Value } else { kvs = append(kvs, kv) } } if kvs == nil { kvs = make([]dbmodel.KeyValue, 0) } return kvs, tagsMap } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/writer_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/metricstest" es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/mocks" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" "github.com/jaegertracing/jaeger/internal/testutils" ) type spanWriterTest struct { client *mocks.Client logger *zap.Logger logBuffer *testutils.Buffer writer *SpanWriter } func withSpanWriter(fn func(w *spanWriterTest)) { client := &mocks.Client{} logger, logBuffer := testutils.NewLogger() metricsFactory := metricstest.NewFactory(0) w := &spanWriterTest{ client: client, logger: logger, logBuffer: logBuffer, writer: NewSpanWriter(SpanWriterParams{ Client: func() es.Client { return client }, Logger: logger, MetricsFactory: metricsFactory, SpanIndex: config.IndexOptions{DateLayout: "2006-01-02"}, ServiceIndex: config.IndexOptions{DateLayout: "2006-01-02"}, }), } fn(w) } var _ spanstore.Writer = &SpanWriterV1{} // check API conformance func TestSpanWriterIndices(t *testing.T) { client := &mocks.Client{} clientFn := func() es.Client { return client } logger, _ := testutils.NewLogger() metricsFactory := metricstest.NewFactory(0) date := time.Now() spanDataLayout := "2006-01-02-15" serviceDataLayout := "2006-01-02" spanDataLayoutFormat := date.UTC().Format(spanDataLayout) serviceDataLayoutFormat := date.UTC().Format(serviceDataLayout) spanIndexOpts := config.IndexOptions{DateLayout: spanDataLayout} serviceIndexOpts := config.IndexOptions{DateLayout: serviceDataLayout} testCases := []struct { indices []string params SpanWriterParams }{ { params: SpanWriterParams{ Client: clientFn, Logger: logger, MetricsFactory: metricsFactory, SpanIndex: spanIndexOpts, ServiceIndex: serviceIndexOpts, }, indices: []string{spanIndexBaseName + spanDataLayoutFormat, serviceIndexBaseName + serviceDataLayoutFormat}, }, { params: SpanWriterParams{ Client: clientFn, Logger: logger, MetricsFactory: metricsFactory, SpanIndex: spanIndexOpts, ServiceIndex: serviceIndexOpts, UseReadWriteAliases: true, }, indices: []string{spanIndexBaseName + "write", serviceIndexBaseName + "write"}, }, { params: SpanWriterParams{ Client: clientFn, Logger: logger, MetricsFactory: metricsFactory, SpanIndex: spanIndexOpts, ServiceIndex: serviceIndexOpts, WriteAliasSuffix: "archive", // ignored because UseReadWriteAliases is false }, indices: []string{spanIndexBaseName + spanDataLayoutFormat, serviceIndexBaseName + serviceDataLayoutFormat}, }, { params: SpanWriterParams{ Client: clientFn, Logger: logger, MetricsFactory: metricsFactory, SpanIndex: spanIndexOpts, ServiceIndex: serviceIndexOpts, IndexPrefix: "foo:", }, indices: []string{"foo:" + config.IndexPrefixSeparator + spanIndexBaseName + spanDataLayoutFormat, "foo:" + config.IndexPrefixSeparator + serviceIndexBaseName + serviceDataLayoutFormat}, }, { params: SpanWriterParams{ Client: clientFn, Logger: logger, MetricsFactory: metricsFactory, SpanIndex: spanIndexOpts, ServiceIndex: serviceIndexOpts, IndexPrefix: "foo:", UseReadWriteAliases: true, }, indices: []string{"foo:-" + spanIndexBaseName + "write", "foo:-" + serviceIndexBaseName + "write"}, }, { params: SpanWriterParams{ Client: clientFn, Logger: logger, MetricsFactory: metricsFactory, SpanIndex: spanIndexOpts, ServiceIndex: serviceIndexOpts, WriteAliasSuffix: "archive", UseReadWriteAliases: true, }, indices: []string{spanIndexBaseName + "archive", serviceIndexBaseName + "archive"}, }, { params: SpanWriterParams{ Client: clientFn, Logger: logger, MetricsFactory: metricsFactory, SpanIndex: spanIndexOpts, ServiceIndex: serviceIndexOpts, IndexPrefix: "foo:", WriteAliasSuffix: "archive", UseReadWriteAliases: true, }, indices: []string{"foo:" + config.IndexPrefixSeparator + spanIndexBaseName + "archive", "foo:" + config.IndexPrefixSeparator + serviceIndexBaseName + "archive"}, }, { params: SpanWriterParams{ Client: clientFn, Logger: logger, MetricsFactory: metricsFactory, SpanIndex: spanIndexOpts, ServiceIndex: serviceIndexOpts, UseReadWriteAliases: true, SpanWriteAlias: "custom-span-write-alias", ServiceWriteAlias: "custom-service-write-alias", }, indices: []string{"custom-span-write-alias", "custom-service-write-alias"}, }, { params: SpanWriterParams{ Client: clientFn, Logger: logger, MetricsFactory: metricsFactory, SpanIndex: spanIndexOpts, ServiceIndex: serviceIndexOpts, UseReadWriteAliases: true, SpanWriteAlias: "custom-span-write-alias", ServiceWriteAlias: "custom-service-write-alias", WriteAliasSuffix: "archive", // Ignored when explicit aliases are used }, indices: []string{"custom-span-write-alias", "custom-service-write-alias"}, }, { params: SpanWriterParams{ Client: clientFn, Logger: logger, MetricsFactory: metricsFactory, SpanIndex: spanIndexOpts, ServiceIndex: serviceIndexOpts, IndexPrefix: "foo:", UseReadWriteAliases: true, SpanWriteAlias: "production-traces-write", ServiceWriteAlias: "production-services-write", }, indices: []string{"production-traces-write", "production-services-write"}, }, } for _, testCase := range testCases { w := NewSpanWriter(testCase.params) spanIndexName, serviceIndexName := w.spanServiceIndex(date) assert.Equal(t, []string{spanIndexName, serviceIndexName}, testCase.indices) } } func TestClientClose(t *testing.T) { withSpanWriter(func(w *spanWriterTest) { w.client.On("Close").Return(nil) w.writer.Close() w.client.AssertNumberOfCalls(t, "Close", 1) }) } // This test behaves as a large test that checks WriteSpan's behavior as a whole. // Extra tests for individual functions are below. func TestSpanWriter_WriteSpan(t *testing.T) { testCases := []struct { caption string serviceIndexExists bool expectedError string expectedLogs []string }{ { caption: "span insertion error", serviceIndexExists: false, expectedError: "", expectedLogs: []string{"Wrote span to ES index"}, }, } for _, tc := range testCases { testCase := tc t.Run(testCase.caption, func(t *testing.T) { withSpanWriter(func(w *spanWriterTest) { date, err := time.Parse(time.RFC3339, "1995-04-21T22:08:41+00:00") require.NoError(t, err) span := &dbmodel.Span{ TraceID: "testing-traceid", SpanID: "testing-spanid", OperationName: "operation", Process: dbmodel.Process{ ServiceName: "service", }, StartTime: model.TimeAsEpochMicroseconds(date), } spanIndexName := "jaeger-span-1995-04-21" serviceIndexName := "jaeger-service-1995-04-21" serviceHash := "de3b5a8f1a79989d" indexService := &mocks.IndexService{} indexServicePut := &mocks.IndexService{} indexSpanPut := &mocks.IndexService{} indexService.On("Index", stringMatcher(spanIndexName)).Return(indexService) indexService.On("Index", stringMatcher(serviceIndexName)).Return(indexService) indexService.On("Type", stringMatcher(serviceType)).Return(indexServicePut) indexService.On("Type", stringMatcher(spanType)).Return(indexSpanPut) indexServicePut.On("Id", stringMatcher(serviceHash)).Return(indexServicePut) indexServicePut.On("BodyJson", mock.AnythingOfType("dbmodel.Service")).Return(indexServicePut) indexServicePut.On("Add") indexSpanPut.On("Id", mock.AnythingOfType("string")).Return(indexSpanPut) indexSpanPut.On("BodyJson", mock.AnythingOfType("**dbmodel.Span")).Return(indexSpanPut) indexSpanPut.On("Add") w.client.On("Index").Return(indexService) w.writer.WriteSpan(date, span) if testCase.expectedError == "" { indexServicePut.AssertNumberOfCalls(t, "Add", 1) indexSpanPut.AssertNumberOfCalls(t, "Add", 1) } else { require.EqualError(t, err, testCase.expectedError) } for _, expectedLog := range testCase.expectedLogs { assert.Contains(t, w.logBuffer.String(), expectedLog, "Log must contain %s, but was %s", expectedLog, w.logBuffer.String()) } if len(testCase.expectedLogs) == 0 { assert.Empty(t, w.logBuffer.String()) } }) }) } } func TestSpanIndexName(t *testing.T) { date, err := time.Parse(time.RFC3339, "1995-04-21T22:08:41+00:00") require.NoError(t, err) span := &model.Span{ StartTime: date, } spanIndexName := indexWithDate(spanIndexBaseName, "2006-01-02", span.StartTime) serviceIndexName := indexWithDate(serviceIndexBaseName, "2006-01-02", span.StartTime) assert.Equal(t, "jaeger-span-1995-04-21", spanIndexName) assert.Equal(t, "jaeger-service-1995-04-21", serviceIndexName) } func TestWriteSpanInternal(t *testing.T) { withSpanWriter(func(w *spanWriterTest) { indexService := &mocks.IndexService{} indexName := "jaeger-1995-04-21" indexService.On("Index", stringMatcher(indexName)).Return(indexService) indexService.On("Type", stringMatcher(spanType)).Return(indexService) indexService.On("BodyJson", mock.AnythingOfType("**dbmodel.Span")).Return(indexService) indexService.On("Add") w.client.On("Index").Return(indexService) jsonSpan := &dbmodel.Span{} w.writer.writeSpanToIndex(indexName, jsonSpan) indexService.AssertNumberOfCalls(t, "Add", 1) assert.Empty(t, w.logBuffer.String()) }) } func TestWriteSpanInternalError(t *testing.T) { withSpanWriter(func(w *spanWriterTest) { indexService := &mocks.IndexService{} indexName := "jaeger-1995-04-21" indexService.On("Index", stringMatcher(indexName)).Return(indexService) indexService.On("Type", stringMatcher(spanType)).Return(indexService) indexService.On("BodyJson", mock.AnythingOfType("**dbmodel.Span")).Return(indexService) indexService.On("Add") w.client.On("Index").Return(indexService) jsonSpan := &dbmodel.Span{ TraceID: dbmodel.TraceID("1"), SpanID: dbmodel.SpanID("0"), } w.writer.writeSpanToIndex(indexName, jsonSpan) indexService.AssertNumberOfCalls(t, "Add", 1) }) } func TestSpanWriterParamsTTL(t *testing.T) { logger, _ := testutils.NewLogger() metricsFactory := metricstest.NewFactory(0) testCases := []struct { serviceTTL time.Duration name string expectedAddCalls int }{ { serviceTTL: 0, name: "uses defaults", expectedAddCalls: 1, }, { serviceTTL: 1 * time.Nanosecond, name: "uses provided values", expectedAddCalls: 3, }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { client := &mocks.Client{} params := SpanWriterParams{ Client: func() es.Client { return client }, Logger: logger, MetricsFactory: metricsFactory, ServiceCacheTTL: test.serviceTTL, } w := NewSpanWriter(params) svc := dbmodel.Service{ ServiceName: "foo", OperationName: "bar", } serviceHash := hashCode(svc) serviceIndexName := "jaeger-service-1995-04-21" indexService := &mocks.IndexService{} indexService.On("Index", stringMatcher(serviceIndexName)).Return(indexService) indexService.On("Type", stringMatcher(serviceType)).Return(indexService) indexService.On("Id", stringMatcher(serviceHash)).Return(indexService) indexService.On("BodyJson", mock.AnythingOfType("dbmodel.Service")).Return(indexService) indexService.On("Add") client.On("Index").Return(indexService) jsonSpan := &dbmodel.Span{ Process: dbmodel.Process{ServiceName: "foo"}, OperationName: "bar", } w.writeService(serviceIndexName, jsonSpan) time.Sleep(1 * time.Nanosecond) w.writeService(serviceIndexName, jsonSpan) time.Sleep(1 * time.Nanosecond) w.writeService(serviceIndexName, jsonSpan) indexService.AssertNumberOfCalls(t, "Add", test.expectedAddCalls) }) } } func TestTagMap(t *testing.T) { tags := []dbmodel.KeyValue{ { Key: "foo", Value: "foo", Type: dbmodel.StringType, }, { Key: "a", Value: true, Type: dbmodel.BoolType, }, { Key: "b.b", Value: int64(1), Type: dbmodel.Int64Type, }, } dbSpan := dbmodel.Span{Tags: tags, Process: dbmodel.Process{Tags: tags}} converter := NewSpanWriter(SpanWriterParams{ Logger: zap.NewNop(), MetricsFactory: metrics.NullFactory, AllTagsAsFields: false, TagKeysAsFields: []string{"a", "b.b", "b*"}, TagDotReplacement: ":", }) converter.convertNestedTagsToFieldTags(&dbSpan) assert.Len(t, dbSpan.Tags, 1) assert.Equal(t, "foo", dbSpan.Tags[0].Key) assert.Len(t, dbSpan.Process.Tags, 1) assert.Equal(t, "foo", dbSpan.Process.Tags[0].Key) tagsMap := map[string]any{} tagsMap["a"] = true tagsMap["b:b"] = int64(1) assert.Equal(t, tagsMap, dbSpan.Tag) assert.Equal(t, tagsMap, dbSpan.Process.Tag) } func TestNewSpanTags(t *testing.T) { testCases := []struct { params SpanWriterParams expected dbmodel.Span name string }{ { params: SpanWriterParams{ AllTagsAsFields: true, TagKeysAsFields: []string{}, TagDotReplacement: "", }, expected: dbmodel.Span{ Tag: map[string]any{"foo": "bar"}, Tags: []dbmodel.KeyValue{}, Process: dbmodel.Process{Tag: map[string]any{"bar": "baz"}, Tags: []dbmodel.KeyValue{}}, }, name: "allTagsAsFields", }, { params: SpanWriterParams{ AllTagsAsFields: false, TagKeysAsFields: []string{"foo", "bar", "rere"}, TagDotReplacement: "", }, expected: dbmodel.Span{ Tag: map[string]any{"foo": "bar"}, Tags: []dbmodel.KeyValue{}, Process: dbmodel.Process{Tag: map[string]any{"bar": "baz"}, Tags: []dbmodel.KeyValue{}}, }, name: "definedTagNames", }, { params: SpanWriterParams{ AllTagsAsFields: false, TagKeysAsFields: []string{}, TagDotReplacement: "", }, expected: dbmodel.Span{ Tags: []dbmodel.KeyValue{{ Key: "foo", Type: dbmodel.StringType, Value: "bar", }}, Process: dbmodel.Process{Tags: []dbmodel.KeyValue{{ Key: "bar", Type: dbmodel.StringType, Value: "baz", }}}, }, name: "noAllTagsAsFields", }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { mSpan := &dbmodel.Span{ Tags: []dbmodel.KeyValue{{Key: "foo", Value: "bar", Type: dbmodel.StringType}}, Process: dbmodel.Process{Tags: []dbmodel.KeyValue{{Key: "bar", Value: "baz", Type: dbmodel.StringType}}}, } params := test.params params.Logger = zap.NewNop() params.MetricsFactory = metrics.NullFactory writer := NewSpanWriter(params) writer.convertNestedTagsToFieldTags(mSpan) assert.Equal(t, test.expected.Tag, mSpan.Tag) assert.Equal(t, test.expected.Tags, mSpan.Tags) assert.Equal(t, test.expected.Process.Tag, mSpan.Process.Tag) assert.Equal(t, test.expected.Process.Tags, mSpan.Process.Tags) }) } } // stringMatcher can match a string argument when it contains a specific substring q func stringMatcher(q string) any { matchFunc := func(s string) bool { return strings.Contains(s, q) } return mock.MatchedBy(matchFunc) } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/writerv1.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "context" "github.com/jaegertracing/jaeger-idl/model/v1" ) type SpanWriterV1 struct { spanWriter CoreSpanWriter } // NewSpanWriterV1 returns the SpanWriterV1 for use func NewSpanWriterV1(p SpanWriterParams) *SpanWriterV1 { return &SpanWriterV1{ spanWriter: NewSpanWriter(p), } } // WriteSpan writes a span and its corresponding service:operation in ElasticSearch func (s *SpanWriterV1) WriteSpan(_ context.Context, span *model.Span) error { converter := NewFromDomain() jsonSpan := converter.FromDomainEmbedProcess(span) s.spanWriter.WriteSpan(span.StartTime, jsonSpan) return nil } // Close closes SpanWriter func (s *SpanWriterV1) Close() error { return s.spanWriter.Close() } ================================================ FILE: internal/storage/v1/elasticsearch/spanstore/writerv1_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package spanstore import ( "context" "testing" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch/spanstore/mocks" ) func TestSpanWriterV1_WriteSpan(t *testing.T) { coreWriter := &mocks.CoreSpanWriter{} s := &model.Span{ Tags: []model.KeyValue{{Key: "foo", VStr: "bar"}}, Process: &model.Process{Tags: []model.KeyValue{{Key: "bar", VStr: "baz"}}}, } writerV1 := &SpanWriterV1{spanWriter: coreWriter} converter := NewFromDomain() coreWriter.On("WriteSpan", s.StartTime, converter.FromDomainEmbedProcess(s)) err := writerV1.WriteSpan(context.Background(), s) require.NoError(t, err) } func TestSpanWriterV1_Close(t *testing.T) { coreWriter := &mocks.CoreSpanWriter{} coreWriter.On("Close").Return(nil) writerV1 := &SpanWriterV1{spanWriter: coreWriter} err := writerV1.Close() require.NoError(t, err) } ================================================ FILE: internal/storage/v1/factory.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package storage import ( "context" "github.com/jaegertracing/jaeger/internal/distributedlock" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore" "github.com/jaegertracing/jaeger/internal/telemetry" ) // Purger defines an interface that is capable of purging the storage. // Only meant to be used from integration tests. type Purger interface { // Purge removes all data from the storage. Purge(context.Context) error } // SamplingStoreFactory defines an interface that is capable of returning the necessary backends for // adaptive sampling. type SamplingStoreFactory interface { // CreateLock creates a distributed lock. CreateLock() (distributedlock.Lock, error) // CreateSamplingStore creates a sampling store. CreateSamplingStore(maxBuckets int) (samplingstore.Store, error) } // MetricStoreFactory defines an interface for a factory that can create implementations of different metrics storage components. type MetricStoreFactory interface { CreateMetricsReader() (metricstore.Reader, error) } // V1MetricStoreFactory is a v1 version of MetricStoreFactory. // Implementations are encouraged to implement storage.Configurable interface. // // # See also // // storage.Configurable type V1MetricStoreFactory interface { MetricStoreFactory // Initialize performs internal initialization of the factory, such as opening connections to the backend store. // It is called after all configuration of the factory itself has been done. Initialize(telset telemetry.Settings) error } // ArchiveCapable is an interface that can be implemented by some storage implementations // to indicate that they are capable of archiving data. type ArchiveCapable interface { IsArchiveCapable() bool } ================================================ FILE: internal/storage/v1/mocks/mocks.go ================================================ // Copyright (c) The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // // Run 'make generate-mocks' to regenerate. // Code generated by mockery; DO NOT EDIT. // github.com/vektra/mockery // template: testify package mocks import ( "context" "flag" "github.com/jaegertracing/jaeger/internal/distributedlock" "github.com/jaegertracing/jaeger/internal/storage/v1/api/metricstore" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore" "github.com/jaegertracing/jaeger/internal/telemetry" "github.com/spf13/viper" mock "github.com/stretchr/testify/mock" "go.uber.org/zap" ) // NewConfigurable creates a new instance of Configurable. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewConfigurable(t interface { mock.TestingT Cleanup(func()) }) *Configurable { mock := &Configurable{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // Configurable is an autogenerated mock type for the Configurable type type Configurable struct { mock.Mock } type Configurable_Expecter struct { mock *mock.Mock } func (_m *Configurable) EXPECT() *Configurable_Expecter { return &Configurable_Expecter{mock: &_m.Mock} } // AddFlags provides a mock function for the type Configurable func (_mock *Configurable) AddFlags(flagSet *flag.FlagSet) { _mock.Called(flagSet) return } // Configurable_AddFlags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFlags' type Configurable_AddFlags_Call struct { *mock.Call } // AddFlags is a helper method to define mock.On call // - flagSet *flag.FlagSet func (_e *Configurable_Expecter) AddFlags(flagSet interface{}) *Configurable_AddFlags_Call { return &Configurable_AddFlags_Call{Call: _e.mock.On("AddFlags", flagSet)} } func (_c *Configurable_AddFlags_Call) Run(run func(flagSet *flag.FlagSet)) *Configurable_AddFlags_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 *flag.FlagSet if args[0] != nil { arg0 = args[0].(*flag.FlagSet) } run( arg0, ) }) return _c } func (_c *Configurable_AddFlags_Call) Return() *Configurable_AddFlags_Call { _c.Call.Return() return _c } func (_c *Configurable_AddFlags_Call) RunAndReturn(run func(flagSet *flag.FlagSet)) *Configurable_AddFlags_Call { _c.Run(run) return _c } // InitFromViper provides a mock function for the type Configurable func (_mock *Configurable) InitFromViper(v *viper.Viper, logger *zap.Logger) { _mock.Called(v, logger) return } // Configurable_InitFromViper_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InitFromViper' type Configurable_InitFromViper_Call struct { *mock.Call } // InitFromViper is a helper method to define mock.On call // - v *viper.Viper // - logger *zap.Logger func (_e *Configurable_Expecter) InitFromViper(v interface{}, logger interface{}) *Configurable_InitFromViper_Call { return &Configurable_InitFromViper_Call{Call: _e.mock.On("InitFromViper", v, logger)} } func (_c *Configurable_InitFromViper_Call) Run(run func(v *viper.Viper, logger *zap.Logger)) *Configurable_InitFromViper_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 *viper.Viper if args[0] != nil { arg0 = args[0].(*viper.Viper) } var arg1 *zap.Logger if args[1] != nil { arg1 = args[1].(*zap.Logger) } run( arg0, arg1, ) }) return _c } func (_c *Configurable_InitFromViper_Call) Return() *Configurable_InitFromViper_Call { _c.Call.Return() return _c } func (_c *Configurable_InitFromViper_Call) RunAndReturn(run func(v *viper.Viper, logger *zap.Logger)) *Configurable_InitFromViper_Call { _c.Run(run) return _c } // NewPurger creates a new instance of Purger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewPurger(t interface { mock.TestingT Cleanup(func()) }) *Purger { mock := &Purger{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // Purger is an autogenerated mock type for the Purger type type Purger struct { mock.Mock } type Purger_Expecter struct { mock *mock.Mock } func (_m *Purger) EXPECT() *Purger_Expecter { return &Purger_Expecter{mock: &_m.Mock} } // Purge provides a mock function for the type Purger func (_mock *Purger) Purge(context1 context.Context) error { ret := _mock.Called(context1) if len(ret) == 0 { panic("no return value specified for Purge") } var r0 error if returnFunc, ok := ret.Get(0).(func(context.Context) error); ok { r0 = returnFunc(context1) } else { r0 = ret.Error(0) } return r0 } // Purger_Purge_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Purge' type Purger_Purge_Call struct { *mock.Call } // Purge is a helper method to define mock.On call // - context1 context.Context func (_e *Purger_Expecter) Purge(context1 interface{}) *Purger_Purge_Call { return &Purger_Purge_Call{Call: _e.mock.On("Purge", context1)} } func (_c *Purger_Purge_Call) Run(run func(context1 context.Context)) *Purger_Purge_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } run( arg0, ) }) return _c } func (_c *Purger_Purge_Call) Return(err error) *Purger_Purge_Call { _c.Call.Return(err) return _c } func (_c *Purger_Purge_Call) RunAndReturn(run func(context1 context.Context) error) *Purger_Purge_Call { _c.Call.Return(run) return _c } // NewSamplingStoreFactory creates a new instance of SamplingStoreFactory. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewSamplingStoreFactory(t interface { mock.TestingT Cleanup(func()) }) *SamplingStoreFactory { mock := &SamplingStoreFactory{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // SamplingStoreFactory is an autogenerated mock type for the SamplingStoreFactory type type SamplingStoreFactory struct { mock.Mock } type SamplingStoreFactory_Expecter struct { mock *mock.Mock } func (_m *SamplingStoreFactory) EXPECT() *SamplingStoreFactory_Expecter { return &SamplingStoreFactory_Expecter{mock: &_m.Mock} } // CreateLock provides a mock function for the type SamplingStoreFactory func (_mock *SamplingStoreFactory) CreateLock() (distributedlock.Lock, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for CreateLock") } var r0 distributedlock.Lock var r1 error if returnFunc, ok := ret.Get(0).(func() (distributedlock.Lock, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() distributedlock.Lock); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(distributedlock.Lock) } } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // SamplingStoreFactory_CreateLock_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateLock' type SamplingStoreFactory_CreateLock_Call struct { *mock.Call } // CreateLock is a helper method to define mock.On call func (_e *SamplingStoreFactory_Expecter) CreateLock() *SamplingStoreFactory_CreateLock_Call { return &SamplingStoreFactory_CreateLock_Call{Call: _e.mock.On("CreateLock")} } func (_c *SamplingStoreFactory_CreateLock_Call) Run(run func()) *SamplingStoreFactory_CreateLock_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *SamplingStoreFactory_CreateLock_Call) Return(lock distributedlock.Lock, err error) *SamplingStoreFactory_CreateLock_Call { _c.Call.Return(lock, err) return _c } func (_c *SamplingStoreFactory_CreateLock_Call) RunAndReturn(run func() (distributedlock.Lock, error)) *SamplingStoreFactory_CreateLock_Call { _c.Call.Return(run) return _c } // CreateSamplingStore provides a mock function for the type SamplingStoreFactory func (_mock *SamplingStoreFactory) CreateSamplingStore(maxBuckets int) (samplingstore.Store, error) { ret := _mock.Called(maxBuckets) if len(ret) == 0 { panic("no return value specified for CreateSamplingStore") } var r0 samplingstore.Store var r1 error if returnFunc, ok := ret.Get(0).(func(int) (samplingstore.Store, error)); ok { return returnFunc(maxBuckets) } if returnFunc, ok := ret.Get(0).(func(int) samplingstore.Store); ok { r0 = returnFunc(maxBuckets) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(samplingstore.Store) } } if returnFunc, ok := ret.Get(1).(func(int) error); ok { r1 = returnFunc(maxBuckets) } else { r1 = ret.Error(1) } return r0, r1 } // SamplingStoreFactory_CreateSamplingStore_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateSamplingStore' type SamplingStoreFactory_CreateSamplingStore_Call struct { *mock.Call } // CreateSamplingStore is a helper method to define mock.On call // - maxBuckets int func (_e *SamplingStoreFactory_Expecter) CreateSamplingStore(maxBuckets interface{}) *SamplingStoreFactory_CreateSamplingStore_Call { return &SamplingStoreFactory_CreateSamplingStore_Call{Call: _e.mock.On("CreateSamplingStore", maxBuckets)} } func (_c *SamplingStoreFactory_CreateSamplingStore_Call) Run(run func(maxBuckets int)) *SamplingStoreFactory_CreateSamplingStore_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 int if args[0] != nil { arg0 = args[0].(int) } run( arg0, ) }) return _c } func (_c *SamplingStoreFactory_CreateSamplingStore_Call) Return(store samplingstore.Store, err error) *SamplingStoreFactory_CreateSamplingStore_Call { _c.Call.Return(store, err) return _c } func (_c *SamplingStoreFactory_CreateSamplingStore_Call) RunAndReturn(run func(maxBuckets int) (samplingstore.Store, error)) *SamplingStoreFactory_CreateSamplingStore_Call { _c.Call.Return(run) return _c } // NewMetricStoreFactory creates a new instance of MetricStoreFactory. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMetricStoreFactory(t interface { mock.TestingT Cleanup(func()) }) *MetricStoreFactory { mock := &MetricStoreFactory{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // MetricStoreFactory is an autogenerated mock type for the MetricStoreFactory type type MetricStoreFactory struct { mock.Mock } type MetricStoreFactory_Expecter struct { mock *mock.Mock } func (_m *MetricStoreFactory) EXPECT() *MetricStoreFactory_Expecter { return &MetricStoreFactory_Expecter{mock: &_m.Mock} } // CreateMetricsReader provides a mock function for the type MetricStoreFactory func (_mock *MetricStoreFactory) CreateMetricsReader() (metricstore.Reader, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for CreateMetricsReader") } var r0 metricstore.Reader var r1 error if returnFunc, ok := ret.Get(0).(func() (metricstore.Reader, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() metricstore.Reader); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(metricstore.Reader) } } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // MetricStoreFactory_CreateMetricsReader_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateMetricsReader' type MetricStoreFactory_CreateMetricsReader_Call struct { *mock.Call } // CreateMetricsReader is a helper method to define mock.On call func (_e *MetricStoreFactory_Expecter) CreateMetricsReader() *MetricStoreFactory_CreateMetricsReader_Call { return &MetricStoreFactory_CreateMetricsReader_Call{Call: _e.mock.On("CreateMetricsReader")} } func (_c *MetricStoreFactory_CreateMetricsReader_Call) Run(run func()) *MetricStoreFactory_CreateMetricsReader_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MetricStoreFactory_CreateMetricsReader_Call) Return(reader metricstore.Reader, err error) *MetricStoreFactory_CreateMetricsReader_Call { _c.Call.Return(reader, err) return _c } func (_c *MetricStoreFactory_CreateMetricsReader_Call) RunAndReturn(run func() (metricstore.Reader, error)) *MetricStoreFactory_CreateMetricsReader_Call { _c.Call.Return(run) return _c } // NewV1MetricStoreFactory creates a new instance of V1MetricStoreFactory. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewV1MetricStoreFactory(t interface { mock.TestingT Cleanup(func()) }) *V1MetricStoreFactory { mock := &V1MetricStoreFactory{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // V1MetricStoreFactory is an autogenerated mock type for the V1MetricStoreFactory type type V1MetricStoreFactory struct { mock.Mock } type V1MetricStoreFactory_Expecter struct { mock *mock.Mock } func (_m *V1MetricStoreFactory) EXPECT() *V1MetricStoreFactory_Expecter { return &V1MetricStoreFactory_Expecter{mock: &_m.Mock} } // CreateMetricsReader provides a mock function for the type V1MetricStoreFactory func (_mock *V1MetricStoreFactory) CreateMetricsReader() (metricstore.Reader, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for CreateMetricsReader") } var r0 metricstore.Reader var r1 error if returnFunc, ok := ret.Get(0).(func() (metricstore.Reader, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() metricstore.Reader); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(metricstore.Reader) } } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // V1MetricStoreFactory_CreateMetricsReader_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateMetricsReader' type V1MetricStoreFactory_CreateMetricsReader_Call struct { *mock.Call } // CreateMetricsReader is a helper method to define mock.On call func (_e *V1MetricStoreFactory_Expecter) CreateMetricsReader() *V1MetricStoreFactory_CreateMetricsReader_Call { return &V1MetricStoreFactory_CreateMetricsReader_Call{Call: _e.mock.On("CreateMetricsReader")} } func (_c *V1MetricStoreFactory_CreateMetricsReader_Call) Run(run func()) *V1MetricStoreFactory_CreateMetricsReader_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *V1MetricStoreFactory_CreateMetricsReader_Call) Return(reader metricstore.Reader, err error) *V1MetricStoreFactory_CreateMetricsReader_Call { _c.Call.Return(reader, err) return _c } func (_c *V1MetricStoreFactory_CreateMetricsReader_Call) RunAndReturn(run func() (metricstore.Reader, error)) *V1MetricStoreFactory_CreateMetricsReader_Call { _c.Call.Return(run) return _c } // Initialize provides a mock function for the type V1MetricStoreFactory func (_mock *V1MetricStoreFactory) Initialize(telset telemetry.Settings) error { ret := _mock.Called(telset) if len(ret) == 0 { panic("no return value specified for Initialize") } var r0 error if returnFunc, ok := ret.Get(0).(func(telemetry.Settings) error); ok { r0 = returnFunc(telset) } else { r0 = ret.Error(0) } return r0 } // V1MetricStoreFactory_Initialize_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Initialize' type V1MetricStoreFactory_Initialize_Call struct { *mock.Call } // Initialize is a helper method to define mock.On call // - telset telemetry.Settings func (_e *V1MetricStoreFactory_Expecter) Initialize(telset interface{}) *V1MetricStoreFactory_Initialize_Call { return &V1MetricStoreFactory_Initialize_Call{Call: _e.mock.On("Initialize", telset)} } func (_c *V1MetricStoreFactory_Initialize_Call) Run(run func(telset telemetry.Settings)) *V1MetricStoreFactory_Initialize_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 telemetry.Settings if args[0] != nil { arg0 = args[0].(telemetry.Settings) } run( arg0, ) }) return _c } func (_c *V1MetricStoreFactory_Initialize_Call) Return(err error) *V1MetricStoreFactory_Initialize_Call { _c.Call.Return(err) return _c } func (_c *V1MetricStoreFactory_Initialize_Call) RunAndReturn(run func(telset telemetry.Settings) error) *V1MetricStoreFactory_Initialize_Call { _c.Call.Return(run) return _c } // NewArchiveCapable creates a new instance of ArchiveCapable. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewArchiveCapable(t interface { mock.TestingT Cleanup(func()) }) *ArchiveCapable { mock := &ArchiveCapable{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // ArchiveCapable is an autogenerated mock type for the ArchiveCapable type type ArchiveCapable struct { mock.Mock } type ArchiveCapable_Expecter struct { mock *mock.Mock } func (_m *ArchiveCapable) EXPECT() *ArchiveCapable_Expecter { return &ArchiveCapable_Expecter{mock: &_m.Mock} } // IsArchiveCapable provides a mock function for the type ArchiveCapable func (_mock *ArchiveCapable) IsArchiveCapable() bool { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for IsArchiveCapable") } var r0 bool if returnFunc, ok := ret.Get(0).(func() bool); ok { r0 = returnFunc() } else { r0 = ret.Get(0).(bool) } return r0 } // ArchiveCapable_IsArchiveCapable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsArchiveCapable' type ArchiveCapable_IsArchiveCapable_Call struct { *mock.Call } // IsArchiveCapable is a helper method to define mock.On call func (_e *ArchiveCapable_Expecter) IsArchiveCapable() *ArchiveCapable_IsArchiveCapable_Call { return &ArchiveCapable_IsArchiveCapable_Call{Call: _e.mock.On("IsArchiveCapable")} } func (_c *ArchiveCapable_IsArchiveCapable_Call) Run(run func()) *ArchiveCapable_IsArchiveCapable_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *ArchiveCapable_IsArchiveCapable_Call) Return(b bool) *ArchiveCapable_IsArchiveCapable_Call { _c.Call.Return(b) return _c } func (_c *ArchiveCapable_IsArchiveCapable_Call) RunAndReturn(run func() bool) *ArchiveCapable_IsArchiveCapable_Call { _c.Call.Return(run) return _c } ================================================ FILE: internal/storage/v1/package_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package storage import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v2/api/depstore/factory.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package depstore type Factory interface { CreateDependencyReader() (Reader, error) } ================================================ FILE: internal/storage/v2/api/depstore/mocks/mocks.go ================================================ // Copyright (c) The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // // Run 'make generate-mocks' to regenerate. // Code generated by mockery; DO NOT EDIT. // github.com/vektra/mockery // template: testify package mocks import ( "context" "time" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" mock "github.com/stretchr/testify/mock" ) // NewFactory creates a new instance of Factory. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewFactory(t interface { mock.TestingT Cleanup(func()) }) *Factory { mock := &Factory{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // Factory is an autogenerated mock type for the Factory type type Factory struct { mock.Mock } type Factory_Expecter struct { mock *mock.Mock } func (_m *Factory) EXPECT() *Factory_Expecter { return &Factory_Expecter{mock: &_m.Mock} } // CreateDependencyReader provides a mock function for the type Factory func (_mock *Factory) CreateDependencyReader() (depstore.Reader, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for CreateDependencyReader") } var r0 depstore.Reader var r1 error if returnFunc, ok := ret.Get(0).(func() (depstore.Reader, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() depstore.Reader); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(depstore.Reader) } } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // Factory_CreateDependencyReader_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateDependencyReader' type Factory_CreateDependencyReader_Call struct { *mock.Call } // CreateDependencyReader is a helper method to define mock.On call func (_e *Factory_Expecter) CreateDependencyReader() *Factory_CreateDependencyReader_Call { return &Factory_CreateDependencyReader_Call{Call: _e.mock.On("CreateDependencyReader")} } func (_c *Factory_CreateDependencyReader_Call) Run(run func()) *Factory_CreateDependencyReader_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *Factory_CreateDependencyReader_Call) Return(reader depstore.Reader, err error) *Factory_CreateDependencyReader_Call { _c.Call.Return(reader, err) return _c } func (_c *Factory_CreateDependencyReader_Call) RunAndReturn(run func() (depstore.Reader, error)) *Factory_CreateDependencyReader_Call { _c.Call.Return(run) return _c } // NewReader creates a new instance of Reader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewReader(t interface { mock.TestingT Cleanup(func()) }) *Reader { mock := &Reader{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // Reader is an autogenerated mock type for the Reader type type Reader struct { mock.Mock } type Reader_Expecter struct { mock *mock.Mock } func (_m *Reader) EXPECT() *Reader_Expecter { return &Reader_Expecter{mock: &_m.Mock} } // GetDependencies provides a mock function for the type Reader func (_mock *Reader) GetDependencies(ctx context.Context, query depstore.QueryParameters) ([]model.DependencyLink, error) { ret := _mock.Called(ctx, query) if len(ret) == 0 { panic("no return value specified for GetDependencies") } var r0 []model.DependencyLink var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, depstore.QueryParameters) ([]model.DependencyLink, error)); ok { return returnFunc(ctx, query) } if returnFunc, ok := ret.Get(0).(func(context.Context, depstore.QueryParameters) []model.DependencyLink); ok { r0 = returnFunc(ctx, query) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]model.DependencyLink) } } if returnFunc, ok := ret.Get(1).(func(context.Context, depstore.QueryParameters) error); ok { r1 = returnFunc(ctx, query) } else { r1 = ret.Error(1) } return r0, r1 } // Reader_GetDependencies_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDependencies' type Reader_GetDependencies_Call struct { *mock.Call } // GetDependencies is a helper method to define mock.On call // - ctx context.Context // - query depstore.QueryParameters func (_e *Reader_Expecter) GetDependencies(ctx interface{}, query interface{}) *Reader_GetDependencies_Call { return &Reader_GetDependencies_Call{Call: _e.mock.On("GetDependencies", ctx, query)} } func (_c *Reader_GetDependencies_Call) Run(run func(ctx context.Context, query depstore.QueryParameters)) *Reader_GetDependencies_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 depstore.QueryParameters if args[1] != nil { arg1 = args[1].(depstore.QueryParameters) } run( arg0, arg1, ) }) return _c } func (_c *Reader_GetDependencies_Call) Return(dependencyLinks []model.DependencyLink, err error) *Reader_GetDependencies_Call { _c.Call.Return(dependencyLinks, err) return _c } func (_c *Reader_GetDependencies_Call) RunAndReturn(run func(ctx context.Context, query depstore.QueryParameters) ([]model.DependencyLink, error)) *Reader_GetDependencies_Call { _c.Call.Return(run) return _c } // NewWriter creates a new instance of Writer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewWriter(t interface { mock.TestingT Cleanup(func()) }) *Writer { mock := &Writer{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // Writer is an autogenerated mock type for the Writer type type Writer struct { mock.Mock } type Writer_Expecter struct { mock *mock.Mock } func (_m *Writer) EXPECT() *Writer_Expecter { return &Writer_Expecter{mock: &_m.Mock} } // WriteDependencies provides a mock function for the type Writer func (_mock *Writer) WriteDependencies(ts time.Time, dependencies []model.DependencyLink) error { ret := _mock.Called(ts, dependencies) if len(ret) == 0 { panic("no return value specified for WriteDependencies") } var r0 error if returnFunc, ok := ret.Get(0).(func(time.Time, []model.DependencyLink) error); ok { r0 = returnFunc(ts, dependencies) } else { r0 = ret.Error(0) } return r0 } // Writer_WriteDependencies_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteDependencies' type Writer_WriteDependencies_Call struct { *mock.Call } // WriteDependencies is a helper method to define mock.On call // - ts time.Time // - dependencies []model.DependencyLink func (_e *Writer_Expecter) WriteDependencies(ts interface{}, dependencies interface{}) *Writer_WriteDependencies_Call { return &Writer_WriteDependencies_Call{Call: _e.mock.On("WriteDependencies", ts, dependencies)} } func (_c *Writer_WriteDependencies_Call) Run(run func(ts time.Time, dependencies []model.DependencyLink)) *Writer_WriteDependencies_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 time.Time if args[0] != nil { arg0 = args[0].(time.Time) } var arg1 []model.DependencyLink if args[1] != nil { arg1 = args[1].([]model.DependencyLink) } run( arg0, arg1, ) }) return _c } func (_c *Writer_WriteDependencies_Call) Return(err error) *Writer_WriteDependencies_Call { _c.Call.Return(err) return _c } func (_c *Writer_WriteDependencies_Call) RunAndReturn(run func(ts time.Time, dependencies []model.DependencyLink) error) *Writer_WriteDependencies_Call { _c.Call.Return(run) return _c } ================================================ FILE: internal/storage/v2/api/depstore/package_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package depstore import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v2/api/depstore/reader.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package depstore import ( "context" "time" "github.com/jaegertracing/jaeger-idl/model/v1" ) // QueryParameters contains the parameters that can be used to query dependencies. type QueryParameters struct { StartTime time.Time EndTime time.Time } // Reader can load service dependencies from storage. type Reader interface { GetDependencies(ctx context.Context, query QueryParameters) ([]model.DependencyLink, error) } ================================================ FILE: internal/storage/v2/api/depstore/writer.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package depstore import ( "time" "github.com/jaegertracing/jaeger-idl/model/v1" ) // Writer write the dependencies into the storage type Writer interface { WriteDependencies(ts time.Time, dependencies []model.DependencyLink) error } ================================================ FILE: internal/storage/v2/api/package_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package storage_v2 import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v2/api/tracestore/empty_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v2/api/tracestore/factory.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore // Factory defines an interface for a factory that can create implementations of // different span storage components. type Factory interface { // CreateTraceReader creates a spanstore.Reader. CreateTraceReader() (Reader, error) // CreateTraceWriter creates a spanstore.Writer. CreateTraceWriter() (Writer, error) } ================================================ FILE: internal/storage/v2/api/tracestore/mocks/mocks.go ================================================ // Copyright (c) The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // // Run 'make generate-mocks' to regenerate. // Code generated by mockery; DO NOT EDIT. // github.com/vektra/mockery // template: testify package mocks import ( "context" "iter" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" mock "github.com/stretchr/testify/mock" "go.opentelemetry.io/collector/pdata/ptrace" ) // NewFactory creates a new instance of Factory. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewFactory(t interface { mock.TestingT Cleanup(func()) }) *Factory { mock := &Factory{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // Factory is an autogenerated mock type for the Factory type type Factory struct { mock.Mock } type Factory_Expecter struct { mock *mock.Mock } func (_m *Factory) EXPECT() *Factory_Expecter { return &Factory_Expecter{mock: &_m.Mock} } // CreateTraceReader provides a mock function for the type Factory func (_mock *Factory) CreateTraceReader() (tracestore.Reader, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for CreateTraceReader") } var r0 tracestore.Reader var r1 error if returnFunc, ok := ret.Get(0).(func() (tracestore.Reader, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() tracestore.Reader); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(tracestore.Reader) } } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // Factory_CreateTraceReader_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateTraceReader' type Factory_CreateTraceReader_Call struct { *mock.Call } // CreateTraceReader is a helper method to define mock.On call func (_e *Factory_Expecter) CreateTraceReader() *Factory_CreateTraceReader_Call { return &Factory_CreateTraceReader_Call{Call: _e.mock.On("CreateTraceReader")} } func (_c *Factory_CreateTraceReader_Call) Run(run func()) *Factory_CreateTraceReader_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *Factory_CreateTraceReader_Call) Return(reader tracestore.Reader, err error) *Factory_CreateTraceReader_Call { _c.Call.Return(reader, err) return _c } func (_c *Factory_CreateTraceReader_Call) RunAndReturn(run func() (tracestore.Reader, error)) *Factory_CreateTraceReader_Call { _c.Call.Return(run) return _c } // CreateTraceWriter provides a mock function for the type Factory func (_mock *Factory) CreateTraceWriter() (tracestore.Writer, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for CreateTraceWriter") } var r0 tracestore.Writer var r1 error if returnFunc, ok := ret.Get(0).(func() (tracestore.Writer, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() tracestore.Writer); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(tracestore.Writer) } } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // Factory_CreateTraceWriter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateTraceWriter' type Factory_CreateTraceWriter_Call struct { *mock.Call } // CreateTraceWriter is a helper method to define mock.On call func (_e *Factory_Expecter) CreateTraceWriter() *Factory_CreateTraceWriter_Call { return &Factory_CreateTraceWriter_Call{Call: _e.mock.On("CreateTraceWriter")} } func (_c *Factory_CreateTraceWriter_Call) Run(run func()) *Factory_CreateTraceWriter_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *Factory_CreateTraceWriter_Call) Return(writer tracestore.Writer, err error) *Factory_CreateTraceWriter_Call { _c.Call.Return(writer, err) return _c } func (_c *Factory_CreateTraceWriter_Call) RunAndReturn(run func() (tracestore.Writer, error)) *Factory_CreateTraceWriter_Call { _c.Call.Return(run) return _c } // NewReader creates a new instance of Reader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewReader(t interface { mock.TestingT Cleanup(func()) }) *Reader { mock := &Reader{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // Reader is an autogenerated mock type for the Reader type type Reader struct { mock.Mock } type Reader_Expecter struct { mock *mock.Mock } func (_m *Reader) EXPECT() *Reader_Expecter { return &Reader_Expecter{mock: &_m.Mock} } // FindTraceIDs provides a mock function for the type Reader func (_mock *Reader) FindTraceIDs(ctx context.Context, query tracestore.TraceQueryParams) iter.Seq2[[]tracestore.FoundTraceID, error] { ret := _mock.Called(ctx, query) if len(ret) == 0 { panic("no return value specified for FindTraceIDs") } var r0 iter.Seq2[[]tracestore.FoundTraceID, error] if returnFunc, ok := ret.Get(0).(func(context.Context, tracestore.TraceQueryParams) iter.Seq2[[]tracestore.FoundTraceID, error]); ok { r0 = returnFunc(ctx, query) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(iter.Seq2[[]tracestore.FoundTraceID, error]) } } return r0 } // Reader_FindTraceIDs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindTraceIDs' type Reader_FindTraceIDs_Call struct { *mock.Call } // FindTraceIDs is a helper method to define mock.On call // - ctx context.Context // - query tracestore.TraceQueryParams func (_e *Reader_Expecter) FindTraceIDs(ctx interface{}, query interface{}) *Reader_FindTraceIDs_Call { return &Reader_FindTraceIDs_Call{Call: _e.mock.On("FindTraceIDs", ctx, query)} } func (_c *Reader_FindTraceIDs_Call) Run(run func(ctx context.Context, query tracestore.TraceQueryParams)) *Reader_FindTraceIDs_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 tracestore.TraceQueryParams if args[1] != nil { arg1 = args[1].(tracestore.TraceQueryParams) } run( arg0, arg1, ) }) return _c } func (_c *Reader_FindTraceIDs_Call) Return(seq2 iter.Seq2[[]tracestore.FoundTraceID, error]) *Reader_FindTraceIDs_Call { _c.Call.Return(seq2) return _c } func (_c *Reader_FindTraceIDs_Call) RunAndReturn(run func(ctx context.Context, query tracestore.TraceQueryParams) iter.Seq2[[]tracestore.FoundTraceID, error]) *Reader_FindTraceIDs_Call { _c.Call.Return(run) return _c } // FindTraces provides a mock function for the type Reader func (_mock *Reader) FindTraces(ctx context.Context, query tracestore.TraceQueryParams) iter.Seq2[[]ptrace.Traces, error] { ret := _mock.Called(ctx, query) if len(ret) == 0 { panic("no return value specified for FindTraces") } var r0 iter.Seq2[[]ptrace.Traces, error] if returnFunc, ok := ret.Get(0).(func(context.Context, tracestore.TraceQueryParams) iter.Seq2[[]ptrace.Traces, error]); ok { r0 = returnFunc(ctx, query) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(iter.Seq2[[]ptrace.Traces, error]) } } return r0 } // Reader_FindTraces_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindTraces' type Reader_FindTraces_Call struct { *mock.Call } // FindTraces is a helper method to define mock.On call // - ctx context.Context // - query tracestore.TraceQueryParams func (_e *Reader_Expecter) FindTraces(ctx interface{}, query interface{}) *Reader_FindTraces_Call { return &Reader_FindTraces_Call{Call: _e.mock.On("FindTraces", ctx, query)} } func (_c *Reader_FindTraces_Call) Run(run func(ctx context.Context, query tracestore.TraceQueryParams)) *Reader_FindTraces_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 tracestore.TraceQueryParams if args[1] != nil { arg1 = args[1].(tracestore.TraceQueryParams) } run( arg0, arg1, ) }) return _c } func (_c *Reader_FindTraces_Call) Return(seq2 iter.Seq2[[]ptrace.Traces, error]) *Reader_FindTraces_Call { _c.Call.Return(seq2) return _c } func (_c *Reader_FindTraces_Call) RunAndReturn(run func(ctx context.Context, query tracestore.TraceQueryParams) iter.Seq2[[]ptrace.Traces, error]) *Reader_FindTraces_Call { _c.Call.Return(run) return _c } // GetOperations provides a mock function for the type Reader func (_mock *Reader) GetOperations(ctx context.Context, query tracestore.OperationQueryParams) ([]tracestore.Operation, error) { ret := _mock.Called(ctx, query) if len(ret) == 0 { panic("no return value specified for GetOperations") } var r0 []tracestore.Operation var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, tracestore.OperationQueryParams) ([]tracestore.Operation, error)); ok { return returnFunc(ctx, query) } if returnFunc, ok := ret.Get(0).(func(context.Context, tracestore.OperationQueryParams) []tracestore.Operation); ok { r0 = returnFunc(ctx, query) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]tracestore.Operation) } } if returnFunc, ok := ret.Get(1).(func(context.Context, tracestore.OperationQueryParams) error); ok { r1 = returnFunc(ctx, query) } else { r1 = ret.Error(1) } return r0, r1 } // Reader_GetOperations_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetOperations' type Reader_GetOperations_Call struct { *mock.Call } // GetOperations is a helper method to define mock.On call // - ctx context.Context // - query tracestore.OperationQueryParams func (_e *Reader_Expecter) GetOperations(ctx interface{}, query interface{}) *Reader_GetOperations_Call { return &Reader_GetOperations_Call{Call: _e.mock.On("GetOperations", ctx, query)} } func (_c *Reader_GetOperations_Call) Run(run func(ctx context.Context, query tracestore.OperationQueryParams)) *Reader_GetOperations_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 tracestore.OperationQueryParams if args[1] != nil { arg1 = args[1].(tracestore.OperationQueryParams) } run( arg0, arg1, ) }) return _c } func (_c *Reader_GetOperations_Call) Return(operations []tracestore.Operation, err error) *Reader_GetOperations_Call { _c.Call.Return(operations, err) return _c } func (_c *Reader_GetOperations_Call) RunAndReturn(run func(ctx context.Context, query tracestore.OperationQueryParams) ([]tracestore.Operation, error)) *Reader_GetOperations_Call { _c.Call.Return(run) return _c } // GetServices provides a mock function for the type Reader func (_mock *Reader) GetServices(ctx context.Context) ([]string, error) { ret := _mock.Called(ctx) if len(ret) == 0 { panic("no return value specified for GetServices") } var r0 []string var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context) ([]string, error)); ok { return returnFunc(ctx) } if returnFunc, ok := ret.Get(0).(func(context.Context) []string); ok { r0 = returnFunc(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]string) } } if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { r1 = returnFunc(ctx) } else { r1 = ret.Error(1) } return r0, r1 } // Reader_GetServices_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetServices' type Reader_GetServices_Call struct { *mock.Call } // GetServices is a helper method to define mock.On call // - ctx context.Context func (_e *Reader_Expecter) GetServices(ctx interface{}) *Reader_GetServices_Call { return &Reader_GetServices_Call{Call: _e.mock.On("GetServices", ctx)} } func (_c *Reader_GetServices_Call) Run(run func(ctx context.Context)) *Reader_GetServices_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } run( arg0, ) }) return _c } func (_c *Reader_GetServices_Call) Return(strings []string, err error) *Reader_GetServices_Call { _c.Call.Return(strings, err) return _c } func (_c *Reader_GetServices_Call) RunAndReturn(run func(ctx context.Context) ([]string, error)) *Reader_GetServices_Call { _c.Call.Return(run) return _c } // GetTraces provides a mock function for the type Reader func (_mock *Reader) GetTraces(ctx context.Context, traceIDs ...tracestore.GetTraceParams) iter.Seq2[[]ptrace.Traces, error] { var tmpRet mock.Arguments if len(traceIDs) > 0 { tmpRet = _mock.Called(ctx, traceIDs) } else { tmpRet = _mock.Called(ctx) } ret := tmpRet if len(ret) == 0 { panic("no return value specified for GetTraces") } var r0 iter.Seq2[[]ptrace.Traces, error] if returnFunc, ok := ret.Get(0).(func(context.Context, ...tracestore.GetTraceParams) iter.Seq2[[]ptrace.Traces, error]); ok { r0 = returnFunc(ctx, traceIDs...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(iter.Seq2[[]ptrace.Traces, error]) } } return r0 } // Reader_GetTraces_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTraces' type Reader_GetTraces_Call struct { *mock.Call } // GetTraces is a helper method to define mock.On call // - ctx context.Context // - traceIDs ...tracestore.GetTraceParams func (_e *Reader_Expecter) GetTraces(ctx interface{}, traceIDs ...interface{}) *Reader_GetTraces_Call { return &Reader_GetTraces_Call{Call: _e.mock.On("GetTraces", append([]interface{}{ctx}, traceIDs...)...)} } func (_c *Reader_GetTraces_Call) Run(run func(ctx context.Context, traceIDs ...tracestore.GetTraceParams)) *Reader_GetTraces_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 []tracestore.GetTraceParams var variadicArgs []tracestore.GetTraceParams if len(args) > 1 { variadicArgs = args[1].([]tracestore.GetTraceParams) } arg1 = variadicArgs run( arg0, arg1..., ) }) return _c } func (_c *Reader_GetTraces_Call) Return(seq2 iter.Seq2[[]ptrace.Traces, error]) *Reader_GetTraces_Call { _c.Call.Return(seq2) return _c } func (_c *Reader_GetTraces_Call) RunAndReturn(run func(ctx context.Context, traceIDs ...tracestore.GetTraceParams) iter.Seq2[[]ptrace.Traces, error]) *Reader_GetTraces_Call { _c.Call.Return(run) return _c } // NewWriter creates a new instance of Writer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewWriter(t interface { mock.TestingT Cleanup(func()) }) *Writer { mock := &Writer{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // Writer is an autogenerated mock type for the Writer type type Writer struct { mock.Mock } type Writer_Expecter struct { mock *mock.Mock } func (_m *Writer) EXPECT() *Writer_Expecter { return &Writer_Expecter{mock: &_m.Mock} } // WriteTraces provides a mock function for the type Writer func (_mock *Writer) WriteTraces(ctx context.Context, td ptrace.Traces) error { ret := _mock.Called(ctx, td) if len(ret) == 0 { panic("no return value specified for WriteTraces") } var r0 error if returnFunc, ok := ret.Get(0).(func(context.Context, ptrace.Traces) error); ok { r0 = returnFunc(ctx, td) } else { r0 = ret.Error(0) } return r0 } // Writer_WriteTraces_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteTraces' type Writer_WriteTraces_Call struct { *mock.Call } // WriteTraces is a helper method to define mock.On call // - ctx context.Context // - td ptrace.Traces func (_e *Writer_Expecter) WriteTraces(ctx interface{}, td interface{}) *Writer_WriteTraces_Call { return &Writer_WriteTraces_Call{Call: _e.mock.On("WriteTraces", ctx, td)} } func (_c *Writer_WriteTraces_Call) Run(run func(ctx context.Context, td ptrace.Traces)) *Writer_WriteTraces_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 ptrace.Traces if args[1] != nil { arg1 = args[1].(ptrace.Traces) } run( arg0, arg1, ) }) return _c } func (_c *Writer_WriteTraces_Call) Return(err error) *Writer_WriteTraces_Call { _c.Call.Return(err) return _c } func (_c *Writer_WriteTraces_Call) RunAndReturn(run func(ctx context.Context, td ptrace.Traces) error) *Writer_WriteTraces_Call { _c.Call.Return(run) return _c } ================================================ FILE: internal/storage/v2/api/tracestore/reader.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "context" "iter" "time" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger/internal/jptrace" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" ) // Reader finds and loads traces and other data from storage. type Reader interface { // GetTraces returns an iterator that retrieves all traces with given IDs. // The iterator is single-use: once consumed, it cannot be used again. // // Chunking requirements: // - A single ptrace.Traces chunk MUST NOT contain spans from multiple traces. // - Large traces MAY be split across multiple, *consecutive* ptrace.Traces chunks. // - Each returned ptrace.Traces object MUST NOT be empty. // // Edge cases: // - If no spans are found for any given trace ID, the ID is ignored. // - If none of the trace IDs are found in the storage, an empty iterator is returned. // - If an error is encountered, the iterator returns the error and stops. GetTraces(ctx context.Context, traceIDs ...GetTraceParams) iter.Seq2[[]ptrace.Traces, error] // GetServices returns all service names known to the backend from spans // within its retention period. GetServices(ctx context.Context) ([]string, error) // GetOperations returns all operation names for a given service // known to the backend from spans within its retention period. GetOperations(ctx context.Context, query OperationQueryParams) ([]Operation, error) // FindTraces returns an iterator that retrieves traces matching query parameters. // The iterator is single-use: once consumed, it cannot be used again. // // The chunking rules is the same as for GetTraces. // // If no matching traces are found, the function returns an empty iterator. // If an error is encountered, the iterator returns the error and stops. // // There's currently an implementation-dependent ambiguity whether all query filters // (such as multiple tags) must apply to the same span within a trace, or can be satisfied // by different spans. FindTraces(ctx context.Context, query TraceQueryParams) iter.Seq2[[]ptrace.Traces, error] // FindTraceIDs returns an iterator that retrieves IDs of traces matching query parameters. // The iterator is single-use: once consumed, it cannot be used again. // // If no matching traces are found, the function returns an empty iterator. // If an error is encountered, the iterator returns the error and stops. // // This function behaves identically to FindTraces, except that it returns only the list // of matching trace IDs. This is useful in some contexts, such as batch jobs, where a // large list of trace IDs may be queried first and then the full traces are loaded // in batches. FindTraceIDs(ctx context.Context, query TraceQueryParams) iter.Seq2[[]FoundTraceID, error] } // GetTraceParams contains single-trace parameters for a GetTraces request. // Some storage backends (e.g. Tempo) perform GetTraces much more efficiently // if they know the approximate time range of the trace. type GetTraceParams struct { // TraceID is the ID of the trace to retrieve. Required. TraceID pcommon.TraceID // Start of the time interval to search for trace ID. Optional. Start time.Time // End of the time interval to search for trace ID. Optional. End time.Time } // TraceQueryParams contains query parameters to find traces. For a detailed // definition of each field in this message, refer to `TraceQueryParameters` in `jaeger.api_v3` // (https://github.com/jaegertracing/jaeger-idl/blob/main/proto/api_v3/query_service.proto). type TraceQueryParams struct { ServiceName string OperationName string // Attributes must initialized with pcommon.NewMap() before use. Attributes pcommon.Map StartTimeMin time.Time StartTimeMax time.Time DurationMin time.Duration DurationMax time.Duration SearchDepth int } // FoundTraceID is a wrapper around trace ID returned from FindTraceIDs // with an optional time range that may be used in GetTraces calls. // // The time range is provided as an optimization hint for some storage backends // that can perform more efficient queries when they know the approximate time range. // The value should not be used for precise time-based filtering or assumptions. // It is meant as a rough boundary and may not be populated in all cases. type FoundTraceID struct { TraceID pcommon.TraceID Start time.Time End time.Time } func (t *TraceQueryParams) ToSpanStoreQueryParameters() *spanstore.TraceQueryParameters { return &spanstore.TraceQueryParameters{ ServiceName: t.ServiceName, OperationName: t.OperationName, Tags: jptrace.PcommonMapToPlainMap(t.Attributes), StartTimeMin: t.StartTimeMin, StartTimeMax: t.StartTimeMax, DurationMin: t.DurationMin, DurationMax: t.DurationMax, NumTraces: t.SearchDepth, } } // OperationQueryParams contains parameters of query operations, empty spanKind means get operations for all kinds of span. type OperationQueryParams struct { ServiceName string SpanKind string } // Operation contains operation name and span kind type Operation struct { Name string SpanKind string } ================================================ FILE: internal/storage/v2/api/tracestore/reader_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "testing" "time" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" ) func TestToSpanStoreQueryParameters(t *testing.T) { now := time.Now() attributes := pcommon.NewMap() attributes.PutStr("tag-a", "val-a") query := &TraceQueryParams{ ServiceName: "service", OperationName: "operation", Attributes: attributes, StartTimeMin: now, StartTimeMax: now.Add(time.Minute), DurationMin: time.Minute, DurationMax: time.Hour, SearchDepth: 10, } expected := &spanstore.TraceQueryParameters{ ServiceName: "service", OperationName: "operation", Tags: map[string]string{"tag-a": "val-a"}, StartTimeMin: now, StartTimeMax: now.Add(time.Minute), DurationMin: time.Minute, DurationMax: time.Hour, NumTraces: 10, } require.Equal(t, expected, query.ToSpanStoreQueryParameters()) } ================================================ FILE: internal/storage/v2/api/tracestore/tracestoremetrics/package_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestoremetrics import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v2/api/tracestore/tracestoremetrics/reader_metrics.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestoremetrics import ( "context" "iter" "time" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" ) var _ tracestore.Reader = (*ReadMetricsDecorator)(nil) // ReadMetricsDecorator wraps a tracestore.Reader and collects metrics around each read operation. type ReadMetricsDecorator struct { traceReader tracestore.Reader findTracesMetrics *queryMetrics findTraceIDsMetrics *queryMetrics getTraceMetrics *queryMetrics getServicesMetrics *queryMetrics getOperationsMetrics *queryMetrics } type queryMetrics struct { Errors metrics.Counter `metric:"requests" tags:"result=err"` Successes metrics.Counter `metric:"requests" tags:"result=ok"` Responses metrics.Counter `metric:"responses"` ErrLatency metrics.Timer `metric:"latency" tags:"result=err"` OKLatency metrics.Timer `metric:"latency" tags:"result=ok"` } func (q *queryMetrics) emit(err error, latency time.Duration, responses int) { if err != nil { q.Errors.Inc(1) q.ErrLatency.Record(latency) } else { q.Successes.Inc(1) q.OKLatency.Record(latency) q.Responses.Inc(int64(responses)) } } // NewReaderDecorator returns a new ReadMetricsDecorator. func NewReaderDecorator(traceReader tracestore.Reader, metricsFactory metrics.Factory) *ReadMetricsDecorator { return &ReadMetricsDecorator{ traceReader: traceReader, findTracesMetrics: buildQueryMetrics("find_traces", metricsFactory), findTraceIDsMetrics: buildQueryMetrics("find_trace_ids", metricsFactory), getTraceMetrics: buildQueryMetrics("get_trace", metricsFactory), getServicesMetrics: buildQueryMetrics("get_services", metricsFactory), getOperationsMetrics: buildQueryMetrics("get_operations", metricsFactory), } } func buildQueryMetrics(operation string, metricsFactory metrics.Factory) *queryMetrics { qMetrics := &queryMetrics{} scoped := metricsFactory.Namespace(metrics.NSOptions{Name: "", Tags: map[string]string{"operation": operation}}) metrics.Init(qMetrics, scoped, nil) return qMetrics } // FindTraces implements tracestore.Reader#FindTraces func (m *ReadMetricsDecorator) FindTraces(ctx context.Context, query tracestore.TraceQueryParams) iter.Seq2[[]ptrace.Traces, error] { return func(yield func([]ptrace.Traces, error) bool) { start := time.Now() var err error length := 0 defer func() { m.findTracesMetrics.emit(err, time.Since(start), length) }() findTracesIter := m.traceReader.FindTraces(ctx, query) for traces, iterErr := range findTracesIter { err = iterErr length += len(traces) if !yield(traces, iterErr) { return } } } } // FindTraceIDs implements tracestore.Reader#FindTraceIDs func (m *ReadMetricsDecorator) FindTraceIDs(ctx context.Context, query tracestore.TraceQueryParams) iter.Seq2[[]tracestore.FoundTraceID, error] { return func(yield func([]tracestore.FoundTraceID, error) bool) { start := time.Now() var err error length := 0 defer func() { m.findTraceIDsMetrics.emit(err, time.Since(start), length) }() findTraceIDsIter := m.traceReader.FindTraceIDs(ctx, query) for traceIds, iterErr := range findTraceIDsIter { err = iterErr length += len(traceIds) if !yield(traceIds, iterErr) { return } } } } // GetTraces implements tracestore.Reader#GetTraces func (m *ReadMetricsDecorator) GetTraces(ctx context.Context, traceIDs ...tracestore.GetTraceParams) iter.Seq2[[]ptrace.Traces, error] { return func(yield func([]ptrace.Traces, error) bool) { start := time.Now() var err error length := 0 defer func() { m.getTraceMetrics.emit(err, time.Since(start), length) }() getTraceIter := m.traceReader.GetTraces(ctx, traceIDs...) for traces, iterErr := range getTraceIter { err = iterErr length += len(traces) if !yield(traces, iterErr) { return } } } } // GetServices implements tracestore.Reader#GetServices func (m *ReadMetricsDecorator) GetServices(ctx context.Context) ([]string, error) { start := time.Now() retMe, err := m.traceReader.GetServices(ctx) m.getServicesMetrics.emit(err, time.Since(start), len(retMe)) return retMe, err } // GetOperations implements tracestore.Reader#GetOperations func (m *ReadMetricsDecorator) GetOperations( ctx context.Context, query tracestore.OperationQueryParams, ) ([]tracestore.Operation, error) { start := time.Now() retMe, err := m.traceReader.GetOperations(ctx, query) m.getOperationsMetrics.emit(err, time.Since(start), len(retMe)) return retMe, err } ================================================ FILE: internal/storage/v2/api/tracestore/tracestoremetrics/reader_metrics_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestoremetrics import ( "context" "iter" "testing" "github.com/stretchr/testify/assert" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger/internal/metricstest" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore/mocks" ) func TestSuccessfulUnderlyingCalls(t *testing.T) { mf := metricstest.NewFactory(0) mockReader := mocks.Reader{} mrs := NewReaderDecorator(&mockReader, mf) traces := []ptrace.Traces{ptrace.NewTraces(), ptrace.NewTraces()} mockReader.On("GetServices", context.Background()).Return([]string{"service-x"}, nil) mrs.GetServices(context.Background()) operationQuery := tracestore.OperationQueryParams{ServiceName: "something"} mockReader.On("GetOperations", context.Background(), operationQuery). Return([]tracestore.Operation{{}}, nil) mrs.GetOperations(context.Background(), operationQuery) mockReader.On("GetTraces", context.Background(), []tracestore.GetTraceParams{{}}).Return(emptyIter[ptrace.Traces](traces, nil)) count := 0 for range mrs.GetTraces(context.Background(), tracestore.GetTraceParams{}) { if count != 0 { break } count++ } mockReader.On("FindTraces", context.Background(), tracestore.TraceQueryParams{}). Return(emptyIter[ptrace.Traces](traces, nil)) count = 0 for range mrs.FindTraces(context.Background(), tracestore.TraceQueryParams{}) { if count != 0 { break } count++ } mockReader.On("FindTraceIDs", context.Background(), tracestore.TraceQueryParams{}). Return(emptyIter[tracestore.FoundTraceID]([]tracestore.FoundTraceID{{TraceID: [16]byte{}}, {TraceID: [16]byte{}}}, nil)) count = 0 for range mrs.FindTraceIDs(context.Background(), tracestore.TraceQueryParams{}) { if count != 0 { break } count++ } counters, gauges := mf.Snapshot() expected := map[string]int64{ "requests|operation=get_operations|result=ok": 1, "requests|operation=get_operations|result=err": 0, "requests|operation=get_trace|result=ok": 1, "requests|operation=get_trace|result=err": 0, "requests|operation=find_traces|result=ok": 1, "requests|operation=find_traces|result=err": 0, "requests|operation=find_trace_ids|result=ok": 1, "requests|operation=find_trace_ids|result=err": 0, "requests|operation=get_services|result=ok": 1, "requests|operation=get_services|result=err": 0, "responses|operation=get_trace": 2, "responses|operation=find_traces": 2, "responses|operation=find_trace_ids": 2, "responses|operation=get_operations": 1, "responses|operation=get_services": 1, } existingKeys := []string{ "latency|operation=get_operations|result=ok.P50", "latency|operation=find_traces|result=ok.P50", // this is not exhaustive } nonExistentKeys := []string{ "latency|operation=get_operations|result=err.P50", } checkExpectedExistingAndNonExistentCounters(t, counters, expected, gauges, existingKeys, nonExistentKeys) } func checkExpectedExistingAndNonExistentCounters(t *testing.T, actualCounters, expectedCounters, actualGauges map[string]int64, existingKeys, nonExistentKeys []string, ) { for k, v := range expectedCounters { assert.Equal(t, v, actualCounters[k], k) } for _, k := range existingKeys { _, ok := actualGauges[k] assert.True(t, ok, k) } for _, k := range nonExistentKeys { _, ok := actualGauges[k] assert.False(t, ok, k) } } func TestFailingUnderlyingCalls(t *testing.T) { mf := metricstest.NewFactory(0) mockReader := mocks.Reader{} mrs := NewReaderDecorator(&mockReader, mf) returningErr := assert.AnError mockReader.On("GetServices", context.Background()). Return(nil, returningErr) mrs.GetServices(context.Background()) operationQuery := tracestore.OperationQueryParams{ServiceName: "something"} mockReader.On("GetOperations", context.Background(), operationQuery). Return(nil, returningErr) mrs.GetOperations(context.Background(), operationQuery) mockReader.On("GetTraces", context.Background(), []tracestore.GetTraceParams{{}}). Return(emptyIter[ptrace.Traces](nil, returningErr)) for range mrs.GetTraces(context.Background(), tracestore.GetTraceParams{}) { t.Log("GetTraces iteration") } mockReader.On("FindTraces", context.Background(), tracestore.TraceQueryParams{}). Return(emptyIter[ptrace.Traces](nil, returningErr)) for range mrs.FindTraces(context.Background(), tracestore.TraceQueryParams{}) { t.Log("FindTraces iteration") } mockReader.On("FindTraceIDs", context.Background(), tracestore.TraceQueryParams{}). Return(emptyIter[tracestore.FoundTraceID](nil, returningErr)) for range mrs.FindTraceIDs(context.Background(), tracestore.TraceQueryParams{}) { t.Log("FindTraceIDs iteration") } counters, gauges := mf.Snapshot() expecteds := map[string]int64{ "requests|operation=get_operations|result=ok": 0, "requests|operation=get_operations|result=err": 1, "requests|operation=get_trace|result=ok": 0, "requests|operation=get_trace|result=err": 1, "requests|operation=find_traces|result=ok": 0, "requests|operation=find_traces|result=err": 1, "requests|operation=find_trace_ids|result=ok": 0, "requests|operation=find_trace_ids|result=err": 1, "requests|operation=get_services|result=ok": 0, "requests|operation=get_services|result=err": 1, } existingKeys := []string{ "latency|operation=get_operations|result=err.P50", } nonExistentKeys := []string{ "latency|operation=get_operations|result=ok.P50", "latency|operation=query|result=ok.P50", // this is not exhaustive } checkExpectedExistingAndNonExistentCounters(t, counters, expecteds, gauges, existingKeys, nonExistentKeys) } func emptyIter[T any](td []T, err error) iter.Seq2[[]T, error] { return func(yield func([]T, error) bool) { if err != nil { yield(nil, err) return } for _, t := range td { if !yield([]T{t}, nil) { return } } } } ================================================ FILE: internal/storage/v2/api/tracestore/writer.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "context" "go.opentelemetry.io/collector/pdata/ptrace" ) // Writer writes spans to storage. type Writer interface { // WriteTraces writes a batch of spans to storage. Idempotent. // Implementations are not required to support atomic transactions, // so if any of the spans fail to be written an error is returned. // Compatible with OTLP Exporter API. WriteTraces(ctx context.Context, td ptrace.Traces) error } ================================================ FILE: internal/storage/v2/badger/factory.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package badger import ( "context" "github.com/jaegertracing/jaeger/internal/distributedlock" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore" "github.com/jaegertracing/jaeger/internal/storage/v1/badger" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" "github.com/jaegertracing/jaeger/internal/storage/v2/v1adapter" "github.com/jaegertracing/jaeger/internal/telemetry" ) type Factory struct { v1Factory *badger.Factory } func NewFactory( cfg badger.Config, telset telemetry.Settings, ) (*Factory, error) { v1Factory := badger.NewFactory() v1Factory.Config = &cfg err := v1Factory.Initialize(telset.Metrics, telset.Logger) if err != nil { return nil, err } f := Factory{v1Factory: v1Factory} return &f, nil } func (f *Factory) CreateTraceWriter() (tracestore.Writer, error) { v1Writer, _ := f.v1Factory.CreateSpanWriter() // error is always nil return v1adapter.NewTraceWriter(v1Writer), nil } func (f *Factory) CreateTraceReader() (tracestore.Reader, error) { v1Reader, _ := f.v1Factory.CreateSpanReader() // error is always nil return v1adapter.NewTraceReader(v1Reader), nil } func (f *Factory) CreateDependencyReader() (depstore.Reader, error) { v1Reader, _ := f.v1Factory.CreateDependencyReader() // error is always nil return v1adapter.NewDependencyReader(v1Reader), nil } func (f *Factory) CreateSamplingStore(maxBuckets int) (samplingstore.Store, error) { return f.v1Factory.CreateSamplingStore(maxBuckets) } func (f *Factory) CreateLock() (distributedlock.Lock, error) { return f.v1Factory.CreateLock() } func (f *Factory) Close() error { return f.v1Factory.Close() } func (f *Factory) Purge(ctx context.Context) error { return f.v1Factory.Purge(ctx) } ================================================ FILE: internal/storage/v2/badger/factory_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package badger import ( "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zaptest" "github.com/jaegertracing/jaeger/internal/storage/v1/badger" "github.com/jaegertracing/jaeger/internal/telemetry" ) func TestNewFac(t *testing.T) { telset := telemetry.NoopSettings() telset.Logger = zaptest.NewLogger(t, zaptest.WrapOptions(zap.AddCaller())) f, err := NewFactory(*badger.DefaultConfig(), telset) require.NoError(t, err) _, err = f.CreateTraceReader() require.NoError(t, err) _, err = f.CreateTraceWriter() require.NoError(t, err) _, err = f.CreateDependencyReader() require.NoError(t, err) _, err = f.CreateSamplingStore(5) require.NoError(t, err) lock, err := f.CreateLock() require.NoError(t, err) assert.NotNil(t, lock) err = f.Purge(context.Background()) require.NoError(t, err) err = f.Close() require.NoError(t, err) } func TestBadgerStorageFactoryWithConfig(t *testing.T) { t.Parallel() cfg := badger.Config{} _, err := NewFactory(cfg, telemetry.NoopSettings()) require.ErrorContains(t, err, "Error Creating Dir: \"\" err: mkdir : no such file or directory") cfg = badger.Config{ Ephemeral: true, MaintenanceInterval: 5, MetricsUpdateInterval: 10, } factory, err := NewFactory(cfg, telemetry.NoopSettings()) require.NoError(t, err) factory.Close() } ================================================ FILE: internal/storage/v2/badger/package_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package badger import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v2/cassandra/factory.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package cassandra import ( "context" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/distributedlock" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore" "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra" cspanstore "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore/tracestoremetrics" ctracestore "github.com/jaegertracing/jaeger/internal/storage/v2/cassandra/tracestore" "github.com/jaegertracing/jaeger/internal/storage/v2/v1adapter" "github.com/jaegertracing/jaeger/internal/telemetry" ) type Factory struct { metricsFactory metrics.Factory logger *zap.Logger v1Factory *cassandra.Factory tracer trace.TracerProvider } // NewFactory creates and initializes the factory func NewFactory(opts cassandra.Options, telset telemetry.Settings) (*Factory, error) { f := &Factory{ metricsFactory: telset.Metrics, logger: telset.Logger, tracer: telset.TracerProvider, } baseFactory, err := newFactoryWithConfig(opts, f.metricsFactory, f.logger, f.tracer) if err != nil { return nil, err } f.v1Factory = baseFactory return f, nil } func (f *Factory) CreateTraceReader() (tracestore.Reader, error) { corereader, err := cspanstore.NewSpanReader( f.v1Factory.GetSession(), f.metricsFactory, f.logger, f.tracer.Tracer("cSpanStore.SpanReader"), ) if err != nil { return nil, err } return tracestoremetrics.NewReaderDecorator( ctracestore.NewTraceReader(corereader), f.metricsFactory, ), nil } func (f *Factory) CreateTraceWriter() (tracestore.Writer, error) { writer, err := f.v1Factory.CreateSpanWriter() if err != nil { return nil, err } return v1adapter.NewTraceWriter(writer), nil } func (f *Factory) CreateDependencyReader() (depstore.Reader, error) { reader, err := f.v1Factory.CreateDependencyReader() if err != nil { return nil, err } return v1adapter.NewDependencyReader(reader), nil } func (f *Factory) CreateSamplingStore(maxBuckets int) (samplingstore.Store, error) { return f.v1Factory.CreateSamplingStore(maxBuckets) } func (f *Factory) Close() error { return f.v1Factory.Close() } func (f *Factory) Purge(ctx context.Context) error { return f.v1Factory.Purge(ctx) } func (f *Factory) CreateLock() (distributedlock.Lock, error) { return f.v1Factory.CreateLock() } // newFactoryWithConfig initializes factory with Config. func newFactoryWithConfig( opts cassandra.Options, metricsFactory metrics.Factory, logger *zap.Logger, tracer trace.TracerProvider, ) (*cassandra.Factory, error) { f := cassandra.NewFactory() // use this to help with testing b := &withConfigBuilder{ f: f, opts: &opts, metricsFactory: metricsFactory, logger: logger, tracer: tracer, initializer: f.Initialize, // this can be mocked in tests } return b.build() } type withConfigBuilder struct { f *cassandra.Factory opts *cassandra.Options metricsFactory metrics.Factory logger *zap.Logger tracer trace.TracerProvider initializer func(metricsFactory metrics.Factory, logger *zap.Logger, tracer trace.TracerProvider) error } func (b *withConfigBuilder) build() (*cassandra.Factory, error) { b.f.ConfigureFromOptions(b.opts) if err := b.opts.Configuration.Validate(); err != nil { return nil, err } err := b.initializer(b.metricsFactory, b.logger, b.tracer) if err != nil { return nil, err } return b.f, nil } ================================================ FILE: internal/storage/v2/cassandra/factory_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package cassandra import ( "errors" "testing" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace/noop" "go.uber.org/zap" "go.uber.org/zap/zaptest" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/storage/cassandra/config" "github.com/jaegertracing/jaeger/internal/storage/cassandra/mocks" "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra" "github.com/jaegertracing/jaeger/internal/telemetry" ) func TestNewFactoryWithConfig(t *testing.T) { t.Run("valid configuration", func(t *testing.T) { opts := &cassandra.Options{ Configuration: config.DefaultConfiguration(), } f := cassandra.NewFactory() b := &withConfigBuilder{ f: f, opts: opts, metricsFactory: metrics.NullFactory, logger: zap.NewNop(), initializer: func(_ metrics.Factory, _ *zap.Logger, _ trace.TracerProvider) error { return nil }, } _, err := b.build() require.NoError(t, err) }) t.Run("connection error", func(t *testing.T) { expErr := errors.New("made-up error") opts := &cassandra.Options{ Configuration: config.DefaultConfiguration(), } f := cassandra.NewFactory() b := &withConfigBuilder{ f: f, opts: opts, metricsFactory: metrics.NullFactory, logger: zap.NewNop(), initializer: func(_ metrics.Factory, _ *zap.Logger, _ trace.TracerProvider) error { return expErr }, } _, err := b.build() require.ErrorIs(t, err, expErr) }) t.Run("invalid configuration", func(t *testing.T) { cfg := cassandra.Options{} _, err := NewFactory(cfg, telemetry.NoopSettings()) require.ErrorContains(t, err, "Servers: non zero value required") }) } func TestNewFactory(t *testing.T) { v1Factory := cassandra.NewFactory() v1Factory.Options = cassandra.NewOptions() var ( session = &mocks.Session{} query = &mocks.Query{} ) session.On("Query", mock.AnythingOfType("string"), mock.Anything).Return(query) session.On("Close").Return() query.On("Exec").Return(nil) cassandra.MockSession(v1Factory, session, nil) require.NoError(t, v1Factory.Initialize(metrics.NullFactory, zap.NewNop(), noop.NewTracerProvider())) f := createFactory(t, v1Factory) _, err := f.CreateTraceWriter() require.NoError(t, err) _, err = f.CreateTraceReader() require.NoError(t, err) _, err = f.CreateDependencyReader() require.NoError(t, err) _, err = f.CreateLock() require.NoError(t, err) _, err = f.CreateSamplingStore(0) require.NoError(t, err) require.NoError(t, f.Close()) } func TestCreateTraceReaderError(t *testing.T) { session := &mocks.Session{} query := &mocks.Query{} session.On("Query", mock.AnythingOfType("string"), mock.Anything).Return(query) session.On("Query", mock.AnythingOfType("string"), mock.Anything).Return(query) query.On("Exec").Return(errors.New("table does not exist")) v1Factory := cassandra.NewFactory() cassandra.MockSession(v1Factory, session, nil) require.NoError(t, v1Factory.Initialize(metrics.NullFactory, zap.NewNop(), noop.NewTracerProvider())) f := createFactory(t, v1Factory) r, err := f.CreateTraceReader() require.ErrorContains(t, err, "neither table operation_names_v2 nor operation_names exist") require.Nil(t, r) } func TestCreateTraceWriterErr(t *testing.T) { v1Factory := cassandra.NewFactory() v1Factory.Options = &cassandra.Options{ Configuration: config.DefaultConfiguration(), Index: cassandra.IndexConfig{ TagBlackList: "a,b,c", TagWhiteList: "a,b,c", }, } var ( session = &mocks.Session{} query = &mocks.Query{} ) session.On("Query", mock.AnythingOfType("string"), mock.Anything).Return(query) query.On("Exec").Return(nil) cassandra.MockSession(v1Factory, session, nil) require.NoError(t, v1Factory.Initialize(metrics.NullFactory, zap.NewNop(), noop.NewTracerProvider())) f := createFactory(t, v1Factory) _, err := f.CreateTraceWriter() require.ErrorContains(t, err, "only one of TagIndexBlacklist and TagIndexWhitelist can be specified") } func createFactory(t *testing.T, v1Factory *cassandra.Factory) *Factory { return &Factory{ v1Factory: v1Factory, metricsFactory: metrics.NullFactory, logger: zaptest.NewLogger(t), tracer: noop.NewTracerProvider(), } } ================================================ FILE: internal/storage/v2/cassandra/package_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package cassandra import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v2/cassandra/tracestore/fixtures/.gitignore ================================================ actual_* ================================================ FILE: internal/storage/v2/cassandra/tracestore/fixtures/cas_01.json ================================================ { "TraceID": "AAAAAAAAAAEAAAAAAAAAAA==", "SpanID": 2, "ParentID": 3, "OperationName": "test-general-conversion", "Flags": 1, "StartTime": 1485467191639875, "Duration": 5, "Tags": [ { "Key": "otel.scope.name", "ValueType": "string", "value_string": "testing-library" }, { "Key": "otel.scope.version", "ValueType": "string", "value_string": "1.1.1" }, { "Key": "peer.service", "ValueType": "string", "value_string": "service-y" }, { "Key": "peer.ipv4", "ValueType": "int64", "value_long": 23456 }, { "Key": "blob", "ValueType": "binary", "value_binary": "AAAwOQ==" }, { "Key": "temperature", "ValueType": "float64", "value_double": 72.5 }, { "Key": "error", "ValueType": "bool", "value_bool": true }, { "Key": "otel.status_description", "ValueType": "string", "value_string": "random-message" }, { "Key": "w3c.tracestate", "ValueType": "string", "value_string": "some-state" } ], "Logs": [ { "Timestamp": 1485467191639875, "Fields": [ { "Key": "event", "ValueType": "string", "value_string": "testing-event" }, { "Key": "event-x", "ValueType": "string", "value_string": "event-y" } ] }, { "Timestamp": 1485467191639875, "Fields": [ { "Key": "x", "ValueType": "string", "value_string": "y" } ] } ], "Refs": [ { "RefType": "child-of", "TraceID": "AAAAAAAAAAEAAAAAAAAAAA==", "SpanID": 3 }, { "RefType": "follows-from", "TraceID": "AAAAAAAAAAEAAAAAAAAAAA==", "SpanID": 4 }, { "RefType": "child-of", "TraceID": "AAAAAAAAAP8AAAAAAAAAAA==", "SpanID": 255 } ], "Process": { "ServiceName": "service-x", "Tags": [ { "Key": "sdk.version", "ValueType": "string", "value_string": "1.2.1" } ] }, "ServiceName": "service-x", "SpanHash": 0 } ================================================ FILE: internal/storage/v2/cassandra/tracestore/fixtures/otel_traces_01.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "service-x" } }, { "key": "sdk.version", "value": { "stringValue": "1.2.1" } } ] }, "scopeSpans": [ { "scope": { "name": "testing-library", "version": "1.1.1" }, "spans": [ { "traceId": "00000000000000010000000000000000", "spanId": "0000000000000002", "parentSpanId": "0000000000000003", "flags": 1, "name": "test-general-conversion", "startTimeUnixNano": "1485467191639875000", "endTimeUnixNano": "1485467191639880000", "attributes": [ { "key": "peer.service", "value": { "stringValue": "service-y" } }, { "key": "peer.ipv4", "value": { "intValue": "23456" } }, { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } }, { "key": "temperature", "value": { "doubleValue": 72.5 } } ], "events": [ { "timeUnixNano": "1485467191639875000", "name": "testing-event", "attributes": [ { "key": "event-x", "value": { "stringValue": "event-y" } } ] }, { "timeUnixNano": "1485467191639875000", "attributes": [ { "key": "x", "value": { "stringValue": "y" } } ] } ], "links": [ { "traceId": "00000000000000010000000000000000", "spanId": "0000000000000004", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "follows_from" } } ] }, { "traceId": "00000000000000ff0000000000000000", "spanId": "00000000000000ff", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "child_of" } } ] } ], "status": { "code": 2, "message": "random-message" }, "traceState": "some-state" } ] } ] } ] } ================================================ FILE: internal/storage/v2/cassandra/tracestore/from_dbmodel.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 // Code originally copied from https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/e49500a9b68447cbbe237fa29526ba99e4963f39/pkg/translator/jaeger/jaegerproto_to_traces.go package tracestore import ( "errors" "fmt" "strconv" "strings" idutils "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/core/xidutils" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/spanstore/dbmodel" "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) var errType = errors.New("invalid type") // FromDBModel converts dbmodel.Span to ptrace.Traces func FromDBModel(spans []dbmodel.Span) ptrace.Traces { traceData := ptrace.NewTraces() if len(spans) == 0 { return traceData } resourceSpans := traceData.ResourceSpans() resourceSpans.EnsureCapacity(len(spans)) dbSpansToSpans(spans, resourceSpans) return traceData } func dbSpansToSpans(dbSpans []dbmodel.Span, resourceSpans ptrace.ResourceSpansSlice) { for i := range dbSpans { span := &dbSpans[i] resourceSpan := resourceSpans.AppendEmpty() dbProcessToResource(span.Process, resourceSpan.Resource()) scopeSpans := resourceSpan.ScopeSpans() scopeSpan := scopeSpans.AppendEmpty() dbSpanToScope(span, scopeSpan) dbSpanToSpan(span, scopeSpan.Spans().AppendEmpty()) } } func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) { serviceName := process.ServiceName tags := process.Tags if serviceName == "" && tags == nil { return } attrs := resource.Attributes() if serviceName != "" && serviceName != noServiceName { attrs.EnsureCapacity(len(tags) + 1) attrs.PutStr(otelsemconv.ServiceNameKey, serviceName) } else { attrs.EnsureCapacity(len(tags)) } dbTagsToAttributes(tags, attrs) } func dbSpanToSpan(dbspan *dbmodel.Span, span ptrace.Span) { span.SetTraceID(pcommon.TraceID(dbspan.TraceID)) //nolint:gosec // G115 // we only care about bits, not the interpretation as integer, and this conversion is bitwise lossless span.SetSpanID(idutils.UInt64ToSpanID(uint64(dbspan.SpanID))) span.SetName(dbspan.OperationName) //nolint:gosec // G115 // dbspan.Flags is guaranteed non-negative by schema constraints span.SetFlags(uint32(dbspan.Flags)) //nolint:gosec // G115 // epoch microseconds are semantically non-negative, safe conversion to uint64 span.SetStartTimestamp(dbTimeStampToOTLPTimeStamp(uint64(dbspan.StartTime))) //nolint:gosec // G115 // dbspan.StartTime and dbspan.Duration is guaranteed non-negative by schema constraints span.SetEndTimestamp(dbTimeStampToOTLPTimeStamp(uint64(dbspan.StartTime + dbspan.Duration))) parentSpanID := dbspan.ParentID if parentSpanID != 0 { //nolint:gosec // G115 // bit-preserving uint64<->int64 conversion for opaque span ID span.SetParentSpanID(idutils.UInt64ToSpanID(uint64(parentSpanID))) } attrs := span.Attributes() attrs.EnsureCapacity(len(dbspan.Tags)) dbTagsToAttributes(dbspan.Tags, attrs) if spanKindAttr, ok := attrs.Get(model.SpanKindKey); ok { span.SetKind(jSpanKindToInternal(spanKindAttr.Str())) attrs.Remove(model.SpanKindKey) } setSpanStatus(attrs, span) span.TraceState().FromRaw(getTraceStateFromAttrs(attrs)) // drop the attributes slice if all of them were replaced during translation if attrs.Len() == 0 { attrs.Clear() } dbLogsToSpanEvents(dbspan.Logs, span.Events()) dbReferencesToSpanLinks(dbspan.Refs, parentSpanID, span.Links()) } func dbTagsToAttributes(tags []dbmodel.KeyValue, attributes pcommon.Map) { for _, tag := range tags { switch tag.ValueType { case dbmodel.StringType: attributes.PutStr(tag.Key, tag.ValueString) case dbmodel.BoolType: attributes.PutBool(tag.Key, tag.ValueBool) case dbmodel.Int64Type: attributes.PutInt(tag.Key, tag.ValueInt64) case dbmodel.Float64Type: attributes.PutDouble(tag.Key, tag.ValueFloat64) case dbmodel.BinaryType: attributes.PutEmptyBytes(tag.Key).FromRaw(tag.ValueBinary) default: attributes.PutStr(tag.Key, fmt.Sprintf("", tag.ValueType)) } } } func setSpanStatus(attrs pcommon.Map, span ptrace.Span) { dest := span.Status() statusCode := ptrace.StatusCodeUnset statusMessage := "" statusExists := false if errorVal, ok := attrs.Get(tagError); ok && errorVal.Type() == pcommon.ValueTypeBool { if errorVal.Bool() { statusCode = ptrace.StatusCodeError attrs.Remove(tagError) statusExists = true if desc, ok := extractStatusDescFromAttr(attrs); ok { statusMessage = desc } else if descAttr, ok := attrs.Get(tagHTTPStatusMsg); ok { statusMessage = descAttr.Str() } } } if codeAttr, ok := attrs.Get(otelsemconv.OtelStatusCode); ok { if !statusExists { // The error tag is the ultimate truth for a Jaeger spans' error // status. Only parse the otel.status_code tag if the error tag is // not set to true. statusExists = true if strings.ToUpper(codeAttr.Str()) == statusOk { statusCode = ptrace.StatusCodeOk } else if strings.ToUpper(codeAttr.Str()) == statusError { statusCode = ptrace.StatusCodeError } if desc, ok := extractStatusDescFromAttr(attrs); ok { statusMessage = desc } } // Regardless of error tag value, remove the otel.status_code tag. The // otel.status_message tag will have already been removed if // statusExists is true. attrs.Remove(otelsemconv.OtelStatusCode) } else if httpCodeAttr, ok := attrs.Get(otelsemconv.HTTPResponseStatusCodeKey); !statusExists && ok { // Fallback to introspecting if this span represents a failed HTTP // request or response, but again, only do so if the `error` tag was // not set to true and no explicit status was sent. if code, err := getStatusCodeFromHTTPStatusAttr(httpCodeAttr, span.Kind()); err == nil { if code != ptrace.StatusCodeUnset { statusExists = true statusCode = code } if msgAttr, ok := attrs.Get(tagHTTPStatusMsg); ok { statusMessage = msgAttr.Str() } } } if statusExists { dest.SetCode(statusCode) dest.SetMessage(statusMessage) } } // extractStatusDescFromAttr returns the OTel status description from attrs // along with true if it is set. Otherwise, an empty string and false are // returned. The OTel status description attribute is deleted from attrs in // the process. func extractStatusDescFromAttr(attrs pcommon.Map) (string, bool) { if msgAttr, ok := attrs.Get(otelsemconv.OtelStatusDescription); ok { msg := msgAttr.Str() attrs.Remove(otelsemconv.OtelStatusDescription) return msg, true } return "", false } // codeFromAttr returns the integer code value from attrVal. An error is // returned if the code is not represented by an integer or string value in // the attrVal or the value is outside the bounds of an int representation. func codeFromAttr(attrVal pcommon.Value) (int64, error) { var val int64 switch attrVal.Type() { case pcommon.ValueTypeInt: val = attrVal.Int() case pcommon.ValueTypeStr: var err error val, err = strconv.ParseInt(attrVal.Str(), 10, 0) if err != nil { return 0, err } default: return 0, fmt.Errorf("%w: %s", errType, attrVal.Type().String()) } return val, nil } func getStatusCodeFromHTTPStatusAttr(attrVal pcommon.Value, kind ptrace.SpanKind) (ptrace.StatusCode, error) { statusCode, err := codeFromAttr(attrVal) if err != nil { return ptrace.StatusCodeUnset, err } // For HTTP status codes in the 4xx range span status MUST be left unset // in case of SpanKind.SERVER and MUST be set to Error in case of SpanKind.CLIENT. // For HTTP status codes in the 5xx range, as well as any other code the client // failed to interpret, span status MUST be set to Error. if statusCode >= 400 && statusCode < 500 { switch kind { case ptrace.SpanKindClient: return ptrace.StatusCodeError, nil case ptrace.SpanKindServer: return ptrace.StatusCodeUnset, nil } } return statusCodeFromHTTP(statusCode), nil } // StatusCodeFromHTTP takes an HTTP status code and return the appropriate OpenTelemetry status code // See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status func statusCodeFromHTTP(httpStatusCode int64) ptrace.StatusCode { if httpStatusCode >= 100 && httpStatusCode < 399 { return ptrace.StatusCodeUnset } return ptrace.StatusCodeError } func jSpanKindToInternal(spanKind string) ptrace.SpanKind { switch spanKind { case "client": return ptrace.SpanKindClient case "server": return ptrace.SpanKindServer case "producer": return ptrace.SpanKindProducer case "consumer": return ptrace.SpanKindConsumer case "internal": return ptrace.SpanKindInternal } return ptrace.SpanKindUnspecified } func dbLogsToSpanEvents(logs []dbmodel.Log, events ptrace.SpanEventSlice) { if len(logs) == 0 { return } events.EnsureCapacity(len(logs)) for i, log := range logs { var event ptrace.SpanEvent if events.Len() > i { event = events.At(i) } else { event = events.AppendEmpty() } //nolint:gosec // G115 // dblog.Timestamp is guaranteed non-negative (epoch microseconds) by schema constraints event.SetTimestamp(dbTimeStampToOTLPTimeStamp(uint64(log.Timestamp))) if len(log.Fields) == 0 { continue } attrs := event.Attributes() attrs.EnsureCapacity(len(log.Fields)) dbTagsToAttributes(log.Fields, attrs) if name, ok := attrs.Get(eventNameAttr); ok { event.SetName(name.Str()) attrs.Remove(eventNameAttr) } } } // dbReferencesToSpanLinks sets internal span links based on jaeger span references skipping excludeParentID func dbReferencesToSpanLinks(refs []dbmodel.SpanRef, excludeParentID int64, spanLinks ptrace.SpanLinkSlice) { if len(refs) == 0 || len(refs) == 1 && refs[0].SpanID == excludeParentID && refs[0].RefType == dbmodel.ChildOf { return } spanLinks.EnsureCapacity(len(refs)) for _, ref := range refs { if ref.SpanID == excludeParentID && ref.RefType == dbmodel.ChildOf { continue } link := spanLinks.AppendEmpty() link.SetTraceID(pcommon.TraceID(ref.TraceID)) //nolint:gosec // G115 // bit-preserving uint64<->int64 conversion for opaque IDs link.SetSpanID(idutils.UInt64ToSpanID(uint64(ref.SpanID))) link.Attributes().PutStr(otelsemconv.AttributeOpentracingRefType, dbRefTypeToAttribute(ref.RefType)) } } func getTraceStateFromAttrs(attrs pcommon.Map) string { traceState := "" // TODO Bring this inline with solution for jaegertracing/jaeger-client-java #702 once available if attr, ok := attrs.Get(tagW3CTraceState); ok { traceState = attr.Str() attrs.Remove(tagW3CTraceState) } return traceState } func dbSpanToScope(span *dbmodel.Span, scopeSpan ptrace.ScopeSpans) { if libraryName, ok := getAndDeleteTag(span, otelsemconv.AttributeOtelScopeName); ok { scopeSpan.Scope().SetName(libraryName) if libraryVersion, ok := getAndDeleteTag(span, otelsemconv.AttributeOtelScopeVersion); ok { scopeSpan.Scope().SetVersion(libraryVersion) } } } func getAndDeleteTag(span *dbmodel.Span, key string) (string, bool) { for i, tag := range span.Tags { if tag.Key == key { val := tag.ValueString span.Tags = append(span.Tags[:i], span.Tags[i+1:]...) return val, true } } return "", false } func dbRefTypeToAttribute(ref string) string { if ref == dbmodel.ChildOf { return otelsemconv.AttributeOpentracingRefTypeChildOf } return otelsemconv.AttributeOpentracingRefTypeFollowsFrom } // dbTimeStampToOTLPTimeStamp converts the db timestamp which is in microseconds // to nanoseconds which is the OTLP standard. func dbTimeStampToOTLPTimeStamp(timestamp uint64) pcommon.Timestamp { return pcommon.Timestamp(timestamp * 1000) } ================================================ FILE: internal/storage/v2/cassandra/tracestore/from_dbmodel_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 // Code originally copied from https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/e49500a9b68447cbbe237fa29526ba99e4963f39/pkg/translator/jaeger/jaegerproto_to_traces_test.go package tracestore import ( "encoding/json" "net/http" "os" "strconv" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/spanstore/dbmodel" "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) // Use timestamp with microsecond granularity to work well with jaeger thrift translation var testSpanEventTime = time.Date(2020, 2, 11, 20, 26, 13, 123000, time.UTC).UnixMicro() func TestCodeFromAttr(t *testing.T) { tests := []struct { name string attr pcommon.Value code int64 err error }{ { name: "ok-string", attr: pcommon.NewValueStr("0"), code: 0, }, { name: "ok-int", attr: pcommon.NewValueInt(1), code: 1, }, { name: "wrong-type", attr: pcommon.NewValueBool(true), code: 0, err: errType, }, { name: "invalid-string", attr: pcommon.NewValueStr("inf"), code: 0, err: strconv.ErrSyntax, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { code, err := codeFromAttr(test.attr) if test.err != nil { require.ErrorIs(t, err, test.err) } else { require.NoError(t, err) } assert.Equal(t, test.code, code) }) } } func TestZeroBatchLength(t *testing.T) { trace := FromDBModel([]dbmodel.Span{}) assert.Equal(t, 0, trace.ResourceSpans().Len()) } func TestEmptyServiceNameAndTags(t *testing.T) { tests := []struct { name string batches []dbmodel.Span }{ { name: "empty service with nil tags", batches: []dbmodel.Span{ { Process: dbmodel.Process{ ServiceName: "", }, }, }, }, { name: "empty service with tags", batches: []dbmodel.Span{ { Process: dbmodel.Process{ ServiceName: "", Tags: []dbmodel.KeyValue{}, }, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { trace := FromDBModel(test.batches) assert.Equal(t, 1, trace.ResourceSpans().Len()) assert.Equal(t, 0, trace.ResourceSpans().At(0).Resource().Attributes().Len()) }) } } func TestEmptySpansAndProcess(t *testing.T) { trace := FromDBModel([]dbmodel.Span{{}}) assert.Equal(t, 1, trace.ResourceSpans().Len()) } func Test_dbSpansToSpans_EmptySpans(t *testing.T) { spans := []dbmodel.Span{{}} traceData := ptrace.NewTraces() rss := traceData.ResourceSpans() dbSpansToSpans(spans, rss) assert.Equal(t, 1, rss.Len()) } func TestGetStatusCodeFromHTTPStatusAttr(t *testing.T) { tests := []struct { name string attr pcommon.Value kind ptrace.SpanKind code ptrace.StatusCode err string }{ { name: "string-unknown", attr: pcommon.NewValueStr("10"), kind: ptrace.SpanKindClient, code: ptrace.StatusCodeError, }, { name: "string-ok", attr: pcommon.NewValueStr("101"), kind: ptrace.SpanKindClient, code: ptrace.StatusCodeUnset, }, { name: "int-not-found", attr: pcommon.NewValueInt(404), kind: ptrace.SpanKindClient, code: ptrace.StatusCodeError, }, { name: "int-not-found-client-span", attr: pcommon.NewValueInt(404), kind: ptrace.SpanKindServer, code: ptrace.StatusCodeUnset, }, { name: "int-invalid-arg", attr: pcommon.NewValueInt(408), kind: ptrace.SpanKindClient, code: ptrace.StatusCodeError, }, { name: "int-internal", attr: pcommon.NewValueInt(500), kind: ptrace.SpanKindClient, code: ptrace.StatusCodeError, }, { name: "wrong value", attr: pcommon.NewValueBool(true), kind: ptrace.SpanKindClient, code: ptrace.StatusCodeUnset, err: "invalid type: Bool", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { code, err := getStatusCodeFromHTTPStatusAttr(test.attr, test.kind) if test.err != "" { require.ErrorContains(t, err, test.err) } else { require.NoError(t, err) } assert.Equal(t, test.code, code) }) } } func Test_dbLogsToSpanEvents(t *testing.T) { traces := ptrace.NewTraces() span := traces.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty() span.Events().AppendEmpty().SetName("event1") span.Events().AppendEmpty().SetName("event2") span.Events().AppendEmpty().Attributes().PutStr(eventNameAttr, "testing") logs := []dbmodel.Log{ { Timestamp: testSpanEventTime, }, { Timestamp: testSpanEventTime, }, } dbLogsToSpanEvents(logs, span.Events()) for i := range logs { assert.Equal(t, testSpanEventTime, int64(span.Events().At(i).Timestamp()/1000)) } assert.Equal(t, 1, span.Events().At(2).Attributes().Len()) assert.Empty(t, span.Events().At(2).Name()) } func Test_dbTagsToAttributes(t *testing.T) { tags := []dbmodel.KeyValue{ { Key: "bool-val", ValueType: dbmodel.BoolType, ValueBool: true, }, { Key: "int-val", ValueType: dbmodel.Int64Type, ValueInt64: 123, }, { Key: "string-val", ValueType: dbmodel.StringType, ValueString: "abc", }, { Key: "double-val", ValueType: dbmodel.Float64Type, ValueFloat64: 1.23, }, { Key: "binary-val", ValueType: dbmodel.BinaryType, ValueBinary: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x7D, 0x98}, }, { Key: "testing-key", ValueType: "some random value", }, } expected := pcommon.NewMap() expected.PutBool("bool-val", true) expected.PutInt("int-val", 123) expected.PutStr("string-val", "abc") expected.PutDouble("double-val", 1.23) expected.PutEmptyBytes("binary-val").FromRaw([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x7D, 0x98}) expected.PutStr("testing-key", "") got := pcommon.NewMap() dbTagsToAttributes(tags, got) require.Equal(t, expected, got) } func TestSetInternalSpanStatus(t *testing.T) { emptyStatus := ptrace.NewStatus() okStatus := ptrace.NewStatus() okStatus.SetCode(ptrace.StatusCodeOk) errorStatus := ptrace.NewStatus() errorStatus.SetCode(ptrace.StatusCodeError) errorStatusWithMessage := ptrace.NewStatus() errorStatusWithMessage.SetCode(ptrace.StatusCodeError) errorStatusWithMessage.SetMessage("Error: Invalid argument") errorStatusWith404Message := ptrace.NewStatus() errorStatusWith404Message.SetCode(ptrace.StatusCodeError) errorStatusWith404Message.SetMessage("HTTP 404: Not Found") tests := []struct { name string attrs map[string]any status ptrace.Status kind ptrace.SpanKind attrsModifiedLen int // Length of attributes map after dropping converted fields }{ { name: "No tags set -> OK status", status: emptyStatus, attrsModifiedLen: 0, }, { name: "error tag set -> Error status", attrs: map[string]any{ tagError: true, }, status: errorStatus, attrsModifiedLen: 0, }, { name: "status.code is set as string", attrs: map[string]any{ otelsemconv.OtelStatusCode: statusOk, }, status: okStatus, attrsModifiedLen: 0, }, { name: "status.code, status.message and error tags are set", attrs: map[string]any{ tagError: true, otelsemconv.OtelStatusCode: statusError, otelsemconv.OtelStatusDescription: "Error: Invalid argument", }, status: errorStatusWithMessage, attrsModifiedLen: 0, }, { name: "http.status_code tag is set as string", attrs: map[string]any{ otelsemconv.HTTPResponseStatusCodeKey: "404", }, status: errorStatus, attrsModifiedLen: 1, }, { name: "http.status_code, http.status_message and error tags are set", attrs: map[string]any{ tagError: true, otelsemconv.HTTPResponseStatusCodeKey: 404, tagHTTPStatusMsg: "HTTP 404: Not Found", }, status: errorStatusWith404Message, attrsModifiedLen: 2, }, { name: "status.code has precedence over http.status_code.", attrs: map[string]any{ otelsemconv.OtelStatusCode: statusOk, otelsemconv.HTTPResponseStatusCodeKey: 500, tagHTTPStatusMsg: "Server Error", }, status: okStatus, attrsModifiedLen: 2, }, { name: "Ignore http.status_code == 200 if error set to true.", attrs: map[string]any{ tagError: true, otelsemconv.HTTPResponseStatusCodeKey: http.StatusOK, }, status: errorStatus, attrsModifiedLen: 1, }, { name: "status.error has precedence over http.status_error.", attrs: map[string]any{ otelsemconv.OtelStatusCode: statusError, otelsemconv.HTTPResponseStatusCodeKey: 500, tagHTTPStatusMsg: "Server Error", }, status: errorStatus, attrsModifiedLen: 2, }, { name: "the 4xx range span status MUST be left unset in case of SpanKind.SERVER", kind: ptrace.SpanKindServer, attrs: map[string]any{ tagError: false, otelsemconv.HTTPResponseStatusCodeKey: 404, }, status: emptyStatus, attrsModifiedLen: 2, }, { name: "whether tagHttpStatusMsg is set as string", attrs: map[string]any{ otelsemconv.HTTPResponseStatusCodeKey: 404, tagHTTPStatusMsg: "HTTP 404: Not Found", }, status: errorStatusWith404Message, attrsModifiedLen: 2, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { span := ptrace.NewSpan() span.SetKind(test.kind) status := span.Status() attrs := pcommon.NewMap() require.NoError(t, attrs.FromRaw(test.attrs)) setSpanStatus(attrs, span) assert.Equal(t, test.status, status) assert.Equal(t, test.attrsModifiedLen, attrs.Len()) }) } } func TestJSpanKindToInternal(t *testing.T) { tests := []struct { jSpanKind string otlpSpanKind ptrace.SpanKind }{ { jSpanKind: "client", otlpSpanKind: ptrace.SpanKindClient, }, { jSpanKind: "server", otlpSpanKind: ptrace.SpanKindServer, }, { jSpanKind: "producer", otlpSpanKind: ptrace.SpanKindProducer, }, { jSpanKind: "consumer", otlpSpanKind: ptrace.SpanKindConsumer, }, { jSpanKind: "internal", otlpSpanKind: ptrace.SpanKindInternal, }, { jSpanKind: "all-others", otlpSpanKind: ptrace.SpanKindUnspecified, }, } for _, test := range tests { t.Run(test.jSpanKind, func(t *testing.T) { assert.Equal(t, test.otlpSpanKind, jSpanKindToInternal(test.jSpanKind)) }) } } func BenchmarkProtoBatchToInternalTraces(b *testing.B) { data, err := os.ReadFile("fixtures/cas_01.json") require.NoError(b, err) var batch dbmodel.Span err = json.Unmarshal(data, &batch) require.NoError(b, err) b.ResetTimer() for n := 0; n < b.N; n++ { FromDBModel([]dbmodel.Span{batch}) } } func TestFromDbModel_Fixtures(t *testing.T) { tracesStr, batchStr := loadFixtures(t, 1) var batch dbmodel.Span err := json.Unmarshal(batchStr, &batch) require.NoError(t, err) td := FromDBModel([]dbmodel.Span{batch}) testTraces(t, tracesStr, td) batches := ToDBModel(td) assert.Len(t, batches, 1) testSpans(t, batchStr, batches[0]) } ================================================ FILE: internal/storage/v2/cassandra/tracestore/package_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v2/cassandra/tracestore/reader.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "context" "iter" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" "github.com/jaegertracing/jaeger/internal/storage/v2/v1adapter" ) type TraceReader struct { reader spanstore.CoreSpanReader fallback tracestore.Reader } func NewTraceReader(reader spanstore.CoreSpanReader) *TraceReader { return &TraceReader{ reader: reader, fallback: v1adapter.NewTraceReader(reader), } } func (r *TraceReader) GetServices(ctx context.Context) ([]string, error) { return r.reader.GetServices(ctx) } func (r *TraceReader) GetOperations(ctx context.Context, query tracestore.OperationQueryParams) ([]tracestore.Operation, error) { return r.reader.GetOperationsV2(ctx, query) } func (r *TraceReader) GetTraces(ctx context.Context, traceIDs ...tracestore.GetTraceParams) iter.Seq2[[]ptrace.Traces, error] { return r.fallback.GetTraces(ctx, traceIDs...) } func (r *TraceReader) FindTraces(ctx context.Context, query tracestore.TraceQueryParams) iter.Seq2[[]ptrace.Traces, error] { return r.fallback.FindTraces(ctx, query) } func (r *TraceReader) FindTraceIDs(ctx context.Context, query tracestore.TraceQueryParams) iter.Seq2[[]tracestore.FoundTraceID, error] { return r.fallback.FindTraceIDs(ctx, query) } ================================================ FILE: internal/storage/v2/cassandra/tracestore/reader_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "context" "errors" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/spanstore/mocks" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" ) func TestNewTraceReader(t *testing.T) { reader := NewTraceReader(&mocks.CoreSpanReader{}) assert.NotNil(t, reader) traceids := reader.FindTraceIDs(context.Background(), tracestore.TraceQueryParams{}) assert.NotNil(t, traceids) trace := reader.GetTraces(context.Background(), tracestore.GetTraceParams{}) assert.NotNil(t, trace) traces := reader.FindTraces(context.Background(), tracestore.TraceQueryParams{}) assert.NotNil(t, traces) } func TestGetServices(t *testing.T) { services := []string{"service-1", "service-2"} reader := mocks.CoreSpanReader{} reader.On("GetServices", mock.Anything).Return(services, nil) tracereader := &TraceReader{reader: &reader} got, err := tracereader.GetServices(context.Background()) require.NoError(t, err) require.Equal(t, services, got) } func TestGetOperationsErr(t *testing.T) { reader := mocks.CoreSpanReader{} reader.On("GetOperationsV2", mock.Anything, mock.Anything).Return(nil, errors.New("error")) tracereader := &TraceReader{reader: &reader} got, err := tracereader.GetOperations(context.Background(), tracestore.OperationQueryParams{ ServiceName: "service-1", SpanKind: "some kind", }) require.ErrorContains(t, err, "error") require.Nil(t, got) } func TestGetOperations(t *testing.T) { reader := mocks.CoreSpanReader{} expected := []tracestore.Operation{ { Name: "operation-1", SpanKind: "some kind", }, { Name: "operation-2", SpanKind: "some kind", }, } reader.On("GetOperationsV2", mock.Anything, mock.Anything).Return(expected, nil) tracereader := &TraceReader{reader: &reader} got, err := tracereader.GetOperations(context.Background(), tracestore.OperationQueryParams{ ServiceName: "service-1", SpanKind: "some kind", }) require.NoError(t, err) require.Equal(t, expected, got) } ================================================ FILE: internal/storage/v2/cassandra/tracestore/to_dbmodel.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 // Code originally copied from https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/e49500a9b68447cbbe237fa29526ba99e4963f39/pkg/translator/jaeger/traces_to_jaegerproto.go package tracestore import ( "bytes" "encoding/binary" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/spanstore/dbmodel" "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) const ( noServiceName = "OTLPResourceNoServiceName" eventNameAttr = "event" statusError = "ERROR" statusOk = "OK" tagError = "error" tagW3CTraceState = "w3c.tracestate" tagHTTPStatusMsg = "http.status_message" ) // ToDBModel translates internal trace data into the DB Spans. // Returns a slice of translated DB Spans. func ToDBModel(td ptrace.Traces) []dbmodel.Span { resourceSpans := td.ResourceSpans() if resourceSpans.Len() == 0 { return []dbmodel.Span{} } batches := make([]dbmodel.Span, 0, td.SpanCount()) for i := 0; i < resourceSpans.Len(); i++ { rs := resourceSpans.At(i) batch := resourceSpansToDbSpans(rs) batches = append(batches, batch...) } return batches } func resourceSpansToDbSpans(resourceSpans ptrace.ResourceSpans) []dbmodel.Span { resource := resourceSpans.Resource() scopeSpans := resourceSpans.ScopeSpans() if scopeSpans.Len() == 0 { return []dbmodel.Span{} } process := resourceToDbProcess(resource) // Approximate the number of the spans as the number of the spans in the first // instrumentation library info. dbSpans := make([]dbmodel.Span, 0, scopeSpans.At(0).Spans().Len()) for _, scopeSpan := range scopeSpans.All() { for _, span := range scopeSpan.Spans().All() { dbSpan := spanToDbSpan(span, scopeSpan.Scope(), process) dbSpans = append(dbSpans, dbSpan) } } return dbSpans } func resourceToDbProcess(resource pcommon.Resource) dbmodel.Process { process := dbmodel.Process{} attrs := resource.Attributes() process.ServiceName = noServiceName if attrs.Len() == 0 { return process } tags := make([]dbmodel.KeyValue, 0, attrs.Len()) for key, attr := range attrs.All() { if key == otelsemconv.ServiceNameKey { process.ServiceName = attr.AsString() continue } tags = append(tags, attributeToDbTag(key, attr)) } process.Tags = tags return process } func appendTagsFromAttributes(tags []dbmodel.KeyValue, attrs pcommon.Map) []dbmodel.KeyValue { if attrs.Len() == 0 { return tags } for key, attr := range attrs.All() { tags = append(tags, attributeToDbTag(key, attr)) } return tags } func attributeToDbTag(key string, attr pcommon.Value) dbmodel.KeyValue { tag := dbmodel.KeyValue{Key: key} switch attr.Type() { case pcommon.ValueTypeInt: tag.ValueType = dbmodel.Int64Type tag.ValueInt64 = attr.Int() case pcommon.ValueTypeBool: tag.ValueType = dbmodel.BoolType tag.ValueBool = attr.Bool() case pcommon.ValueTypeDouble: tag.ValueType = dbmodel.Float64Type tag.ValueFloat64 = attr.Double() case pcommon.ValueTypeBytes: tag.ValueType = dbmodel.BinaryType tag.ValueBinary = attr.Bytes().AsRaw() case pcommon.ValueTypeMap, pcommon.ValueTypeSlice: tag.ValueType = dbmodel.StringType tag.ValueString = attr.AsString() default: tag.ValueType = dbmodel.StringType tag.ValueString = attr.Str() } return tag } func spanToDbSpan(span ptrace.Span, scope pcommon.InstrumentationScope, process dbmodel.Process) dbmodel.Span { dbTraceId := dbmodel.TraceID(span.TraceID()) dbReferences := linksToDbSpanRefs(span.Links(), spanIDToDbSpanId(span.ParentSpanID()), dbTraceId) startTime := span.StartTimestamp().AsTime() return dbmodel.Span{ TraceID: dbTraceId, SpanID: spanIDToDbSpanId(span.SpanID()), OperationName: span.Name(), Refs: dbReferences, //nolint:gosec // G115 // OTLP timestamp is nanoseconds since epoch (semantically non-negative), safe to convert to int64 microseconds StartTime: int64(model.TimeAsEpochMicroseconds(startTime)), //nolint:gosec // G115 // span.EndTime - span.StartTime is guaranteed non-negative by schema constraints Duration: int64(model.DurationAsMicroseconds(span.EndTimestamp().AsTime().Sub(startTime))), Tags: getDbTags(span, scope), Logs: spanEventsToDbLogs(span.Events()), Process: process, //nolint:gosec // G115 // span.Flags is uint32, converting to int32 for DB storage (semantically non-negative, fits in int32) Flags: int32(span.Flags()), ServiceName: process.ServiceName, ParentID: spanIDToDbSpanId(span.ParentSpanID()), } } func getDbTags(span ptrace.Span, scope pcommon.InstrumentationScope) []dbmodel.KeyValue { var spanKindTag, statusCodeTag, statusMsgTag dbmodel.KeyValue var spanKindTagFound, statusCodeTagFound, statusMsgTagFound bool libraryTags, libraryTagsFound := getTagsFromInstrumentationLibrary(scope) tagsCount := span.Attributes().Len() + len(libraryTags) spanKindTag, spanKindTagFound = getTagFromSpanKind(span.Kind()) if spanKindTagFound { tagsCount++ } status := span.Status() statusCodeTag, statusCodeTagFound = getTagFromStatusCode(status.Code()) if statusCodeTagFound { tagsCount++ } statusMsgTag, statusMsgTagFound = getTagFromStatusMsg(status.Message()) if statusMsgTagFound { tagsCount++ } traceStateTags, traceStateTagsFound := getTagsFromTraceState(span.TraceState().AsRaw()) if traceStateTagsFound { tagsCount += len(traceStateTags) } if tagsCount == 0 { return nil } tags := make([]dbmodel.KeyValue, 0, tagsCount) if libraryTagsFound { tags = append(tags, libraryTags...) } tags = appendTagsFromAttributes(tags, span.Attributes()) if spanKindTagFound { tags = append(tags, spanKindTag) } if statusCodeTagFound { tags = append(tags, statusCodeTag) } if statusMsgTagFound { tags = append(tags, statusMsgTag) } if traceStateTagsFound { tags = append(tags, traceStateTags...) } return tags } func spanIDToDbSpanId(spanID pcommon.SpanID) int64 { //nolint:gosec // G115 // bit-preserving conversion between uint64 and int64 for opaque SpanID return int64(binary.BigEndian.Uint64(spanID[:])) } // linksToDbSpanRefs constructs jaeger span references based on parent span ID and span links. // The parent span ID is used to add a CHILD_OF reference, _unless_ it is referenced from one of the links. func linksToDbSpanRefs(links ptrace.SpanLinkSlice, parentSpanID int64, traceID dbmodel.TraceID) []dbmodel.SpanRef { refsCount := links.Len() if parentSpanID != 0 { refsCount++ } if refsCount == 0 { return nil } refs := make([]dbmodel.SpanRef, 0, refsCount) // Put parent span ID at the first place because usually backends look for it // as the first CHILD_OF item in the model.SpanRef slice. if parentSpanID != 0 { refs = append(refs, dbmodel.SpanRef{ TraceID: traceID, SpanID: parentSpanID, RefType: dbmodel.ChildOf, }) } for i := 0; i < links.Len(); i++ { link := links.At(i) linkTraceID := dbmodel.TraceID(link.TraceID()) linkSpanID := spanIDToDbSpanId(link.SpanID()) linkRefType := dbRefTypeFromLink(link) if parentSpanID != 0 && bytes.Equal(linkTraceID[:], traceID[:]) && linkSpanID == parentSpanID { // We already added a reference to this span, but maybe with the wrong type, so override. refs[0].RefType = linkRefType continue } refs = append(refs, dbmodel.SpanRef{ TraceID: linkTraceID, SpanID: linkSpanID, RefType: linkRefType, }) } return refs } func spanEventsToDbLogs(events ptrace.SpanEventSlice) []dbmodel.Log { if events.Len() == 0 { return nil } logs := make([]dbmodel.Log, 0, events.Len()) for i := 0; i < events.Len(); i++ { event := events.At(i) fields := make([]dbmodel.KeyValue, 0, event.Attributes().Len()+1) _, eventAttrFound := event.Attributes().Get(eventNameAttr) if event.Name() != "" && !eventAttrFound { fields = append(fields, dbmodel.KeyValue{ Key: eventNameAttr, ValueType: dbmodel.StringType, ValueString: event.Name(), }) } fields = appendTagsFromAttributes(fields, event.Attributes()) logs = append(logs, dbmodel.Log{ //nolint:gosec // G115 // Timestamp is guaranteed non-negative by schema constraints Timestamp: int64(model.TimeAsEpochMicroseconds(event.Timestamp().AsTime())), Fields: fields, }) } return logs } func getTagFromSpanKind(spanKind ptrace.SpanKind) (dbmodel.KeyValue, bool) { var tagStr string switch spanKind { case ptrace.SpanKindClient: tagStr = string(model.SpanKindClient) case ptrace.SpanKindServer: tagStr = string(model.SpanKindServer) case ptrace.SpanKindProducer: tagStr = string(model.SpanKindProducer) case ptrace.SpanKindConsumer: tagStr = string(model.SpanKindConsumer) case ptrace.SpanKindInternal: tagStr = string(model.SpanKindInternal) default: return dbmodel.KeyValue{}, false } return dbmodel.KeyValue{ Key: model.SpanKindKey, ValueType: dbmodel.StringType, ValueString: tagStr, }, true } func getTagFromStatusCode(statusCode ptrace.StatusCode) (dbmodel.KeyValue, bool) { switch statusCode { // For backward compatibility, we also include the error tag // which was previously used in the test fixtures case ptrace.StatusCodeError: return dbmodel.KeyValue{ Key: tagError, ValueType: dbmodel.BoolType, ValueBool: true, }, true case ptrace.StatusCodeOk: return dbmodel.KeyValue{ Key: otelsemconv.OtelStatusCode, ValueType: dbmodel.StringType, ValueString: statusOk, }, true } return dbmodel.KeyValue{}, false } func getTagFromStatusMsg(statusMsg string) (dbmodel.KeyValue, bool) { if statusMsg == "" { return dbmodel.KeyValue{}, false } return dbmodel.KeyValue{ Key: otelsemconv.OtelStatusDescription, ValueString: statusMsg, ValueType: dbmodel.StringType, }, true } func getTagsFromTraceState(traceState string) ([]dbmodel.KeyValue, bool) { var keyValues []dbmodel.KeyValue exists := traceState != "" if exists { // TODO Bring this inline with solution for jaegertracing/jaeger-client-java #702 once available kv := dbmodel.KeyValue{ Key: tagW3CTraceState, ValueString: traceState, ValueType: dbmodel.StringType, } keyValues = append(keyValues, kv) } return keyValues, exists } func getTagsFromInstrumentationLibrary(scope pcommon.InstrumentationScope) ([]dbmodel.KeyValue, bool) { var keyValues []dbmodel.KeyValue if ilName := scope.Name(); ilName != "" { kv := dbmodel.KeyValue{ Key: otelsemconv.AttributeOtelScopeName, ValueString: ilName, ValueType: dbmodel.StringType, } keyValues = append(keyValues, kv) } if ilVersion := scope.Version(); ilVersion != "" { kv := dbmodel.KeyValue{ Key: otelsemconv.AttributeOtelScopeVersion, ValueString: ilVersion, ValueType: dbmodel.StringType, } keyValues = append(keyValues, kv) } return keyValues, len(keyValues) > 0 } func dbRefTypeFromLink(link ptrace.SpanLink) string { refTypeAttr, ok := link.Attributes().Get(otelsemconv.AttributeOpentracingRefType) if !ok { return dbmodel.FollowsFrom } attr := refTypeAttr.Str() if attr == otelsemconv.AttributeOpentracingRefTypeChildOf { return dbmodel.ChildOf } // There are only 2 types of SpanRefType we assume that everything // that's not a model.ChildOf is a model.FollowsFrom return dbmodel.FollowsFrom } ================================================ FILE: internal/storage/v2/cassandra/tracestore/to_dbmodel_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 // Code originally copied from https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/e49500a9b68447cbbe237fa29526ba99e4963f39/pkg/translator/jaeger/traces_to_jaegerproto_test.go package tracestore import ( "bytes" "encoding/json" "fmt" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/cassandra/spanstore/dbmodel" "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) func TestGetTagFromStatusCode(t *testing.T) { tests := []struct { name string code ptrace.StatusCode tag dbmodel.KeyValue }{ { name: "ok", code: ptrace.StatusCodeOk, tag: dbmodel.KeyValue{ Key: otelsemconv.OtelStatusCode, ValueType: dbmodel.StringType, ValueString: statusOk, }, }, { name: "error", code: ptrace.StatusCodeError, tag: dbmodel.KeyValue{ Key: tagError, ValueType: dbmodel.BoolType, ValueBool: true, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { got, ok := getTagFromStatusCode(test.code) assert.True(t, ok) assert.Equal(t, test.tag, got) }) } } func TestEmptyAttributes(t *testing.T) { traces := ptrace.NewTraces() spans := traces.ResourceSpans().AppendEmpty() scopeSpans := spans.ScopeSpans().AppendEmpty() spanScope := scopeSpans.Scope() span := scopeSpans.Spans().AppendEmpty() modelSpan := spanToDbSpan(span, spanScope, dbmodel.Process{}) assert.Empty(t, modelSpan.Tags) } func TestEmptyLinkRefs(t *testing.T) { traces := ptrace.NewTraces() spans := traces.ResourceSpans().AppendEmpty() scopeSpans := spans.ScopeSpans().AppendEmpty() spanScope := scopeSpans.Scope() span := scopeSpans.Spans().AppendEmpty() spanLink := span.Links().AppendEmpty() spanLink.Attributes().PutStr("testing-key", "testing-value") modelSpan := spanToDbSpan(span, spanScope, dbmodel.Process{}) assert.Len(t, modelSpan.Refs, 1) assert.Equal(t, dbmodel.FollowsFrom, modelSpan.Refs[0].RefType) } func TestGetTagFromStatusMsg(t *testing.T) { _, ok := getTagFromStatusMsg("") assert.False(t, ok) got, ok := getTagFromStatusMsg("test-error") assert.True(t, ok) assert.Equal(t, dbmodel.KeyValue{ Key: otelsemconv.OtelStatusDescription, ValueString: "test-error", ValueType: dbmodel.StringType, }, got) } func Test_resourceToDbProcess_WhenOnlyServiceNameIsPresent(t *testing.T) { traces := ptrace.NewTraces() spans := traces.ResourceSpans().AppendEmpty() spans.Resource().Attributes().PutStr(otelsemconv.ServiceNameKey, "service") process := resourceToDbProcess(spans.Resource()) assert.Equal(t, "service", process.ServiceName) } func Test_resourceToDbProcess_DefaultServiceName(t *testing.T) { traces := ptrace.NewTraces() spans := traces.ResourceSpans().AppendEmpty() spans.Resource().Attributes().PutStr("some attribute", "some value") process := resourceToDbProcess(spans.Resource()) assert.Equal(t, noServiceName, process.ServiceName) } func TestGetTagFromSpanKind(t *testing.T) { tests := []struct { name string kind ptrace.SpanKind tag dbmodel.KeyValue ok bool }{ { name: "unspecified", kind: ptrace.SpanKindUnspecified, tag: dbmodel.KeyValue{}, ok: false, }, { name: "client", kind: ptrace.SpanKindClient, tag: dbmodel.KeyValue{ Key: model.SpanKindKey, ValueType: dbmodel.StringType, ValueString: string(model.SpanKindClient), }, ok: true, }, { name: "server", kind: ptrace.SpanKindServer, tag: dbmodel.KeyValue{ Key: model.SpanKindKey, ValueType: dbmodel.StringType, ValueString: string(model.SpanKindServer), }, ok: true, }, { name: "producer", kind: ptrace.SpanKindProducer, tag: dbmodel.KeyValue{ Key: model.SpanKindKey, ValueType: dbmodel.StringType, ValueString: string(model.SpanKindProducer), }, ok: true, }, { name: "consumer", kind: ptrace.SpanKindConsumer, tag: dbmodel.KeyValue{ Key: model.SpanKindKey, ValueType: dbmodel.StringType, ValueString: string(model.SpanKindConsumer), }, ok: true, }, { name: "internal", kind: ptrace.SpanKindInternal, tag: dbmodel.KeyValue{ Key: model.SpanKindKey, ValueType: dbmodel.StringType, ValueString: string(model.SpanKindInternal), }, ok: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { got, ok := getTagFromSpanKind(test.kind) assert.Equal(t, test.ok, ok) assert.Equal(t, test.tag, got) }) } } func TestAttributesToDbTags(t *testing.T) { attributes := pcommon.NewMap() attributes.PutBool("bool-val", true) attributes.PutInt("int-val", 123) attributes.PutStr("string-val", "abc") attributes.PutDouble("double-val", 1.23) attributes.PutEmptyBytes("bytes-val").FromRaw([]byte{1, 2, 3, 4}) attributes.PutStr(otelsemconv.ServiceNameKey, "service-name") expected := []dbmodel.KeyValue{ { Key: "bool-val", ValueType: dbmodel.BoolType, ValueBool: true, }, { Key: "int-val", ValueType: dbmodel.Int64Type, ValueInt64: 123, }, { Key: "string-val", ValueType: dbmodel.StringType, ValueString: "abc", }, { Key: "double-val", ValueType: dbmodel.Float64Type, ValueFloat64: 1.23, }, { Key: "bytes-val", ValueType: dbmodel.BinaryType, ValueBinary: []byte{1, 2, 3, 4}, }, { Key: otelsemconv.ServiceNameKey, ValueType: dbmodel.StringType, ValueString: "service-name", }, } got := appendTagsFromAttributes(make([]dbmodel.KeyValue, 0, len(expected)), attributes) require.Equal(t, expected, got) } func TestAttributesToJaegerProtoTags_MapType(t *testing.T) { attributes := pcommon.NewMap() attributes.PutEmptyMap("empty-map") got := appendTagsFromAttributes(make([]dbmodel.KeyValue, 0, 1), attributes) expected := []dbmodel.KeyValue{ { Key: "empty-map", ValueType: dbmodel.StringType, ValueString: "{}", }, } require.Equal(t, expected, got) } func BenchmarkInternalTracesToDbModel(b *testing.B) { unmarshaller := ptrace.JSONUnmarshaler{} data, err := os.ReadFile("fixtures/otel_traces_01.json") require.NoError(b, err) td, err := unmarshaller.UnmarshalTraces(data) require.NoError(b, err) b.ResetTimer() for n := 0; n < b.N; n++ { batches := ToDBModel(td) assert.NotEmpty(b, batches) } } func TestToDbModel_Fixtures(t *testing.T) { tracesData, spansData := loadFixtures(t, 1) unmarshaller := ptrace.JSONUnmarshaler{} expectedTd, err := unmarshaller.UnmarshalTraces(tracesData) require.NoError(t, err) batches := ToDBModel(expectedTd) assert.Len(t, batches, 1) testSpans(t, spansData, batches[0]) actualTd := FromDBModel(batches) testTraces(t, tracesData, actualTd) } func TestEdgeCases(t *testing.T) { tests := []struct { name string setupTraces func() ptrace.Traces expected any testFunc func(ptrace.Traces) any description string }{ { name: "empty span attributes", setupTraces: func() ptrace.Traces { traces := ptrace.NewTraces() spans := traces.ResourceSpans().AppendEmpty() scopeSpans := spans.ScopeSpans().AppendEmpty() scopeSpans.Spans().AppendEmpty() return traces }, expected: true, testFunc: func(traces ptrace.Traces) any { modelSpan := ToDBModel(traces)[0] return len(modelSpan.Tags) == 0 && len(modelSpan.Process.Tags) == 0 && modelSpan.Process.ServiceName == noServiceName }, description: "Empty span attributes should result in no tags", }, { name: "resource spans with no scope spans", setupTraces: func() ptrace.Traces { traces := ptrace.NewTraces() traces.ResourceSpans().AppendEmpty() return traces }, expected: true, testFunc: func(traces ptrace.Traces) any { dbSpans := ToDBModel(traces) return len(dbSpans) == 0 }, description: "Resource spans with no scope spans should return empty slice", }, { name: "traces with no resource spans", setupTraces: ptrace.NewTraces, expected: true, testFunc: func(traces ptrace.Traces) any { dbSpans := ToDBModel(traces) return len(dbSpans) == 0 }, description: "Traces with no resource spans should return empty slice", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { traces := tt.setupTraces() result := tt.testFunc(traces) assert.Equal(t, tt.expected, result, tt.description) }) } } func writeActualData(t *testing.T, name string, data []byte) { var prettyJson bytes.Buffer err := json.Indent(&prettyJson, data, "", " ") require.NoError(t, err) path := "fixtures/actual_" + name + ".json" err = os.WriteFile(path, prettyJson.Bytes(), 0o644) require.NoError(t, err) t.Log("Saved the actual " + name + " to " + path) } // Loads and returns domain model and JSON model fixtures with given number i. func loadFixtures(t *testing.T, i int) (tracesData []byte, spansData []byte) { tracesData = loadTraces(t, i) spansData = loadSpans(t, i) return tracesData, spansData } func loadTraces(t *testing.T, i int) []byte { inTraces := fmt.Sprintf("fixtures/otel_traces_%02d.json", i) tracesData, err := os.ReadFile(inTraces) require.NoError(t, err) return tracesData } func loadSpans(t *testing.T, i int) []byte { inSpans := fmt.Sprintf("fixtures/cas_%02d.json", i) spansData, err := os.ReadFile(inSpans) require.NoError(t, err) return spansData } func testTraces(t *testing.T, expectedTraces []byte, actualTraces ptrace.Traces) { unmarshaller := ptrace.JSONUnmarshaler{} expectedTd, err := unmarshaller.UnmarshalTraces(expectedTraces) require.NoError(t, err) if !assert.Equal(t, expectedTd, actualTraces) { marshaller := ptrace.JSONMarshaler{} actualTd, err := marshaller.MarshalTraces(actualTraces) require.NoError(t, err) writeActualData(t, "traces", actualTd) } } func testSpans(t *testing.T, expectedSpan []byte, actualSpan dbmodel.Span) { buf := &bytes.Buffer{} enc := json.NewEncoder(buf) enc.SetIndent("", " ") require.NoError(t, enc.Encode(actualSpan)) if !assert.Equal(t, string(expectedSpan), buf.String()) { writeActualData(t, "spans", buf.Bytes()) } } ================================================ FILE: internal/storage/v2/clickhouse/README.md ================================================ ## Clickhouse ### Differences from the implementation in [otel collector contrib](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/clickhouseexporter) #### Trace Storage Format The most significant difference lies in the handling of **Attributes**. In the OTel-contrib implementation, everything within the Attributes is converted to [strings](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/80b3df26b7028a4bbe1eb606a6142cd4df9c3c74/exporter/clickhouseexporter/internal/metrics_model.go#L171-L177): ```golang func AttributesToMap(attributes pcommon.Map) column.IterableOrderedMap { return orderedmap.CollectN(func(yield func(string, string) bool) { for k, v := range attributes.All() { yield(k, v.AsString()) } }, attributes.Len()) } ``` The primary reason for this is that it leads to the loss of the original data types and \~\~cannot be used directly as query parameters\~\~ (Clickhouse provides casting functions). For example, if an attribute has an `int64` value, we might want to perform the following operation: ```sql SELECT * FROM test WHERE resource.attributes['container.restart.count'] > 10 ``` To address the above issues, the following improvements have been implemented: * Instead of directly using a Map for storage, the key and value are split into two separate arrays. * More Columns are used to store values of different types: * For basic types like bool, double, int, and string, corresponding type array columns are used for storage: `Array(Int64)`, `Array(Bool)`, etc. * For complex types like slice and map, they are serialized into JSON format strings before storage: `Array(String)`. The `Value` type here actually refers to the `pdata` data types from the `otel-collector` pipeline. In our architecture, the `value_warpper` is responsible for wrapping the Protobuf-generated Go structures (which are the concrete implementation of `pdata`) into the `Value` type. Although `pdata` itself is based on the OTLP specification, encapsulating it into `Value` via the `value_warpper` creates a higher-level abstraction, which presents some challenges for directly storing `Value` in ClickHouse. Specifically, when deserializing `Slice` and `Map` data contained within the `Value`, the fact that JSON cannot natively distinguish whether a `Number` is an integer (`int`) or a floating-point number (`double`) leads to a loss of type information. Furthermore, directly handling the potentially dynamically nested `pdata` structures within the `Value` can also be quite complex. Therefore, to ensure the accuracy and completeness of data types in ClickHouse, and to effectively handle these nested telemetry data, we need to convert the `pdata` data inside `Value` into the standard `OTLP/JSON` format for storage. #### Data Read and Write Methods The OTel-contrib implementation uses `database/sql` for writing data. Using the provided generic interface is unnecessary; using the client provided by Clickhouse is a better choice. For write operations, `ch-go`'s `chpool` is used in batch mode. For read operations, `clickhouse-go` is used. ================================================ FILE: internal/storage/v2/clickhouse/clickhousetest/package_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package clickhousetest import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v2/clickhouse/clickhousetest/server.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package clickhousetest import ( "io" "net/http" "net/http/httptest" "github.com/ClickHouse/ch-go/proto" "github.com/ClickHouse/clickhouse-go/v2" chproto "github.com/ClickHouse/clickhouse-go/v2/lib/proto" ) var ( PingQuery = "SELECT 1" HandshakeQuery = "SELECT displayName(), version(), revision(), timezone()" ) // FailureConfig is a map of query body to error type FailureConfig map[string]error // NewServer creates a new HTTP test server that simulates a ClickHouse server. // It should only be used in tests. func NewServer(failures FailureConfig) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) query := string(body) block := chproto.NewBlock() if err, shouldFail := failures[query]; shouldFail { http.Error(w, err.Error(), http.StatusInternalServerError) return } switch query { case PingQuery: block.AddColumn("1", "UInt8") block.Append(uint8(1)) case HandshakeQuery: block.AddColumn("displayName()", "String") block.AddColumn("version()", "String") block.AddColumn("revision()", "UInt32") block.AddColumn("timezone()", "String") block.Append("mock-server", "23.3.1", chproto.DBMS_MIN_REVISION_WITH_CUSTOM_SERIALIZATION, "UTC") default: } var buf proto.Buffer block.Encode(&buf, clickhouse.ClientTCPProtocolVersion) w.Header().Set("Content-Type", "application/octet-stream") w.Write(buf.Buf) })) } ================================================ FILE: internal/storage/v2/clickhouse/config.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package clickhouse import ( "time" "github.com/asaskevich/govalidator" "github.com/open-telemetry/opentelemetry-collector-contrib/extension/basicauthextension" "go.opentelemetry.io/collector/config/configoptional" ) const ( defaultProtocol = "native" defaultDatabase = "jaeger" defaultSearchDepth = 1000 defaultMaxSearchDepth = 10000 ) type Configuration struct { // Protocol is the protocol to use to connect to ClickHouse. // Supported values are "native" and "http". Default is "native". Protocol string `mapstructure:"protocol" valid:"in(native|http),optional"` // Addresses contains a list of ClickHouse server addresses to connect to. Addresses []string `mapstructure:"addresses" valid:"required"` // Database is the ClickHouse database to connect to. Database string `mapstructure:"database"` // Auth contains the authentication configuration to connect to ClickHouse. Auth Authentication `mapstructure:"auth"` // DialTimeout is the timeout for establishing a connection to ClickHouse. DialTimeout time.Duration `mapstructure:"dial_timeout"` // CreateSchema, if set to true, will create the ClickHouse schema if it does not exist. CreateSchema bool `mapstructure:"create_schema"` // DefaultSearchDepth is the default search depth for queries. // This is the maximum number of trace IDs that will be returned when searching for traces // if a limit is not specified in the query. DefaultSearchDepth int `mapstructure:"default_search_depth"` // MaxSearchDepth is the maximum allowed search depth for queries. // This limits the number of trace IDs that can be returned when searching for traces. MaxSearchDepth int `mapstructure:"max_search_depth"` // TODO: add more settings } type Authentication struct { Basic configoptional.Optional[basicauthextension.ClientAuthSettings] `mapstructure:"basic"` // TODO: add JWT } func (cfg *Configuration) Validate() error { _, err := govalidator.ValidateStruct(cfg) return err } func (cfg *Configuration) applyDefaults() { if cfg.Protocol == "" { cfg.Protocol = "native" } if cfg.Database == "" { cfg.Database = defaultDatabase } if cfg.DefaultSearchDepth == 0 { cfg.DefaultSearchDepth = defaultSearchDepth } if cfg.MaxSearchDepth == 0 { cfg.MaxSearchDepth = defaultMaxSearchDepth } } ================================================ FILE: internal/storage/v2/clickhouse/config_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package clickhouse import ( "testing" "github.com/stretchr/testify/require" ) func TestValidate(t *testing.T) { tests := []struct { name string cfg Configuration wantErr bool }{ { name: "valid config with native protocol", cfg: Configuration{ Protocol: "native", Addresses: []string{"localhost:9000"}, }, wantErr: false, }, { name: "valid config with http protocol", cfg: Configuration{ Protocol: "http", Addresses: []string{"localhost:8123"}, }, wantErr: false, }, { name: "valid config with empty protocol", cfg: Configuration{ Addresses: []string{"localhost:9000"}, }, wantErr: false, }, { name: "valid config with multiple addresses", cfg: Configuration{ Protocol: "native", Addresses: []string{"localhost:9000", "localhost:9001"}, }, wantErr: false, }, { name: "invalid config with unsupported protocol", cfg: Configuration{ Protocol: "grpc", Addresses: []string{"localhost:9000"}, }, wantErr: true, }, { name: "invalid config with empty addresses", cfg: Configuration{ Protocol: "native", Addresses: []string{}, }, wantErr: true, }, { name: "invalid config with nil addresses", cfg: Configuration{ Protocol: "native", }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.cfg.Validate() if tt.wantErr { require.Error(t, err) } else { require.NoError(t, err) } }) } } func TestConfigurationApplyDefaults(t *testing.T) { config := &Configuration{} config.applyDefaults() require.Equal(t, defaultProtocol, config.Protocol) require.Equal(t, defaultDatabase, config.Database) require.Equal(t, defaultSearchDepth, config.DefaultSearchDepth) require.Equal(t, defaultMaxSearchDepth, config.MaxSearchDepth) } ================================================ FILE: internal/storage/v2/clickhouse/depstore/package_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package depstore import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v2/clickhouse/depstore/reader.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package depstore import ( "context" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" ) var _ depstore.Reader = (*Reader)(nil) type Reader struct{} func NewDependencyReader() *Reader { return &Reader{} } func (*Reader) GetDependencies(context.Context, depstore.QueryParameters) ([]model.DependencyLink, error) { panic("not implemented") } ================================================ FILE: internal/storage/v2/clickhouse/depstore/reader_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package depstore import ( "context" "testing" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" ) func TestReader_GetDependencies(t *testing.T) { reader := NewDependencyReader() ctx := context.Background() query := depstore.QueryParameters{} require.Panics(t, func() { reader.GetDependencies(ctx, query) }) } ================================================ FILE: internal/storage/v2/clickhouse/factory.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package clickhouse import ( "context" "errors" "fmt" "io" "github.com/ClickHouse/clickhouse-go/v2" "github.com/ClickHouse/clickhouse-go/v2/lib/driver" "github.com/jaegertracing/jaeger/internal/storage/v1" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" chdepstore "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/depstore" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/sql" chtracestore "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/tracestore" "github.com/jaegertracing/jaeger/internal/telemetry" ) var ( _ io.Closer = (*Factory)(nil) _ depstore.Factory = (*Factory)(nil) _ tracestore.Factory = (*Factory)(nil) _ storage.Purger = (*Factory)(nil) ) type Factory struct { config Configuration telset telemetry.Settings conn driver.Conn } func NewFactory(ctx context.Context, cfg Configuration, telset telemetry.Settings) (*Factory, error) { cfg.applyDefaults() f := &Factory{ config: cfg, telset: telset, } opts := &clickhouse.Options{ Protocol: getProtocol(f.config.Protocol), Addr: f.config.Addresses, Auth: clickhouse.Auth{ Database: f.config.Database, }, DialTimeout: f.config.DialTimeout, } basicAuth := f.config.Auth.Basic.Get() if basicAuth != nil { opts.Auth.Username = basicAuth.Username opts.Auth.Password = string(basicAuth.Password) } conn, err := clickhouse.Open(opts) if err != nil { return nil, fmt.Errorf("failed to create ClickHouse connection: %w", err) } err = conn.Ping(ctx) if err != nil { return nil, errors.Join( fmt.Errorf("failed to ping ClickHouse: %w", err), conn.Close(), ) } if f.config.CreateSchema { schemas := []struct { name string query string }{ {"spans table", sql.CreateSpansTable}, {"services table", sql.CreateServicesTable}, {"services materialized view", sql.CreateServicesMaterializedView}, {"operations table", sql.CreateOperationsTable}, {"operations materialized view", sql.CreateOperationsMaterializedView}, {"trace id timestamps table", sql.CreateTraceIDTimestampsTable}, {"trace id timestamps materialized view", sql.CreateTraceIDTimestampsMaterializedView}, {"attribute metadata table", sql.CreateAttributeMetadataTable}, {"attribute metadata materialized view", sql.CreateAttributeMetadataMaterializedView}, {"event attribute metadata materialized view", sql.CreateEventAttributeMetadataMaterializedView}, {"link attribute metadata materialized view", sql.CreateLinkAttributeMetadataMaterializedView}, } for _, schema := range schemas { if err = conn.Exec(ctx, schema.query); err != nil { return nil, errors.Join(fmt.Errorf("failed to create %s: %w", schema.name, err), conn.Close()) } } } f.conn = conn return f, nil } func (f *Factory) CreateTraceReader() (tracestore.Reader, error) { return chtracestore.NewReader(f.conn, chtracestore.ReaderConfig{ DefaultSearchDepth: f.config.DefaultSearchDepth, MaxSearchDepth: f.config.MaxSearchDepth, }), nil } func (f *Factory) CreateTraceWriter() (tracestore.Writer, error) { return chtracestore.NewWriter(f.conn), nil } func (*Factory) CreateDependencyReader() (depstore.Reader, error) { return chdepstore.NewDependencyReader(), nil } func (f *Factory) Close() error { return f.conn.Close() } func (f *Factory) Purge(ctx context.Context) error { tables := []struct { name string query string }{ {"spans", sql.TruncateSpans}, {"services", sql.TruncateServices}, {"operations", sql.TruncateOperations}, {"trace_id_timestamps", sql.TruncateTraceIDTimestamps}, {"attribute_metadata", sql.TruncateAttributeMetadata}, } for _, table := range tables { if err := f.conn.Exec(ctx, table.query); err != nil { return fmt.Errorf("failed to purge %s: %w", table.name, err) } } return nil } func getProtocol(protocol string) clickhouse.Protocol { if protocol == "http" { return clickhouse.HTTP } return clickhouse.Native } ================================================ FILE: internal/storage/v2/clickhouse/factory_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package clickhouse import ( "context" "testing" "time" "github.com/ClickHouse/clickhouse-go/v2" "github.com/open-telemetry/opentelemetry-collector-contrib/extension/basicauthextension" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/config/configoptional" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/clickhousetest" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/sql" "github.com/jaegertracing/jaeger/internal/telemetry" ) func TestFactory(t *testing.T) { tests := []struct { name string createSchema bool }{ { name: "without schema creation", createSchema: false, }, { name: "with schema creation", createSchema: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { srv := clickhousetest.NewServer(clickhousetest.FailureConfig{}) defer srv.Close() cfg := Configuration{ Protocol: "http", Addresses: []string{ srv.Listener.Addr().String(), }, Database: "default", Auth: Authentication{ Basic: configoptional.Some(basicauthextension.ClientAuthSettings{ Username: "user", Password: "password", }), }, } f, err := NewFactory(context.Background(), cfg, telemetry.Settings{}) require.NoError(t, err) require.NotNil(t, f) tr, err := f.CreateTraceReader() require.NoError(t, err) require.NotNil(t, tr) tw, err := f.CreateTraceWriter() require.NoError(t, err) require.NotNil(t, tw) dr, err := f.CreateDependencyReader() require.NoError(t, err) require.NotNil(t, dr) err = f.Purge(context.Background()) require.NoError(t, err) require.NoError(t, f.Close()) }) } } func TestNewFactory_Errors(t *testing.T) { tests := []struct { name string failureConfig clickhousetest.FailureConfig expectedError string }{ { name: "ping error", failureConfig: clickhousetest.FailureConfig{ clickhousetest.PingQuery: assert.AnError, }, expectedError: "failed to ping ClickHouse", }, { name: "spans table creation error", failureConfig: clickhousetest.FailureConfig{ sql.CreateSpansTable: assert.AnError, }, expectedError: "failed to create spans table", }, { name: "services table creation error", failureConfig: clickhousetest.FailureConfig{ sql.CreateServicesTable: assert.AnError, }, expectedError: "failed to create services table", }, { name: "services materialized view creation error", failureConfig: clickhousetest.FailureConfig{ sql.CreateServicesMaterializedView: assert.AnError, }, expectedError: "failed to create services materialized view", }, { name: "operations table creation error", failureConfig: clickhousetest.FailureConfig{ sql.CreateOperationsTable: assert.AnError, }, expectedError: "failed to create operations table", }, { name: "operations materialized view creation error", failureConfig: clickhousetest.FailureConfig{ sql.CreateOperationsMaterializedView: assert.AnError, }, expectedError: "failed to create operations materialized view", }, { name: "trace id timestamps table creation error", failureConfig: clickhousetest.FailureConfig{ sql.CreateTraceIDTimestampsTable: assert.AnError, }, expectedError: "failed to create trace id timestamps table", }, { name: "trace id timestamps materialized view creation error", failureConfig: clickhousetest.FailureConfig{ sql.CreateTraceIDTimestampsMaterializedView: assert.AnError, }, expectedError: "failed to create trace id timestamps materialized view", }, { name: "attribute metadata table creation error", failureConfig: clickhousetest.FailureConfig{ sql.CreateAttributeMetadataTable: assert.AnError, }, expectedError: "failed to create attribute metadata table", }, { name: "attribute metadata materialized view creation error", failureConfig: clickhousetest.FailureConfig{ sql.CreateAttributeMetadataMaterializedView: assert.AnError, }, expectedError: "failed to create attribute metadata materialized view", }, { name: "event attribute metadata materialized view creation error", failureConfig: clickhousetest.FailureConfig{ sql.CreateEventAttributeMetadataMaterializedView: assert.AnError, }, expectedError: "failed to create event attribute metadata materialized view", }, { name: "link attribute metadata materialized view creation error", failureConfig: clickhousetest.FailureConfig{ sql.CreateLinkAttributeMetadataMaterializedView: assert.AnError, }, expectedError: "failed to create link attribute metadata materialized view", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { srv := clickhousetest.NewServer(tt.failureConfig) defer srv.Close() cfg := Configuration{ Protocol: "http", Addresses: []string{ srv.Listener.Addr().String(), }, DialTimeout: 1 * time.Second, CreateSchema: true, } f, err := NewFactory(context.Background(), cfg, telemetry.Settings{}) require.ErrorContains(t, err, tt.expectedError) require.Nil(t, f) }) } } func TestPurge(t *testing.T) { tests := []struct { name string failureConfig clickhousetest.FailureConfig expectedError string }{ { name: "truncate spans table error", failureConfig: clickhousetest.FailureConfig{ sql.TruncateSpans: assert.AnError, }, expectedError: "failed to purge spans", }, { name: "truncate services table error", failureConfig: clickhousetest.FailureConfig{ sql.TruncateServices: assert.AnError, }, expectedError: "failed to purge services", }, { name: "truncate operations table error", failureConfig: clickhousetest.FailureConfig{ sql.TruncateOperations: assert.AnError, }, expectedError: "failed to purge operations", }, { name: "truncate trace_id_timestamps table error", failureConfig: clickhousetest.FailureConfig{ sql.TruncateTraceIDTimestamps: assert.AnError, }, expectedError: "failed to purge trace_id_timestamps", }, { name: "truncate attribute_metadata table error", failureConfig: clickhousetest.FailureConfig{ sql.TruncateAttributeMetadata: assert.AnError, }, expectedError: "failed to purge attribute_metadata", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { srv := clickhousetest.NewServer(tt.failureConfig) defer srv.Close() cfg := Configuration{ Protocol: "http", Addresses: []string{ srv.Listener.Addr().String(), }, DialTimeout: 1 * time.Second, CreateSchema: true, } f, err := NewFactory(context.Background(), cfg, telemetry.Settings{}) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, f.Close()) }) err = f.Purge(context.Background()) require.ErrorContains(t, err, tt.expectedError) }) } } func TestGetProtocol(t *testing.T) { tests := []struct { protocol string expected clickhouse.Protocol }{ { protocol: "http", expected: clickhouse.HTTP, }, { protocol: "native", expected: clickhouse.Native, }, { protocol: "", expected: clickhouse.Native, }, { protocol: "unknown", expected: clickhouse.Native, }, } for _, tt := range tests { t.Run(tt.protocol, func(t *testing.T) { result := getProtocol(tt.protocol) require.Equal(t, tt.expected, result) }) } } ================================================ FILE: internal/storage/v2/clickhouse/package_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package clickhouse import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v2/clickhouse/sql/create_attribute_metadata_mv.sql ================================================ CREATE MATERIALIZED VIEW IF NOT EXISTS attribute_metadata_mv TO attribute_metadata AS SELECT tp.1 AS attribute_key, tp.2 AS type, tp.3 AS level FROM ( SELECT arrayJoin(arrayConcat( arrayMap(k -> (k, 'bool', 'span'), bool_attributes.key), arrayMap(k -> (k, 'bool', 'resource'), resource_bool_attributes.key), arrayMap(k -> (k, 'bool', 'scope'), scope_bool_attributes.key), arrayMap(k -> (k, 'double', 'span'), double_attributes.key), arrayMap(k -> (k, 'double', 'resource'), resource_double_attributes.key), arrayMap(k -> (k, 'double', 'scope'), scope_double_attributes.key), arrayMap(k -> (k, 'int', 'span'), int_attributes.key), arrayMap(k -> (k, 'int', 'resource'), resource_int_attributes.key), arrayMap(k -> (k, 'int', 'scope'), scope_int_attributes.key), arrayMap(k -> (k, 'str', 'span'), str_attributes.key), arrayMap(k -> (k, 'str', 'resource'), resource_str_attributes.key), arrayMap(k -> (k, 'str', 'scope'), scope_str_attributes.key), arrayMap(k -> ( multiIf(startsWith(k, '@bytes@'), substring(k, 8), startsWith(k, '@map@'), substring(k, 6), startsWith(k, '@slice@'), substring(k, 8), k), multiIf(startsWith(k, '@bytes@'), 'bytes', startsWith(k, '@map@'), 'map', startsWith(k, '@slice@'), 'slice', ''), 'span' ), complex_attributes.key), arrayMap(k -> ( multiIf(startsWith(k, '@bytes@'), substring(k, 8), startsWith(k, '@map@'), substring(k, 6), startsWith(k, '@slice@'), substring(k, 8), k), multiIf(startsWith(k, '@bytes@'), 'bytes', startsWith(k, '@map@'), 'map', startsWith(k, '@slice@'), 'slice', ''), 'resource' ), resource_complex_attributes.key), arrayMap(k -> ( multiIf(startsWith(k, '@bytes@'), substring(k, 8), startsWith(k, '@map@'), substring(k, 6), startsWith(k, '@slice@'), substring(k, 8), k), multiIf(startsWith(k, '@bytes@'), 'bytes', startsWith(k, '@map@'), 'map', startsWith(k, '@slice@'), 'slice', ''), 'scope' ), scope_complex_attributes.key) )) AS tp FROM spans ) GROUP BY attribute_key, type, level; ================================================ FILE: internal/storage/v2/clickhouse/sql/create_attribute_metadata_table.sql ================================================ CREATE TABLE IF NOT EXISTS attribute_metadata ( attribute_key String, type String, -- 'bool', 'double', 'int', 'str', 'bytes', 'map', 'slice' level String -- 'resource', 'scope', 'span' ) ENGINE = AggregatingMergeTree ORDER BY (attribute_key, type, level) ================================================ FILE: internal/storage/v2/clickhouse/sql/create_event_attribute_metadata_mv.sql ================================================ -- Events and links are stored in a nested list. If we processed them in the same view -- as the main span data, ClickHouse would have to create a "duplicate" row -- of the span for every single link found because of the ARRAY JOIN operation. -- Example: A span with 10 attributes and 5 links would turn into 5 rows -- of 10 attributes each, forcing the database to process 50 items instead of 15. CREATE MATERIALIZED VIEW IF NOT EXISTS event_attribute_metadata_mv TO attribute_metadata AS SELECT tp.1 AS attribute_key, tp.2 AS type, 'event' AS level FROM spans ARRAY JOIN events AS e ARRAY JOIN arrayConcat( arrayMap(k -> (k, 'bool'), e.bool_attributes.key), arrayMap(k -> (k, 'double'), e.double_attributes.key), arrayMap(k -> (k, 'int'), e.int_attributes.key), arrayMap(k -> (k, 'str'), e.str_attributes.key), arrayMap(k -> ( multiIf(startsWith(k, '@bytes@'), substring(k, 8), startsWith(k, '@map@'), substring(k, 6), startsWith(k, '@slice@'), substring(k, 8), k), multiIf(startsWith(k, '@bytes@'), 'bytes', startsWith(k, '@map@'), 'map', startsWith(k, '@slice@'), 'slice', '') ), e.complex_attributes.key) ) AS tp GROUP BY attribute_key, type, level; ================================================ FILE: internal/storage/v2/clickhouse/sql/create_link_attribute_metadata_mv.sql ================================================ CREATE MATERIALIZED VIEW IF NOT EXISTS link_attribute_metadata_mv TO attribute_metadata AS SELECT tp.1 AS attribute_key, tp.2 AS type, 'link' AS level FROM spans ARRAY JOIN links AS l ARRAY JOIN arrayConcat( arrayMap(k -> (k, 'bool'), l.bool_attributes.key), arrayMap(k -> (k, 'double'), l.double_attributes.key), arrayMap(k -> (k, 'int'), l.int_attributes.key), arrayMap(k -> (k, 'str'), l.str_attributes.key), arrayMap(k -> ( multiIf(startsWith(k, '@bytes@'), substring(k, 8), startsWith(k, '@map@'), substring(k, 6), startsWith(k, '@slice@'), substring(k, 8), k), multiIf(startsWith(k, '@bytes@'), 'bytes', startsWith(k, '@map@'), 'map', startsWith(k, '@slice@'), 'slice', '') ), l.complex_attributes.key) ) AS tp GROUP BY attribute_key, type, level; ================================================ FILE: internal/storage/v2/clickhouse/sql/create_operations_mv.sql ================================================ CREATE MATERIALIZED VIEW IF NOT EXISTS operations_mv TO operations AS SELECT name, kind AS span_kind, service_name FROM spans; ================================================ FILE: internal/storage/v2/clickhouse/sql/create_operations_table.sql ================================================ CREATE TABLE IF NOT EXISTS operations ( service_name String, name String, span_kind String ) ENGINE = AggregatingMergeTree ORDER BY (service_name, span_kind); ================================================ FILE: internal/storage/v2/clickhouse/sql/create_services_mv.sql ================================================ CREATE MATERIALIZED VIEW IF NOT EXISTS services_mv TO services AS SELECT service_name AS name FROM spans GROUP BY service_name; ================================================ FILE: internal/storage/v2/clickhouse/sql/create_services_table.sql ================================================ CREATE TABLE IF NOT EXISTS services (name String) ENGINE = AggregatingMergeTree ORDER BY (name); ================================================ FILE: internal/storage/v2/clickhouse/sql/create_spans_table.sql ================================================ CREATE TABLE IF NOT EXISTS spans ( id String, trace_id String, trace_state String, parent_span_id String, name String, kind String, start_time DateTime64 (9), status_code String, status_message String, duration Int64, bool_attributes Nested (key String, value Bool), double_attributes Nested (key String, value Float64), int_attributes Nested (key String, value Int64), str_attributes Nested (key String, value String), complex_attributes Nested (key String, value String), events Nested ( name String, timestamp DateTime64 (9), bool_attributes Nested (key String, value Bool), double_attributes Nested (key String, value Float64), int_attributes Nested (key String, value Int64), str_attributes Nested (key String, value String), complex_attributes Nested (key String, value String) ), links Nested ( trace_id String, span_id String, trace_state String, bool_attributes Nested (key String, value Bool), double_attributes Nested (key String, value Float64), int_attributes Nested (key String, value Int64), str_attributes Nested (key String, value String), complex_attributes Nested (key String, value String) ), service_name String, resource_bool_attributes Nested (key String, value Bool), resource_double_attributes Nested (key String, value Float64), resource_int_attributes Nested (key String, value Int64), resource_str_attributes Nested (key String, value String), resource_complex_attributes Nested (key String, value String), scope_name String, scope_version String, scope_bool_attributes Nested (key String, value Bool), scope_double_attributes Nested (key String, value Float64), scope_int_attributes Nested (key String, value Int64), scope_str_attributes Nested (key String, value String), scope_complex_attributes Nested (key String, value String), INDEX idx_service_name service_name TYPE set(500) GRANULARITY 1, INDEX idx_name name TYPE set(1000) GRANULARITY 1, INDEX idx_start_time start_time TYPE minmax GRANULARITY 1, INDEX idx_duration duration TYPE minmax GRANULARITY 1, INDEX idx_attributes_keys str_attributes.key TYPE bloom_filter GRANULARITY 1, INDEX idx_attributes_values str_attributes.value TYPE bloom_filter GRANULARITY 1, INDEX idx_resource_attributes_keys resource_str_attributes.key TYPE bloom_filter GRANULARITY 1, INDEX idx_resource_attributes_values resource_str_attributes.value TYPE bloom_filter GRANULARITY 1, ) ENGINE = MergeTree PARTITION BY toDate(start_time) ORDER BY (trace_id) ================================================ FILE: internal/storage/v2/clickhouse/sql/create_trace_id_timestamps_mv.sql ================================================ CREATE MATERIALIZED VIEW IF NOT EXISTS trace_id_timestamps_mv TO trace_id_timestamps AS SELECT trace_id, min(start_time) AS start, max(start_time) AS end FROM spans GROUP BY trace_id; ================================================ FILE: internal/storage/v2/clickhouse/sql/create_trace_id_timestamps_table.sql ================================================ CREATE TABLE IF NOT EXISTS trace_id_timestamps ( trace_id String, start SimpleAggregateFunction(min, DateTime64(9)), end SimpleAggregateFunction(max, DateTime64(9)) ) ENGINE = AggregatingMergeTree() ORDER BY (trace_id); ================================================ FILE: internal/storage/v2/clickhouse/sql/package_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package sql import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v2/clickhouse/sql/queries.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package sql import _ "embed" const InsertSpan = ` INSERT INTO spans ( id, trace_id, trace_state, parent_span_id, name, kind, start_time, status_code, status_message, duration, bool_attributes.key, bool_attributes.value, double_attributes.key, double_attributes.value, int_attributes.key, int_attributes.value, str_attributes.key, str_attributes.value, complex_attributes.key, complex_attributes.value, events.name, events.timestamp, events.bool_attributes, events.double_attributes, events.int_attributes, events.str_attributes, events.complex_attributes, links.trace_id, links.span_id, links.trace_state, links.bool_attributes, links.double_attributes, links.int_attributes, links.str_attributes, links.complex_attributes, service_name, resource_bool_attributes.key, resource_bool_attributes.value, resource_double_attributes.key, resource_double_attributes.value, resource_int_attributes.key, resource_int_attributes.value, resource_str_attributes.key, resource_str_attributes.value, resource_complex_attributes.key, resource_complex_attributes.value, scope_name, scope_version, scope_bool_attributes.key, scope_bool_attributes.value, scope_double_attributes.key, scope_double_attributes.value, scope_int_attributes.key, scope_int_attributes.value, scope_str_attributes.key, scope_str_attributes.value, scope_complex_attributes.key, scope_complex_attributes.value ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) ` const SelectSpansQuery = ` SELECT id, trace_id, trace_state, parent_span_id, name, kind, start_time, status_code, status_message, duration, bool_attributes.key, bool_attributes.value, double_attributes.key, double_attributes.value, int_attributes.key, int_attributes.value, str_attributes.key, str_attributes.value, complex_attributes.key, complex_attributes.value, events.name, events.timestamp, events.bool_attributes.key, events.bool_attributes.value, events.double_attributes.key, events.double_attributes.value, events.int_attributes.key, events.int_attributes.value, events.str_attributes.key, events.str_attributes.value, events.complex_attributes.key, events.complex_attributes.value, links.trace_id, links.span_id, links.trace_state, links.bool_attributes.key, links.bool_attributes.value, links.double_attributes.key, links.double_attributes.value, links.int_attributes.key, links.int_attributes.value, links.str_attributes.key, links.str_attributes.value, links.complex_attributes.key, links.complex_attributes.value, service_name, resource_bool_attributes.key, resource_bool_attributes.value, resource_double_attributes.key, resource_double_attributes.value, resource_int_attributes.key, resource_int_attributes.value, resource_str_attributes.key, resource_str_attributes.value, resource_complex_attributes.key, resource_complex_attributes.value, scope_name, scope_version, scope_bool_attributes.key, scope_bool_attributes.value, scope_double_attributes.key, scope_double_attributes.value, scope_int_attributes.key, scope_int_attributes.value, scope_str_attributes.key, scope_str_attributes.value, scope_complex_attributes.key, scope_complex_attributes.value FROM spans s ` const SelectSpansByTraceID = SelectSpansQuery + " WHERE s.trace_id = ?" // SearchTraceIDsBase is the inner SQL fragment for finding distinct trace IDs. // // The query begins with a no-op predicate (`WHERE 1=1`) so that additional // filters can be appended unconditionally using `AND` without needing to check // whether this is the first WHERE clause. const SearchTraceIDsBase = `SELECT DISTINCT s.trace_id FROM spans s WHERE 1=1` // SearchTraceIDs wraps a trace ID subquery with a JOIN to // trace_id_timestamps to retrieve the start and end times for each trace. // The %s placeholder is replaced with the complete inner subquery // (SearchTraceIDsBase + conditions + LIMIT). const SearchTraceIDs = ` SELECT l.trace_id, min(t.start) AS start, max(t.end) AS end FROM ( %s ) l LEFT JOIN trace_id_timestamps t ON l.trace_id = t.trace_id GROUP BY l.trace_id` const SelectServices = ` SELECT name FROM services GROUP BY name ` const SelectOperationsAllKinds = ` SELECT name, span_kind FROM operations WHERE service_name = ? GROUP BY name, span_kind ` const SelectOperationsByKind = ` SELECT name, span_kind FROM operations WHERE service_name = ? AND span_kind = ? GROUP BY name, span_kind ` const SelectAttributeMetadata = ` SELECT attribute_key, type, level FROM attribute_metadata` const TruncateSpans = `TRUNCATE TABLE spans` const TruncateServices = `TRUNCATE TABLE services` const TruncateOperations = `TRUNCATE TABLE operations` const TruncateTraceIDTimestamps = `TRUNCATE TABLE trace_id_timestamps` const TruncateAttributeMetadata = `TRUNCATE TABLE attribute_metadata` //go:embed create_spans_table.sql var CreateSpansTable string //go:embed create_services_table.sql var CreateServicesTable string //go:embed create_services_mv.sql var CreateServicesMaterializedView string //go:embed create_operations_table.sql var CreateOperationsTable string //go:embed create_operations_mv.sql var CreateOperationsMaterializedView string //go:embed create_trace_id_timestamps_table.sql var CreateTraceIDTimestampsTable string //go:embed create_trace_id_timestamps_mv.sql var CreateTraceIDTimestampsMaterializedView string //go:embed create_attribute_metadata_table.sql var CreateAttributeMetadataTable string //go:embed create_attribute_metadata_mv.sql var CreateAttributeMetadataMaterializedView string //go:embed create_event_attribute_metadata_mv.sql var CreateEventAttributeMetadataMaterializedView string //go:embed create_link_attribute_metadata_mv.sql var CreateLinkAttributeMetadataMaterializedView string ================================================ FILE: internal/storage/v2/clickhouse/tracestore/assert_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "encoding/base64" "strings" "testing" "time" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "go.opentelemetry.io/collector/pdata/xpdata" "github.com/jaegertracing/jaeger/internal/jptrace" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/tracestore/dbmodel" ) func requireTracesEqual(t *testing.T, expected []*dbmodel.SpanRow, actual []ptrace.Traces) { t.Helper() require.Len(t, actual, len(expected)) for i, e := range expected { resources := actual[i].ResourceSpans() require.Equal(t, 1, resources.Len()) scopes := resources.At(0).ScopeSpans() require.Equal(t, 1, scopes.Len()) requireScopeEqual(t, e, scopes.At(0).Scope()) spans := scopes.At(0).Spans() require.Equal(t, 1, spans.Len()) requireSpanEqual(t, e, spans.At(0)) } } func requireScopeEqual(t *testing.T, expected *dbmodel.SpanRow, actual pcommon.InstrumentationScope) { t.Helper() require.Equal(t, expected.ScopeName, actual.Name()) require.Equal(t, expected.ScopeVersion, actual.Version()) } func requireSpanEqual(t *testing.T, expected *dbmodel.SpanRow, actual ptrace.Span) { t.Helper() require.Equal(t, expected.ID, actual.SpanID().String()) require.Equal(t, expected.TraceID, actual.TraceID().String()) require.Equal(t, expected.TraceState, actual.TraceState().AsRaw()) require.Equal(t, expected.ParentSpanID, actual.ParentSpanID().String()) require.Equal(t, expected.Name, actual.Name()) require.Equal(t, expected.Kind, jptrace.SpanKindToString(actual.Kind())) require.Equal(t, expected.StartTime.UnixNano(), actual.StartTimestamp().AsTime().UnixNano()) require.Equal(t, expected.StatusCode, actual.Status().Code().String()) require.Equal(t, expected.StatusMessage, actual.Status().Message()) require.Equal(t, time.Duration(expected.Duration), actual.EndTimestamp().AsTime().Sub(actual.StartTimestamp().AsTime())) requireBoolAttrs(t, expected.Attributes.BoolKeys, expected.Attributes.BoolValues, actual.Attributes()) requireDoubleAttrs(t, expected.Attributes.DoubleKeys, expected.Attributes.DoubleValues, actual.Attributes()) requireIntAttrs(t, expected.Attributes.IntKeys, expected.Attributes.IntValues, actual.Attributes()) requireStrAttrs(t, expected.Attributes.StrKeys, expected.Attributes.StrValues, actual.Attributes()) requireComplexAttrs(t, expected.Attributes.ComplexKeys, expected.Attributes.ComplexValues, actual.Attributes()) require.Len(t, expected.EventNames, actual.Events().Len()) for i, e := range actual.Events().All() { require.Equal(t, expected.EventNames[i], e.Name()) require.Equal(t, expected.EventTimestamps[i].UnixNano(), e.Timestamp().AsTime().UnixNano()) requireBoolAttrs(t, expected.EventAttributes.BoolKeys[i], expected.EventAttributes.BoolValues[i], e.Attributes()) requireDoubleAttrs(t, expected.EventAttributes.DoubleKeys[i], expected.EventAttributes.DoubleValues[i], e.Attributes()) requireIntAttrs(t, expected.EventAttributes.IntKeys[i], expected.EventAttributes.IntValues[i], e.Attributes()) requireStrAttrs(t, expected.EventAttributes.StrKeys[i], expected.EventAttributes.StrValues[i], e.Attributes()) requireComplexAttrs(t, expected.EventAttributes.ComplexKeys[i], expected.EventAttributes.ComplexValues[i], e.Attributes()) } require.Len(t, expected.LinkSpanIDs, actual.Links().Len()) for i, l := range actual.Links().All() { require.Equal(t, expected.LinkTraceIDs[i], l.TraceID().String()) require.Equal(t, expected.LinkSpanIDs[i], l.SpanID().String()) require.Equal(t, expected.LinkTraceStates[i], l.TraceState().AsRaw()) } } func requireBoolAttrs(t *testing.T, expectedKeys []string, expectedVals []bool, attrs pcommon.Map) { for i, k := range expectedKeys { val, ok := attrs.Get(k) require.True(t, ok) require.Equal(t, expectedVals[i], val.Bool()) } } func requireDoubleAttrs(t *testing.T, expectedKeys []string, expectedVals []float64, attrs pcommon.Map) { for i, k := range expectedKeys { val, ok := attrs.Get(k) require.True(t, ok) require.InEpsilon(t, expectedVals[i], val.Double(), 1e-9) } } func requireIntAttrs(t *testing.T, expectedKeys []string, expectedVals []int64, attrs pcommon.Map) { for i, k := range expectedKeys { val, ok := attrs.Get(k) require.True(t, ok) require.Equal(t, expectedVals[i], val.Int()) } } func requireStrAttrs(t *testing.T, expectedKeys []string, expectedVals []string, attrs pcommon.Map) { for i, k := range expectedKeys { val, ok := attrs.Get(k) require.True(t, ok) require.Equal(t, expectedVals[i], val.Str()) } } func requireComplexAttrs(t *testing.T, expectedKeys []string, expectedVals []string, attrs pcommon.Map) { for i, k := range expectedKeys { switch { case strings.HasPrefix(k, "@bytes@"): key := strings.TrimPrefix(expectedKeys[i], "@bytes@") val, ok := attrs.Get(key) require.True(t, ok) decoded, err := base64.StdEncoding.DecodeString(expectedVals[i]) require.NoError(t, err) require.Equal(t, decoded, val.Bytes().AsRaw()) case strings.HasPrefix(k, "@map@"): key := strings.TrimPrefix(expectedKeys[i], "@map@") val, ok := attrs.Get(key) require.True(t, ok) m := &xpdata.JSONUnmarshaler{} expectedVal, err := m.UnmarshalValue([]byte(expectedVals[i])) require.NoError(t, err) require.True(t, expectedVal.Map().Equal(val.Map())) case strings.HasPrefix(k, "@slice@"): key := strings.TrimPrefix(expectedKeys[i], "@slice@") val, ok := attrs.Get(key) require.True(t, ok) m := &xpdata.JSONUnmarshaler{} expectedVal, err := m.UnmarshalValue([]byte(expectedVals[i])) require.NoError(t, err) require.True(t, expectedVal.Slice().Equal(val.Slice())) default: t.Fatalf("unsupported complex attribute key: %s", k) } } } ================================================ FILE: internal/storage/v2/clickhouse/tracestore/attribute_metadata.go ================================================ // Copyright (c) 2026 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "context" "fmt" "go.opentelemetry.io/collector/pdata/pcommon" "github.com/jaegertracing/jaeger/internal/jptrace" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/tracestore/dbmodel" ) // attrTypes holds the value types for an attribute key at different levels. // These types are populated by the materialized views defined in // internal/storage/v2/clickhouse/sql/create_attribute_metadata_mv.sql, // internal/storage/v2/clickhouse/sql/create_event_attribute_metadata_mv.sql, and // internal/storage/v2/clickhouse/sql/create_link_attribute_metadata_mv.sql type attrTypes struct { resource []pcommon.ValueType scope []pcommon.ValueType span []pcommon.ValueType event []pcommon.ValueType link []pcommon.ValueType } // attributeMetadata maps attribute keys to their types per level. // Example: attributeMetadata["http.status"].span = ["int", "str"] type attributeMetadata map[string]attrTypes // getAttributeMetadata retrieves the types stored in ClickHouse for attributes that arrive as strings. // // Query Flow: // 1. HTTP/gRPC API receives tag filters as query parameters (e.g., ?tag=http.status:200) // 2. The query parser parses them into map[string]string // 3. The map gets converted to a pcommon.Map using PutStr() for all values // 4. This function receives those string-typed attributes and looks up their actual storage types // // The query APIs (both HTTP and gRPC) only accept string values for tag filters, regardless // of how attributes were originally stored in ClickHouse. For example: // - A bool attribute stored as true arrives as the string "true" // - An int attribute stored as 123 arrives as the string "123" // - A string attribute stored as "ok" arrives as the string "ok" // // To query ClickHouse correctly, we need to: // 1. Look up the actual type(s) from the attribute_metadata table // 2. Convert the string back to the original type for filtering // 3. Query the appropriate typed column (bool_attributes, int_attributes, etc.) // // Since attributes can be stored with different types across different spans // (e.g. "http.status" could be an int in one span and a string in another), // the metadata can return multiple types for a single key. We build OR conditions // to match any of the possible types. // // Only string-typed attributes from pcommon.Map are looked up since those are the ones // that originated from the query API's string-only input format. func (r *Reader) getAttributeMetadata(ctx context.Context, attributes pcommon.Map) (attributeMetadata, error) { query, args := buildSelectAttributeMetadataQuery(attributes) metadata := make(attributeMetadata) if len(args) == 0 { // No string attributes to look up return metadata, nil } rows, err := r.conn.Query(ctx, query, args...) if err != nil { return nil, fmt.Errorf("failed to query attribute metadata: %w", err) } defer rows.Close() for rows.Next() { var attrMeta dbmodel.AttributeMetadata if err := rows.ScanStruct(&attrMeta); err != nil { return nil, fmt.Errorf("failed to scan row: %w", err) } levels := metadata[attrMeta.AttributeKey] switch attrMeta.Level { case "resource": levels.resource = append(levels.resource, jptrace.StringToValueType(attrMeta.Type)) case "scope": levels.scope = append(levels.scope, jptrace.StringToValueType(attrMeta.Type)) case "span": levels.span = append(levels.span, jptrace.StringToValueType(attrMeta.Type)) case "event": levels.event = append(levels.event, jptrace.StringToValueType(attrMeta.Type)) case "link": levels.link = append(levels.link, jptrace.StringToValueType(attrMeta.Type)) default: return nil, fmt.Errorf("unknown attribute level %q", attrMeta.Level) } metadata[attrMeta.AttributeKey] = levels } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating attribute metadata rows: %w", err) } return metadata, nil } ================================================ FILE: internal/storage/v2/clickhouse/tracestore/attribute_metadata_test.go ================================================ // Copyright (c) 2026 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/sql" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/tracestore/dbmodel" ) func TestGetAttributeMetadata_ErrorCases(t *testing.T) { attrs := pcommon.NewMap() attrs.PutStr("http.method", "GET") tests := []struct { name string driver *testDriver expectedErr string }{ { name: "QueryError", driver: &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectAttributeMetadata: { rows: nil, err: assert.AnError, }, }, }, expectedErr: "failed to query attribute metadata", }, { name: "ScanStructError", driver: &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectAttributeMetadata: { rows: &testRows[dbmodel.AttributeMetadata]{ data: []dbmodel.AttributeMetadata{{ AttributeKey: "http.method", Type: "str", Level: "span", }}, scanErr: assert.AnError, }, err: nil, }, }, }, expectedErr: "failed to scan row", }, { name: "RowsIterationError", driver: &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectAttributeMetadata: { rows: &testRows[dbmodel.AttributeMetadata]{ data: []dbmodel.AttributeMetadata{{ AttributeKey: "http.method", Type: "str", Level: "span", }}, scanFn: func(dest any, src dbmodel.AttributeMetadata) error { ptr, ok := dest.(*dbmodel.AttributeMetadata) if !ok { return assert.AnError } *ptr = src return nil }, rowsErr: assert.AnError, }, err: nil, }, }, }, expectedErr: "error iterating attribute metadata rows", }, { name: "UnknownLevelError", driver: &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectAttributeMetadata: { rows: &testRows[dbmodel.AttributeMetadata]{ data: []dbmodel.AttributeMetadata{{ AttributeKey: "http.method", Type: "str", Level: "unknown", }}, scanFn: func(dest any, src dbmodel.AttributeMetadata) error { ptr, ok := dest.(*dbmodel.AttributeMetadata) if !ok { return assert.AnError } *ptr = src return nil }, }, err: nil, }, }, }, expectedErr: "unknown attribute level", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { reader := NewReader(tt.driver, ReaderConfig{}) _, err := reader.getAttributeMetadata(t.Context(), attrs) require.Error(t, err) assert.ErrorContains(t, err, tt.expectedErr) }) } } func TestGetAttributeMetadata_NoStringAttributes(t *testing.T) { attrs := pcommon.NewMap() attrs.PutBool("some.bool", true) attrs.PutInt("some.int", 42) attrs.PutDouble("some.double", 3.14) driver := &testDriver{ t: t, } reader := NewReader(driver, ReaderConfig{}) metadata, err := reader.getAttributeMetadata(t.Context(), attrs) require.NoError(t, err) assert.Empty(t, metadata) assert.Empty(t, driver.recordedQueries) } ================================================ FILE: internal/storage/v2/clickhouse/tracestore/dbmodel/attribute_metadata.go ================================================ // Copyright (c) 2026 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel // AttributeMetadata represents metadata about an attribute stored in ClickHouse. // This is populated by the attribute_metadata materialized view which tracks // all unique (attribute_key, type, level) tuples observed in the spans table. // // The same attribute key can have multiple entries with different types or levels. // For example, "http.status" might appear as both type="int" and type="str" if // different spans store it with different types. type AttributeMetadata struct { // AttributeKey is the name of the attribute (e.g., "http.status", "service.name") AttributeKey string `ch:"attribute_key"` // Type is the data type of the attribute value. // One of: "bool", "double", "int", "str", "bytes", "map", "slice" Type string `ch:"type"` // Level is the scope level where this attribute appears. // One of: "span", "resource", "scope" Level string `ch:"level"` } ================================================ FILE: internal/storage/v2/clickhouse/tracestore/dbmodel/dbmodel_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "encoding/base64" "testing" "time" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "go.opentelemetry.io/collector/pdata/xpdata" "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) func TestRoundTrip(t *testing.T) { now := time.Now().UTC() duration := 2 * time.Second t.Run("ToRow->FromRow", func(t *testing.T) { rs := createTestResource() sc := createTestScope() span := createTestSpan(now, duration) expected := createTestTrace(now, duration) row := ToRow(rs, sc, span) trace := FromRow(row) require.Equal(t, expected, trace) }) t.Run("FromRow->ToRow", func(t *testing.T) { spanRow := createTestSpanRow(t, now, duration) trace := FromRow(spanRow) rs := trace.ResourceSpans().At(0).Resource() sc := trace.ResourceSpans().At(0).ScopeSpans().At(0).Scope() span := trace.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0) row := ToRow(rs, sc, span) require.Equal(t, spanRow, row) }) } func createTestTrace(now time.Time, duration time.Duration) ptrace.Traces { rs := createTestResource() sc := createTestScope() span := createTestSpan(now, duration) td := ptrace.NewTraces() rsSpans := td.ResourceSpans().AppendEmpty() rs.CopyTo(rsSpans.Resource()) scSpans := rsSpans.ScopeSpans().AppendEmpty() sc.CopyTo(scSpans.Scope()) span.CopyTo(scSpans.Spans().AppendEmpty()) return td } func createTestResource() pcommon.Resource { rs := pcommon.NewResource() rs.Attributes().PutStr(otelsemconv.ServiceNameKey, "test-service") addTestAttributes(rs.Attributes()) return rs } func createTestScope() pcommon.InstrumentationScope { sc := pcommon.NewInstrumentationScope() sc.SetName("test-scope") sc.SetVersion("v1.0.0") addTestAttributes(sc.Attributes()) return sc } func createTestSpan(now time.Time, duration time.Duration) ptrace.Span { span := ptrace.NewSpan() span.SetSpanID(pcommon.SpanID([8]byte{0, 0, 0, 0, 0, 0, 0, 1})) span.SetTraceID(pcommon.TraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})) span.TraceState().FromRaw("state1") span.SetParentSpanID([8]byte{0, 0, 0, 0, 0, 0, 0, 2}) span.SetName("test-span") span.SetKind(ptrace.SpanKindServer) span.SetStartTimestamp(pcommon.NewTimestampFromTime(now)) span.SetEndTimestamp(pcommon.NewTimestampFromTime(now.Add(duration))) span.Status().SetCode(ptrace.StatusCodeOk) span.Status().SetMessage("test-status-message") addTestAttributes(span.Attributes()) addSpanEvent(span, now) addSpanLink(span) return span } func addSpanEvent(span ptrace.Span, now time.Time) { event := span.Events().AppendEmpty() event.SetName("test-event") event.SetTimestamp(pcommon.NewTimestampFromTime(now)) addTestAttributes(event.Attributes()) } func addSpanLink(span ptrace.Span) { link := span.Links().AppendEmpty() link.SetTraceID(pcommon.TraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3})) link.SetSpanID(pcommon.SpanID([8]byte{0, 0, 0, 0, 0, 0, 0, 4})) link.TraceState().FromRaw("link-state") addTestAttributes(link.Attributes()) } func addTestAttributes(attrs pcommon.Map) { attrs.PutBool("bool_attr", true) attrs.PutDouble("double_attr", 3.14) attrs.PutInt("int_attr", 42) attrs.PutStr("string_attr", "string_value") attrs.PutEmptyBytes("bytes_attr").FromRaw([]byte("bytes_value")) attrs.PutEmptyMap("map_attr").FromRaw(map[string]any{"key": "value"}) attrs.PutEmptySlice("slice_attr").FromRaw([]any{1, 2, 3}) } func createTestSpanRow(t *testing.T, now time.Time, duration time.Duration) *SpanRow { t.Helper() encodedBytes := base64.StdEncoding.EncodeToString([]byte("bytes_value")) vm := pcommon.NewValueMap() vm.Map().PutStr("key", "value") m := &xpdata.JSONMarshaler{} vmJSON, err := m.MarshalValue(vm) require.NoError(t, err) vs := pcommon.NewValueSlice() vs.Slice().AppendEmpty().SetInt(1) vs.Slice().AppendEmpty().SetInt(2) vs.Slice().AppendEmpty().SetInt(3) vsJSON, err := m.MarshalValue(vs) require.NoError(t, err) return &SpanRow{ ID: "0000000000000001", TraceID: "00000000000000000000000000000001", TraceState: "state1", ParentSpanID: "0000000000000002", Name: "test-span", Kind: "server", StartTime: now, StatusCode: "Ok", StatusMessage: "test-status-message", Duration: duration.Nanoseconds(), Attributes: Attributes{ BoolKeys: []string{"bool_attr"}, BoolValues: []bool{true}, DoubleKeys: []string{"double_attr"}, DoubleValues: []float64{3.14}, IntKeys: []string{"int_attr"}, IntValues: []int64{42}, StrKeys: []string{"string_attr"}, StrValues: []string{"string_value"}, ComplexKeys: []string{"@bytes@bytes_attr", "@map@map_attr", "@slice@slice_attr"}, ComplexValues: []string{encodedBytes, string(vmJSON), string(vsJSON)}, }, EventNames: []string{"test-event"}, EventTimestamps: []time.Time{now}, EventAttributes: Attributes2D{ BoolKeys: [][]string{{"bool_attr"}}, BoolValues: [][]bool{{true}}, DoubleKeys: [][]string{{"double_attr"}}, DoubleValues: [][]float64{{3.14}}, IntKeys: [][]string{{"int_attr"}}, IntValues: [][]int64{{42}}, StrKeys: [][]string{{"string_attr"}}, StrValues: [][]string{{"string_value"}}, ComplexKeys: [][]string{{"@bytes@bytes_attr", "@map@map_attr", "@slice@slice_attr"}}, ComplexValues: [][]string{{encodedBytes, string(vmJSON), string(vsJSON)}}, }, LinkTraceIDs: []string{"00000000000000000000000000000003"}, LinkSpanIDs: []string{"0000000000000004"}, LinkTraceStates: []string{"link-state"}, LinkAttributes: Attributes2D{ BoolKeys: [][]string{{"bool_attr"}}, BoolValues: [][]bool{{true}}, DoubleKeys: [][]string{{"double_attr"}}, DoubleValues: [][]float64{{3.14}}, IntKeys: [][]string{{"int_attr"}}, IntValues: [][]int64{{42}}, StrKeys: [][]string{{"string_attr"}}, StrValues: [][]string{{"string_value"}}, ComplexKeys: [][]string{{"@bytes@bytes_attr", "@map@map_attr", "@slice@slice_attr"}}, ComplexValues: [][]string{{encodedBytes, string(vmJSON), string(vsJSON)}}, }, ServiceName: "test-service", ResourceAttributes: Attributes{ BoolKeys: []string{"bool_attr"}, BoolValues: []bool{true}, DoubleKeys: []string{"double_attr"}, DoubleValues: []float64{3.14}, IntKeys: []string{"int_attr"}, IntValues: []int64{42}, StrKeys: []string{"service.name", "string_attr"}, StrValues: []string{"test-service", "string_value"}, ComplexKeys: []string{"@bytes@bytes_attr", "@map@map_attr", "@slice@slice_attr"}, ComplexValues: []string{encodedBytes, string(vmJSON), string(vsJSON)}, }, ScopeName: "test-scope", ScopeVersion: "v1.0.0", ScopeAttributes: Attributes{ BoolKeys: []string{"bool_attr"}, BoolValues: []bool{true}, DoubleKeys: []string{"double_attr"}, DoubleValues: []float64{3.14}, IntKeys: []string{"int_attr"}, IntValues: []int64{42}, StrKeys: []string{"string_attr"}, StrValues: []string{"string_value"}, ComplexKeys: []string{"@bytes@bytes_attr", "@map@map_attr", "@slice@slice_attr"}, ComplexValues: []string{encodedBytes, string(vmJSON), string(vsJSON)}, }, } } ================================================ FILE: internal/storage/v2/clickhouse/tracestore/dbmodel/from.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "encoding/base64" "encoding/hex" "fmt" "strings" "time" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "go.opentelemetry.io/collector/pdata/xpdata" "github.com/jaegertracing/jaeger/internal/jptrace" "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) // FromRow converts a ClickHouse stored span row to an OpenTelemetry Traces object. func FromRow(storedSpan *SpanRow) ptrace.Traces { trace := ptrace.NewTraces() resourceSpans := trace.ResourceSpans().AppendEmpty() scopeSpans := resourceSpans.ScopeSpans().AppendEmpty() span := scopeSpans.Spans().AppendEmpty() sp, err := convertSpan(storedSpan) sp.CopyTo(span) if err != nil { jptrace.AddWarnings(span, err.Error()) } resource := resourceSpans.Resource() rs := convertResource(storedSpan, span) rs.CopyTo(resource) scope := scopeSpans.Scope() sc := convertScope(storedSpan, span) sc.CopyTo(scope) return trace } func convertResource(sr *SpanRow, spanForWarnings ptrace.Span) pcommon.Resource { resource := ptrace.NewResourceSpans().Resource() resource.Attributes().PutStr(otelsemconv.ServiceNameKey, sr.ServiceName) putAttributes( resource.Attributes(), &sr.ResourceAttributes, spanForWarnings, ) return resource } func convertScope(sr *SpanRow, spanForWarnings ptrace.Span) pcommon.InstrumentationScope { scope := ptrace.NewScopeSpans().Scope() scope.SetName(sr.ScopeName) scope.SetVersion(sr.ScopeVersion) putAttributes( scope.Attributes(), &sr.ScopeAttributes, spanForWarnings, ) return scope } func convertSpan(sr *SpanRow) (ptrace.Span, error) { span := ptrace.NewSpan() span.SetStartTimestamp(pcommon.NewTimestampFromTime(sr.StartTime)) traceId, err := hex.DecodeString(sr.TraceID) if err != nil { return span, fmt.Errorf("failed to decode trace ID: %w", err) } span.SetTraceID(pcommon.TraceID(traceId)) spanId, err := hex.DecodeString(sr.ID) if err != nil { return span, fmt.Errorf("failed to decode span ID: %w", err) } span.SetSpanID(pcommon.SpanID(spanId)) parentSpanId, err := hex.DecodeString(sr.ParentSpanID) if err != nil { return span, fmt.Errorf("failed to decode parent span ID: %w", err) } if len(parentSpanId) != 0 { span.SetParentSpanID(pcommon.SpanID(parentSpanId)) } span.TraceState().FromRaw(sr.TraceState) span.SetName(sr.Name) span.SetKind(jptrace.StringToSpanKind(sr.Kind)) span.SetEndTimestamp(pcommon.NewTimestampFromTime(sr.StartTime.Add(time.Duration(sr.Duration)))) span.Status().SetCode(jptrace.StringToStatusCode(sr.StatusCode)) span.Status().SetMessage(sr.StatusMessage) putAttributes( span.Attributes(), &sr.Attributes, span, ) for i, e := range sr.EventNames { event := span.Events().AppendEmpty() event.SetName(e) event.SetTimestamp(pcommon.NewTimestampFromTime(sr.EventTimestamps[i])) putAttributes2D(event.Attributes(), &sr.EventAttributes, i, span) } for i, l := range sr.LinkTraceIDs { link := span.Links().AppendEmpty() traceID, err := hex.DecodeString(l) if err != nil { jptrace.AddWarnings(span, fmt.Sprintf("failed to decode link trace ID: %v", err)) continue } link.SetTraceID(pcommon.TraceID(traceID)) spanID, err := hex.DecodeString(sr.LinkSpanIDs[i]) if err != nil { jptrace.AddWarnings(span, fmt.Sprintf("failed to decode link span ID: %v", err)) continue } link.SetSpanID(pcommon.SpanID(spanID)) link.TraceState().FromRaw(sr.LinkTraceStates[i]) putAttributes2D(link.Attributes(), &sr.LinkAttributes, i, span) } return span, nil } func putAttributes2D( attrs pcommon.Map, storedAttrs *Attributes2D, idx int, spanForWarnings ptrace.Span, ) { putAttributes( attrs, &Attributes{ BoolKeys: storedAttrs.BoolKeys[idx], BoolValues: storedAttrs.BoolValues[idx], DoubleKeys: storedAttrs.DoubleKeys[idx], DoubleValues: storedAttrs.DoubleValues[idx], IntKeys: storedAttrs.IntKeys[idx], IntValues: storedAttrs.IntValues[idx], StrKeys: storedAttrs.StrKeys[idx], StrValues: storedAttrs.StrValues[idx], ComplexKeys: storedAttrs.ComplexKeys[idx], ComplexValues: storedAttrs.ComplexValues[idx], }, spanForWarnings, ) } func putAttributes( attrs pcommon.Map, storedAttrs *Attributes, spanForWarnings ptrace.Span, ) { for i := 0; i < len(storedAttrs.BoolKeys); i++ { attrs.PutBool(storedAttrs.BoolKeys[i], storedAttrs.BoolValues[i]) } for i := 0; i < len(storedAttrs.DoubleKeys); i++ { attrs.PutDouble(storedAttrs.DoubleKeys[i], storedAttrs.DoubleValues[i]) } for i := 0; i < len(storedAttrs.IntKeys); i++ { attrs.PutInt(storedAttrs.IntKeys[i], storedAttrs.IntValues[i]) } for i := 0; i < len(storedAttrs.StrKeys); i++ { attrs.PutStr(storedAttrs.StrKeys[i], storedAttrs.StrValues[i]) } for i := 0; i < len(storedAttrs.ComplexKeys); i++ { switch { case strings.HasPrefix(storedAttrs.ComplexKeys[i], "@bytes@"): decoded, err := base64.StdEncoding.DecodeString(storedAttrs.ComplexValues[i]) if err != nil { jptrace.AddWarnings(spanForWarnings, fmt.Sprintf("failed to decode bytes attribute %q: %s", storedAttrs.ComplexKeys[i], err.Error())) continue } k := strings.TrimPrefix(storedAttrs.ComplexKeys[i], "@bytes@") attrs.PutEmptyBytes(k).FromRaw(decoded) case strings.HasPrefix(storedAttrs.ComplexKeys[i], "@slice@"): k := strings.TrimPrefix(storedAttrs.ComplexKeys[i], "@slice@") m := &xpdata.JSONUnmarshaler{} val, err := m.UnmarshalValue([]byte(storedAttrs.ComplexValues[i])) if err != nil { jptrace.AddWarnings( spanForWarnings, fmt.Sprintf( "failed to unmarshal slice attribute %q: %s", storedAttrs.ComplexKeys[i], err.Error(), ), ) continue } attrs.PutEmptySlice(k).FromRaw(val.Slice().AsRaw()) case strings.HasPrefix(storedAttrs.ComplexKeys[i], "@map@"): k := strings.TrimPrefix(storedAttrs.ComplexKeys[i], "@map@") m := &xpdata.JSONUnmarshaler{} val, err := m.UnmarshalValue([]byte(storedAttrs.ComplexValues[i])) if err != nil { jptrace.AddWarnings( spanForWarnings, fmt.Sprintf("failed to unmarshal map attribute %q: %s", storedAttrs.ComplexKeys[i], err.Error(), ), ) continue } attrs.PutEmptyMap(k).FromRaw(val.Map().AsRaw()) default: jptrace.AddWarnings( spanForWarnings, fmt.Sprintf("unsupported complex attribute key: %q", storedAttrs.ComplexKeys[i]), ) } } } ================================================ FILE: internal/storage/v2/clickhouse/tracestore/dbmodel/from_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "testing" "time" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger/internal/jptrace" ) func TestFromRow(t *testing.T) { now := time.Now().UTC() duration := 2 * time.Second spanRow := createTestSpanRow(t, now, duration) expected := createTestTrace(now, duration) row := FromRow(spanRow) require.Equal(t, expected, row) } func TestFromRow_DecodeID(t *testing.T) { tests := []struct { name string arg *SpanRow want string }{ { name: "decode span trace id failed", arg: &SpanRow{ TraceID: "0x", }, want: "failed to decode trace ID: encoding/hex: invalid byte: U+0078 'x'", }, { name: "decode span id failed", arg: &SpanRow{ TraceID: "00010001000100010001000100010001", ID: "0x", }, want: "failed to decode span ID: encoding/hex: invalid byte: U+0078 'x'", }, { name: "decode span parent id failed", arg: &SpanRow{ TraceID: "00010001000100010001000100010001", ID: "0001000100010001", ParentSpanID: "0x", }, want: "failed to decode parent span ID: encoding/hex: invalid byte: U+0078 'x'", }, { name: "decode link trace id failed", arg: &SpanRow{ TraceID: "00010001000100010001000100010001", ID: "0001000100010001", ParentSpanID: "0001000100010001", LinkTraceIDs: []string{"0x"}, }, want: "failed to decode link trace ID: encoding/hex: invalid byte: U+0078 'x'", }, { name: "decode link span id failed", arg: &SpanRow{ TraceID: "00010001000100010001000100010001", ID: "0001000100010001", ParentSpanID: "0001000100010001", LinkTraceIDs: []string{"00010001000100010001000100010001"}, LinkSpanIDs: []string{"0x"}, }, want: "failed to decode link span ID: encoding/hex: invalid byte: U+0078 'x'", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { trace := FromRow(tt.arg) span := trace.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0) require.Contains(t, jptrace.GetWarnings(span), tt.want) }) } } func TestPutAttributes_Warnings(t *testing.T) { tests := []struct { name string complexKeys []string complexValues []string expectedWarnContains string }{ { name: "bytes attribute with invalid base64", complexKeys: []string{"@bytes@bytes-key"}, complexValues: []string{"invalid-base64"}, expectedWarnContains: "failed to decode bytes attribute \"@bytes@bytes-key\"", }, { name: "failed to unmarshal slice attribute", complexKeys: []string{"@slice@slice-key"}, complexValues: []string{"notjson"}, expectedWarnContains: "failed to unmarshal slice attribute \"@slice@slice-key\"", }, { name: "failed to unmarshal map attribute", complexKeys: []string{"@map@map-key"}, complexValues: []string{"notjson"}, expectedWarnContains: "failed to unmarshal map attribute \"@map@map-key\"", }, { name: "unsupported complex attribute key", complexKeys: []string{"unsupported"}, complexValues: []string{"{\"kvlistValue\":{\"values\":[{\"key\":\"key\",\"value\":{\"stringValue\":\"value\"}}]}}"}, expectedWarnContains: "unsupported complex attribute key: \"unsupported\"", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { span := ptrace.NewSpan() attributes := pcommon.NewMap() putAttributes( attributes, &Attributes{ ComplexKeys: tt.complexKeys, ComplexValues: tt.complexValues, }, span, ) warnings := jptrace.GetWarnings(span) require.Len(t, warnings, 1) require.Contains(t, warnings[0], tt.expectedWarnContains) }) } } ================================================ FILE: internal/storage/v2/clickhouse/tracestore/dbmodel/operation.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel // Operation represents a single row in the ClickHouse `operations` table. type Operation struct { Name string `ch:"name"` // SpanKind holds the string representation of the span kind from ptrace.SpanKind // in lowercase. SpanKind string `ch:"span_kind"` } ================================================ FILE: internal/storage/v2/clickhouse/tracestore/dbmodel/package_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v2/clickhouse/tracestore/dbmodel/service.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel // Service represents a single row in the ClickHouse `services` table. type Service struct { Name string `ch:"name"` } ================================================ FILE: internal/storage/v2/clickhouse/tracestore/dbmodel/spanrow.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "time" "github.com/ClickHouse/clickhouse-go/v2/lib/driver" ) // SpanRow represents a single record in the ClickHouse `spans` table. // // Complex attributes are non-primitive OTLP types that require special serialization // before being stored. These types are encoded as follows: // // - pcommon.ValueTypeBytes: // Represents raw byte data. The value is Base64-encoded and stored as a string. // Keys for this type are prefixed with `@bytes@`. // // - pcommon.ValueTypeSlice: // Represents an OTLP slice (array). The value is serialized to JSON and stored // as a string. Keys for this type are prefixed with `@slice@`. // // - pcommon.ValueTypeMap: // Represents an OTLP map. The value is serialized to JSON and stored // as a string. Keys for this type are prefixed with `@map@`. type SpanRow struct { // --- Span --- ID string TraceID string TraceState string ParentSpanID string Name string Kind string StartTime time.Time StatusCode string StatusMessage string Duration int64 Attributes Attributes EventNames []string EventTimestamps []time.Time EventAttributes Attributes2D LinkTraceIDs []string LinkSpanIDs []string LinkTraceStates []string LinkAttributes Attributes2D // --- Resource --- ServiceName string ResourceAttributes Attributes // --- Scope --- ScopeName string ScopeVersion string ScopeAttributes Attributes } type Attributes struct { BoolKeys []string BoolValues []bool DoubleKeys []string DoubleValues []float64 IntKeys []string IntValues []int64 StrKeys []string StrValues []string ComplexKeys []string ComplexValues []string } type Attributes2D struct { BoolKeys [][]string BoolValues [][]bool DoubleKeys [][]string DoubleValues [][]float64 IntKeys [][]string IntValues [][]int64 StrKeys [][]string StrValues [][]string ComplexKeys [][]string ComplexValues [][]string } func ScanRow(rows driver.Rows) (*SpanRow, error) { var sr SpanRow err := rows.Scan( &sr.ID, &sr.TraceID, &sr.TraceState, &sr.ParentSpanID, &sr.Name, &sr.Kind, &sr.StartTime, &sr.StatusCode, &sr.StatusMessage, &sr.Duration, &sr.Attributes.BoolKeys, &sr.Attributes.BoolValues, &sr.Attributes.DoubleKeys, &sr.Attributes.DoubleValues, &sr.Attributes.IntKeys, &sr.Attributes.IntValues, &sr.Attributes.StrKeys, &sr.Attributes.StrValues, &sr.Attributes.ComplexKeys, &sr.Attributes.ComplexValues, &sr.EventNames, &sr.EventTimestamps, &sr.EventAttributes.BoolKeys, &sr.EventAttributes.BoolValues, &sr.EventAttributes.DoubleKeys, &sr.EventAttributes.DoubleValues, &sr.EventAttributes.IntKeys, &sr.EventAttributes.IntValues, &sr.EventAttributes.StrKeys, &sr.EventAttributes.StrValues, &sr.EventAttributes.ComplexKeys, &sr.EventAttributes.ComplexValues, &sr.LinkTraceIDs, &sr.LinkSpanIDs, &sr.LinkTraceStates, &sr.LinkAttributes.BoolKeys, &sr.LinkAttributes.BoolValues, &sr.LinkAttributes.DoubleKeys, &sr.LinkAttributes.DoubleValues, &sr.LinkAttributes.IntKeys, &sr.LinkAttributes.IntValues, &sr.LinkAttributes.StrKeys, &sr.LinkAttributes.StrValues, &sr.LinkAttributes.ComplexKeys, &sr.LinkAttributes.ComplexValues, &sr.ServiceName, &sr.ResourceAttributes.BoolKeys, &sr.ResourceAttributes.BoolValues, &sr.ResourceAttributes.DoubleKeys, &sr.ResourceAttributes.DoubleValues, &sr.ResourceAttributes.IntKeys, &sr.ResourceAttributes.IntValues, &sr.ResourceAttributes.StrKeys, &sr.ResourceAttributes.StrValues, &sr.ResourceAttributes.ComplexKeys, &sr.ResourceAttributes.ComplexValues, &sr.ScopeName, &sr.ScopeVersion, &sr.ScopeAttributes.BoolKeys, &sr.ScopeAttributes.BoolValues, &sr.ScopeAttributes.DoubleKeys, &sr.ScopeAttributes.DoubleValues, &sr.ScopeAttributes.IntKeys, &sr.ScopeAttributes.IntValues, &sr.ScopeAttributes.StrKeys, &sr.ScopeAttributes.StrValues, &sr.ScopeAttributes.ComplexKeys, &sr.ScopeAttributes.ComplexValues, ) if err != nil { return nil, err } return &sr, nil } ================================================ FILE: internal/storage/v2/clickhouse/tracestore/dbmodel/to.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "encoding/base64" "fmt" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "go.opentelemetry.io/collector/pdata/xpdata" "github.com/jaegertracing/jaeger/internal/jptrace" "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) // ToRow converts an OpenTelemetry Span along with its Resource and Scope to a // span row that can be stored in ClickHouse. func ToRow( resource pcommon.Resource, scope pcommon.InstrumentationScope, span ptrace.Span, ) *SpanRow { // we assume a sanitizer was applied upstream to guarantee non-empty service name serviceName, _ := resource.Attributes().Get(otelsemconv.ServiceNameKey) duration := span.EndTimestamp().AsTime().Sub(span.StartTimestamp().AsTime()).Nanoseconds() sr := &SpanRow{ ID: span.SpanID().String(), TraceID: span.TraceID().String(), TraceState: span.TraceState().AsRaw(), ParentSpanID: span.ParentSpanID().String(), Name: span.Name(), Kind: jptrace.SpanKindToString(span.Kind()), StartTime: span.StartTimestamp().AsTime(), StatusCode: span.Status().Code().String(), StatusMessage: span.Status().Message(), Duration: duration, ServiceName: serviceName.Str(), ScopeName: scope.Name(), ScopeVersion: scope.Version(), } appendAttributes(&sr.Attributes, span.Attributes()) for _, event := range span.Events().All() { sr.appendEvent(event) } for _, link := range span.Links().All() { sr.appendLink(link) } appendAttributes(&sr.ResourceAttributes, resource.Attributes()) appendAttributes(&sr.ScopeAttributes, scope.Attributes()) return sr } func appendAttributes(dest *Attributes, attrs pcommon.Map) { a := extractAttributes(attrs) dest.BoolKeys = append(dest.BoolKeys, a.BoolKeys...) dest.BoolValues = append(dest.BoolValues, a.BoolValues...) dest.DoubleKeys = append(dest.DoubleKeys, a.DoubleKeys...) dest.DoubleValues = append(dest.DoubleValues, a.DoubleValues...) dest.IntKeys = append(dest.IntKeys, a.IntKeys...) dest.IntValues = append(dest.IntValues, a.IntValues...) dest.StrKeys = append(dest.StrKeys, a.StrKeys...) dest.StrValues = append(dest.StrValues, a.StrValues...) dest.ComplexKeys = append(dest.ComplexKeys, a.ComplexKeys...) dest.ComplexValues = append(dest.ComplexValues, a.ComplexValues...) } func appendAttributes2D(dest *Attributes2D, attrs pcommon.Map) { a := extractAttributes(attrs) dest.BoolKeys = append(dest.BoolKeys, a.BoolKeys) dest.BoolValues = append(dest.BoolValues, a.BoolValues) dest.DoubleKeys = append(dest.DoubleKeys, a.DoubleKeys) dest.DoubleValues = append(dest.DoubleValues, a.DoubleValues) dest.IntKeys = append(dest.IntKeys, a.IntKeys) dest.IntValues = append(dest.IntValues, a.IntValues) dest.StrKeys = append(dest.StrKeys, a.StrKeys) dest.StrValues = append(dest.StrValues, a.StrValues) dest.ComplexKeys = append(dest.ComplexKeys, a.ComplexKeys) dest.ComplexValues = append(dest.ComplexValues, a.ComplexValues) } func (sr *SpanRow) appendEvent(event ptrace.SpanEvent) { sr.EventNames = append(sr.EventNames, event.Name()) sr.EventTimestamps = append(sr.EventTimestamps, event.Timestamp().AsTime()) appendAttributes2D(&sr.EventAttributes, event.Attributes()) } func (sr *SpanRow) appendLink(link ptrace.SpanLink) { sr.LinkTraceIDs = append(sr.LinkTraceIDs, link.TraceID().String()) sr.LinkSpanIDs = append(sr.LinkSpanIDs, link.SpanID().String()) sr.LinkTraceStates = append(sr.LinkTraceStates, link.TraceState().AsRaw()) appendAttributes2D(&sr.LinkAttributes, link.Attributes()) } func extractAttributes(attrs pcommon.Map) *Attributes { out := &Attributes{} attrs.Range(func(k string, v pcommon.Value) bool { switch v.Type() { case pcommon.ValueTypeBool: out.BoolKeys = append(out.BoolKeys, k) out.BoolValues = append(out.BoolValues, v.Bool()) case pcommon.ValueTypeDouble: out.DoubleKeys = append(out.DoubleKeys, k) out.DoubleValues = append(out.DoubleValues, v.Double()) case pcommon.ValueTypeInt: out.IntKeys = append(out.IntKeys, k) out.IntValues = append(out.IntValues, v.Int()) case pcommon.ValueTypeStr: out.StrKeys = append(out.StrKeys, k) out.StrValues = append(out.StrValues, v.Str()) case pcommon.ValueTypeBytes: key := "@bytes@" + k encoded := base64.StdEncoding.EncodeToString(v.Bytes().AsRaw()) out.ComplexKeys = append(out.ComplexKeys, key) out.ComplexValues = append(out.ComplexValues, encoded) case pcommon.ValueTypeSlice: key := "@slice@" + k m := &xpdata.JSONMarshaler{} b, err := m.MarshalValue(v) if err != nil { out.StrKeys = append(out.StrKeys, jptrace.WarningsAttribute) out.StrValues = append( out.StrValues, fmt.Sprintf("failed to marshal slice attribute %q: %v", k, err)) break } out.ComplexKeys = append(out.ComplexKeys, key) out.ComplexValues = append(out.ComplexValues, string(b)) case pcommon.ValueTypeMap: key := "@map@" + k m := &xpdata.JSONMarshaler{} b, err := m.MarshalValue(v) if err != nil { out.StrKeys = append(out.StrKeys, jptrace.WarningsAttribute) out.StrValues = append( out.StrValues, fmt.Sprintf("failed to marshal map attribute %q: %v", k, err)) break } out.ComplexKeys = append(out.ComplexKeys, key) out.ComplexValues = append(out.ComplexValues, string(b)) default: } return true }) return out } ================================================ FILE: internal/storage/v2/clickhouse/tracestore/dbmodel/to_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "testing" "time" "github.com/stretchr/testify/require" ) func TestToRow(t *testing.T) { now := time.Now().UTC() duration := 2 * time.Second rs := createTestResource() sc := createTestScope() span := createTestSpan(now, duration) expected := createTestSpanRow(t, now, duration) row := ToRow(rs, sc, span) require.Equal(t, expected, row) } ================================================ FILE: internal/storage/v2/clickhouse/tracestore/driver_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "context" "errors" "os" "path/filepath" "strconv" "strings" "testing" "github.com/ClickHouse/clickhouse-go/v2/lib/driver" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( snapshotLocation = "./snapshots/" ) // Snapshots can be regenerated via: // // REGENERATE_SNAPSHOTS=true go test -v ./internal/storage/v2/clickhouse/tracestore/... var regenerateSnapshots = os.Getenv("REGENERATE_SNAPSHOTS") == "true" // verifyQuerySnapshot verifies one or more SQL queries against their snapshot files. // Queries are indexed sequentially starting from 1, and snapshot files are named as: // // snapshots/_1.sql, snapshots/_2.sql, etc. // // The order of queries passed to this function determines their index and filename. // For example, verifyQuerySnapshot(t, query1, query2, query3) will verify against: // // snapshots/_1.sql, snapshots/_2.sql, snapshots/_3.sql func verifyQuerySnapshot(t *testing.T, queries ...string) { testName := t.Name() for i, query := range queries { index := i + 1 snapshotFile := filepath.Join(snapshotLocation, testName+"_"+strconv.Itoa(index)+".sql") query = strings.TrimSpace(query) if regenerateSnapshots { dir := filepath.Dir(snapshotFile) if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatalf("failed to create snapshot directory: %v", err) } if err := os.WriteFile(snapshotFile, []byte(query+"\n"), 0o644); err != nil { t.Fatalf("failed to write snapshot file: %v", err) } } snapshot, err := os.ReadFile(snapshotFile) require.NoError(t, err) assert.Equal(t, strings.TrimSpace(string(snapshot)), query, "comparing against stored snapshot. Use REGENERATE_SNAPSHOTS=true to rebuild snapshots.") } } type testBatch struct { driver.Batch t *testing.T appended [][]any appendErr error sendCalled bool sendErr error } func (tb *testBatch) Append(v ...any) error { if tb.appendErr != nil { return tb.appendErr } tb.appended = append(tb.appended, v) return nil } func (tb *testBatch) Send() error { if tb.sendErr != nil { return tb.sendErr } tb.sendCalled = true return nil } func (*testBatch) Close() error { return nil } type testQueryResponse struct { rows driver.Rows err error } type testBatchResponse struct { batch *testBatch err error } type testDriver struct { driver.Conn t *testing.T queryResponses map[string]*testQueryResponse batchResponses map[string]*testBatchResponse recordedQueries []string } func (t *testDriver) Query(_ context.Context, query string, _ ...any) (driver.Rows, error) { t.recordedQueries = append(t.recordedQueries, query) // Normalize whitespace so substring matching works regardless of indentation. normalized := strings.Join(strings.Fields(query), " ") for querySubstring, response := range t.queryResponses { normalizedQuerySubstring := strings.Join(strings.Fields(querySubstring), " ") if strings.Contains(normalized, normalizedQuerySubstring) { return response.rows, response.err } } return nil, nil } type testRows[T any] struct { driver.Rows data []T index int scanErr error scanFn func(dest any, src T) error closeErr error rowsErr error } func (tr *testRows[T]) Close() error { return tr.closeErr } func (tr *testRows[T]) Err() error { return tr.rowsErr } func (tr *testRows[T]) Next() bool { return tr.index < len(tr.data) } func (tr *testRows[T]) ScanStruct(dest any) error { if tr.scanErr != nil { return tr.scanErr } if tr.index >= len(tr.data) { return errors.New("no more rows") } if tr.scanFn == nil { return errors.New("scanFn is not provided") } err := tr.scanFn(dest, tr.data[tr.index]) tr.index++ return err } func (tr *testRows[T]) Scan(dest ...any) error { if tr.scanErr != nil { return tr.scanErr } if tr.index >= len(tr.data) { return errors.New("no more rows") } if tr.scanFn == nil { return errors.New("scanFn is not provided") } err := tr.scanFn(dest, tr.data[tr.index]) tr.index++ return err } func (t *testDriver) PrepareBatch( _ context.Context, query string, _ ...driver.PrepareBatchOption, ) (driver.Batch, error) { t.recordedQueries = append(t.recordedQueries, query) for querySubstring, response := range t.batchResponses { if strings.Contains(query, querySubstring) { return response.batch, response.err } } return nil, nil } ================================================ FILE: internal/storage/v2/clickhouse/tracestore/package_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v2/clickhouse/tracestore/query_builder.go ================================================ // Copyright (c) 2026 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "context" "encoding/base64" "fmt" "strconv" "strings" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/xpdata" "github.com/jaegertracing/jaeger/internal/jptrace" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/sql" ) // marshalValueForQuery is a simpler wrapper around xpdata.JSONMarshaler. // It can be overridden in tests to simulate marshaling errors. var marshalValueForQuery = func(v pcommon.Value) (string, error) { m := &xpdata.JSONMarshaler{} b, err := m.MarshalValue(v) if err != nil { return "", err } return string(b), nil } type typedAttributeValue struct { key string value any valueType pcommon.ValueType } func appendNewlineAndIndent(q *strings.Builder, indent int) { q.WriteString("\n") for range indent { q.WriteString("\t") } } func indentBlock(s string) string { return "\t" + strings.ReplaceAll(s, "\n", "\n\t") } func appendAnd(q *strings.Builder, cond string) { appendNewlineAndIndent(q, 1) q.WriteString("AND ") q.WriteString(cond) } type arrayExistsFn func(q *strings.Builder, indent int, prefix string, valueType pcommon.ValueType) func appendArrayExists(q *strings.Builder, indent int, prefix string, valueType pcommon.ValueType) { strColumnType := jptrace.ValueTypeToString(valueType) if valueType == pcommon.ValueTypeBytes || valueType == pcommon.ValueTypeMap || valueType == pcommon.ValueTypeSlice { strColumnType = "complex" } columnPrefix := "" if prefix != "" { columnPrefix = prefix + "_" } appendNewlineAndIndent(q, indent) q.WriteString("arrayExists((key, value) -> key = ? AND value = ?, s." + columnPrefix + strColumnType + "_attributes.key, s." + columnPrefix + strColumnType + "_attributes.value)") } // appendNestedArrayExists appends a condition that checks for a key-value pair in nested array attributes. // Events and links are stored as nested arrays within spans, so we need to use a nested arrayExists to search // through all items and their attributes. func appendNestedArrayExists(q *strings.Builder, indent int, nestedArray string, valueType pcommon.ValueType) { strColumnType := jptrace.ValueTypeToString(valueType) if valueType == pcommon.ValueTypeBytes || valueType == pcommon.ValueTypeMap || valueType == pcommon.ValueTypeSlice { strColumnType = "complex" } appendNewlineAndIndent(q, indent) q.WriteString("arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x." + strColumnType + "_attributes.key, x." + strColumnType + "_attributes.value), s." + nestedArray + ")") } func appendStringAttributeFallback(q *strings.Builder, args []any, key string, attr pcommon.Value) []any { appendArrayExists(q, 2, "", pcommon.ValueTypeStr) appendNewlineAndIndent(q, 2) q.WriteString("OR ") appendArrayExists(q, 2, "resource", pcommon.ValueTypeStr) appendNewlineAndIndent(q, 2) q.WriteString("OR ") appendArrayExists(q, 2, "scope", pcommon.ValueTypeStr) appendNewlineAndIndent(q, 2) q.WriteString("OR ") appendNestedArrayExists(q, 2, "events", pcommon.ValueTypeStr) appendNewlineAndIndent(q, 2) q.WriteString("OR ") appendNestedArrayExists(q, 2, "links", pcommon.ValueTypeStr) return append(args, key, attr.Str(), key, attr.Str(), key, attr.Str(), key, attr.Str(), key, attr.Str()) } func buildGetTracesQuery(params tracestore.GetTraceParams) (string, []any) { var q strings.Builder q.WriteString(sql.SelectSpansByTraceID) args := []any{params.TraceID} if !params.Start.IsZero() { q.WriteString(" AND s.start_time >= ?") args = append(args, params.Start) } if !params.End.IsZero() { q.WriteString(" AND s.start_time <= ?") args = append(args, params.End) } return q.String(), args } func buildFindTracesQuery(traceIDsQuery string) string { inner := indentBlock("SELECT trace_id FROM (\n" + indentBlock(strings.TrimSpace(traceIDsQuery)) + "\n)") base := strings.TrimRight(sql.SelectSpansQuery, "\n") return base + "\nWHERE s.trace_id IN (\n" + inner + "\n)\nORDER BY s.trace_id" } func (r *Reader) buildFindTraceIDsQuery( ctx context.Context, query tracestore.TraceQueryParams, ) (string, []any, error) { limit := query.SearchDepth if limit == 0 { limit = r.config.DefaultSearchDepth } if limit > r.config.MaxSearchDepth { return "", nil, fmt.Errorf("search depth %d exceeds maximum allowed %d", limit, r.config.MaxSearchDepth) } // Build the inner subquery that finds distinct trace IDs from spans. var inner strings.Builder inner.WriteString(sql.SearchTraceIDsBase) args := []any{} if query.ServiceName != "" { appendAnd(&inner, "s.service_name = ?") args = append(args, query.ServiceName) } if query.OperationName != "" { appendAnd(&inner, "s.name = ?") args = append(args, query.OperationName) } if query.DurationMin > 0 { appendAnd(&inner, "s.duration >= ?") args = append(args, query.DurationMin.Nanoseconds()) } if query.DurationMax > 0 { appendAnd(&inner, "s.duration <= ?") args = append(args, query.DurationMax.Nanoseconds()) } if !query.StartTimeMin.IsZero() { appendAnd(&inner, "s.start_time >= ?") args = append(args, query.StartTimeMin) } if !query.StartTimeMax.IsZero() { appendAnd(&inner, "s.start_time <= ?") args = append(args, query.StartTimeMax) } attributeMetadata, err := r.getAttributeMetadata(ctx, query.Attributes) if err != nil { return "", nil, fmt.Errorf("failed to get attribute metadata: %w", err) } args, err = buildAttributeConditions(&inner, args, query.Attributes, attributeMetadata) if err != nil { return "", nil, err } inner.WriteString("\nLIMIT ?") args = append(args, limit) // Wrap the inner subquery with a JOIN to trace_id_timestamps // to retrieve start/end times only for the limited set of trace IDs. q := fmt.Sprintf(sql.SearchTraceIDs, indentBlock(inner.String())) return q, args, nil } func buildAttributeConditions(q *strings.Builder, args []any, attributes pcommon.Map, metadata attributeMetadata) ([]any, error) { for key, attr := range attributes.All() { appendAnd(q, "(") var err error switch attr.Type() { case pcommon.ValueTypeBool: args = buildSimpleAttributeCondition(q, args, key, pcommon.ValueTypeBool, attr.Bool()) case pcommon.ValueTypeDouble: args = buildSimpleAttributeCondition(q, args, key, pcommon.ValueTypeDouble, attr.Double()) case pcommon.ValueTypeInt: args = buildSimpleAttributeCondition(q, args, key, pcommon.ValueTypeInt, attr.Int()) case pcommon.ValueTypeStr: args = buildStringAttributeCondition(q, args, key, attr, metadata) case pcommon.ValueTypeBytes: args = buildBytesAttributeCondition(q, args, key, attr) case pcommon.ValueTypeSlice: args, err = buildSliceAttributeCondition(q, args, key, attr) if err != nil { return args, err } case pcommon.ValueTypeMap: args, err = buildMapAttributeCondition(q, args, key, attr) if err != nil { return args, err } default: return args, fmt.Errorf("unsupported attribute type %v for key %s", attr.Type(), key) } appendNewlineAndIndent(q, 1) q.WriteString(")") } return args, nil } func buildSimpleAttributeCondition(q *strings.Builder, args []any, key string, valueType pcommon.ValueType, value any) []any { appendArrayExists(q, 2, "", valueType) appendNewlineAndIndent(q, 2) q.WriteString("OR ") appendArrayExists(q, 2, "resource", valueType) appendNewlineAndIndent(q, 2) q.WriteString("OR ") appendNestedArrayExists(q, 2, "events", valueType) appendNewlineAndIndent(q, 2) q.WriteString("OR ") appendNestedArrayExists(q, 2, "links", valueType) return append(args, key, value, key, value, key, value, key, value) } func buildBytesAttributeCondition(q *strings.Builder, args []any, key string, attr pcommon.Value) []any { return buildSimpleAttributeCondition(q, args, "@bytes@"+key, pcommon.ValueTypeBytes, base64.StdEncoding.EncodeToString(attr.Bytes().AsRaw())) } func buildSliceAttributeCondition(q *strings.Builder, args []any, key string, attr pcommon.Value) ([]any, error) { b, err := marshalValueForQuery(attr) if err != nil { return args, fmt.Errorf("failed to marshal slice attribute %q: %w", key, err) } return buildSimpleAttributeCondition(q, args, "@slice@"+key, pcommon.ValueTypeSlice, b), nil } func buildMapAttributeCondition(q *strings.Builder, args []any, key string, attr pcommon.Value) ([]any, error) { b, err := marshalValueForQuery(attr) if err != nil { return args, fmt.Errorf("failed to marshal map attribute %q: %w", key, err) } return buildSimpleAttributeCondition(q, args, "@map@"+key, pcommon.ValueTypeMap, b), nil } func parseStringToTypedValue(key string, attr pcommon.Value, t pcommon.ValueType) (typedAttributeValue, error) { switch t { case pcommon.ValueTypeBool: b, parseErr := strconv.ParseBool(attr.Str()) if parseErr != nil { return typedAttributeValue{}, fmt.Errorf("failed to parse bool attribute %q: %w", key, parseErr) } return typedAttributeValue{key: key, value: b, valueType: t}, nil case pcommon.ValueTypeDouble: f, parseErr := strconv.ParseFloat(attr.Str(), 64) if parseErr != nil { return typedAttributeValue{}, fmt.Errorf("failed to parse double attribute %q: %w", key, parseErr) } return typedAttributeValue{key: key, value: f, valueType: t}, nil case pcommon.ValueTypeInt: i, parseErr := strconv.ParseInt(attr.Str(), 10, 64) if parseErr != nil { return typedAttributeValue{}, fmt.Errorf("failed to parse int attribute %q: %w", key, parseErr) } return typedAttributeValue{key: key, value: i, valueType: t}, nil case pcommon.ValueTypeStr: return typedAttributeValue{key: key, value: attr.Str(), valueType: t}, nil case pcommon.ValueTypeBytes: return typedAttributeValue{key: "@bytes@" + key, value: attr.Str(), valueType: t}, nil case pcommon.ValueTypeMap: return typedAttributeValue{key: "@map@" + key, value: attr.Str(), valueType: t}, nil case pcommon.ValueTypeSlice: return typedAttributeValue{key: "@slice@" + key, value: attr.Str(), valueType: t}, nil default: return typedAttributeValue{}, fmt.Errorf("unsupported attribute type %v for key %q", t, key) } } // buildStringAttributeCondition adds a condition for string attributes by looking up their // actual stored type(s) and level(s) from the attribute_metadata table. // // String attributes require special handling because the query service passes all // attributes as strings (via AsString()), regardless of their actual stored type. // We must look up the attribute_metadata to determine the actual type(s) and // level(s) where this attribute is stored, then convert the string back to the // appropriate type for querying. // // If metadata exists but the value cannot be parsed as any of the metadata types, // we fall back to treating it as a string attribute. func buildStringAttributeCondition( q *strings.Builder, args []any, key string, attr pcommon.Value, metadata attributeMetadata, ) []any { levelTypes, ok := metadata[key] // if no metadata found, assume string type if !ok { return appendStringAttributeFallback(q, args, key, attr) } generatedCondition := false appendLevel := func(types []pcommon.ValueType, prefix string, fn arrayExistsFn) { for _, t := range types { tav, err := parseStringToTypedValue(key, attr, t) if err != nil { // Skip types that can't parse this value continue } if generatedCondition { appendNewlineAndIndent(q, 2) q.WriteString("OR ") } generatedCondition = true fn(q, 2, prefix, tav.valueType) args = append(args, tav.key, tav.value) } } appendLevel(levelTypes.resource, "resource", appendArrayExists) appendLevel(levelTypes.scope, "scope", appendArrayExists) appendLevel(levelTypes.span, "", appendArrayExists) appendLevel(levelTypes.event, "events", appendNestedArrayExists) appendLevel(levelTypes.link, "links", appendNestedArrayExists) // If no conditions were generated (all types failed to parse), // fall back to treating it as a string attribute if !generatedCondition { return appendStringAttributeFallback(q, args, key, attr) } return args } func buildSelectAttributeMetadataQuery(attributes pcommon.Map) (string, []any) { args := []any{} var placeholders []string for key, attr := range attributes.All() { if attr.Type() == pcommon.ValueTypeStr { placeholders = append(placeholders, "?") args = append(args, key) } } var q strings.Builder q.WriteString(sql.SelectAttributeMetadata) if len(placeholders) > 0 { appendNewlineAndIndent(&q, 0) q.WriteString("WHERE") appendNewlineAndIndent(&q, 1) q.WriteString("attribute_key IN (") q.WriteString(strings.Join(placeholders, ", ")) q.WriteString(")") } appendNewlineAndIndent(&q, 0) q.WriteString("GROUP BY") appendNewlineAndIndent(&q, 1) q.WriteString("attribute_key,") appendNewlineAndIndent(&q, 1) q.WriteString("type,") appendNewlineAndIndent(&q, 1) q.WriteString("level") return q.String(), args } ================================================ FILE: internal/storage/v2/clickhouse/tracestore/query_builder_test.go ================================================ // Copyright (c) 2026 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/sql" ) func TestBuildFindTraceIDsQuery_MarshalErrors(t *testing.T) { orig := marshalValueForQuery t.Cleanup(func() { marshalValueForQuery = orig }) marshalValueForQuery = func(pcommon.Value) (string, error) { return "", assert.AnError } t.Run("marshal slice error", func(t *testing.T) { attrs := pcommon.NewMap() s := attrs.PutEmptySlice("bad_slice") s.AppendEmpty() reader := NewReader(&testDriver{t: t}, testReaderConfig) _, _, err := reader.buildFindTraceIDsQuery(t.Context(), tracestore.TraceQueryParams{Attributes: attrs}) require.Error(t, err) require.ErrorContains(t, err, "failed to marshal slice attribute") }) t.Run("marshal map error", func(t *testing.T) { attrs := pcommon.NewMap() m := attrs.PutEmptyMap("bad_map") m.PutEmpty("key") reader := NewReader(&testDriver{t: t}, testReaderConfig) _, _, err := reader.buildFindTraceIDsQuery(t.Context(), tracestore.TraceQueryParams{Attributes: attrs}) require.Error(t, err) require.ErrorContains(t, err, "failed to marshal map attribute") }) } func TestBuildFindTraceIDsQuery_AttributeMetadataError(t *testing.T) { td := &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectAttributeMetadata: { rows: nil, err: assert.AnError, }, }, } reader := NewReader(td, testReaderConfig) _, _, err := reader.buildFindTraceIDsQuery(t.Context(), tracestore.TraceQueryParams{Attributes: buildTestAttributes()}) require.ErrorContains(t, err, "failed to get attribute metadata") } func TestBuildStringAttributeCondition_Fallbacks(t *testing.T) { cases := []struct { name string attrValue string metadata attributeMetadata }{ { name: "parse bool fails falls back to str", attrValue: "not-bool", metadata: attributeMetadata{ "k": {span: []pcommon.ValueType{pcommon.ValueTypeBool}}, }, }, { name: "parse double fails falls back to str", attrValue: "not-float", metadata: attributeMetadata{ "k": {span: []pcommon.ValueType{pcommon.ValueTypeDouble}}, }, }, { name: "parse int fails falls back to str", attrValue: "not-int", metadata: attributeMetadata{ "k": {span: []pcommon.ValueType{pcommon.ValueTypeInt}}, }, }, { name: "unsupported type falls back to str", attrValue: "whatever", metadata: attributeMetadata{ "k": {span: []pcommon.ValueType{pcommon.ValueTypeEmpty}}, }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { attr := pcommon.NewValueStr(tc.attrValue) var q strings.Builder var args []any args = buildStringAttributeCondition(&q, args, "k", attr, tc.metadata) query := q.String() assert.Contains(t, query, "str_attributes") assert.Contains(t, query, "resource_str_attributes") assert.Contains(t, query, "scope_str_attributes") assert.Contains(t, query, "events") assert.Contains(t, query, "links") assert.Len(t, args, 10) }) } } func TestBuildGetTracesQuery(t *testing.T) { tests := []struct { name string params tracestore.GetTraceParams expectedSQL string expectedArgs []any }{ { name: "without time range", params: tracestore.GetTraceParams{ TraceID: traceID, }, expectedSQL: sql.SelectSpansByTraceID, expectedArgs: []any{traceID}, }, { name: "with both start and end", params: tracestore.GetTraceParams{ TraceID: traceID, Start: now.Add(-1 * time.Hour), End: now, }, expectedSQL: sql.SelectSpansByTraceID + " AND s.start_time >= ? AND s.start_time <= ?", expectedArgs: []any{traceID, now.Add(-1 * time.Hour), now}, }, { name: "with only start time", params: tracestore.GetTraceParams{ TraceID: traceID, Start: now.Add(-1 * time.Hour), }, expectedSQL: sql.SelectSpansByTraceID + " AND s.start_time >= ?", expectedArgs: []any{traceID, now.Add(-1 * time.Hour)}, }, { name: "with only end time", params: tracestore.GetTraceParams{ TraceID: traceID, End: now, }, expectedSQL: sql.SelectSpansByTraceID + " AND s.start_time <= ?", expectedArgs: []any{traceID, now}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { query, args := buildGetTracesQuery(tt.params) require.Equal(t, tt.expectedSQL, query) require.Equal(t, tt.expectedArgs, args) }) } } func TestBuildStringAttributeCondition_MultipleTypes(t *testing.T) { attr := pcommon.NewValueStr("123") // parses as both int and str var q strings.Builder var args []any metadata := attributeMetadata{ "http.status": {span: []pcommon.ValueType{pcommon.ValueTypeInt, pcommon.ValueTypeStr}}, } args = buildStringAttributeCondition(&q, args, "http.status", attr, metadata) query := q.String() assert.Contains(t, query, "int_attributes") assert.Contains(t, query, "OR") assert.Contains(t, query, "str_attributes") assert.Len(t, args, 4) } ================================================ FILE: internal/storage/v2/clickhouse/tracestore/reader.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "context" "encoding/hex" "fmt" "iter" "time" "github.com/ClickHouse/clickhouse-go/v2/lib/driver" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/sql" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/tracestore/dbmodel" ) var _ tracestore.Reader = (*Reader)(nil) type ReaderConfig struct { // DefaultSearchDepth is the default number of trace IDs to return when searching for traces. // This value is used when the SearchDepth field in TraceQueryParams is not set. DefaultSearchDepth int // MaxSearchDepth is the maximum number of trace IDs that can be returned when searching for traces. // This value is used to limit the SearchDepth field in TraceQueryParams. MaxSearchDepth int } type Reader struct { conn driver.Conn config ReaderConfig } // NewReader returns a new Reader instance that uses the given ClickHouse connection // to read trace data. // // The provided connection is used exclusively for reading traces, meaning it is safe // to enable instrumentation on the connection without risk of recursively generating traces. func NewReader(conn driver.Conn, cfg ReaderConfig) *Reader { return &Reader{conn: conn, config: cfg} } func (r *Reader) GetTraces( ctx context.Context, traceIDs ...tracestore.GetTraceParams, ) iter.Seq2[[]ptrace.Traces, error] { return func(yield func([]ptrace.Traces, error) bool) { for _, traceID := range traceIDs { query, args := buildGetTracesQuery(traceID) rows, err := r.conn.Query(ctx, query, args...) if err != nil { yield(nil, fmt.Errorf("failed to query trace: %w", err)) return } done := false for rows.Next() { span, err := dbmodel.ScanRow(rows) if err != nil { if !yield(nil, fmt.Errorf("failed to scan span row: %w", err)) { done = true break } continue } trace := dbmodel.FromRow(span) if !yield([]ptrace.Traces{trace}, nil) { done = true break } } if err := rows.Close(); err != nil { yield(nil, fmt.Errorf("failed to close rows: %w", err)) return } if done { return } } } } func (r *Reader) GetServices(ctx context.Context) ([]string, error) { rows, err := r.conn.Query(ctx, sql.SelectServices) if err != nil { return nil, fmt.Errorf("failed to query services: %w", err) } defer rows.Close() var services []string for rows.Next() { var service dbmodel.Service if err := rows.ScanStruct(&service); err != nil { return nil, fmt.Errorf("failed to scan row: %w", err) } services = append(services, service.Name) } return services, nil } func (r *Reader) GetOperations( ctx context.Context, query tracestore.OperationQueryParams, ) ([]tracestore.Operation, error) { var rows driver.Rows var err error if query.SpanKind == "" { rows, err = r.conn.Query(ctx, sql.SelectOperationsAllKinds, query.ServiceName) } else { rows, err = r.conn.Query(ctx, sql.SelectOperationsByKind, query.ServiceName, query.SpanKind) } if err != nil { return nil, fmt.Errorf("failed to query operations: %w", err) } defer rows.Close() var operations []tracestore.Operation for rows.Next() { var operation dbmodel.Operation if err := rows.ScanStruct(&operation); err != nil { return nil, fmt.Errorf("failed to scan row: %w", err) } o := tracestore.Operation{ Name: operation.Name, SpanKind: operation.SpanKind, } operations = append(operations, o) } return operations, nil } func (r *Reader) FindTraces( ctx context.Context, query tracestore.TraceQueryParams, ) iter.Seq2[[]ptrace.Traces, error] { return func(yield func([]ptrace.Traces, error) bool) { traceIDsQuery, args, err := r.buildFindTraceIDsQuery(ctx, query) if err != nil { yield(nil, fmt.Errorf("failed to build query: %w", err)) return } rows, err := r.conn.Query(ctx, buildFindTracesQuery(traceIDsQuery), args...) if err != nil { yield(nil, fmt.Errorf("failed to query traces: %w", err)) return } defer rows.Close() for rows.Next() { span, err := dbmodel.ScanRow(rows) if err != nil { if !yield(nil, fmt.Errorf("failed to scan span row: %w", err)) { break } continue } trace := dbmodel.FromRow(span) if !yield([]ptrace.Traces{trace}, nil) { break } } } } func readRowIntoTraceID(rows driver.Rows) ([]tracestore.FoundTraceID, error) { var traceIDHex string var start, end time.Time if err := rows.Scan(&traceIDHex, &start, &end); err != nil { return nil, fmt.Errorf("failed to scan row: %w", err) } b, err := hex.DecodeString(traceIDHex) if err != nil { return nil, fmt.Errorf("failed to decode trace ID: %w", err) } traceID := tracestore.FoundTraceID{ TraceID: pcommon.TraceID(b), } if !start.IsZero() { traceID.Start = start } if !end.IsZero() { traceID.End = end } return []tracestore.FoundTraceID{ traceID, }, nil } func (r *Reader) FindTraceIDs( ctx context.Context, query tracestore.TraceQueryParams, ) iter.Seq2[[]tracestore.FoundTraceID, error] { return func(yield func([]tracestore.FoundTraceID, error) bool) { q, args, err := r.buildFindTraceIDsQuery(ctx, query) if err != nil { yield(nil, fmt.Errorf("failed to build query: %w", err)) return } rows, err := r.conn.Query(ctx, q, args...) if err != nil { yield(nil, fmt.Errorf("failed to query trace IDs: %w", err)) return } defer rows.Close() for rows.Next() { traceID, err := readRowIntoTraceID(rows) if !yield(traceID, err) { return } } } } ================================================ FILE: internal/storage/v2/clickhouse/tracestore/reader_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "context" "errors" "fmt" "reflect" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger/internal/jiter" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/sql" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/tracestore/dbmodel" ) var ( testReaderConfig = ReaderConfig{ DefaultSearchDepth: 100, MaxSearchDepth: 1000, } testTraceIDsData = [][]any{ { traceIDHex1, now.Add(-1 * time.Hour), now, }, { traceIDHex2, time.Time{}, time.Time{}, }, } testAttributeMetadata = []dbmodel.AttributeMetadata{ {AttributeKey: "span.flag", Type: "bool", Level: "span"}, {AttributeKey: "resource.latency", Type: "double", Level: "resource"}, {AttributeKey: "scope.attempt", Type: "int", Level: "scope"}, {AttributeKey: "http.method", Type: "str", Level: "span"}, {AttributeKey: "http.method", Type: "int", Level: "span"}, {AttributeKey: "resource.checksum", Type: "bytes", Level: "resource"}, {AttributeKey: "metadata", Type: "map", Level: "span"}, {AttributeKey: "tags", Type: "slice", Level: "span"}, {AttributeKey: "event.attr", Type: "str", Level: "event"}, } ) func buildTestAttributes() pcommon.Map { attrs := pcommon.NewMap() attrs.PutBool("login_successful", true) attrs.PutDouble("response_time", 0.123) attrs.PutInt("attempt_count", 1) b := attrs.PutEmptyBytes("file.checksum") s := attrs.PutEmptySlice("http.headers") m := attrs.PutEmptyMap("http.cookies") b.FromRaw([]byte{0x12, 0x34, 0x56, 0x78}) s.AppendEmpty().SetStr("header1: value1") m.PutStr("session_id", "abc123") // these attributes will require type lookup from attribute_metadata attrs.PutStr("no.metadata", "nonexistent") // no metadata entry attrs.PutStr("http.method", "GET") attrs.PutStr("span.flag", "true") attrs.PutStr("resource.latency", "0.5") attrs.PutStr("scope.attempt", "7") attrs.PutStr("resource.checksum", "EjRWeA==") attrs.PutStr("metadata", "{\"kvlistValue\":{\"values\":[{\"key\":\"key\",\"value\":{\"stringValue\":\"value\"}}]}}") attrs.PutStr("tags", "{\"arrayValue\":{\"values\":[{\"intValue\":\"1\"},{\"intValue\":\"2\"},{\"intValue\":\"3\"}]}}") attrs.PutStr("event.attr", "event-value") return attrs } func scanSpanRowFn() func(dest any, src *dbmodel.SpanRow) error { return func(dest any, src *dbmodel.SpanRow) error { ptrs, ok := dest.([]any) if !ok { return fmt.Errorf("expected []any for dest, got %T", dest) } if len(ptrs) != 68 { return fmt.Errorf("expected 68 destination arguments, got %d", len(ptrs)) } values := []any{ &src.ID, &src.TraceID, &src.TraceState, &src.ParentSpanID, &src.Name, &src.Kind, &src.StartTime, &src.StatusCode, &src.StatusMessage, &src.Duration, &src.Attributes.BoolKeys, &src.Attributes.BoolValues, &src.Attributes.DoubleKeys, &src.Attributes.DoubleValues, &src.Attributes.IntKeys, &src.Attributes.IntValues, &src.Attributes.StrKeys, &src.Attributes.StrValues, &src.Attributes.ComplexKeys, &src.Attributes.ComplexValues, &src.EventNames, &src.EventTimestamps, &src.EventAttributes.BoolKeys, &src.EventAttributes.BoolValues, &src.EventAttributes.DoubleKeys, &src.EventAttributes.DoubleValues, &src.EventAttributes.IntKeys, &src.EventAttributes.IntValues, &src.EventAttributes.StrKeys, &src.EventAttributes.StrValues, &src.EventAttributes.ComplexKeys, &src.EventAttributes.ComplexValues, &src.LinkTraceIDs, &src.LinkSpanIDs, &src.LinkTraceStates, &src.LinkAttributes.BoolKeys, &src.LinkAttributes.BoolValues, &src.LinkAttributes.DoubleKeys, &src.LinkAttributes.DoubleValues, &src.LinkAttributes.IntKeys, &src.LinkAttributes.IntValues, &src.LinkAttributes.StrKeys, &src.LinkAttributes.StrValues, &src.LinkAttributes.ComplexKeys, &src.LinkAttributes.ComplexValues, &src.ServiceName, &src.ResourceAttributes.BoolKeys, &src.ResourceAttributes.BoolValues, &src.ResourceAttributes.DoubleKeys, &src.ResourceAttributes.DoubleValues, &src.ResourceAttributes.IntKeys, &src.ResourceAttributes.IntValues, &src.ResourceAttributes.StrKeys, &src.ResourceAttributes.StrValues, &src.ResourceAttributes.ComplexKeys, &src.ResourceAttributes.ComplexValues, &src.ScopeName, &src.ScopeVersion, &src.ScopeAttributes.BoolKeys, &src.ScopeAttributes.BoolValues, &src.ScopeAttributes.DoubleKeys, &src.ScopeAttributes.DoubleValues, &src.ScopeAttributes.IntKeys, &src.ScopeAttributes.IntValues, &src.ScopeAttributes.StrKeys, &src.ScopeAttributes.StrValues, &src.ScopeAttributes.ComplexKeys, &src.ScopeAttributes.ComplexValues, } for i := range ptrs { reflect.ValueOf(ptrs[i]).Elem().Set(reflect.ValueOf(values[i]).Elem()) } return nil } } func scanAttributeMetadataFn() func(dest any, src dbmodel.AttributeMetadata) error { return func(dest any, src dbmodel.AttributeMetadata) error { ptr, ok := dest.(*dbmodel.AttributeMetadata) if !ok { return fmt.Errorf("expected *dbmodel.AttributeMetadata for dest, got %T", dest) } *ptr = src return nil } } func scanTraceIDFn() func(dest any, src []any) error { return func(dest any, src []any) error { ptrs, ok := dest.([]any) if !ok { return fmt.Errorf("expected []any for dest, got %T", dest) } if len(ptrs) != 3 { fmt.Println(src) return fmt.Errorf("expected 3 destination arguments, got %d", len(ptrs)) } ptr, ok := ptrs[0].(*string) if !ok { return fmt.Errorf("expected *string for dest[0], got %T", ptrs[0]) } startPtr, ok := ptrs[1].(*time.Time) if !ok { return fmt.Errorf("expected *time.Time for dest[1], got %T", ptrs[1]) } endPtr, ok := ptrs[2].(*time.Time) if !ok { return fmt.Errorf("expected *time.Time for dest[2], got %T", ptrs[2]) } *ptr = src[0].(string) *startPtr = src[1].(time.Time) *endPtr = src[2].(time.Time) return nil } } func TestGetTraces_Success(t *testing.T) { tests := []struct { name string params tracestore.GetTraceParams data []*dbmodel.SpanRow }{ { name: "single span", params: tracestore.GetTraceParams{ TraceID: traceID, }, data: singleSpan, }, { name: "multiple spans", params: tracestore.GetTraceParams{ TraceID: traceID, }, data: multipleSpans, }, { name: "with time range", params: tracestore.GetTraceParams{ TraceID: traceID, Start: now.Add(-1 * time.Hour), End: now, }, data: singleSpan, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { conn := &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectSpansByTraceID: { rows: &testRows[*dbmodel.SpanRow]{ data: tt.data, scanFn: scanSpanRowFn(), }, err: nil, }, }, } reader := NewReader(conn, testReaderConfig) getTracesIter := reader.GetTraces(context.Background(), tt.params) traces, err := jiter.FlattenWithErrors(getTracesIter) require.NoError(t, err) require.Len(t, conn.recordedQueries, 1) verifyQuerySnapshot(t, conn.recordedQueries...) requireTracesEqual(t, tt.data, traces) }) } } func TestGetTraces_ErrorCases(t *testing.T) { tests := []struct { name string driver *testDriver expectedErr string }{ { name: "QueryError", driver: &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectSpansByTraceID: { rows: nil, err: assert.AnError, }, }, }, expectedErr: "failed to query trace", }, { name: "ScanError", driver: &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectSpansByTraceID: { rows: &testRows[*dbmodel.SpanRow]{ data: singleSpan, scanErr: assert.AnError, }, err: nil, }, }, }, expectedErr: "failed to scan span row", }, { name: "CloseError", driver: &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectSpansByTraceID: { rows: &testRows[*dbmodel.SpanRow]{ data: singleSpan, scanFn: scanSpanRowFn(), closeErr: assert.AnError, }, err: nil, }, }, }, expectedErr: "failed to close rows", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { reader := NewReader(test.driver, testReaderConfig) iter := reader.GetTraces(context.Background(), tracestore.GetTraceParams{ TraceID: traceID, }) _, err := jiter.FlattenWithErrors(iter) require.ErrorContains(t, err, test.expectedErr) }) } } func TestGetTraces_ScanErrorContinues(t *testing.T) { scanCalled := 0 scanFn := func(dest any, src *dbmodel.SpanRow) error { scanCalled++ if scanCalled == 1 { return assert.AnError // simulate scan error on the first row } return scanSpanRowFn()(dest, src) } conn := &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectSpansByTraceID: { rows: &testRows[*dbmodel.SpanRow]{ data: multipleSpans, scanFn: scanFn, }, err: nil, }, }, } reader := NewReader(conn, testReaderConfig) getTracesIter := reader.GetTraces(context.Background(), tracestore.GetTraceParams{ TraceID: traceID, }) expected := multipleSpans[1:] // skip the first span which caused the error for trace, err := range getTracesIter { if err != nil { require.ErrorIs(t, err, assert.AnError) continue } requireTracesEqual(t, expected, trace) } } func TestGetTraces_YieldFalseOnSuccessStopsIteration(t *testing.T) { conn := &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectSpansByTraceID: { rows: &testRows[*dbmodel.SpanRow]{ data: multipleSpans, scanFn: scanSpanRowFn(), }, err: nil, }, }, } reader := NewReader(conn, testReaderConfig) getTracesIter := reader.GetTraces(context.Background(), tracestore.GetTraceParams{ TraceID: traceID, }) var gotTraces []ptrace.Traces getTracesIter(func(traces []ptrace.Traces, err error) bool { require.NoError(t, err) gotTraces = append(gotTraces, traces...) return false // stop iteration after the first span }) require.Len(t, gotTraces, 1) requireTracesEqual(t, multipleSpans[0:1], gotTraces) } func TestGetServices(t *testing.T) { tests := []struct { name string conn *testDriver expected []string expectError string }{ { name: "successfully returns services", conn: &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectServices: { rows: &testRows[dbmodel.Service]{ data: []dbmodel.Service{ {Name: "serviceA"}, {Name: "serviceB"}, {Name: "serviceC"}, }, scanFn: func(dest any, src dbmodel.Service) error { svc, ok := dest.(*dbmodel.Service) if !ok { return errors.New("dest is not *dbmodel.Service") } *svc = src return nil }, }, err: nil, }, }, }, expected: []string{"serviceA", "serviceB", "serviceC"}, }, { name: "query error", conn: &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectServices: { rows: nil, err: assert.AnError, }, }, }, expectError: "failed to query services", }, { name: "scan error", conn: &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectServices: { rows: &testRows[dbmodel.Service]{ data: []dbmodel.Service{ {Name: "serviceA"}, {Name: "serviceB"}, {Name: "serviceC"}, }, scanErr: assert.AnError, }, err: nil, }, }, }, expectError: "failed to scan row", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { reader := NewReader(test.conn, testReaderConfig) result, err := reader.GetServices(context.Background()) if test.expectError != "" { require.ErrorContains(t, err, test.expectError) } else { require.NoError(t, err) require.Len(t, test.conn.recordedQueries, 1) verifyQuerySnapshot(t, test.conn.recordedQueries...) require.Equal(t, test.expected, result) } }) } } func TestGetOperations(t *testing.T) { tests := []struct { name string conn *testDriver query tracestore.OperationQueryParams expected []tracestore.Operation expectError string }{ { name: "successfully returns operations for all kinds", conn: &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectOperationsAllKinds: { rows: &testRows[dbmodel.Operation]{ data: []dbmodel.Operation{ {Name: "operationA"}, {Name: "operationB"}, {Name: "operationC"}, }, scanFn: func(dest any, src dbmodel.Operation) error { svc, ok := dest.(*dbmodel.Operation) if !ok { return errors.New("dest is not *dbmodel.Operation") } *svc = src return nil }, }, err: nil, }, }, }, query: tracestore.OperationQueryParams{ ServiceName: "serviceA", }, expected: []tracestore.Operation{ { Name: "operationA", }, { Name: "operationB", }, { Name: "operationC", }, }, }, { name: "successfully returns operations by kind", conn: &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectOperationsByKind: { rows: &testRows[dbmodel.Operation]{ data: []dbmodel.Operation{ {Name: "operationA", SpanKind: "server"}, {Name: "operationB", SpanKind: "server"}, {Name: "operationC", SpanKind: "server"}, }, scanFn: func(dest any, src dbmodel.Operation) error { svc, ok := dest.(*dbmodel.Operation) if !ok { return errors.New("dest is not *dbmodel.Operation") } *svc = src return nil }, }, err: nil, }, }, }, query: tracestore.OperationQueryParams{ ServiceName: "serviceA", SpanKind: "server", }, expected: []tracestore.Operation{ { Name: "operationA", SpanKind: "server", }, { Name: "operationB", SpanKind: "server", }, { Name: "operationC", SpanKind: "server", }, }, }, { name: "query error", conn: &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectOperationsAllKinds: { rows: nil, err: assert.AnError, }, }, }, expectError: "failed to query operations", }, { name: "scan error", conn: &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectOperationsAllKinds: { rows: &testRows[dbmodel.Operation]{ data: []dbmodel.Operation{ {Name: "operationA"}, {Name: "operationB"}, {Name: "operationC"}, }, scanErr: assert.AnError, }, err: nil, }, }, }, expectError: "failed to scan row", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { reader := NewReader(test.conn, testReaderConfig) result, err := reader.GetOperations(context.Background(), test.query) if test.expectError != "" { require.ErrorContains(t, err, test.expectError) } else { require.NoError(t, err) require.Len(t, test.conn.recordedQueries, 1) verifyQuerySnapshot(t, test.conn.recordedQueries...) require.Equal(t, test.expected, result) } }) } } func TestFindTraces_Success(t *testing.T) { tests := []struct { name string data []*dbmodel.SpanRow }{ { name: "single span", data: singleSpan, }, { name: "multiple spans", data: multipleSpans, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { conn := &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectSpansQuery: { rows: &testRows[*dbmodel.SpanRow]{ data: tt.data, scanFn: scanSpanRowFn(), }, err: nil, }, }, } reader := NewReader(conn, testReaderConfig) findTracesIter := reader.FindTraces(context.Background(), tracestore.TraceQueryParams{ Attributes: pcommon.NewMap(), }) traces, err := jiter.FlattenWithErrors(findTracesIter) require.NoError(t, err) require.Len(t, conn.recordedQueries, 1) verifyQuerySnapshot(t, conn.recordedQueries...) requireTracesEqual(t, tt.data, traces) }) } } func TestFindTraces_WithFilters(t *testing.T) { conn := &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectAttributeMetadata: { rows: &testRows[dbmodel.AttributeMetadata]{ data: testAttributeMetadata, scanFn: scanAttributeMetadataFn(), }, }, sql.SelectSpansQuery: { rows: &testRows[*dbmodel.SpanRow]{ data: multipleSpans, scanFn: scanSpanRowFn(), }, err: nil, }, }, } reader := NewReader(conn, testReaderConfig) attributes := buildTestAttributes() iter := reader.FindTraces(context.Background(), tracestore.TraceQueryParams{ ServiceName: "serviceA", OperationName: "operationA", DurationMin: 1 * time.Nanosecond, DurationMax: 1 * time.Second, StartTimeMin: now.Add(-1 * time.Hour), StartTimeMax: now, Attributes: attributes, SearchDepth: 5, }) traces, err := jiter.FlattenWithErrors(iter) require.NoError(t, err) require.Len(t, conn.recordedQueries, 2) verifyQuerySnapshot(t, conn.recordedQueries...) requireTracesEqual(t, multipleSpans, traces) } func TestFindTraces_SearchDepthExceedsMax(t *testing.T) { driver := &testDriver{ t: t, } reader := NewReader(driver, testReaderConfig) iter := reader.FindTraces(context.Background(), tracestore.TraceQueryParams{ SearchDepth: 10000, Attributes: pcommon.NewMap(), }) _, err := jiter.FlattenWithErrors(iter) require.ErrorContains(t, err, "search depth 10000 exceeds maximum allowed 1000") } func TestFindTraces_YieldFalseOnSuccessStopsIteration(t *testing.T) { conn := &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectSpansQuery: { rows: &testRows[*dbmodel.SpanRow]{ data: multipleSpans, scanFn: scanSpanRowFn(), }, err: nil, }, }, } reader := NewReader(conn, testReaderConfig) findTracesIter := reader.FindTraces(context.Background(), tracestore.TraceQueryParams{ Attributes: pcommon.NewMap(), }) var gotTraces []ptrace.Traces findTracesIter(func(traces []ptrace.Traces, err error) bool { require.NoError(t, err) gotTraces = append(gotTraces, traces...) return false // stop iteration after the first span }) require.Len(t, gotTraces, 1) requireTracesEqual(t, multipleSpans[0:1], gotTraces) } func TestFindTraces_ScanErrorContinues(t *testing.T) { scanCalled := 0 scanFn := func(dest any, src *dbmodel.SpanRow) error { scanCalled++ if scanCalled == 1 { return assert.AnError // simulate scan error on the first row } return scanSpanRowFn()(dest, src) } conn := &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectSpansQuery: { rows: &testRows[*dbmodel.SpanRow]{ data: multipleSpans, scanFn: scanFn, }, err: nil, }, }, } reader := NewReader(conn, testReaderConfig) findTracesIter := reader.FindTraces(context.Background(), tracestore.TraceQueryParams{ Attributes: pcommon.NewMap(), }) expected := multipleSpans[1:] // skip the first span which caused the error for trace, err := range findTracesIter { if err != nil { require.ErrorIs(t, err, assert.AnError) continue } requireTracesEqual(t, expected, trace) } } func TestFindTraces_ErrorCases(t *testing.T) { tests := []struct { name string driver *testDriver expectedErr string }{ { name: "QueryError", driver: &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectSpansQuery: { rows: nil, err: assert.AnError, }, }, }, expectedErr: "failed to query traces", }, { name: "ScanError", driver: &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectSpansQuery: { rows: &testRows[*dbmodel.SpanRow]{ data: singleSpan, scanErr: assert.AnError, }, err: nil, }, }, }, expectedErr: "failed to scan span row", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { reader := NewReader(test.driver, testReaderConfig) iter := reader.FindTraces(context.Background(), tracestore.TraceQueryParams{ Attributes: pcommon.NewMap(), }) _, err := jiter.FlattenWithErrors(iter) require.ErrorContains(t, err, test.expectedErr) }) } } func TestFindTraces_BuildQueryError(t *testing.T) { orig := marshalValueForQuery t.Cleanup(func() { marshalValueForQuery = orig }) marshalValueForQuery = func(pcommon.Value) (string, error) { return "", assert.AnError } attrs := pcommon.NewMap() attrs.PutEmptySlice("bad_slice").AppendEmpty() reader := NewReader(&testDriver{t: t}, testReaderConfig) iter := reader.FindTraces(context.Background(), tracestore.TraceQueryParams{ Attributes: attrs, SearchDepth: 1, }) _, err := jiter.FlattenWithErrors(iter) require.ErrorContains(t, err, "failed to build query") } func TestFindTraceIDs(t *testing.T) { driver := &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SelectAttributeMetadata: { rows: &testRows[dbmodel.AttributeMetadata]{ data: testAttributeMetadata, scanFn: scanAttributeMetadataFn(), }, }, sql.SearchTraceIDsBase: { rows: &testRows[[]any]{ data: testTraceIDsData, scanFn: scanTraceIDFn(), }, err: nil, }, }, } reader := NewReader(driver, testReaderConfig) attributes := buildTestAttributes() iter := reader.FindTraceIDs(context.Background(), tracestore.TraceQueryParams{ ServiceName: "serviceA", OperationName: "operationA", DurationMin: 1 * time.Nanosecond, DurationMax: 1 * time.Second, StartTimeMin: now.Add(-1 * time.Hour), StartTimeMax: now, Attributes: attributes, SearchDepth: 5, }) ids, err := jiter.FlattenWithErrors(iter) require.NoError(t, err) require.Len(t, driver.recordedQueries, 2) verifyQuerySnapshot(t, driver.recordedQueries...) require.Equal(t, []tracestore.FoundTraceID{ { TraceID: pcommon.TraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}), Start: now.Add(-1 * time.Hour), End: now, }, { TraceID: pcommon.TraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}), }, }, ids) } func TestFindTraceIDs_SearchDepthExceedsMax(t *testing.T) { driver := &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SearchTraceIDsBase: { rows: &testRows[[]any]{ data: [][]any{ { "00000000000000000000000000000001", time.Now().Add(-1 * time.Hour), time.Now().Add(-1 * time.Minute), }, { "00000000000000000000000000000002", time.Now().Add(-2 * time.Hour), time.Now().Add(-2 * time.Minute), }, }, scanFn: scanTraceIDFn(), }, err: nil, }, }, } reader := NewReader(driver, testReaderConfig) iter := reader.FindTraceIDs(context.Background(), tracestore.TraceQueryParams{ SearchDepth: 10000, }) _, err := jiter.FlattenWithErrors(iter) require.ErrorContains(t, err, "search depth 10000 exceeds maximum allowed 1000") } func TestFindTraceIDs_YieldFalseOnSuccessStopsIteration(t *testing.T) { conn := &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SearchTraceIDsBase: { rows: &testRows[[]any]{ data: testTraceIDsData, scanFn: scanTraceIDFn(), }, err: nil, }, }, } reader := NewReader(conn, testReaderConfig) findTraceIDsIter := reader.FindTraceIDs(context.Background(), tracestore.TraceQueryParams{ Attributes: pcommon.NewMap(), }) var gotTraceIDs []tracestore.FoundTraceID findTraceIDsIter(func(traceIDs []tracestore.FoundTraceID, err error) bool { require.NoError(t, err) gotTraceIDs = append(gotTraceIDs, traceIDs...) return false // stop iteration after the first trace ID }) require.Len(t, gotTraceIDs, 1) require.Equal(t, []tracestore.FoundTraceID{ { TraceID: pcommon.TraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}), Start: now.Add(-1 * time.Hour), End: now, }, }, gotTraceIDs) } func TestFindTraceIDs_ScanErrorContinues(t *testing.T) { scanCalled := 0 scanFn := func(dest any, src []any) error { scanCalled++ if scanCalled == 1 { return assert.AnError // simulate scan error on the first row } return scanTraceIDFn()(dest, src) } conn := &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SearchTraceIDsBase: { rows: &testRows[[]any]{ data: testTraceIDsData, scanFn: scanFn, }, err: nil, }, }, } reader := NewReader(conn, testReaderConfig) findTraceIDsIter := reader.FindTraceIDs(context.Background(), tracestore.TraceQueryParams{ Attributes: pcommon.NewMap(), }) expected := []tracestore.FoundTraceID{ { TraceID: pcommon.TraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}), }, } for traceID, err := range findTraceIDsIter { if err != nil { require.ErrorIs(t, err, assert.AnError) continue } require.Equal(t, expected, traceID) } } func TestFindTraceIDs_DecodeErrorContinues(t *testing.T) { conn := &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SearchTraceIDsBase: { rows: &testRows[[]any]{ data: [][]any{ testTraceIDsData[0], { "0x", time.Now().Add(-2 * time.Hour), time.Now().Add(-2 * time.Minute), }, { "invalid", time.Now().Add(-3 * time.Hour), time.Now().Add(-3 * time.Minute), }, testTraceIDsData[1], }, scanFn: scanTraceIDFn(), }, err: nil, }, }, } reader := NewReader(conn, ReaderConfig{}) findTraceIDsIter := reader.FindTraceIDs(context.Background(), tracestore.TraceQueryParams{ Attributes: pcommon.NewMap(), }) expectedValidTraceIDs := []tracestore.FoundTraceID{ { TraceID: pcommon.TraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}), Start: now.Add(-1 * time.Hour), End: now, }, { TraceID: pcommon.TraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}), }, } var gotTraceIDs []tracestore.FoundTraceID var errorCount int for traceID, err := range findTraceIDsIter { if err != nil { require.ErrorContains(t, err, "failed to decode trace ID") errorCount++ continue } gotTraceIDs = append(gotTraceIDs, traceID...) } require.Equal(t, 2, errorCount) require.Equal(t, expectedValidTraceIDs, gotTraceIDs) } func TestFindTraceIDs_ErrorCases(t *testing.T) { tests := []struct { name string driver *testDriver expectedErr string }{ { name: "QueryError", driver: &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SearchTraceIDsBase: { rows: nil, err: assert.AnError, }, }, }, expectedErr: "failed to query trace IDs", }, { name: "ScanError", driver: &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SearchTraceIDsBase: { rows: &testRows[[]any]{ data: testTraceIDsData, scanErr: assert.AnError, }, err: nil, }, }, }, expectedErr: "failed to scan row", }, { name: "DecodeError", driver: &testDriver{ t: t, queryResponses: map[string]*testQueryResponse{ sql.SearchTraceIDsBase: { rows: &testRows[[]any]{ data: [][]any{ { "0x", time.Now().Add(-1 * time.Hour), time.Now().Add(-1 * time.Minute), }, }, scanFn: scanTraceIDFn(), }, err: nil, }, }, }, expectedErr: "failed to decode trace ID", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { reader := NewReader(test.driver, ReaderConfig{}) iter := reader.FindTraceIDs(context.Background(), tracestore.TraceQueryParams{ Attributes: pcommon.NewMap(), }) _, err := jiter.FlattenWithErrors(iter) require.ErrorContains(t, err, test.expectedErr) }) } } func TestFindTraceIDs_BuildQueryError(t *testing.T) { orig := marshalValueForQuery t.Cleanup(func() { marshalValueForQuery = orig }) marshalValueForQuery = func(pcommon.Value) (string, error) { return "", assert.AnError } attrs := pcommon.NewMap() attrs.PutEmptyMap("bad_map").PutEmpty("key") reader := NewReader(&testDriver{t: t}, testReaderConfig) iter := reader.FindTraceIDs(context.Background(), tracestore.TraceQueryParams{ Attributes: attrs, SearchDepth: 1, }) _, err := jiter.FlattenWithErrors(iter) require.ErrorContains(t, err, "failed to build query") } ================================================ FILE: internal/storage/v2/clickhouse/tracestore/snapshots/TestFindTraceIDs_1.sql ================================================ SELECT attribute_key, type, level FROM attribute_metadata WHERE attribute_key IN (?, ?, ?, ?, ?, ?, ?, ?, ?) GROUP BY attribute_key, type, level ================================================ FILE: internal/storage/v2/clickhouse/tracestore/snapshots/TestFindTraceIDs_2.sql ================================================ SELECT l.trace_id, min(t.start) AS start, max(t.end) AS end FROM ( SELECT DISTINCT s.trace_id FROM spans s WHERE 1=1 AND s.service_name = ? AND s.name = ? AND s.duration >= ? AND s.duration <= ? AND s.start_time >= ? AND s.start_time <= ? AND ( arrayExists((key, value) -> key = ? AND value = ?, s.bool_attributes.key, s.bool_attributes.value) OR arrayExists((key, value) -> key = ? AND value = ?, s.resource_bool_attributes.key, s.resource_bool_attributes.value) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.bool_attributes.key, x.bool_attributes.value), s.events) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.bool_attributes.key, x.bool_attributes.value), s.links) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.double_attributes.key, s.double_attributes.value) OR arrayExists((key, value) -> key = ? AND value = ?, s.resource_double_attributes.key, s.resource_double_attributes.value) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.double_attributes.key, x.double_attributes.value), s.events) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.double_attributes.key, x.double_attributes.value), s.links) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.int_attributes.key, s.int_attributes.value) OR arrayExists((key, value) -> key = ? AND value = ?, s.resource_int_attributes.key, s.resource_int_attributes.value) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.int_attributes.key, x.int_attributes.value), s.events) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.int_attributes.key, x.int_attributes.value), s.links) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.complex_attributes.key, s.complex_attributes.value) OR arrayExists((key, value) -> key = ? AND value = ?, s.resource_complex_attributes.key, s.resource_complex_attributes.value) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.events) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.links) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.complex_attributes.key, s.complex_attributes.value) OR arrayExists((key, value) -> key = ? AND value = ?, s.resource_complex_attributes.key, s.resource_complex_attributes.value) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.events) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.links) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.complex_attributes.key, s.complex_attributes.value) OR arrayExists((key, value) -> key = ? AND value = ?, s.resource_complex_attributes.key, s.resource_complex_attributes.value) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.events) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.links) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.str_attributes.key, s.str_attributes.value) OR arrayExists((key, value) -> key = ? AND value = ?, s.resource_str_attributes.key, s.resource_str_attributes.value) OR arrayExists((key, value) -> key = ? AND value = ?, s.scope_str_attributes.key, s.scope_str_attributes.value) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.str_attributes.key, x.str_attributes.value), s.events) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.str_attributes.key, x.str_attributes.value), s.links) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.str_attributes.key, s.str_attributes.value) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.bool_attributes.key, s.bool_attributes.value) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.resource_double_attributes.key, s.resource_double_attributes.value) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.scope_int_attributes.key, s.scope_int_attributes.value) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.resource_complex_attributes.key, s.resource_complex_attributes.value) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.complex_attributes.key, s.complex_attributes.value) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.complex_attributes.key, s.complex_attributes.value) ) AND ( arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.str_attributes.key, x.str_attributes.value), s.events) ) LIMIT ? ) l LEFT JOIN trace_id_timestamps t ON l.trace_id = t.trace_id GROUP BY l.trace_id ================================================ FILE: internal/storage/v2/clickhouse/tracestore/snapshots/TestFindTraces_Success/multiple_spans_1.sql ================================================ SELECT id, trace_id, trace_state, parent_span_id, name, kind, start_time, status_code, status_message, duration, bool_attributes.key, bool_attributes.value, double_attributes.key, double_attributes.value, int_attributes.key, int_attributes.value, str_attributes.key, str_attributes.value, complex_attributes.key, complex_attributes.value, events.name, events.timestamp, events.bool_attributes.key, events.bool_attributes.value, events.double_attributes.key, events.double_attributes.value, events.int_attributes.key, events.int_attributes.value, events.str_attributes.key, events.str_attributes.value, events.complex_attributes.key, events.complex_attributes.value, links.trace_id, links.span_id, links.trace_state, links.bool_attributes.key, links.bool_attributes.value, links.double_attributes.key, links.double_attributes.value, links.int_attributes.key, links.int_attributes.value, links.str_attributes.key, links.str_attributes.value, links.complex_attributes.key, links.complex_attributes.value, service_name, resource_bool_attributes.key, resource_bool_attributes.value, resource_double_attributes.key, resource_double_attributes.value, resource_int_attributes.key, resource_int_attributes.value, resource_str_attributes.key, resource_str_attributes.value, resource_complex_attributes.key, resource_complex_attributes.value, scope_name, scope_version, scope_bool_attributes.key, scope_bool_attributes.value, scope_double_attributes.key, scope_double_attributes.value, scope_int_attributes.key, scope_int_attributes.value, scope_str_attributes.key, scope_str_attributes.value, scope_complex_attributes.key, scope_complex_attributes.value FROM spans s WHERE s.trace_id IN ( SELECT trace_id FROM ( SELECT l.trace_id, min(t.start) AS start, max(t.end) AS end FROM ( SELECT DISTINCT s.trace_id FROM spans s WHERE 1=1 LIMIT ? ) l LEFT JOIN trace_id_timestamps t ON l.trace_id = t.trace_id GROUP BY l.trace_id ) ) ORDER BY s.trace_id ================================================ FILE: internal/storage/v2/clickhouse/tracestore/snapshots/TestFindTraces_Success/single_span_1.sql ================================================ SELECT id, trace_id, trace_state, parent_span_id, name, kind, start_time, status_code, status_message, duration, bool_attributes.key, bool_attributes.value, double_attributes.key, double_attributes.value, int_attributes.key, int_attributes.value, str_attributes.key, str_attributes.value, complex_attributes.key, complex_attributes.value, events.name, events.timestamp, events.bool_attributes.key, events.bool_attributes.value, events.double_attributes.key, events.double_attributes.value, events.int_attributes.key, events.int_attributes.value, events.str_attributes.key, events.str_attributes.value, events.complex_attributes.key, events.complex_attributes.value, links.trace_id, links.span_id, links.trace_state, links.bool_attributes.key, links.bool_attributes.value, links.double_attributes.key, links.double_attributes.value, links.int_attributes.key, links.int_attributes.value, links.str_attributes.key, links.str_attributes.value, links.complex_attributes.key, links.complex_attributes.value, service_name, resource_bool_attributes.key, resource_bool_attributes.value, resource_double_attributes.key, resource_double_attributes.value, resource_int_attributes.key, resource_int_attributes.value, resource_str_attributes.key, resource_str_attributes.value, resource_complex_attributes.key, resource_complex_attributes.value, scope_name, scope_version, scope_bool_attributes.key, scope_bool_attributes.value, scope_double_attributes.key, scope_double_attributes.value, scope_int_attributes.key, scope_int_attributes.value, scope_str_attributes.key, scope_str_attributes.value, scope_complex_attributes.key, scope_complex_attributes.value FROM spans s WHERE s.trace_id IN ( SELECT trace_id FROM ( SELECT l.trace_id, min(t.start) AS start, max(t.end) AS end FROM ( SELECT DISTINCT s.trace_id FROM spans s WHERE 1=1 LIMIT ? ) l LEFT JOIN trace_id_timestamps t ON l.trace_id = t.trace_id GROUP BY l.trace_id ) ) ORDER BY s.trace_id ================================================ FILE: internal/storage/v2/clickhouse/tracestore/snapshots/TestFindTraces_WithFilters_1.sql ================================================ SELECT attribute_key, type, level FROM attribute_metadata WHERE attribute_key IN (?, ?, ?, ?, ?, ?, ?, ?, ?) GROUP BY attribute_key, type, level ================================================ FILE: internal/storage/v2/clickhouse/tracestore/snapshots/TestFindTraces_WithFilters_2.sql ================================================ SELECT id, trace_id, trace_state, parent_span_id, name, kind, start_time, status_code, status_message, duration, bool_attributes.key, bool_attributes.value, double_attributes.key, double_attributes.value, int_attributes.key, int_attributes.value, str_attributes.key, str_attributes.value, complex_attributes.key, complex_attributes.value, events.name, events.timestamp, events.bool_attributes.key, events.bool_attributes.value, events.double_attributes.key, events.double_attributes.value, events.int_attributes.key, events.int_attributes.value, events.str_attributes.key, events.str_attributes.value, events.complex_attributes.key, events.complex_attributes.value, links.trace_id, links.span_id, links.trace_state, links.bool_attributes.key, links.bool_attributes.value, links.double_attributes.key, links.double_attributes.value, links.int_attributes.key, links.int_attributes.value, links.str_attributes.key, links.str_attributes.value, links.complex_attributes.key, links.complex_attributes.value, service_name, resource_bool_attributes.key, resource_bool_attributes.value, resource_double_attributes.key, resource_double_attributes.value, resource_int_attributes.key, resource_int_attributes.value, resource_str_attributes.key, resource_str_attributes.value, resource_complex_attributes.key, resource_complex_attributes.value, scope_name, scope_version, scope_bool_attributes.key, scope_bool_attributes.value, scope_double_attributes.key, scope_double_attributes.value, scope_int_attributes.key, scope_int_attributes.value, scope_str_attributes.key, scope_str_attributes.value, scope_complex_attributes.key, scope_complex_attributes.value FROM spans s WHERE s.trace_id IN ( SELECT trace_id FROM ( SELECT l.trace_id, min(t.start) AS start, max(t.end) AS end FROM ( SELECT DISTINCT s.trace_id FROM spans s WHERE 1=1 AND s.service_name = ? AND s.name = ? AND s.duration >= ? AND s.duration <= ? AND s.start_time >= ? AND s.start_time <= ? AND ( arrayExists((key, value) -> key = ? AND value = ?, s.bool_attributes.key, s.bool_attributes.value) OR arrayExists((key, value) -> key = ? AND value = ?, s.resource_bool_attributes.key, s.resource_bool_attributes.value) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.bool_attributes.key, x.bool_attributes.value), s.events) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.bool_attributes.key, x.bool_attributes.value), s.links) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.double_attributes.key, s.double_attributes.value) OR arrayExists((key, value) -> key = ? AND value = ?, s.resource_double_attributes.key, s.resource_double_attributes.value) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.double_attributes.key, x.double_attributes.value), s.events) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.double_attributes.key, x.double_attributes.value), s.links) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.int_attributes.key, s.int_attributes.value) OR arrayExists((key, value) -> key = ? AND value = ?, s.resource_int_attributes.key, s.resource_int_attributes.value) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.int_attributes.key, x.int_attributes.value), s.events) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.int_attributes.key, x.int_attributes.value), s.links) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.complex_attributes.key, s.complex_attributes.value) OR arrayExists((key, value) -> key = ? AND value = ?, s.resource_complex_attributes.key, s.resource_complex_attributes.value) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.events) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.links) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.complex_attributes.key, s.complex_attributes.value) OR arrayExists((key, value) -> key = ? AND value = ?, s.resource_complex_attributes.key, s.resource_complex_attributes.value) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.events) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.links) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.complex_attributes.key, s.complex_attributes.value) OR arrayExists((key, value) -> key = ? AND value = ?, s.resource_complex_attributes.key, s.resource_complex_attributes.value) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.events) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.links) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.str_attributes.key, s.str_attributes.value) OR arrayExists((key, value) -> key = ? AND value = ?, s.resource_str_attributes.key, s.resource_str_attributes.value) OR arrayExists((key, value) -> key = ? AND value = ?, s.scope_str_attributes.key, s.scope_str_attributes.value) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.str_attributes.key, x.str_attributes.value), s.events) OR arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.str_attributes.key, x.str_attributes.value), s.links) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.str_attributes.key, s.str_attributes.value) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.bool_attributes.key, s.bool_attributes.value) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.resource_double_attributes.key, s.resource_double_attributes.value) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.scope_int_attributes.key, s.scope_int_attributes.value) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.resource_complex_attributes.key, s.resource_complex_attributes.value) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.complex_attributes.key, s.complex_attributes.value) ) AND ( arrayExists((key, value) -> key = ? AND value = ?, s.complex_attributes.key, s.complex_attributes.value) ) AND ( arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.str_attributes.key, x.str_attributes.value), s.events) ) LIMIT ? ) l LEFT JOIN trace_id_timestamps t ON l.trace_id = t.trace_id GROUP BY l.trace_id ) ) ORDER BY s.trace_id ================================================ FILE: internal/storage/v2/clickhouse/tracestore/snapshots/TestGetOperations/successfully_returns_operations_by_kind_1.sql ================================================ SELECT name, span_kind FROM operations WHERE service_name = ? AND span_kind = ? GROUP BY name, span_kind ================================================ FILE: internal/storage/v2/clickhouse/tracestore/snapshots/TestGetOperations/successfully_returns_operations_for_all_kinds_1.sql ================================================ SELECT name, span_kind FROM operations WHERE service_name = ? GROUP BY name, span_kind ================================================ FILE: internal/storage/v2/clickhouse/tracestore/snapshots/TestGetServices/successfully_returns_services_1.sql ================================================ SELECT name FROM services GROUP BY name ================================================ FILE: internal/storage/v2/clickhouse/tracestore/snapshots/TestGetTraces_Success/multiple_spans_1.sql ================================================ SELECT id, trace_id, trace_state, parent_span_id, name, kind, start_time, status_code, status_message, duration, bool_attributes.key, bool_attributes.value, double_attributes.key, double_attributes.value, int_attributes.key, int_attributes.value, str_attributes.key, str_attributes.value, complex_attributes.key, complex_attributes.value, events.name, events.timestamp, events.bool_attributes.key, events.bool_attributes.value, events.double_attributes.key, events.double_attributes.value, events.int_attributes.key, events.int_attributes.value, events.str_attributes.key, events.str_attributes.value, events.complex_attributes.key, events.complex_attributes.value, links.trace_id, links.span_id, links.trace_state, links.bool_attributes.key, links.bool_attributes.value, links.double_attributes.key, links.double_attributes.value, links.int_attributes.key, links.int_attributes.value, links.str_attributes.key, links.str_attributes.value, links.complex_attributes.key, links.complex_attributes.value, service_name, resource_bool_attributes.key, resource_bool_attributes.value, resource_double_attributes.key, resource_double_attributes.value, resource_int_attributes.key, resource_int_attributes.value, resource_str_attributes.key, resource_str_attributes.value, resource_complex_attributes.key, resource_complex_attributes.value, scope_name, scope_version, scope_bool_attributes.key, scope_bool_attributes.value, scope_double_attributes.key, scope_double_attributes.value, scope_int_attributes.key, scope_int_attributes.value, scope_str_attributes.key, scope_str_attributes.value, scope_complex_attributes.key, scope_complex_attributes.value FROM spans s WHERE s.trace_id = ? ================================================ FILE: internal/storage/v2/clickhouse/tracestore/snapshots/TestGetTraces_Success/single_span_1.sql ================================================ SELECT id, trace_id, trace_state, parent_span_id, name, kind, start_time, status_code, status_message, duration, bool_attributes.key, bool_attributes.value, double_attributes.key, double_attributes.value, int_attributes.key, int_attributes.value, str_attributes.key, str_attributes.value, complex_attributes.key, complex_attributes.value, events.name, events.timestamp, events.bool_attributes.key, events.bool_attributes.value, events.double_attributes.key, events.double_attributes.value, events.int_attributes.key, events.int_attributes.value, events.str_attributes.key, events.str_attributes.value, events.complex_attributes.key, events.complex_attributes.value, links.trace_id, links.span_id, links.trace_state, links.bool_attributes.key, links.bool_attributes.value, links.double_attributes.key, links.double_attributes.value, links.int_attributes.key, links.int_attributes.value, links.str_attributes.key, links.str_attributes.value, links.complex_attributes.key, links.complex_attributes.value, service_name, resource_bool_attributes.key, resource_bool_attributes.value, resource_double_attributes.key, resource_double_attributes.value, resource_int_attributes.key, resource_int_attributes.value, resource_str_attributes.key, resource_str_attributes.value, resource_complex_attributes.key, resource_complex_attributes.value, scope_name, scope_version, scope_bool_attributes.key, scope_bool_attributes.value, scope_double_attributes.key, scope_double_attributes.value, scope_int_attributes.key, scope_int_attributes.value, scope_str_attributes.key, scope_str_attributes.value, scope_complex_attributes.key, scope_complex_attributes.value FROM spans s WHERE s.trace_id = ? ================================================ FILE: internal/storage/v2/clickhouse/tracestore/snapshots/TestGetTraces_Success/with_time_range_1.sql ================================================ SELECT id, trace_id, trace_state, parent_span_id, name, kind, start_time, status_code, status_message, duration, bool_attributes.key, bool_attributes.value, double_attributes.key, double_attributes.value, int_attributes.key, int_attributes.value, str_attributes.key, str_attributes.value, complex_attributes.key, complex_attributes.value, events.name, events.timestamp, events.bool_attributes.key, events.bool_attributes.value, events.double_attributes.key, events.double_attributes.value, events.int_attributes.key, events.int_attributes.value, events.str_attributes.key, events.str_attributes.value, events.complex_attributes.key, events.complex_attributes.value, links.trace_id, links.span_id, links.trace_state, links.bool_attributes.key, links.bool_attributes.value, links.double_attributes.key, links.double_attributes.value, links.int_attributes.key, links.int_attributes.value, links.str_attributes.key, links.str_attributes.value, links.complex_attributes.key, links.complex_attributes.value, service_name, resource_bool_attributes.key, resource_bool_attributes.value, resource_double_attributes.key, resource_double_attributes.value, resource_int_attributes.key, resource_int_attributes.value, resource_str_attributes.key, resource_str_attributes.value, resource_complex_attributes.key, resource_complex_attributes.value, scope_name, scope_version, scope_bool_attributes.key, scope_bool_attributes.value, scope_double_attributes.key, scope_double_attributes.value, scope_int_attributes.key, scope_int_attributes.value, scope_str_attributes.key, scope_str_attributes.value, scope_complex_attributes.key, scope_complex_attributes.value FROM spans s WHERE s.trace_id = ? AND s.start_time >= ? AND s.start_time <= ? ================================================ FILE: internal/storage/v2/clickhouse/tracestore/snapshots/TestWriter_Success_1.sql ================================================ INSERT INTO spans ( id, trace_id, trace_state, parent_span_id, name, kind, start_time, status_code, status_message, duration, bool_attributes.key, bool_attributes.value, double_attributes.key, double_attributes.value, int_attributes.key, int_attributes.value, str_attributes.key, str_attributes.value, complex_attributes.key, complex_attributes.value, events.name, events.timestamp, events.bool_attributes, events.double_attributes, events.int_attributes, events.str_attributes, events.complex_attributes, links.trace_id, links.span_id, links.trace_state, links.bool_attributes, links.double_attributes, links.int_attributes, links.str_attributes, links.complex_attributes, service_name, resource_bool_attributes.key, resource_bool_attributes.value, resource_double_attributes.key, resource_double_attributes.value, resource_int_attributes.key, resource_int_attributes.value, resource_str_attributes.key, resource_str_attributes.value, resource_complex_attributes.key, resource_complex_attributes.value, scope_name, scope_version, scope_bool_attributes.key, scope_bool_attributes.value, scope_double_attributes.key, scope_double_attributes.value, scope_int_attributes.key, scope_int_attributes.value, scope_str_attributes.key, scope_str_attributes.value, scope_complex_attributes.key, scope_complex_attributes.value ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) ================================================ FILE: internal/storage/v2/clickhouse/tracestore/spans_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "time" "go.opentelemetry.io/collector/pdata/pcommon" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/tracestore/dbmodel" ) var traceID = pcommon.TraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}) var ( traceIDHex1 = "00000000000000000000000000000001" traceIDHex2 = "00000000000000000000000000000002" ) var now = time.Date(2025, 6, 14, 10, 0, 0, 0, time.UTC) var singleSpan = []*dbmodel.SpanRow{ { ID: "0000000000000001", TraceID: traceID.String(), TraceState: "state1", Name: "GET /api/user", Kind: "server", StartTime: now, StatusCode: "Ok", StatusMessage: "success", Duration: 1_000_000_000, Attributes: dbmodel.Attributes{ BoolKeys: []string{"authenticated", "cache_hit"}, BoolValues: []bool{true, false}, DoubleKeys: []string{"response_time", "cpu_usage"}, DoubleValues: []float64{0.123, 45.67}, IntKeys: []string{"user_id", "request_size"}, IntValues: []int64{12345, 1024}, StrKeys: []string{"http.method", "http.url"}, StrValues: []string{"GET", "/api/user"}, ComplexKeys: []string{ "@bytes@request_body", "@map@metadata", "@slice@tags", }, ComplexValues: []string{ "eyJuYW1lIjoidGVzdCJ9", "{\"kvlistValue\":{\"values\":[{\"key\":\"key\",\"value\":{\"stringValue\":\"value\"}}]}}", "{\"arrayValue\":{\"values\":[{\"intValue\":\"1\"},{\"intValue\":\"2\"},{\"intValue\":\"3\"}]}}", }, }, EventNames: []string{"login"}, EventTimestamps: []time.Time{now}, EventAttributes: dbmodel.Attributes2D{ BoolKeys: [][]string{{"event.authenticated", "event.cached"}}, BoolValues: [][]bool{{true, false}}, DoubleKeys: [][]string{{"event.response_time"}}, DoubleValues: [][]float64{{0.001}}, IntKeys: [][]string{{"event.sequence"}}, IntValues: [][]int64{{1}}, StrKeys: [][]string{{"event.message"}}, StrValues: [][]string{{"user login successful"}}, ComplexKeys: [][]string{ { "@bytes@event.payload", "@map@metadata", "@slice@tags", }, }, ComplexValues: [][]string{ { "eyJ1c2VyX2lkIjoxMjM0NX0=", "{\"kvlistValue\":{\"values\":[{\"key\":\"key\",\"value\":{\"stringValue\":\"value\"}}]}}", "{\"arrayValue\":{\"values\":[{\"intValue\":\"1\"},{\"intValue\":\"2\"},{\"intValue\":\"3\"}]}}", }, }, }, LinkTraceIDs: []string{"00000000000000000000000000000002"}, LinkSpanIDs: []string{"0000000000000002"}, LinkTraceStates: []string{"state2"}, LinkAttributes: dbmodel.Attributes2D{ BoolKeys: [][]string{{"link.validated", "link.active"}}, BoolValues: [][]bool{{true, true}}, DoubleKeys: [][]string{{"link.weight"}}, DoubleValues: [][]float64{{0.8}}, IntKeys: [][]string{{"link.priority"}}, IntValues: [][]int64{{1}}, StrKeys: [][]string{{"link.type"}}, StrValues: [][]string{{"follows_from"}}, ComplexKeys: [][]string{ { "@bytes@link.metadata", "@map@metadata", "@slice@tags", }, }, ComplexValues: [][]string{ { "eyJsaW5rX2lkIjoxfQ==", "{\"kvlistValue\":{\"values\":[{\"key\":\"key\",\"value\":{\"stringValue\":\"value\"}}]}}", "{\"arrayValue\":{\"values\":[{\"intValue\":\"1\"},{\"intValue\":\"2\"},{\"intValue\":\"3\"}]}}", }, }, }, ServiceName: "user-service", ResourceAttributes: dbmodel.Attributes{ BoolKeys: []string{"resource.available", "resource.active"}, BoolValues: []bool{true, true}, DoubleKeys: []string{"resource.cpu_limit", "resource.memory_usage"}, DoubleValues: []float64{2.5, 80.5}, IntKeys: []string{"resource.instance_id", "resource.port"}, IntValues: []int64{12345, 8080}, StrKeys: []string{"service.name", "resource.host", "resource.region"}, StrValues: []string{"user-service", "host-1", "us-west-1"}, ComplexKeys: []string{ "@bytes@resource.metadata", "@map@metadata", "@slice@tags", }, ComplexValues: []string{ "eyJkZXBsb3ltZW50IjoicHJvZCJ9", "{\"kvlistValue\":{\"values\":[{\"key\":\"key\",\"value\":{\"stringValue\":\"value\"}}]}}", "{\"arrayValue\":{\"values\":[{\"intValue\":\"1\"},{\"intValue\":\"2\"},{\"intValue\":\"3\"}]}}", }, }, ScopeName: "auth-scope", ScopeVersion: "v1.0.0", ScopeAttributes: dbmodel.Attributes{ BoolKeys: []string{"scope.enabled", "scope.persistent"}, BoolValues: []bool{true, false}, DoubleKeys: []string{"scope.version_number", "scope.priority"}, DoubleValues: []float64{1.0, 0.8}, IntKeys: []string{"scope.instance_count", "scope.max_spans"}, IntValues: []int64{5, 1000}, StrKeys: []string{"scope.environment", "scope.component"}, StrValues: []string{"production", "auth"}, ComplexKeys: []string{ "@bytes@scope.metadata", "@map@metadata", "@slice@tags", }, ComplexValues: []string{ "eyJzY29wZV90eXBlIjoiYXV0aGVudGljYXRpb24ifQ==", "{\"kvlistValue\":{\"values\":[{\"key\":\"key\",\"value\":{\"stringValue\":\"value\"}}]}}", "{\"arrayValue\":{\"values\":[{\"intValue\":\"1\"},{\"intValue\":\"2\"},{\"intValue\":\"3\"}]}}", }, }, }, } var multipleSpans = []*dbmodel.SpanRow{ { ID: "0000000000000001", TraceID: traceID.String(), TraceState: "state1", Name: "GET /api/user", Kind: "server", StartTime: now, StatusCode: "Ok", StatusMessage: "success", Duration: 1_000_000_000, Attributes: dbmodel.Attributes{ BoolKeys: []string{"authenticated", "cache_hit"}, BoolValues: []bool{true, false}, DoubleKeys: []string{"response_time", "cpu_usage"}, DoubleValues: []float64{0.123, 45.67}, IntKeys: []string{"user_id", "request_size"}, IntValues: []int64{12345, 1024}, StrKeys: []string{"http.method", "http.url"}, StrValues: []string{"GET", "/api/user"}, ComplexKeys: []string{ "@bytes@request_body", "@map@metadata", "@slice@tags", }, ComplexValues: []string{ "eyJuYW1lIjoidGVzdCJ9", "{\"kvlistValue\":{\"values\":[{\"key\":\"key\",\"value\":{\"stringValue\":\"value\"}}]}}", "{\"arrayValue\":{\"values\":[{\"intValue\":\"1\"},{\"intValue\":\"2\"},{\"intValue\":\"3\"}]}}", }, }, EventNames: []string{"login"}, EventTimestamps: []time.Time{now}, EventAttributes: dbmodel.Attributes2D{ BoolKeys: [][]string{{"event.authenticated", "event.cached"}}, BoolValues: [][]bool{{true, false}}, DoubleKeys: [][]string{{"event.response_time"}}, DoubleValues: [][]float64{{0.001}}, IntKeys: [][]string{{"event.sequence"}}, IntValues: [][]int64{{1}}, StrKeys: [][]string{{"event.message"}}, StrValues: [][]string{{"user login successful"}}, ComplexKeys: [][]string{ { "@bytes@event.payload", "@map@metadata", "@slice@tags", }, }, ComplexValues: [][]string{ { "eyJ1c2VyX2lkIjoxMjM0NX0=", "{\"kvlistValue\":{\"values\":[{\"key\":\"key\",\"value\":{\"stringValue\":\"value\"}}]}}", "{\"arrayValue\":{\"values\":[{\"intValue\":\"1\"},{\"intValue\":\"2\"},{\"intValue\":\"3\"}]}}", }, }, }, LinkTraceIDs: []string{"00000000000000000000000000000002"}, LinkSpanIDs: []string{"0000000000000002"}, LinkTraceStates: []string{"state2"}, LinkAttributes: dbmodel.Attributes2D{ BoolKeys: [][]string{{"link.validated", "link.active"}}, BoolValues: [][]bool{{true, true}}, DoubleKeys: [][]string{{"link.weight"}}, DoubleValues: [][]float64{{0.8}}, IntKeys: [][]string{{"link.priority"}}, IntValues: [][]int64{{1}}, StrKeys: [][]string{{"link.type"}}, StrValues: [][]string{{"follows_from"}}, ComplexKeys: [][]string{ { "@bytes@link.metadata", "@map@metadata", "@slice@tags", }, }, ComplexValues: [][]string{ { "eyJsaW5rX2lkIjoxfQ==", "{\"kvlistValue\":{\"values\":[{\"key\":\"key\",\"value\":{\"stringValue\":\"value\"}}]}}", "{\"arrayValue\":{\"values\":[{\"intValue\":\"1\"},{\"intValue\":\"2\"},{\"intValue\":\"3\"}]}}", }, }, }, ServiceName: "user-service", ResourceAttributes: dbmodel.Attributes{ BoolKeys: []string{"resource.available", "resource.active"}, BoolValues: []bool{true, true}, DoubleKeys: []string{"resource.cpu_limit", "resource.memory_usage"}, DoubleValues: []float64{2.5, 80.5}, IntKeys: []string{"resource.instance_id", "resource.port"}, IntValues: []int64{12345, 8080}, StrKeys: []string{"service.name", "resource.host", "resource.region"}, StrValues: []string{"user-service", "host-1", "us-west-1"}, ComplexKeys: []string{ "@bytes@resource.metadata", "@map@metadata", "@slice@tags", }, ComplexValues: []string{ "eyJkZXBsb3ltZW50IjoicHJvZCJ9", "{\"kvlistValue\":{\"values\":[{\"key\":\"key\",\"value\":{\"stringValue\":\"value\"}}]}}", "{\"arrayValue\":{\"values\":[{\"intValue\":\"1\"},{\"intValue\":\"2\"},{\"intValue\":\"3\"}]}}", }, }, ScopeName: "auth-scope", ScopeVersion: "v1.0.0", ScopeAttributes: dbmodel.Attributes{ BoolKeys: []string{"scope.enabled", "scope.persistent"}, BoolValues: []bool{true, false}, DoubleKeys: []string{"scope.version_number", "scope.priority"}, DoubleValues: []float64{1.0, 0.8}, IntKeys: []string{"scope.instance_count", "scope.max_spans"}, IntValues: []int64{5, 1000}, StrKeys: []string{"scope.environment", "scope.component"}, StrValues: []string{"production", "auth"}, ComplexKeys: []string{ "@bytes@scope.metadata", "@map@metadata", "@slice@tags", }, ComplexValues: []string{ "eyJzY29wZV90eXBlIjoiYXV0aGVudGljYXRpb24ifQ==", "{\"kvlistValue\":{\"values\":[{\"key\":\"key\",\"value\":{\"stringValue\":\"value\"}}]}}", "{\"arrayValue\":{\"values\":[{\"intValue\":\"1\"},{\"intValue\":\"2\"},{\"intValue\":\"3\"}]}}", }, }, }, { ID: "0000000000000003", TraceID: traceID.String(), TraceState: "state1", ParentSpanID: "0000000000000001", Name: "SELECT /db/query", Kind: "client", StartTime: now.Add(10 * time.Millisecond), StatusCode: "Ok", StatusMessage: "success", Duration: 500_000_000, Attributes: dbmodel.Attributes{ BoolKeys: []string{"db.cached", "db.readonly"}, BoolValues: []bool{false, true}, DoubleKeys: []string{"db.latency", "db.connections"}, DoubleValues: []float64{0.05, 5.0}, IntKeys: []string{"db.rows_affected", "db.connection_id"}, IntValues: []int64{150, 42}, StrKeys: []string{"db.statement", "db.name"}, StrValues: []string{"SELECT * FROM users", "userdb"}, ComplexKeys: []string{ "@bytes@db.query_plan", "@map@metadata", "@slice@tags", }, ComplexValues: []string{ "UExBTiBTRUxFQ1Q=", "{\"kvlistValue\":{\"values\":[{\"key\":\"key\",\"value\":{\"stringValue\":\"value\"}}]}}", "{\"arrayValue\":{\"values\":[{\"intValue\":\"1\"},{\"intValue\":\"2\"},{\"intValue\":\"3\"}]}}", }, }, EventNames: []string{"query-start", "query-end"}, EventTimestamps: []time.Time{now.Add(10 * time.Millisecond), now.Add(510 * time.Millisecond)}, EventAttributes: dbmodel.Attributes2D{ BoolKeys: [][]string{{"db.optimized", "db.indexed"}, {"db.cached", "db.successful"}}, BoolValues: [][]bool{{true, false}, {true, false}}, DoubleKeys: [][]string{{"db.query_time"}, {"db.result_time"}}, DoubleValues: [][]float64{{0.001}, {0.5}}, IntKeys: [][]string{{"db.connection_pool_size"}, {"db.result_count"}}, IntValues: [][]int64{{10}, {150}}, StrKeys: [][]string{{"db.event.type"}, {"db.event.status"}}, StrValues: [][]string{{"query_execution_start"}, {"query_execution_complete"}}, ComplexKeys: [][]string{ { "@bytes@db.query_metadata", "@map@metadata", "@slice@tags", }, { "@bytes@db.result_metadata", }, }, ComplexValues: [][]string{ { "eyJxdWVyeV9pZCI6MTIzfQ==", "{\"kvlistValue\":{\"values\":[{\"key\":\"key\",\"value\":{\"stringValue\":\"value\"}}]}}", "{\"arrayValue\":{\"values\":[{\"intValue\":\"1\"},{\"intValue\":\"2\"},{\"intValue\":\"3\"}]}}", }, { "eyJyb3dfY291bnQiOjE1MH0=", }, }, }, LinkTraceIDs: []string{"00000000000000000000000000000004"}, LinkSpanIDs: []string{"0000000000000004"}, LinkTraceStates: []string{"state3"}, LinkAttributes: dbmodel.Attributes2D{ BoolKeys: [][]string{{"link.persistent", "link.direct"}}, BoolValues: [][]bool{{true, false}}, DoubleKeys: [][]string{{"link.confidence"}}, DoubleValues: [][]float64{{0.95}}, IntKeys: [][]string{{"link.sequence"}}, IntValues: [][]int64{{2}}, StrKeys: [][]string{{"link.operation"}}, StrValues: [][]string{{"child_of"}}, ComplexKeys: [][]string{ { "@bytes@link.context", "@map@metadata", "@slice@tags", }, }, ComplexValues: [][]string{ { "eyJkYl9jb250ZXh0IjoidXNlcmRiIn0=", "{\"kvlistValue\":{\"values\":[{\"key\":\"key\",\"value\":{\"stringValue\":\"value\"}}]}}", "{\"arrayValue\":{\"values\":[{\"intValue\":\"1\"},{\"intValue\":\"2\"},{\"intValue\":\"3\"}]}}", }, }, }, ServiceName: "db-service", ResourceAttributes: dbmodel.Attributes{ BoolKeys: []string{"resource.persistent", "resource.pooled"}, BoolValues: []bool{true, true}, DoubleKeys: []string{"resource.cpu_limit", "resource.memory_limit"}, DoubleValues: []float64{1.5, 512.0}, IntKeys: []string{"resource.instance_id", "resource.max_connections"}, IntValues: []int64{67890, 100}, StrKeys: []string{"service.name", "resource.host", "resource.database_type"}, StrValues: []string{"db-service", "db-host-1", "postgresql"}, ComplexKeys: []string{ "@bytes@resource.config", "@map@metadata", "@slice@tags", }, ComplexValues: []string{ "eyJkYl90eXBlIjoicG9zdGdyZXNxbCJ9", "{\"kvlistValue\":{\"values\":[{\"key\":\"key\",\"value\":{\"stringValue\":\"value\"}}]}}", "{\"arrayValue\":{\"values\":[{\"intValue\":\"1\"},{\"intValue\":\"2\"},{\"intValue\":\"3\"}]}}", }, }, ScopeName: "db-scope", ScopeVersion: "v1.0.0", ScopeAttributes: dbmodel.Attributes{ BoolKeys: []string{"scope.enabled", "scope.persistent"}, BoolValues: []bool{true, false}, DoubleKeys: []string{"scope.version_number", "scope.priority"}, DoubleValues: []float64{1.0, 0.8}, IntKeys: []string{"scope.instance_count", "scope.max_spans"}, IntValues: []int64{5, 1000}, StrKeys: []string{"scope.environment", "scope.component"}, StrValues: []string{"production", "database"}, ComplexKeys: []string{ "@bytes@scope.metadata", "@map@metadata", "@slice@tags", }, ComplexValues: []string{ "eyJzY29wZV90eXBlIjoiZGF0YWJhc2UifQ==", "{\"kvlistValue\":{\"values\":[{\"key\":\"key\",\"value\":{\"stringValue\":\"value\"}}]}}", "{\"arrayValue\":{\"values\":[{\"intValue\":\"1\"},{\"intValue\":\"2\"},{\"intValue\":\"3\"}]}}", }, }, }, } ================================================ FILE: internal/storage/v2/clickhouse/tracestore/writer.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "context" "fmt" "github.com/ClickHouse/clickhouse-go/v2/lib/driver" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/sql" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/tracestore/dbmodel" ) type Writer struct { conn driver.Conn } // NewWriter returns a new Writer instance that uses the given ClickHouse connection // to write trace data. // // The provided connection is used for writing traces. // This connection should not have instrumentation enabled to avoid recursively generating traces. func NewWriter(conn driver.Conn) *Writer { return &Writer{conn: conn} } func (w *Writer) WriteTraces(ctx context.Context, td ptrace.Traces) error { batch, err := w.conn.PrepareBatch(ctx, sql.InsertSpan) if err != nil { return fmt.Errorf("failed to prepare batch: %w", err) } defer batch.Close() for _, rs := range td.ResourceSpans().All() { for _, ss := range rs.ScopeSpans().All() { for _, span := range ss.Spans().All() { sr := dbmodel.ToRow(rs.Resource(), ss.Scope(), span) err = batch.Append( sr.ID, sr.TraceID, sr.TraceState, sr.ParentSpanID, sr.Name, sr.Kind, sr.StartTime, sr.StatusCode, sr.StatusMessage, sr.Duration, sr.Attributes.BoolKeys, sr.Attributes.BoolValues, sr.Attributes.DoubleKeys, sr.Attributes.DoubleValues, sr.Attributes.IntKeys, sr.Attributes.IntValues, sr.Attributes.StrKeys, sr.Attributes.StrValues, sr.Attributes.ComplexKeys, sr.Attributes.ComplexValues, sr.EventNames, sr.EventTimestamps, toTuple(sr.EventAttributes.BoolKeys, sr.EventAttributes.BoolValues), toTuple(sr.EventAttributes.DoubleKeys, sr.EventAttributes.DoubleValues), toTuple(sr.EventAttributes.IntKeys, sr.EventAttributes.IntValues), toTuple(sr.EventAttributes.StrKeys, sr.EventAttributes.StrValues), toTuple(sr.EventAttributes.ComplexKeys, sr.EventAttributes.ComplexValues), sr.LinkTraceIDs, sr.LinkSpanIDs, sr.LinkTraceStates, toTuple(sr.LinkAttributes.BoolKeys, sr.LinkAttributes.BoolValues), toTuple(sr.LinkAttributes.DoubleKeys, sr.LinkAttributes.DoubleValues), toTuple(sr.LinkAttributes.IntKeys, sr.LinkAttributes.IntValues), toTuple(sr.LinkAttributes.StrKeys, sr.LinkAttributes.StrValues), toTuple(sr.LinkAttributes.ComplexKeys, sr.LinkAttributes.ComplexValues), sr.ServiceName, sr.ResourceAttributes.BoolKeys, sr.ResourceAttributes.BoolValues, sr.ResourceAttributes.DoubleKeys, sr.ResourceAttributes.DoubleValues, sr.ResourceAttributes.IntKeys, sr.ResourceAttributes.IntValues, sr.ResourceAttributes.StrKeys, sr.ResourceAttributes.StrValues, sr.ResourceAttributes.ComplexKeys, sr.ResourceAttributes.ComplexValues, sr.ScopeName, sr.ScopeVersion, sr.ScopeAttributes.BoolKeys, sr.ScopeAttributes.BoolValues, sr.ScopeAttributes.DoubleKeys, sr.ScopeAttributes.DoubleValues, sr.ScopeAttributes.IntKeys, sr.ScopeAttributes.IntValues, sr.ScopeAttributes.StrKeys, sr.ScopeAttributes.StrValues, sr.ScopeAttributes.ComplexKeys, sr.ScopeAttributes.ComplexValues, ) if err != nil { return fmt.Errorf("failed to append span to batch: %w", err) } } } } if err := batch.Send(); err != nil { return fmt.Errorf("failed to send batch: %w", err) } return nil } func toTuple[T any](keys [][]string, values [][]T) [][][]any { tuple := make([][][]any, 0, len(keys)) for i := range keys { inner := make([][]any, 0, len(keys[i])) for j := range keys[i] { inner = append(inner, []any{keys[i][j], values[i][j]}) } tuple = append(tuple, inner) } return tuple } ================================================ FILE: internal/storage/v2/clickhouse/tracestore/writer_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "context" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/sql" "github.com/jaegertracing/jaeger/internal/storage/v2/clickhouse/tracestore/dbmodel" ) func tracesFromSpanRows(rows []*dbmodel.SpanRow) ptrace.Traces { td := ptrace.NewTraces() rs := td.ResourceSpans() for _, r := range rows { trace := dbmodel.FromRow(r) srcRS := trace.ResourceSpans() for i := 0; i < srcRS.Len(); i++ { srcRS.At(i).CopyTo(rs.AppendEmpty()) } } return td } func TestWriter_Success(t *testing.T) { b := &testBatch{t: t} conn := &testDriver{ t: t, batchResponses: map[string]*testBatchResponse{ sql.InsertSpan: { batch: b, }, }, } w := NewWriter(conn) td := tracesFromSpanRows(multipleSpans) err := w.WriteTraces(context.Background(), td) require.NoError(t, err) require.Len(t, conn.recordedQueries, 1) verifyQuerySnapshot(t, conn.recordedQueries[0]) require.True(t, b.sendCalled) require.Len(t, b.appended, len(multipleSpans)) for i, expected := range multipleSpans { row := b.appended[i] require.Equal(t, expected.ID, row[0]) // SpanID require.Equal(t, expected.TraceID, row[1]) // TraceID require.Equal(t, expected.TraceState, row[2]) // TraceState require.Equal(t, expected.ParentSpanID, row[3]) // ParentSpanID require.Equal(t, expected.Name, row[4]) // Name require.Equal(t, strings.ToLower(expected.Kind), row[5]) // Kind require.Equal(t, expected.StartTime, row[6]) // StartTimestamp require.Equal(t, expected.StatusCode, row[7]) // Status code require.Equal(t, expected.StatusMessage, row[8]) // Status message require.EqualValues(t, expected.Duration, row[9]) // Duration require.Equal(t, expected.Attributes.BoolKeys, row[10]) // Bool attribute keys require.Equal(t, expected.Attributes.BoolValues, row[11]) // Bool attribute values require.Equal(t, expected.Attributes.DoubleKeys, row[12]) // Double attribute keys require.Equal(t, expected.Attributes.DoubleValues, row[13]) // Double attribute values require.Equal(t, expected.Attributes.IntKeys, row[14]) // Int attribute keys require.Equal(t, expected.Attributes.IntValues, row[15]) // Int attribute values require.Equal(t, expected.Attributes.StrKeys, row[16]) // Str attribute keys require.Equal(t, expected.Attributes.StrValues, row[17]) // Str attribute values require.Equal(t, expected.Attributes.ComplexKeys, row[18]) // Complex attribute keys require.Equal(t, expected.Attributes.ComplexValues, row[19]) // Complex attribute values require.Equal(t, expected.EventNames, row[20]) // Event names require.Equal(t, expected.EventTimestamps, row[21]) // Event timestamps require.Equal(t, toTuple(expected.EventAttributes.BoolKeys, expected.EventAttributes.BoolValues), row[22], ) // Event bool attributes require.Equal(t, toTuple(expected.EventAttributes.DoubleKeys, expected.EventAttributes.DoubleValues), row[23], ) // Event double attributes require.Equal(t, toTuple(expected.EventAttributes.IntKeys, expected.EventAttributes.IntValues), row[24], ) // Event int attributes require.Equal(t, toTuple(expected.EventAttributes.StrKeys, expected.EventAttributes.StrValues), row[25], ) // Event str attributes require.Equal(t, toTuple(expected.EventAttributes.ComplexKeys, expected.EventAttributes.ComplexValues), row[26], ) // Event complex attributes require.Equal(t, expected.LinkTraceIDs, row[27]) // Link TraceIDs require.Equal(t, expected.LinkSpanIDs, row[28]) // Link SpanIDs require.Equal(t, expected.LinkTraceStates, row[29]) // Link TraceStates require.Equal(t, toTuple(expected.LinkAttributes.BoolKeys, expected.LinkAttributes.BoolValues), row[30], ) // Link bool attributes require.Equal(t, toTuple(expected.LinkAttributes.DoubleKeys, expected.LinkAttributes.DoubleValues), row[31], ) // Link double attributes require.Equal(t, toTuple(expected.LinkAttributes.IntKeys, expected.LinkAttributes.IntValues), row[32], ) // Link int attributes require.Equal(t, toTuple(expected.LinkAttributes.StrKeys, expected.LinkAttributes.StrValues), row[33], ) // Link str attributes require.Equal(t, toTuple(expected.LinkAttributes.ComplexKeys, expected.LinkAttributes.ComplexValues), row[34], ) // Link complex attributes require.Equal(t, expected.ServiceName, row[35]) // Service name require.Equal(t, expected.ResourceAttributes.BoolKeys, row[36]) // Resource bool attribute keys require.Equal(t, expected.ResourceAttributes.BoolValues, row[37]) // Resource bool attribute values require.Equal(t, expected.ResourceAttributes.DoubleKeys, row[38]) // Resource double attribute keys require.Equal(t, expected.ResourceAttributes.DoubleValues, row[39]) // Resource double attribute values require.Equal(t, expected.ResourceAttributes.IntKeys, row[40]) // Resource int attribute keys require.Equal(t, expected.ResourceAttributes.IntValues, row[41]) // Resource int attribute values require.Equal(t, expected.ResourceAttributes.StrKeys, row[42]) // Resource str attribute keys require.Equal(t, expected.ResourceAttributes.StrValues, row[43]) // Resource str attribute values require.Equal(t, expected.ResourceAttributes.ComplexKeys, row[44]) // Resource complex attribute keys require.Equal(t, expected.ResourceAttributes.ComplexValues, row[45]) // Resource complex attribute values require.Equal(t, expected.ScopeName, row[46]) // Scope name require.Equal(t, expected.ScopeVersion, row[47]) // Scope version require.Equal(t, expected.ScopeAttributes.BoolKeys, row[48]) // Scope bool attribute keys require.Equal(t, expected.ScopeAttributes.BoolValues, row[49]) // Scope bool attribute values require.Equal(t, expected.ScopeAttributes.DoubleKeys, row[50]) // Scope double attribute keys require.Equal(t, expected.ScopeAttributes.DoubleValues, row[51]) // Scope double attribute values require.Equal(t, expected.ScopeAttributes.IntKeys, row[52]) // Scope int attribute keys require.Equal(t, expected.ScopeAttributes.IntValues, row[53]) // Scope int attribute values require.Equal(t, expected.ScopeAttributes.StrKeys, row[54]) // Scope str attribute keys require.Equal(t, expected.ScopeAttributes.StrValues, row[55]) // Scope str attribute values require.Equal(t, expected.ScopeAttributes.ComplexKeys, row[56]) // Scope complex attribute keys require.Equal(t, expected.ScopeAttributes.ComplexValues, row[57]) // Scope complex attribute values } } func TestWriter_PrepareBatchError(t *testing.T) { conn := &testDriver{ t: t, batchResponses: map[string]*testBatchResponse{ sql.InsertSpan: { batch: nil, err: assert.AnError, }, }, } w := NewWriter(conn) err := w.WriteTraces(context.Background(), tracesFromSpanRows(multipleSpans)) require.ErrorContains(t, err, "failed to prepare batch") require.ErrorIs(t, err, assert.AnError) } func TestWriter_AppendBatchError(t *testing.T) { b := &testBatch{t: t, appendErr: assert.AnError} conn := &testDriver{ t: t, batchResponses: map[string]*testBatchResponse{ sql.InsertSpan: { batch: b, }, }, } w := NewWriter(conn) err := w.WriteTraces(context.Background(), tracesFromSpanRows(multipleSpans)) require.ErrorContains(t, err, "failed to append span to batch") require.ErrorIs(t, err, assert.AnError) require.False(t, b.sendCalled) } func TestWriter_SendError(t *testing.T) { b := &testBatch{t: t, sendErr: assert.AnError} conn := &testDriver{ t: t, batchResponses: map[string]*testBatchResponse{ sql.InsertSpan: { batch: b, }, }, } w := NewWriter(conn) err := w.WriteTraces(context.Background(), tracesFromSpanRows(multipleSpans)) require.ErrorContains(t, err, "failed to send batch") require.ErrorIs(t, err, assert.AnError) require.False(t, b.sendCalled) } func TestToTuple(t *testing.T) { tests := []struct { name string keys [][]string values [][]int expected [][][]any }{ { name: "empty slices", keys: [][]string{}, values: [][]int{}, expected: [][][]any{}, }, { name: "single empty inner slice", keys: [][]string{{}}, values: [][]int{{}}, expected: [][][]any{{}}, }, { name: "single element", keys: [][]string{{"key1"}}, values: [][]int{{42}}, expected: [][][]any{ { {"key1", 42}, }, }, }, { name: "multiple elements in single slice", keys: [][]string{{"key1", "key2", "key3"}}, values: [][]int{{10, 20, 30}}, expected: [][][]any{ { {"key1", 10}, {"key2", 20}, {"key3", 30}, }, }, }, { name: "multiple slices with multiple elements", keys: [][]string{{"key1", "key2"}, {"key3"}, {"key4", "key5", "key6"}}, values: [][]int{{1, 2}, {3}, {4, 5, 6}}, expected: [][][]any{ { {"key1", 1}, {"key2", 2}, }, { {"key3", 3}, }, { {"key4", 4}, {"key5", 5}, {"key6", 6}, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := toTuple(tt.keys, tt.values) require.Equal(t, tt.expected, result) }) } } ================================================ FILE: internal/storage/v2/elasticsearch/depstore/dbmodel/converter.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import "github.com/jaegertracing/jaeger-idl/model/v1" // FromDomainDependencies converts model dependencies to database representation func FromDomainDependencies(dLinks []model.DependencyLink) []DependencyLink { if dLinks == nil { return nil } ret := make([]DependencyLink, len(dLinks)) for i, d := range dLinks { ret[i] = DependencyLink{ CallCount: d.CallCount, Parent: d.Parent, Child: d.Child, } } return ret } // ToDomainDependencies converts database representation of dependencies to model func ToDomainDependencies(dLinks []DependencyLink) []model.DependencyLink { if dLinks == nil { return nil } ret := make([]model.DependencyLink, len(dLinks)) for i, d := range dLinks { ret[i] = model.DependencyLink{ CallCount: d.CallCount, Parent: d.Parent, Child: d.Child, } } return ret } ================================================ FILE: internal/storage/v2/elasticsearch/depstore/dbmodel/converter_test.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import ( "strconv" "testing" "github.com/stretchr/testify/assert" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestConvertDependencies(t *testing.T) { tests := []struct { dLinks []model.DependencyLink }{ { dLinks: []model.DependencyLink{{CallCount: 1, Parent: "foo", Child: "bar"}}, }, { dLinks: []model.DependencyLink{{CallCount: 3, Parent: "foo"}}, }, { dLinks: []model.DependencyLink{}, }, { dLinks: nil, }, } for i, test := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { got := FromDomainDependencies(test.dLinks) a := ToDomainDependencies(got) assert.Equal(t, test.dLinks, a) }) } } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v2/elasticsearch/depstore/dbmodel/model.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package dbmodel import "time" // TimeDependencies encapsulates dependencies created at a given time type TimeDependencies struct { Timestamp time.Time `json:"timestamp"` Dependencies []DependencyLink `json:"dependencies"` } // DependencyLink shows dependencies between services type DependencyLink struct { Parent string `json:"parent"` Child string `json:"child"` CallCount uint64 `json:"callCount"` } ================================================ FILE: internal/storage/v2/elasticsearch/depstore/mocks/mocks.go ================================================ // Copyright (c) The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // // Run 'make generate-mocks' to regenerate. // Code generated by mockery; DO NOT EDIT. // github.com/vektra/mockery // template: testify package mocks import ( "context" "time" "github.com/jaegertracing/jaeger/internal/storage/v2/elasticsearch/depstore/dbmodel" mock "github.com/stretchr/testify/mock" ) // NewCoreDependencyStore creates a new instance of CoreDependencyStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewCoreDependencyStore(t interface { mock.TestingT Cleanup(func()) }) *CoreDependencyStore { mock := &CoreDependencyStore{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // CoreDependencyStore is an autogenerated mock type for the CoreDependencyStore type type CoreDependencyStore struct { mock.Mock } type CoreDependencyStore_Expecter struct { mock *mock.Mock } func (_m *CoreDependencyStore) EXPECT() *CoreDependencyStore_Expecter { return &CoreDependencyStore_Expecter{mock: &_m.Mock} } // CreateTemplates provides a mock function for the type CoreDependencyStore func (_mock *CoreDependencyStore) CreateTemplates(dependenciesTemplate string) error { ret := _mock.Called(dependenciesTemplate) if len(ret) == 0 { panic("no return value specified for CreateTemplates") } var r0 error if returnFunc, ok := ret.Get(0).(func(string) error); ok { r0 = returnFunc(dependenciesTemplate) } else { r0 = ret.Error(0) } return r0 } // CoreDependencyStore_CreateTemplates_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateTemplates' type CoreDependencyStore_CreateTemplates_Call struct { *mock.Call } // CreateTemplates is a helper method to define mock.On call // - dependenciesTemplate string func (_e *CoreDependencyStore_Expecter) CreateTemplates(dependenciesTemplate interface{}) *CoreDependencyStore_CreateTemplates_Call { return &CoreDependencyStore_CreateTemplates_Call{Call: _e.mock.On("CreateTemplates", dependenciesTemplate)} } func (_c *CoreDependencyStore_CreateTemplates_Call) Run(run func(dependenciesTemplate string)) *CoreDependencyStore_CreateTemplates_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *CoreDependencyStore_CreateTemplates_Call) Return(err error) *CoreDependencyStore_CreateTemplates_Call { _c.Call.Return(err) return _c } func (_c *CoreDependencyStore_CreateTemplates_Call) RunAndReturn(run func(dependenciesTemplate string) error) *CoreDependencyStore_CreateTemplates_Call { _c.Call.Return(run) return _c } // GetDependencies provides a mock function for the type CoreDependencyStore func (_mock *CoreDependencyStore) GetDependencies(ctx context.Context, endTs time.Time, lookback time.Duration) ([]dbmodel.DependencyLink, error) { ret := _mock.Called(ctx, endTs, lookback) if len(ret) == 0 { panic("no return value specified for GetDependencies") } var r0 []dbmodel.DependencyLink var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, time.Time, time.Duration) ([]dbmodel.DependencyLink, error)); ok { return returnFunc(ctx, endTs, lookback) } if returnFunc, ok := ret.Get(0).(func(context.Context, time.Time, time.Duration) []dbmodel.DependencyLink); ok { r0 = returnFunc(ctx, endTs, lookback) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]dbmodel.DependencyLink) } } if returnFunc, ok := ret.Get(1).(func(context.Context, time.Time, time.Duration) error); ok { r1 = returnFunc(ctx, endTs, lookback) } else { r1 = ret.Error(1) } return r0, r1 } // CoreDependencyStore_GetDependencies_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDependencies' type CoreDependencyStore_GetDependencies_Call struct { *mock.Call } // GetDependencies is a helper method to define mock.On call // - ctx context.Context // - endTs time.Time // - lookback time.Duration func (_e *CoreDependencyStore_Expecter) GetDependencies(ctx interface{}, endTs interface{}, lookback interface{}) *CoreDependencyStore_GetDependencies_Call { return &CoreDependencyStore_GetDependencies_Call{Call: _e.mock.On("GetDependencies", ctx, endTs, lookback)} } func (_c *CoreDependencyStore_GetDependencies_Call) Run(run func(ctx context.Context, endTs time.Time, lookback time.Duration)) *CoreDependencyStore_GetDependencies_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { arg0 = args[0].(context.Context) } var arg1 time.Time if args[1] != nil { arg1 = args[1].(time.Time) } var arg2 time.Duration if args[2] != nil { arg2 = args[2].(time.Duration) } run( arg0, arg1, arg2, ) }) return _c } func (_c *CoreDependencyStore_GetDependencies_Call) Return(dependencyLinks []dbmodel.DependencyLink, err error) *CoreDependencyStore_GetDependencies_Call { _c.Call.Return(dependencyLinks, err) return _c } func (_c *CoreDependencyStore_GetDependencies_Call) RunAndReturn(run func(ctx context.Context, endTs time.Time, lookback time.Duration) ([]dbmodel.DependencyLink, error)) *CoreDependencyStore_GetDependencies_Call { _c.Call.Return(run) return _c } // WriteDependencies provides a mock function for the type CoreDependencyStore func (_mock *CoreDependencyStore) WriteDependencies(ts time.Time, dependencies []dbmodel.DependencyLink) error { ret := _mock.Called(ts, dependencies) if len(ret) == 0 { panic("no return value specified for WriteDependencies") } var r0 error if returnFunc, ok := ret.Get(0).(func(time.Time, []dbmodel.DependencyLink) error); ok { r0 = returnFunc(ts, dependencies) } else { r0 = ret.Error(0) } return r0 } // CoreDependencyStore_WriteDependencies_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteDependencies' type CoreDependencyStore_WriteDependencies_Call struct { *mock.Call } // WriteDependencies is a helper method to define mock.On call // - ts time.Time // - dependencies []dbmodel.DependencyLink func (_e *CoreDependencyStore_Expecter) WriteDependencies(ts interface{}, dependencies interface{}) *CoreDependencyStore_WriteDependencies_Call { return &CoreDependencyStore_WriteDependencies_Call{Call: _e.mock.On("WriteDependencies", ts, dependencies)} } func (_c *CoreDependencyStore_WriteDependencies_Call) Run(run func(ts time.Time, dependencies []dbmodel.DependencyLink)) *CoreDependencyStore_WriteDependencies_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 time.Time if args[0] != nil { arg0 = args[0].(time.Time) } var arg1 []dbmodel.DependencyLink if args[1] != nil { arg1 = args[1].([]dbmodel.DependencyLink) } run( arg0, arg1, ) }) return _c } func (_c *CoreDependencyStore_WriteDependencies_Call) Return(err error) *CoreDependencyStore_WriteDependencies_Call { _c.Call.Return(err) return _c } func (_c *CoreDependencyStore_WriteDependencies_Call) RunAndReturn(run func(ts time.Time, dependencies []dbmodel.DependencyLink) error) *CoreDependencyStore_WriteDependencies_Call { _c.Call.Return(run) return _c } ================================================ FILE: internal/storage/v2/elasticsearch/depstore/storage.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package depstore import ( "context" "encoding/json" "errors" "fmt" "time" "github.com/olivere/elastic/v7" "go.uber.org/zap" es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" esquery "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/query" "github.com/jaegertracing/jaeger/internal/storage/v2/elasticsearch/depstore/dbmodel" ) const ( dependencyType = "dependencies" dependencyIndexBaseName = "jaeger-dependencies-" ) // CoreDependencyStore is a DB Level abstraction which directly read/write dependencies into ElasticSearch type CoreDependencyStore interface { // WriteDependencies write dependencies to Elasticsearch WriteDependencies(ts time.Time, dependencies []dbmodel.DependencyLink) error // CreateTemplates creates index templates. CreateTemplates(dependenciesTemplate string) error // GetDependencies returns all interservice dependencies GetDependencies(ctx context.Context, endTs time.Time, lookback time.Duration) ([]dbmodel.DependencyLink, error) } // DependencyStore handles all queries and insertions to ElasticSearch dependencies type DependencyStore struct { client func() es.Client logger *zap.Logger dependencyIndexPrefix string indexDateLayout string maxDocCount int useReadWriteAliases bool } // DependencyStoreParams holds constructor parameters for NewDependencyStore type Params struct { Client func() es.Client Logger *zap.Logger IndexPrefix config.IndexPrefix IndexDateLayout string MaxDocCount int UseReadWriteAliases bool } // NewDependencyStore returns a DependencyStore func NewDependencyStore(p Params) *DependencyStore { return &DependencyStore{ client: p.Client, logger: p.Logger, dependencyIndexPrefix: p.IndexPrefix.Apply(dependencyIndexBaseName), indexDateLayout: p.IndexDateLayout, maxDocCount: p.MaxDocCount, useReadWriteAliases: p.UseReadWriteAliases, } } // WriteDependencies write dependencies to Elasticsearch func (s *DependencyStore) WriteDependencies(ts time.Time, dependencies []dbmodel.DependencyLink) error { writeIndexName := s.getWriteIndex(ts) s.writeDependenciesToIndex(writeIndexName, ts, dependencies) return nil } // CreateTemplates creates index templates. func (s *DependencyStore) CreateTemplates(dependenciesTemplate string) error { _, err := s.client().CreateTemplate("jaeger-dependencies").Body(dependenciesTemplate).Do(context.Background()) if err != nil { return err } return nil } func (s *DependencyStore) writeDependenciesToIndex(indexName string, ts time.Time, dependencies []dbmodel.DependencyLink) { s.client().Index().Index(indexName).Type(dependencyType). BodyJson(&dbmodel.TimeDependencies{ Timestamp: ts, Dependencies: dependencies, }).Add() } // GetDependencies returns all interservice dependencies func (s *DependencyStore) GetDependencies(ctx context.Context, endTs time.Time, lookback time.Duration) ([]dbmodel.DependencyLink, error) { indices := s.getReadIndices(endTs, lookback) searchResult, err := s.client().Search(indices...). Size(s.maxDocCount). Query(buildTSQuery(endTs, lookback)). IgnoreUnavailable(true). Do(ctx) if err != nil { return nil, fmt.Errorf("failed to search for dependencies: %w", err) } var retDependencies []dbmodel.DependencyLink hits := searchResult.Hits.Hits for _, hit := range hits { source := hit.Source var tToD dbmodel.TimeDependencies if err := json.Unmarshal(source, &tToD); err != nil { return nil, errors.New("unmarshalling ElasticSearch documents failed") } retDependencies = append(retDependencies, tToD.Dependencies...) } return retDependencies, nil } func buildTSQuery(endTs time.Time, lookback time.Duration) elastic.Query { return esquery.NewRangeQuery("timestamp").Gte(endTs.Add(-lookback)).Lte(endTs) } func (s *DependencyStore) getReadIndices(ts time.Time, lookback time.Duration) []string { if s.useReadWriteAliases { return []string{s.dependencyIndexPrefix + "read"} } var indices []string firstIndex := indexWithDate(s.dependencyIndexPrefix, s.indexDateLayout, ts.Add(-lookback)) currentIndex := indexWithDate(s.dependencyIndexPrefix, s.indexDateLayout, ts) for currentIndex != firstIndex { indices = append(indices, currentIndex) ts = ts.Add(-24 * time.Hour) currentIndex = indexWithDate(s.dependencyIndexPrefix, s.indexDateLayout, ts) } return append(indices, firstIndex) } func indexWithDate(indexNamePrefix, indexDateLayout string, date time.Time) string { return indexNamePrefix + date.UTC().Format(indexDateLayout) } func (s *DependencyStore) getWriteIndex(ts time.Time) string { if s.useReadWriteAliases { return s.dependencyIndexPrefix + "write" } return indexWithDate(s.dependencyIndexPrefix, s.indexDateLayout, ts) } ================================================ FILE: internal/storage/v2/elasticsearch/depstore/storage_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package depstore import ( "context" "errors" "strings" "testing" "time" "github.com/olivere/elastic/v7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.uber.org/zap" es "github.com/jaegertracing/jaeger/internal/storage/elasticsearch" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/mocks" "github.com/jaegertracing/jaeger/internal/storage/v2/elasticsearch/depstore/dbmodel" "github.com/jaegertracing/jaeger/internal/testutils" ) const defaultMaxDocCount = 10_000 type depStorageTest struct { client *mocks.Client logger *zap.Logger logBuffer *testutils.Buffer storage *DependencyStore } func withDepStorage(indexPrefix config.IndexPrefix, indexDateLayout string, maxDocCount int, fn func(r *depStorageTest)) { client := &mocks.Client{} logger, logBuffer := testutils.NewLogger() r := &depStorageTest{ client: client, logger: logger, logBuffer: logBuffer, storage: NewDependencyStore(Params{ Client: func() es.Client { return client }, Logger: logger, IndexPrefix: indexPrefix, IndexDateLayout: indexDateLayout, MaxDocCount: maxDocCount, }), } fn(r) } var _ CoreDependencyStore = &DependencyStore{} // check API conformance func TestNewSpanReaderIndexPrefix(t *testing.T) { testCases := []struct { prefix config.IndexPrefix expected string }{ {prefix: "", expected: ""}, {prefix: "foo", expected: "foo-"}, {prefix: ":", expected: ":-"}, } for _, testCase := range testCases { client := &mocks.Client{} r := NewDependencyStore(Params{ Client: func() es.Client { return client }, Logger: zap.NewNop(), IndexPrefix: testCase.prefix, IndexDateLayout: "2006-01-02", MaxDocCount: defaultMaxDocCount, }) assert.Equal(t, testCase.expected+dependencyIndexBaseName, r.dependencyIndexPrefix) } } func TestWriteDependencies(t *testing.T) { testCases := []struct { writeError error expectedError string esVersion uint }{ { expectedError: "", esVersion: 7, }, } for _, testCase := range testCases { withDepStorage("", "2006-01-02", defaultMaxDocCount, func(r *depStorageTest) { fixedTime := time.Date(1995, time.April, 21, 4, 21, 19, 95, time.UTC) indexName := indexWithDate("", "2006-01-02", fixedTime) writeService := &mocks.IndexService{} r.client.On("Index").Return(writeService) r.client.On("GetVersion").Return(testCase.esVersion) writeService.On("Index", stringMatcher(indexName)).Return(writeService) writeService.On("Type", stringMatcher(dependencyType)).Return(writeService) writeService.On("BodyJson", mock.Anything).Return(writeService) writeService.On("Add", mock.Anything).Return(nil, testCase.writeError) err := r.storage.WriteDependencies(fixedTime, []dbmodel.DependencyLink{}) if testCase.expectedError != "" { require.EqualError(t, err, testCase.expectedError) } else { require.NoError(t, err) } }) } } func TestGetDependencies(t *testing.T) { goodDependencies := `{ "ts": 798434479000000, "dependencies": [ { "parent": "hello", "child": "world", "callCount": 12 } ] }` badDependencies := `badJson{hello}world` testCases := []struct { searchResult *elastic.SearchResult searchError error expectedError string expectedOutput []dbmodel.DependencyLink indexPrefix config.IndexPrefix maxDocCount int indices []any }{ { searchResult: createSearchResult(goodDependencies), expectedOutput: []dbmodel.DependencyLink{ { Parent: "hello", Child: "world", CallCount: 12, }, }, indices: []any{"jaeger-dependencies-1995-04-21", "jaeger-dependencies-1995-04-20"}, maxDocCount: 1000, // can be anything, assertion will check this value is used in search query. }, { searchResult: createSearchResult(badDependencies), expectedError: "unmarshalling ElasticSearch documents failed", indices: []any{"jaeger-dependencies-1995-04-21", "jaeger-dependencies-1995-04-20"}, }, { searchError: errors.New("search failure"), expectedError: "failed to search for dependencies: search failure", indices: []any{"jaeger-dependencies-1995-04-21", "jaeger-dependencies-1995-04-20"}, }, { searchError: errors.New("search failure"), expectedError: "failed to search for dependencies: search failure", indexPrefix: "foo", indices: []any{"foo-jaeger-dependencies-1995-04-21", "foo-jaeger-dependencies-1995-04-20"}, }, } for _, testCase := range testCases { withDepStorage(testCase.indexPrefix, "2006-01-02", testCase.maxDocCount, func(r *depStorageTest) { fixedTime := time.Date(1995, time.April, 21, 4, 21, 19, 95, time.UTC) searchService := &mocks.SearchService{} r.client.On("Search", mock.AnythingOfType("[]string")).Return(searchService) searchService.On("Size", mock.MatchedBy(func(size int) bool { return size == testCase.maxDocCount })).Return(searchService) searchService.On("Query", mock.Anything).Return(searchService) searchService.On("IgnoreUnavailable", mock.AnythingOfType("bool")).Return(searchService) searchService.On("Do", mock.Anything).Return(testCase.searchResult, testCase.searchError) actual, err := r.storage.GetDependencies(context.Background(), fixedTime, 24*time.Hour) if testCase.expectedError != "" { require.EqualError(t, err, testCase.expectedError) assert.Nil(t, actual) } else { require.NoError(t, err) assert.Equal(t, testCase.expectedOutput, actual) } }) } } func createSearchResult(dependencyLink string) *elastic.SearchResult { dependencyLinkRaw := []byte(dependencyLink) hits := make([]*elastic.SearchHit, 1) hits[0] = &elastic.SearchHit{ Source: dependencyLinkRaw, } searchResult := &elastic.SearchResult{Hits: &elastic.SearchHits{Hits: hits}} return searchResult } func TestGetReadIndices(t *testing.T) { fixedTime := time.Date(1995, time.April, 21, 4, 12, 19, 95, time.UTC) testCases := []struct { indices []string lookback time.Duration params Params }{ { params: Params{IndexPrefix: "", IndexDateLayout: "2006-01-02", UseReadWriteAliases: true}, lookback: 23 * time.Hour, indices: []string{ dependencyIndexBaseName + "read", }, }, { params: Params{IndexPrefix: "", IndexDateLayout: "2006-01-02"}, lookback: 23 * time.Hour, indices: []string{ dependencyIndexBaseName + fixedTime.Format("2006-01-02"), dependencyIndexBaseName + fixedTime.Add(-23*time.Hour).Format("2006-01-02"), }, }, { params: Params{IndexPrefix: "", IndexDateLayout: "2006-01-02"}, lookback: 13 * time.Hour, indices: []string{ dependencyIndexBaseName + fixedTime.UTC().Format("2006-01-02"), dependencyIndexBaseName + fixedTime.Add(-13*time.Hour).Format("2006-01-02"), }, }, { params: Params{IndexPrefix: "foo:", IndexDateLayout: "2006-01-02"}, lookback: 1 * time.Hour, indices: []string{ "foo:" + config.IndexPrefixSeparator + dependencyIndexBaseName + fixedTime.Format("2006-01-02"), }, }, { params: Params{IndexPrefix: "foo-", IndexDateLayout: "2006-01-02"}, lookback: 0, indices: []string{ "foo" + config.IndexPrefixSeparator + dependencyIndexBaseName + fixedTime.Format("2006-01-02"), }, }, } for _, testCase := range testCases { s := NewDependencyStore(testCase.params) assert.Equal(t, testCase.indices, s.getReadIndices(fixedTime, testCase.lookback)) } } func TestGetWriteIndex(t *testing.T) { fixedTime := time.Date(1995, time.April, 21, 4, 12, 19, 95, time.UTC) testCases := []struct { writeIndex string lookback time.Duration params Params }{ { params: Params{IndexPrefix: "", IndexDateLayout: "2006-01-02", UseReadWriteAliases: true}, writeIndex: dependencyIndexBaseName + "write", }, { params: Params{IndexPrefix: "", IndexDateLayout: "2006-01-02", UseReadWriteAliases: false}, writeIndex: dependencyIndexBaseName + fixedTime.Format("2006-01-02"), }, } for _, testCase := range testCases { s := NewDependencyStore(testCase.params) assert.Equal(t, testCase.writeIndex, s.getWriteIndex(fixedTime)) } } // stringMatcher can match a string argument when it contains a specific substring q func stringMatcher(q string) any { matchFunc := func(s string) bool { return strings.Contains(s, q) } return mock.MatchedBy(matchFunc) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v2/elasticsearch/depstore/storagev2.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package depstore import ( "context" "time" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" "github.com/jaegertracing/jaeger/internal/storage/v2/elasticsearch/depstore/dbmodel" ) type DependencyStoreV2 struct { store CoreDependencyStore } // NewDependencyStoreV2 returns a DependencyStoreV2 func NewDependencyStoreV2(p Params) *DependencyStoreV2 { return &DependencyStoreV2{ store: NewDependencyStore(p), } } func (s *DependencyStoreV2) GetDependencies(ctx context.Context, query depstore.QueryParameters) ([]model.DependencyLink, error) { dbDependencies, err := s.store.GetDependencies(ctx, query.EndTime, query.EndTime.Sub(query.StartTime)) if err != nil { return nil, err } dependencies := dbmodel.ToDomainDependencies(dbDependencies) return dependencies, nil } func (s *DependencyStoreV2) WriteDependencies(ts time.Time, dependencies []model.DependencyLink) error { dbDependencies := dbmodel.FromDomainDependencies(dependencies) return s.store.WriteDependencies(ts, dbDependencies) } ================================================ FILE: internal/storage/v2/elasticsearch/depstore/storagev2_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package depstore import ( "context" "errors" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" "github.com/jaegertracing/jaeger/internal/storage/v2/elasticsearch/depstore/dbmodel" "github.com/jaegertracing/jaeger/internal/storage/v2/elasticsearch/depstore/mocks" ) func TestV2GetDependencies(t *testing.T) { tests := []struct { name string mockOutput []dbmodel.DependencyLink mockErr error expectedOutput []model.DependencyLink expectedErr string }{ { name: "error from core reader", mockErr: errors.New("error from core reader"), expectedErr: "error from core reader", }, { name: "success output", mockOutput: []dbmodel.DependencyLink{ { Parent: "testing-parent", Child: "testing-child", CallCount: 1, }, }, expectedOutput: []model.DependencyLink{ { Parent: "testing-parent", Child: "testing-child", CallCount: 1, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { coreReader := &mocks.CoreDependencyStore{} store := DependencyStoreV2{ store: coreReader, } query := depstore.QueryParameters{ StartTime: time.Now(), EndTime: time.Now(), } coreReader.On("GetDependencies", mock.Anything, query.EndTime, query.EndTime.Sub(query.StartTime)).Return(tt.mockOutput, tt.mockErr) actual, err := store.GetDependencies(context.Background(), query) if tt.expectedErr != "" { assert.ErrorContains(t, err, tt.expectedErr) } else { require.NoError(t, err) assert.Equal(t, tt.expectedOutput, actual) } }) } } func TestV2WriteDependencies(t *testing.T) { tests := []struct { name string returningErr error expectedErr string }{ { name: "error from core writer", returningErr: errors.New("error from core writer"), expectedErr: "error from core writer", }, { name: "success", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { coreReader := &mocks.CoreDependencyStore{} store := DependencyStoreV2{ store: coreReader, } coreReader.On("WriteDependencies", mock.Anything, mock.Anything).Return(tt.returningErr) err := store.WriteDependencies(time.Now(), []model.DependencyLink{}) if tt.expectedErr != "" { assert.ErrorContains(t, err, tt.expectedErr) } else { require.NoError(t, err) } }) } } func TestNewDependencyStoreV2(t *testing.T) { store := NewDependencyStoreV2(Params{Logger: zap.NewNop()}) assert.IsType(t, &DependencyStore{}, store.store) } ================================================ FILE: internal/storage/v2/elasticsearch/factory.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "context" "io" "strings" "go.opentelemetry.io/collector/extension/extensionauth" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/metrics" escfg "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore" "github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore/tracestoremetrics" v2depstore "github.com/jaegertracing/jaeger/internal/storage/v2/elasticsearch/depstore" v2tracestore "github.com/jaegertracing/jaeger/internal/storage/v2/elasticsearch/tracestore" "github.com/jaegertracing/jaeger/internal/telemetry" ) const tagError = "error" var ( _ io.Closer = (*Factory)(nil) _ tracestore.Factory = (*Factory)(nil) _ depstore.Factory = (*Factory)(nil) ) type Factory struct { coreFactory *elasticsearch.FactoryBase config escfg.Configuration metricsFactory metrics.Factory } func NewFactory(ctx context.Context, cfg escfg.Configuration, telset telemetry.Settings, httpAuth extensionauth.HTTPClient) (*Factory, error) { // Ensure required fields are always included in tagsAsFields cfg = ensureRequiredFields(cfg) coreFactory, err := elasticsearch.NewFactoryBase(ctx, cfg, telset.Metrics, telset.Logger, httpAuth) if err != nil { return nil, err } f := &Factory{ coreFactory: coreFactory, config: cfg, metricsFactory: telset.Metrics, } return f, nil } func (f *Factory) CreateTraceReader() (tracestore.Reader, error) { params := f.coreFactory.GetSpanReaderParams() return tracestoremetrics.NewReaderDecorator(v2tracestore.NewTraceReader(params), f.metricsFactory), nil } func (f *Factory) CreateTraceWriter() (tracestore.Writer, error) { params := f.coreFactory.GetSpanWriterParams() wr := v2tracestore.NewTraceWriter(params) return wr, nil } func (f *Factory) CreateDependencyReader() (depstore.Reader, error) { params := f.coreFactory.GetDependencyStoreParams() return v2depstore.NewDependencyStoreV2(params), nil } func (f *Factory) CreateSamplingStore(maxBuckets int) (samplingstore.Store, error) { return f.coreFactory.CreateSamplingStore(maxBuckets) } func (f *Factory) Close() error { return f.coreFactory.Close() } func (f *Factory) Purge(ctx context.Context) error { return f.coreFactory.Purge(ctx) } // ensureRequiredFields adds span.kind and span.status error to tags-as-fields configuration // regardless of user settings func ensureRequiredFields(cfg escfg.Configuration) escfg.Configuration { if cfg.Tags.AllAsFields { return cfg } // Return new configuration with updated includes if cfg.Tags.Include != "" && !strings.HasSuffix(cfg.Tags.Include, ",") { cfg.Tags.Include += "," } cfg.Tags.Include += model.SpanKindKey + "," + tagError return cfg } ================================================ FILE: internal/storage/v2/elasticsearch/factory_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "context" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger-idl/model/v1" escfg "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config" "github.com/jaegertracing/jaeger/internal/telemetry" ) var mockEsServerResponse = []byte(` { "Version": { "Number": "6" } } `) // func TestNewFactory(t *testing.T) { // cfg := escfg.Configuration{} // coreFactory := getTestingFactoryBase(t, &cfg) // f := &Factory{coreFactory: coreFactory, config: cfg, metricsFactory: metrics.NullFactory} // _, err := f.CreateTraceReader() // require.NoError(t, err) // _, err = f.CreateTraceWriter() // require.NoError(t, err) // _, err = f.CreateDependencyReader() // require.NoError(t, err) // _, err = f.CreateSamplingStore(1) // require.NoError(t, err) // err = f.Close() // require.NoError(t, err) // err = f.Purge(context.Background()) // require.NoError(t, err) // } func TestESStorageFactoryWithConfig(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Write(mockEsServerResponse) })) defer server.Close() cfg := escfg.Configuration{ Servers: []string{server.URL}, LogLevel: "error", } factory, err := NewFactory(context.Background(), cfg, telemetry.NoopSettings(), nil) require.NoError(t, err) factory.Close() } func TestESStorageFactoryErr(t *testing.T) { f, err := NewFactory(context.Background(), escfg.Configuration{}, telemetry.NoopSettings(), nil) require.ErrorContains(t, err, "failed to create Elasticsearch client: no servers specified") require.Nil(t, f) } // func getTestingFactoryBase(t *testing.T, cfg *escfg.Configuration) *elasticsearch.FactoryBase { // f := &elasticsearch.FactoryBase{} // err := elasticsearch.SetFactoryForTest(f, zaptest.NewLogger(t), metrics.NullFactory, cfg) // require.NoError(t, err) // return f // } func TestAlwaysIncludesRequiredTags(t *testing.T) { // Set up mock Elasticsearch server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Write(mockEsServerResponse) })) defer server.Close() tests := []struct { name string tagsConfig escfg.TagsAsFields }{ { name: "Empty TagsAsFields with feature enabled", tagsConfig: escfg.TagsAsFields{ Include: "", }, }, { name: "With some tags with feature enabled", tagsConfig: escfg.TagsAsFields{ Include: "foo.bar,baz.qux", }, }, { name: "With one required tag already with feature enabled", tagsConfig: escfg.TagsAsFields{ Include: "span.kind", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := escfg.Configuration{ Servers: []string{server.URL}, LogLevel: "error", Tags: tt.tagsConfig, } factory, err := NewFactory(context.Background(), cfg, telemetry.NoopSettings(), nil) require.NoError(t, err) defer factory.Close() // Verify tag behavior based on test expectations includeTags := factory.config.Tags.Include require.Contains(t, includeTags, model.SpanKindKey) require.Contains(t, includeTags, tagError) }) } } func TestEnsureRequiredFields_AllAsFieldsTrue(t *testing.T) { originalCfg := escfg.Configuration{ Tags: escfg.TagsAsFields{ AllAsFields: true, Include: "custom1,custom2,span.kind,error", }, } // Make an exact copy for comparison expectedCfg := originalCfg result := ensureRequiredFields(originalCfg) require.Equal(t, expectedCfg, result) } ================================================ FILE: internal/storage/v2/elasticsearch/package_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package elasticsearch import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v2/elasticsearch/tracestore/fixtures/.gitignore ================================================ actual_* ================================================ FILE: internal/storage/v2/elasticsearch/tracestore/fixtures/es_01.json ================================================ { "traceID": "00000000000000010000000000000000", "spanID": "0000000000000002", "flags": 1, "operationName": "test-general-conversion", "references": [ { "refType": "CHILD_OF", "traceID": "00000000000000010000000000000000", "spanID": "0000000000000003" }, { "refType": "FOLLOWS_FROM", "traceID": "00000000000000010000000000000000", "spanID": "0000000000000004" }, { "refType": "CHILD_OF", "traceID": "00000000000000ff0000000000000000", "spanID": "00000000000000ff" } ], "startTime": 1485467191639875, "startTimeMillis": 1485467191639, "duration": 5, "tags": [ { "key": "otel.scope.name", "type": "string", "value": "testing-library" }, { "key": "otel.scope.version", "type": "string", "value": "1.1.1" }, { "key": "peer.service", "type": "string", "value": "service-y" }, { "key": "peer.ipv4", "type": "int64", "value": 23456 }, { "key": "blob", "type": "binary", "value": "00003039" }, { "key": "temperature", "type": "float64", "value": 72.5 }, { "key": "error", "type": "bool", "value": true } ], "logs": [ { "timestamp": 1485467191639875, "fields": [ { "key": "event", "type": "string", "value": "testing-event" }, { "key": "event-x", "type": "string", "value": "event-y" } ] }, { "timestamp": 1485467191639875, "fields": [ { "key": "x", "type": "string", "value": "y" } ] } ], "process": { "serviceName": "service-x", "tags": [ { "key": "sdk.version", "type": "string", "value": "1.2.1" } ] } } ================================================ FILE: internal/storage/v2/elasticsearch/tracestore/fixtures/es_01_string_tags.json ================================================ { "traceID": "00000000000000010000000000000000", "spanID": "0000000000000002", "flags": 1, "operationName": "test-general-conversion", "references": [ { "refType": "CHILD_OF", "traceID": "00000000000000010000000000000000", "spanID": "0000000000000003" }, { "refType": "FOLLOWS_FROM", "traceID": "00000000000000010000000000000000", "spanID": "0000000000000004" }, { "refType": "CHILD_OF", "traceID": "00000000000000ff0000000000000000", "spanID": "00000000000000ff" } ], "startTime": 1485467191639875, "startTimeMillis": 1485467191639, "duration": 5, "tags": [ { "key": "otel.scope.name", "type": "string", "value": "testing-library" }, { "key": "otel.scope.version", "type": "string", "value": "1.1.1" }, { "key": "peer.service", "type": "string", "value": "service-y" }, { "key": "peer.ipv4", "type": "int64", "value": "23456" }, { "key": "blob", "type": "binary", "value": "00003039" }, { "key": "temperature", "type": "float64", "value": "72.5" }, { "key": "error", "type": "bool", "value": "true" } ], "logs": [ { "timestamp": 1485467191639875, "fields": [ { "key": "event", "type": "string", "value": "testing-event" }, { "key": "event-x", "type": "string", "value": "event-y" } ] }, { "timestamp": 1485467191639875, "fields": [ { "key": "x", "type": "string", "value": "y" } ] } ], "process": { "serviceName": "service-x", "tags": [ { "key": "sdk.version", "type": "string", "value": "1.2.1" } ] } } ================================================ FILE: internal/storage/v2/elasticsearch/tracestore/fixtures/otel_traces_01.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "service-x" } }, { "key": "sdk.version", "value": { "stringValue": "1.2.1" } } ] }, "scopeSpans": [ { "scope": { "name": "testing-library", "version": "1.1.1" }, "spans": [ { "traceId": "00000000000000010000000000000000", "spanId": "0000000000000002", "parentSpanId": "0000000000000003", "flags": 1, "name": "test-general-conversion", "startTimeUnixNano": "1485467191639875000", "endTimeUnixNano": "1485467191639880000", "attributes": [ { "key": "peer.service", "value": { "stringValue": "service-y" } }, { "key": "peer.ipv4", "value": { "intValue": "23456" } }, { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } }, { "key": "temperature", "value": { "doubleValue": 72.5 } } ], "events": [ { "timeUnixNano": "1485467191639875000", "name": "testing-event", "attributes": [ { "key": "event-x", "value": { "stringValue": "event-y" } } ] }, { "timeUnixNano": "1485467191639875000", "attributes": [ { "key": "x", "value": { "stringValue": "y" } } ] } ], "links": [ { "traceId": "00000000000000010000000000000000", "spanId": "0000000000000004", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "follows_from" } } ] }, { "traceId": "00000000000000ff0000000000000000", "spanId": "00000000000000ff", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "child_of" } } ] } ], "status": { "code": 2 } } ] } ] } ] } ================================================ FILE: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 // Code originally copied from https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/e49500a9b68447cbbe237fa29526ba99e4963f39/pkg/translator/jaeger/jaegerproto_to_traces.go package tracestore import ( "encoding/hex" "encoding/json" "errors" "fmt" "strconv" "strings" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" conventions "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) var errType = errors.New("invalid type") // FromDBModel converts multiple ES DB Spans to internal traces func FromDBModel(spans []dbmodel.Span) (ptrace.Traces, error) { traceData := ptrace.NewTraces() if len(spans) == 0 { return traceData, nil } resourceSpans := traceData.ResourceSpans() resourceSpans.EnsureCapacity(len(spans)) err := dbSpansToSpans(spans, resourceSpans) return traceData, err } func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) { serviceName := process.ServiceName tags := process.Tags if serviceName == "" && tags == nil { return } attrs := resource.Attributes() if serviceName != "" && serviceName != noServiceName { attrs.EnsureCapacity(len(tags) + 1) attrs.PutStr(string(conventions.ServiceNameKey), serviceName) } else { attrs.EnsureCapacity(len(tags)) } dbTagsToAttributes(tags, attrs) } func dbSpansToSpans(dbSpans []dbmodel.Span, resourceSpans ptrace.ResourceSpansSlice) error { for i := range dbSpans { span := &dbSpans[i] resourceSpan := resourceSpans.AppendEmpty() dbProcessToResource(span.Process, resourceSpan.Resource()) scopeSpans := resourceSpan.ScopeSpans() scopeSpan := scopeSpans.AppendEmpty() dbSpanToScope(span, scopeSpan) // TODO there should be no errors returned from translation from db model err := dbSpanToSpan(span, scopeSpan.Spans().AppendEmpty()) if err != nil { return err } } return nil } func dbSpanToSpan(dbSpan *dbmodel.Span, span ptrace.Span) error { traceId, err := convertTraceIDFromDB(dbSpan.TraceID) if err != nil { return err } spanId, err := fromDbSpanId(dbSpan.SpanID) if err != nil { return err } if dbSpan.ParentSpanID != "" { parentSpanId, err := fromDbSpanId(dbSpan.ParentSpanID) if err != nil { return err } span.SetParentSpanID(parentSpanId) } span.SetTraceID(traceId) span.SetSpanID(spanId) span.SetName(dbSpan.OperationName) span.SetFlags(dbSpan.Flags) startTime := model.EpochMicrosecondsAsTime(dbSpan.StartTime) span.SetStartTimestamp(pcommon.NewTimestampFromTime(startTime)) duration := model.MicrosecondsAsDuration(dbSpan.Duration) endTime := startTime.Add(duration) span.SetEndTimestamp(pcommon.NewTimestampFromTime(endTime)) // TODO rewrite this to use a single loop over tags // and map special tag names to OTEL Span fields attrs := span.Attributes() attrs.EnsureCapacity(len(dbSpan.Tags)) dbTagsToAttributes(dbSpan.Tags, attrs) if spanKindAttr, ok := attrs.Get(model.SpanKindKey); ok { span.SetKind(dbSpanKindToOTELSpanKind(spanKindAttr.Str())) attrs.Remove(model.SpanKindKey) } setSpanStatus(attrs, span) span.TraceState().FromRaw(getTraceStateFromAttrs(attrs)) // drop the attributes slice if all of them were replaced during translation if attrs.Len() == 0 { attrs.Clear() } dbParentSpanId := getParentSpanId(dbSpan) if dbParentSpanId != "" { parentSpanId, err := fromDbSpanId(dbParentSpanId) if err != nil { return err } span.SetParentSpanID(parentSpanId) } dbSpanLogsToSpanEvents(dbSpan.Logs, span.Events()) return dbSpanRefsToSpanEvents(dbSpan.References, dbParentSpanId, span.Links()) } func dbTagsToAttributes(tags []dbmodel.KeyValue, attributes pcommon.Map) { for _, tag := range tags { tagValue, ok := tag.Value.(string) if !ok { switch tag.Type { case dbmodel.Float64Type, dbmodel.Int64Type: fromDBNumber(tag, attributes) case dbmodel.BoolType: v, ok := tag.Value.(bool) if !ok { recordTagInvalidTypeError(tag, attributes) } else { attributes.PutBool(tag.Key, v) } default: // This means type is string/binary but value is of non string type, hence record the type error recordTagInvalidTypeError(tag, attributes) } continue } switch tag.Type { case dbmodel.StringType: attributes.PutStr(tag.Key, tagValue) case dbmodel.BoolType: convBoolVal, err := strconv.ParseBool(tagValue) if err != nil { recordTagConversionError(tag, err, attributes) } else { attributes.PutBool(tag.Key, convBoolVal) } case dbmodel.Int64Type: intVal, err := strconv.ParseInt(tagValue, 10, 64) if err != nil { recordTagConversionError(tag, err, attributes) } else { attributes.PutInt(tag.Key, intVal) } case dbmodel.Float64Type: floatVal, err := strconv.ParseFloat(tagValue, 64) if err != nil { recordTagConversionError(tag, err, attributes) } else { attributes.PutDouble(tag.Key, floatVal) } case dbmodel.BinaryType: value, err := hex.DecodeString(tagValue) if err != nil { recordTagConversionError(tag, err, attributes) } else { attributes.PutEmptyBytes(tag.Key).FromRaw(value) } default: attributes.PutStr(tag.Key, fmt.Sprintf("", tag.Type)) } } } func fromDBNumber(kv dbmodel.KeyValue, dest pcommon.Map) { switch kv.Type { case dbmodel.Int64Type: switch v := kv.Value.(type) { case int64: dest.PutInt(kv.Key, v) case float64: // This case is possible as unmarshalling the JSON converts every int value to float64 dest.PutInt(kv.Key, int64(v)) case json.Number: n, err := v.Int64() if err == nil { dest.PutInt(kv.Key, n) } default: recordTagInvalidTypeError(kv, dest) } case dbmodel.Float64Type: switch v := kv.Value.(type) { case float64: dest.PutDouble(kv.Key, v) case json.Number: n, err := v.Float64() if err == nil { dest.PutDouble(kv.Key, n) } default: recordTagInvalidTypeError(kv, dest) } default: } } func recordTagInvalidTypeError(kv dbmodel.KeyValue, dest pcommon.Map) { dest.PutStr(kv.Key, fmt.Sprintf("invalid %s type in %+v", string(kv.Type), kv.Value)) } func recordTagConversionError(kv dbmodel.KeyValue, err error, dest pcommon.Map) { dest.PutStr(kv.Key, fmt.Sprintf("Can't convert the type %s for the key %s: %v", string(kv.Type), kv.Key, err)) } func setSpanStatus(attrs pcommon.Map, span ptrace.Span) { dest := span.Status() statusCode := ptrace.StatusCodeUnset statusMessage := "" statusExists := false if errorVal, ok := attrs.Get(tagError); ok && errorVal.Type() == pcommon.ValueTypeBool { if errorVal.Bool() { statusCode = ptrace.StatusCodeError attrs.Remove(tagError) statusExists = true if desc, ok := extractStatusDescFromAttr(attrs); ok { statusMessage = desc } else if descAttr, ok := attrs.Get(tagHTTPStatusMsg); ok { statusMessage = descAttr.Str() } } } if codeAttr, ok := attrs.Get(conventions.OtelStatusCode); ok { if !statusExists { // The error tag is the ultimate truth for a Jaeger spans' error // status. Only parse the otel.status_code tag if the error tag is // not set to true. statusExists = true switch strings.ToUpper(codeAttr.Str()) { case statusOk: statusCode = ptrace.StatusCodeOk case statusError: statusCode = ptrace.StatusCodeError default: statusCode = ptrace.StatusCodeUnset } if desc, ok := extractStatusDescFromAttr(attrs); ok { statusMessage = desc } } // Regardless of error tag inputValue, remove the otel.status_code tag. The // otel.status_message tag will have already been removed if // statusExists is true. attrs.Remove(conventions.OtelStatusCode) } else if httpCodeAttr, ok := attrs.Get(string(conventions.HTTPResponseStatusCodeKey)); !statusExists && ok { // Fallback to introspecting if this span represents a failed HTTP // request or response, but again, only do so if the `error` tag was // not set to true and no explicit status was sent. if code, err := getStatusCodeFromHTTPStatusAttr(httpCodeAttr, span.Kind()); err == nil { if code != ptrace.StatusCodeUnset { statusExists = true statusCode = code } if msgAttr, ok := attrs.Get(tagHTTPStatusMsg); ok { statusMessage = msgAttr.Str() } } } if statusExists { dest.SetCode(statusCode) dest.SetMessage(statusMessage) } } // extractStatusDescFromAttr returns the OTel status description from attrs // along with true if it is set. Otherwise, an empty string and false are // returned. The OTel status description attribute is deleted from attrs in // the process. func extractStatusDescFromAttr(attrs pcommon.Map) (string, bool) { if msgAttr, ok := attrs.Get(conventions.OtelStatusDescription); ok { msg := msgAttr.Str() attrs.Remove(conventions.OtelStatusDescription) return msg, true } return "", false } // codeFromAttr returns the integer code inputValue from attrVal. An error is // returned if the code is not represented by an integer or string inputValue in // the attrVal or the inputValue is outside the bounds of an int representation. func codeFromAttr(attrVal pcommon.Value) (int64, error) { var val int64 switch attrVal.Type() { case pcommon.ValueTypeInt: val = attrVal.Int() case pcommon.ValueTypeStr: var err error val, err = strconv.ParseInt(attrVal.Str(), 10, 0) if err != nil { return 0, err } default: return 0, fmt.Errorf("%w: %s", errType, attrVal.Type().String()) } return val, nil } func getStatusCodeFromHTTPStatusAttr(attrVal pcommon.Value, kind ptrace.SpanKind) (ptrace.StatusCode, error) { statusCode, err := codeFromAttr(attrVal) if err != nil { return ptrace.StatusCodeUnset, err } // For HTTP status codes in the 4xx range span status MUST be left unset // in case of SpanKind.SERVER and MUST be set to Error in case of SpanKind.CLIENT. // For HTTP status codes in the 5xx range, as well as any other code the client // failed to interpret, span status MUST be set to Error. if statusCode >= 400 && statusCode < 500 { switch kind { case ptrace.SpanKindServer: return ptrace.StatusCodeUnset, nil default: return ptrace.StatusCodeError, nil } } return statusCodeFromHTTP(statusCode), nil } // StatusCodeFromHTTP takes an HTTP status code and return the appropriate OpenTelemetry status code // See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status func statusCodeFromHTTP(httpStatusCode int64) ptrace.StatusCode { if httpStatusCode >= 100 && httpStatusCode < 399 { return ptrace.StatusCodeUnset } return ptrace.StatusCodeError } func dbSpanKindToOTELSpanKind(spanKind string) ptrace.SpanKind { switch spanKind { case "client": return ptrace.SpanKindClient case "server": return ptrace.SpanKindServer case "producer": return ptrace.SpanKindProducer case "consumer": return ptrace.SpanKindConsumer case "internal": return ptrace.SpanKindInternal default: return ptrace.SpanKindUnspecified } } func dbSpanLogsToSpanEvents(logs []dbmodel.Log, events ptrace.SpanEventSlice) { if len(logs) == 0 { return } events.EnsureCapacity(len(logs)) for i, log := range logs { var event ptrace.SpanEvent if events.Len() > i { event = events.At(i) } else { event = events.AppendEmpty() } event.SetTimestamp(pcommon.NewTimestampFromTime(model.EpochMicrosecondsAsTime(log.Timestamp))) if len(log.Fields) == 0 { continue } attrs := event.Attributes() attrs.EnsureCapacity(len(log.Fields)) dbTagsToAttributes(log.Fields, attrs) if name, ok := attrs.Get(eventNameAttr); ok && name.Type() == pcommon.ValueTypeStr { event.SetName(name.Str()) attrs.Remove(eventNameAttr) } } } // dbSpanRefsToSpanEvents sets internal span links based on db references skipping excludeParentID func dbSpanRefsToSpanEvents(refs []dbmodel.Reference, excludeParentID dbmodel.SpanID, spanLinks ptrace.SpanLinkSlice) error { if len(refs) == 0 || len(refs) == 1 && refs[0].SpanID == excludeParentID && refs[0].RefType == dbmodel.ChildOf { return nil } spanLinks.EnsureCapacity(len(refs)) for _, ref := range refs { if ref.SpanID == excludeParentID && ref.RefType == dbmodel.ChildOf { continue } link := spanLinks.AppendEmpty() refTraceId, err := convertTraceIDFromDB(ref.TraceID) if err != nil { return err } refSpanId, err := fromDbSpanId(ref.SpanID) if err != nil { return err } link.SetTraceID(refTraceId) link.SetSpanID(refSpanId) link.Attributes().PutStr(conventions.AttributeOpentracingRefType, dbRefTypeToAttribute(ref.RefType)) } return nil } func getTraceStateFromAttrs(attrs pcommon.Map) string { traceState := "" // TODO Bring this inline with solution for jaegertracing/jaeger-client-java #702 once available if attr, ok := attrs.Get(tagW3CTraceState); ok { traceState = attr.Str() attrs.Remove(tagW3CTraceState) } return traceState } func dbSpanToScope(span *dbmodel.Span, scopeSpan ptrace.ScopeSpans) { if libraryName, ok := getAndDeleteTag(span, conventions.AttributeOtelScopeName); ok { scopeSpan.Scope().SetName(libraryName) if libraryVersion, ok := getAndDeleteTag(span, conventions.AttributeOtelScopeVersion); ok { scopeSpan.Scope().SetVersion(libraryVersion) } } } func getAndDeleteTag(span *dbmodel.Span, key string) (string, bool) { for i := range span.Tags { if span.Tags[i].Key == key { if val, ok := span.Tags[i].Value.(string); ok { span.Tags = append(span.Tags[:i], span.Tags[i+1:]...) return val, true } } } return "", false } func dbRefTypeToAttribute(ref dbmodel.ReferenceType) string { if ref == dbmodel.ChildOf { return conventions.AttributeOpentracingRefTypeChildOf } return conventions.AttributeOpentracingRefTypeFollowsFrom } ================================================ FILE: internal/storage/v2/elasticsearch/tracestore/from_dbmodel_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 // Code originally copied from https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/e49500a9b68447cbbe237fa29526ba99e4963f39/pkg/translator/jaeger/jaegerproto_to_traces_test.go package tracestore import ( "encoding/hex" "encoding/json" "os" "strconv" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" conventions "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) var testSpanEventTime = time.Date(2020, 2, 11, 20, 26, 13, 123000, time.UTC) func TestCodeFromAttr(t *testing.T) { tests := []struct { name string attr pcommon.Value code int64 err error }{ { name: "ok-string", attr: pcommon.NewValueStr("0"), code: 0, }, { name: "ok-int", attr: pcommon.NewValueInt(1), code: 1, }, { name: "wrong-type", attr: pcommon.NewValueBool(true), code: 0, err: errType, }, { name: "invalid-string", attr: pcommon.NewValueStr("inf"), code: 0, err: strconv.ErrSyntax, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { code, err := codeFromAttr(test.attr) if test.err != nil { require.ErrorIs(t, err, test.err) } else { require.NoError(t, err) } assert.Equal(t, test.code, code) }) } } func TestZeroBatchLength(t *testing.T) { trace, err := FromDBModel([]dbmodel.Span{}) require.NoError(t, err) assert.Equal(t, 0, trace.ResourceSpans().Len()) } func TestEmptySpansAndProcess(t *testing.T) { trace, err := FromDBModel([]dbmodel.Span{}) require.NoError(t, err) assert.Equal(t, 0, trace.ResourceSpans().Len()) } func TestGetStatusCodeFromHTTPStatusAttr(t *testing.T) { tests := []struct { name string attr pcommon.Value kind ptrace.SpanKind code ptrace.StatusCode err string }{ { name: "string-unknown", attr: pcommon.NewValueStr("10"), kind: ptrace.SpanKindClient, code: ptrace.StatusCodeError, }, { name: "string-ok", attr: pcommon.NewValueStr("101"), kind: ptrace.SpanKindClient, code: ptrace.StatusCodeUnset, }, { name: "int-not-found", attr: pcommon.NewValueInt(404), kind: ptrace.SpanKindClient, code: ptrace.StatusCodeError, }, { name: "int-not-found-client-span", attr: pcommon.NewValueInt(404), kind: ptrace.SpanKindServer, code: ptrace.StatusCodeUnset, }, { name: "int-invalid-arg", attr: pcommon.NewValueInt(408), kind: ptrace.SpanKindClient, code: ptrace.StatusCodeError, }, { name: "int-internal", attr: pcommon.NewValueInt(500), kind: ptrace.SpanKindClient, code: ptrace.StatusCodeError, }, { name: "wrong inputValue", attr: pcommon.NewValueBool(true), kind: ptrace.SpanKindClient, code: ptrace.StatusCodeUnset, err: "invalid type: Bool", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { code, err := getStatusCodeFromHTTPStatusAttr(test.attr, test.kind) if test.err != "" { require.ErrorContains(t, err, test.err) } else { require.NoError(t, err) } assert.Equal(t, test.code, code) }) } } func TestGetStatusCodeFromHTTPStatusAttr_DefaultSpanKind(t *testing.T) { value := pcommon.NewValueInt(404) statusCode, err := getStatusCodeFromHTTPStatusAttr(value, ptrace.SpanKindInternal) require.NoError(t, err) assert.Equal(t, ptrace.StatusCodeError, statusCode) statusCode, err = getStatusCodeFromHTTPStatusAttr(value, ptrace.SpanKindProducer) require.NoError(t, err) assert.Equal(t, ptrace.StatusCodeError, statusCode) statusCode, err = getStatusCodeFromHTTPStatusAttr(value, ptrace.SpanKindConsumer) require.NoError(t, err) assert.Equal(t, ptrace.StatusCodeError, statusCode) } func Test_SetSpanEventsFromDbSpanLogs(t *testing.T) { traces := ptrace.NewTraces() span := traces.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty() span.Events().AppendEmpty().SetName("event1") span.Events().AppendEmpty().SetName("event2") span.Events().AppendEmpty().Attributes().PutStr(eventNameAttr, "testing") logs := []dbmodel.Log{ { Timestamp: model.TimeAsEpochMicroseconds(testSpanEventTime), }, { Timestamp: model.TimeAsEpochMicroseconds(testSpanEventTime), }, } dbSpanLogsToSpanEvents(logs, span.Events()) for i := range logs { assert.Equal(t, testSpanEventTime, span.Events().At(i).Timestamp().AsTime()) } assert.Equal(t, 1, span.Events().At(2).Attributes().Len()) assert.Empty(t, span.Events().At(2).Name()) } func TestSetAttributesFromDbTags(t *testing.T) { wrongValue := "wrong-inputValue" tests := []struct { name string keyModel dbmodel.KeyValue expectedValueFn func(pcommon.Map) }{ { name: "wrong bool input value", keyModel: dbmodel.KeyValue{ Key: "bool-val", Type: dbmodel.BoolType, Value: wrongValue, }, expectedValueFn: func(p pcommon.Map) { p.PutStr("bool-val", "Can't convert the type bool for the key bool-val: strconv.ParseBool: parsing \"wrong-inputValue\": invalid syntax") }, }, { name: "right bool input value", keyModel: dbmodel.KeyValue{ Key: "bool-val", Type: dbmodel.BoolType, Value: "true", }, expectedValueFn: func(p pcommon.Map) { p.PutBool("bool-val", true) }, }, { name: "non string bool value", keyModel: dbmodel.KeyValue{ Key: "bool-val", Type: dbmodel.BoolType, Value: true, }, expectedValueFn: func(p pcommon.Map) { p.PutBool("bool-val", true) }, }, { name: "wrong non string int input value", keyModel: dbmodel.KeyValue{ Key: "bool-val", Type: dbmodel.BoolType, Value: 12, }, expectedValueFn: func(p pcommon.Map) { p.PutStr("bool-val", "invalid bool type in 12") }, }, { name: "wrong int input value", keyModel: dbmodel.KeyValue{ Key: "int-val", Type: dbmodel.Int64Type, Value: wrongValue, }, expectedValueFn: func(p pcommon.Map) { p.PutStr("int-val", "Can't convert the type int64 for the key int-val: strconv.ParseInt: parsing \"wrong-inputValue\": invalid syntax") }, }, { name: "right int input value", keyModel: dbmodel.KeyValue{ Key: "int-val", Type: dbmodel.Int64Type, Value: "123", }, expectedValueFn: func(p pcommon.Map) { p.PutInt("int-val", 123) }, }, { name: "right non string int input value", keyModel: dbmodel.KeyValue{ Key: "int-val", Type: dbmodel.Int64Type, Value: int64(123), }, expectedValueFn: func(p pcommon.Map) { p.PutInt("int-val", 123) }, }, { name: "right non string int float input value", keyModel: dbmodel.KeyValue{ Key: "int-val", Type: dbmodel.Int64Type, Value: float64(123), }, expectedValueFn: func(p pcommon.Map) { p.PutInt("int-val", 123) }, }, { name: "right non string json number int input value", keyModel: dbmodel.KeyValue{ Key: "int-val", Type: dbmodel.Int64Type, Value: json.Number("123"), }, expectedValueFn: func(p pcommon.Map) { p.PutInt("int-val", 123) }, }, { name: "wrong non string int input value", keyModel: dbmodel.KeyValue{ Key: "int-val", Type: dbmodel.Int64Type, Value: true, }, expectedValueFn: func(p pcommon.Map) { p.PutStr("int-val", "invalid int64 type in true") }, }, { name: "wrong double input value", keyModel: dbmodel.KeyValue{ Key: "double-val", Type: dbmodel.Float64Type, Value: wrongValue, }, expectedValueFn: func(p pcommon.Map) { p.PutStr("double-val", "Can't convert the type float64 for the key double-val: strconv.ParseFloat: parsing \"wrong-inputValue\": invalid syntax") }, }, { name: "right double input value", keyModel: dbmodel.KeyValue{ Key: "double-val", Type: dbmodel.Float64Type, Value: "1.23", }, expectedValueFn: func(p pcommon.Map) { p.PutDouble("double-val", 1.23) }, }, { name: "right non string double input value", keyModel: dbmodel.KeyValue{ Key: "double-val", Type: dbmodel.Float64Type, Value: 25.6, }, expectedValueFn: func(p pcommon.Map) { p.PutDouble("double-val", 25.6) }, }, { name: "right non string json number double input value", keyModel: dbmodel.KeyValue{ Key: "double-val", Type: dbmodel.Float64Type, Value: json.Number("123.56"), }, expectedValueFn: func(p pcommon.Map) { p.PutDouble("double-val", 123.56) }, }, { name: "wrong non string float input value", keyModel: dbmodel.KeyValue{ Key: "double-val", Type: dbmodel.Float64Type, Value: true, }, expectedValueFn: func(p pcommon.Map) { p.PutStr("double-val", "invalid float64 type in true") }, }, { name: "wrong binary input value", keyModel: dbmodel.KeyValue{ Key: "binary-val", Type: dbmodel.BinaryType, Value: wrongValue, }, expectedValueFn: func(p pcommon.Map) { p.PutStr("binary-val", "Can't convert the type binary for the key binary-val: encoding/hex: invalid byte: U+0077 'w'") }, }, { name: "right binary input value", keyModel: dbmodel.KeyValue{ Key: "binary-val", Type: dbmodel.BinaryType, Value: hex.EncodeToString([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x7D, 0x98}), }, expectedValueFn: func(p pcommon.Map) { p.PutEmptyBytes("binary-val").FromRaw([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x7D, 0x98}) }, }, { name: "non-string input value with string type", keyModel: dbmodel.KeyValue{ Key: "bool-val", Type: dbmodel.StringType, Value: 123, }, expectedValueFn: func(p pcommon.Map) { p.PutStr("bool-val", "invalid string type in 123") }, }, { name: "right string input value", keyModel: dbmodel.KeyValue{ Key: "string-val", Type: dbmodel.StringType, Value: "right-value", }, expectedValueFn: func(p pcommon.Map) { p.PutStr("string-val", "right-value") }, }, { name: "unknown type", keyModel: dbmodel.KeyValue{ Key: "unknown", Type: dbmodel.ValueType("unknown"), Value: "any", }, expectedValueFn: func(p pcommon.Map) { p.PutStr("unknown", "") }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { expected := pcommon.NewMap() test.expectedValueFn(expected) got := pcommon.NewMap() dbTagsToAttributes([]dbmodel.KeyValue{test.keyModel}, got) assert.Equal(t, expected, got) }) } } func TestFromDBModelErrors(t *testing.T) { tests := []struct { name string err string dbSpans []dbmodel.Span }{ { name: "wrong trace-id", dbSpans: []dbmodel.Span{{TraceID: dbmodel.TraceID("trace-id")}}, err: "encoding/hex: invalid byte: U+0074 't'", }, { name: "wrong span-id", dbSpans: []dbmodel.Span{{SpanID: dbmodel.SpanID("span-id")}}, err: "encoding/hex: invalid byte: U+0073 's'", }, { name: "wrong parent span-id", dbSpans: []dbmodel.Span{{ParentSpanID: dbmodel.SpanID("parent-span-id")}}, err: "encoding/hex: invalid byte: U+0070 'p'", }, { name: "wrong-ref-trace-id", dbSpans: []dbmodel.Span{{References: []dbmodel.Reference{{TraceID: dbmodel.TraceID("ref-trace-id")}}}}, err: "encoding/hex: invalid byte: U+0072 'r'", }, { name: "wrong-ref-span-id", dbSpans: []dbmodel.Span{{References: []dbmodel.Reference{{SpanID: dbmodel.SpanID("ref-span-id")}}}}, err: "encoding/hex: invalid byte: U+0072 'r'", }, { name: "wrong parent span-id with valid trace-id", dbSpans: []dbmodel.Span{{ TraceID: dbmodel.TraceID("0123456789abcdef0123456789abcdef"), SpanID: dbmodel.SpanID("0123456789abcdef"), ParentSpanID: dbmodel.SpanID("invalid-parent-id"), }}, err: "encoding/hex: invalid byte: U+0069 'i'", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { _, err := FromDBModel(test.dbSpans) require.ErrorContains(t, err, test.err) }) } } func TestSetParentId(t *testing.T) { parentSpanId := [8]byte{0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8} trace, err := FromDBModel([]dbmodel.Span{{ParentSpanID: getDbSpanIdFromByteArray(parentSpanId)}}) require.NoError(t, err) assert.Equal(t, pcommon.SpanID(parentSpanId), trace.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0).ParentSpanID()) } func TestParentIdWhenRefTraceIdIsDifferent(t *testing.T) { traceId := getDbTraceIdFromByteArray([16]byte{0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, 0x80}) refTraceId := getDbTraceIdFromByteArray([16]byte{0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, 0x81}) trace, err := FromDBModel([]dbmodel.Span{{TraceID: traceId, References: []dbmodel.Reference{{TraceID: refTraceId}}}}) require.NoError(t, err) assert.True(t, trace.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0).ParentSpanID().IsEmpty()) } func TestDbSpanToSpanWithSpanKind(t *testing.T) { tests := []struct { name string spanKind string expectedKind ptrace.SpanKind description string }{ { name: "span with client kind", spanKind: "client", expectedKind: ptrace.SpanKindClient, description: "Span with client kind should be converted properly", }, { name: "span with server kind", spanKind: "server", expectedKind: ptrace.SpanKindServer, description: "Span with server kind should be converted properly", }, { name: "span with unspecified kind", spanKind: "unknown", expectedKind: ptrace.SpanKindUnspecified, description: "Span with unknown kind should be unspecified", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dbSpan := &dbmodel.Span{ TraceID: dbmodel.TraceID("0123456789abcdef0123456789abcdef"), SpanID: dbmodel.SpanID("0123456789abcdef"), Tags: []dbmodel.KeyValue{ {Key: model.SpanKindKey, Value: tt.spanKind, Type: dbmodel.StringType}, }, } span := ptrace.NewSpan() err := dbSpanToSpan(dbSpan, span) require.NoError(t, err) assert.Equal(t, tt.expectedKind, span.Kind(), tt.description) // Verify span kind attribute was removed _, exists := span.Attributes().Get(model.SpanKindKey) assert.False(t, exists, "Span kind attribute should be removed") }) } } func TestDbProcessToResource(t *testing.T) { tests := []struct { name string process dbmodel.Process expectedAttrs map[string]any description string }{ { name: "process with service name and tags", process: dbmodel.Process{ ServiceName: "test-service", Tags: []dbmodel.KeyValue{ {Key: "key1", Value: "value1", Type: dbmodel.StringType}, {Key: "key2", Value: "value2", Type: dbmodel.StringType}, }, }, expectedAttrs: map[string]any{ string(conventions.ServiceNameKey): "test-service", "key1": "value1", "key2": "value2", }, description: "Process with service name should trigger first branch", }, { name: "process with empty service name and tags", process: dbmodel.Process{ ServiceName: "", Tags: []dbmodel.KeyValue{ {Key: "key1", Value: "value1", Type: dbmodel.StringType}, {Key: "key2", Value: "value2", Type: dbmodel.StringType}, }, }, expectedAttrs: map[string]any{ "key1": "value1", "key2": "value2", }, description: "Process with empty service name should trigger else branch", }, { name: "process with noServiceName and tags", process: dbmodel.Process{ ServiceName: noServiceName, Tags: []dbmodel.KeyValue{ {Key: "key1", Value: "value1", Type: dbmodel.StringType}, }, }, expectedAttrs: map[string]any{ "key1": "value1", }, description: "Process with noServiceName should trigger else branch", }, { name: "process with empty service name and no tags", process: dbmodel.Process{ ServiceName: "", Tags: nil, }, expectedAttrs: map[string]any{}, description: "Process with no service name and no tags should return early", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { resource := pcommon.NewResource() dbProcessToResource(tt.process, resource) // Instead of comparing the entire map structure, compare individual attributes attrs := resource.Attributes() for key, expectedValue := range tt.expectedAttrs { actualValue, exists := attrs.Get(key) assert.True(t, exists, "Expected attribute %s to exist", key) // Fixed: Replace switch with if-then as suggested by linter if v, ok := expectedValue.(string); ok { assert.Equal(t, v, actualValue.Str(), "Attribute %s value mismatch", key) } } // Verify the total number of attributes matches assert.Equal(t, len(tt.expectedAttrs), attrs.Len(), "Total attribute count mismatch") }) } } func TestGetTraceStateFromAttrs(t *testing.T) { tests := []struct { name string attrs map[string]any expectedTraceState string expectedAttrCount int description string }{ { name: "attrs with w3c trace state", attrs: map[string]any{ tagW3CTraceState: "vendor1=value1,vendor2=value2", "other-attr": "other-value", }, expectedTraceState: "vendor1=value1,vendor2=value2", expectedAttrCount: 1, description: "Should extract and remove W3C trace state", }, { name: "attrs without w3c trace state", attrs: map[string]any{ "other-attr": "other-value", }, expectedTraceState: "", expectedAttrCount: 1, description: "Should return empty string when no trace state", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { attrs := pcommon.NewMap() require.NoError(t, attrs.FromRaw(tt.attrs)) traceState := getTraceStateFromAttrs(attrs) assert.Equal(t, tt.expectedTraceState, traceState, tt.description) assert.Equal(t, tt.expectedAttrCount, attrs.Len(), "Attribute count should match expected") }) } } func TestSetInternalSpanStatus(t *testing.T) { okStatus := ptrace.NewStatus() okStatus.SetCode(ptrace.StatusCodeOk) errorStatus := ptrace.NewStatus() errorStatus.SetCode(ptrace.StatusCodeError) errorStatusWithMessage := ptrace.NewStatus() errorStatusWithMessage.SetCode(ptrace.StatusCodeError) errorStatusWithMessage.SetMessage("Error: Invalid argument") errorStatusWith404Message := ptrace.NewStatus() errorStatusWith404Message.SetCode(ptrace.StatusCodeError) errorStatusWith404Message.SetMessage("HTTP 404: Not Found") tests := []struct { name string attrs map[string]any status ptrace.Status kind ptrace.SpanKind attrsModifiedLen int // Length of attributes map after dropping converted fields }{ { name: "status.code is set as string", attrs: map[string]any{ conventions.OtelStatusCode: statusOk, }, status: okStatus, attrsModifiedLen: 0, }, { name: "status.code, status.message and error tags are set", attrs: map[string]any{ conventions.OtelStatusCode: statusError, conventions.OtelStatusDescription: "Error: Invalid argument", }, status: errorStatusWithMessage, attrsModifiedLen: 0, }, { name: "http.status_code tag is set as string", attrs: map[string]any{ conventions.HTTPResponseStatusCodeKey: "404", }, status: errorStatus, attrsModifiedLen: 1, }, { name: "http.status_code, http.status_message and error tags are set", attrs: map[string]any{ conventions.HTTPResponseStatusCodeKey: 404, tagHTTPStatusMsg: "HTTP 404: Not Found", }, status: errorStatusWith404Message, attrsModifiedLen: 2, }, { name: "status.code has precedence over http.status_code.", attrs: map[string]any{ conventions.OtelStatusCode: statusOk, conventions.HTTPResponseStatusCodeKey: 500, tagHTTPStatusMsg: "Server Error", }, status: okStatus, attrsModifiedLen: 2, }, { name: "status.error has precedence over http.status_error.", attrs: map[string]any{ conventions.OtelStatusCode: statusError, conventions.HTTPResponseStatusCodeKey: 500, tagHTTPStatusMsg: "Server Error", }, status: errorStatus, attrsModifiedLen: 2, }, { name: "whether tagHttpStatusMsg is set as string", attrs: map[string]any{ conventions.HTTPResponseStatusCodeKey: 404, tagHTTPStatusMsg: "HTTP 404: Not Found", }, status: errorStatusWith404Message, attrsModifiedLen: 2, }, { name: "error tag set and message present", attrs: map[string]any{ tagError: true, conventions.OtelStatusDescription: "Error: Invalid argument", }, status: errorStatusWithMessage, attrsModifiedLen: 0, }, { name: "error tag set and http tag message present", attrs: map[string]any{ tagError: true, tagHTTPStatusMsg: "HTTP 404: Not Found", }, status: errorStatusWith404Message, attrsModifiedLen: 1, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { span := ptrace.NewSpan() span.SetKind(test.kind) status := span.Status() attrs := pcommon.NewMap() require.NoError(t, attrs.FromRaw(test.attrs)) setSpanStatus(attrs, span) assert.Equal(t, test.status, status) assert.Equal(t, test.attrsModifiedLen, attrs.Len()) }) } } func TestDBSpanKindToOTELSpanKind(t *testing.T) { tests := []struct { jSpanKind string otlpSpanKind ptrace.SpanKind }{ { jSpanKind: "client", otlpSpanKind: ptrace.SpanKindClient, }, { jSpanKind: "server", otlpSpanKind: ptrace.SpanKindServer, }, { jSpanKind: "producer", otlpSpanKind: ptrace.SpanKindProducer, }, { jSpanKind: "consumer", otlpSpanKind: ptrace.SpanKindConsumer, }, { jSpanKind: "internal", otlpSpanKind: ptrace.SpanKindInternal, }, { jSpanKind: "all-others", otlpSpanKind: ptrace.SpanKindUnspecified, }, } for _, test := range tests { t.Run(test.jSpanKind, func(t *testing.T) { assert.Equal(t, test.otlpSpanKind, dbSpanKindToOTELSpanKind(test.jSpanKind)) }) } } func TestDbSpanKindToOTELSpanKind_DefaultCase(t *testing.T) { result := dbSpanKindToOTELSpanKind("unknown-span-kind") assert.Equal(t, ptrace.SpanKindUnspecified, result) result = dbSpanKindToOTELSpanKind("") assert.Equal(t, ptrace.SpanKindUnspecified, result) result = dbSpanKindToOTELSpanKind("invalid") assert.Equal(t, ptrace.SpanKindUnspecified, result) } func TestSetInternalSpanStatus_DefaultCase(t *testing.T) { span := ptrace.NewSpan() status := span.Status() attrs := pcommon.NewMap() attrs.PutStr(conventions.OtelStatusCode, "UNKNOWN_STATUS") setSpanStatus(attrs, span) assert.Equal(t, ptrace.StatusCodeUnset, status.Code()) assert.Empty(t, status.Message()) } func TestFromDbModel_Fixtures(t *testing.T) { tracesData, spansData := loadFixtures(t, 1) unmarshaller := ptrace.JSONUnmarshaler{} expectedTd, err := unmarshaller.UnmarshalTraces(tracesData) require.NoError(t, err) spans := ToDBModel(expectedTd) assert.Len(t, spans, 1) testSpans(t, spansData, spans[0]) actualTd, err := FromDBModel(spans) require.NoError(t, err) testTraces(t, tracesData, actualTd) } func TestToDbModel_Fixtures_StringTags(t *testing.T) { spanData, err := os.ReadFile("fixtures/es_01_string_tags.json") require.NoError(t, err) var dbSpan dbmodel.Span require.NoError(t, json.Unmarshal(spanData, &dbSpan)) td, err := FromDBModel([]dbmodel.Span{dbSpan}) require.NoError(t, err) expectedTraces := loadTraces(t, 1) testTraces(t, expectedTraces, td) } func getDbTraceIdFromByteArray(arr [16]byte) dbmodel.TraceID { return dbmodel.TraceID(hex.EncodeToString(arr[:])) } func getDbSpanIdFromByteArray(arr [8]byte) dbmodel.SpanID { return dbmodel.SpanID(hex.EncodeToString(arr[:])) } func BenchmarkProtoBatchToInternalTraces(b *testing.B) { data, err := os.ReadFile("fixtures.es_01.json") require.NoError(b, err) var dbSpan dbmodel.Span err = json.Unmarshal(data, &dbSpan) require.NoError(b, err) jb := []dbmodel.Span{dbSpan} for b.Loop() { _, err := FromDBModel(jb) assert.NoError(b, err) } } ================================================ FILE: internal/storage/v2/elasticsearch/tracestore/ids.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "encoding/hex" "go.opentelemetry.io/collector/pdata/pcommon" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" ) func convertTraceIDFromDB(dbTraceId dbmodel.TraceID) (pcommon.TraceID, error) { var traceId [16]byte traceBytes, err := hex.DecodeString(string(dbTraceId)) if err != nil { return pcommon.TraceID{}, err } copy(traceId[:], traceBytes) return traceId, nil } func fromDbSpanId(dbSpanId dbmodel.SpanID) (pcommon.SpanID, error) { var spanId [8]byte spanIdBytes, err := hex.DecodeString(string(dbSpanId)) if err != nil { return pcommon.SpanID{}, err } copy(spanId[:], spanIdBytes) return spanId, nil } // TODO extend DB model to support parent span ID directly func getParentSpanId(dbSpan *dbmodel.Span) dbmodel.SpanID { var followsFromRef *dbmodel.Reference for i := range dbSpan.References { ref := dbSpan.References[i] if ref.TraceID != dbSpan.TraceID { continue } if ref.RefType == dbmodel.ChildOf { return ref.SpanID } if followsFromRef == nil && ref.RefType == dbmodel.FollowsFrom { followsFromRef = &ref } } if followsFromRef != nil { return followsFromRef.SpanID } return "" } ================================================ FILE: internal/storage/v2/elasticsearch/tracestore/package_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v2/elasticsearch/tracestore/reader.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "context" "iter" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" "github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" ) // TraceReader is a wrapper around spanstore.CoreSpanReader which return the output parallel to OTLP Models type TraceReader struct { spanReader spanstore.CoreSpanReader } // NewTraceReader returns an instance of TraceReader func NewTraceReader(p spanstore.SpanReaderParams) *TraceReader { return &TraceReader{ spanReader: spanstore.NewSpanReader(p), } } func (t *TraceReader) GetTraces(ctx context.Context, params ...tracestore.GetTraceParams) iter.Seq2[[]ptrace.Traces, error] { return func(yield func([]ptrace.Traces, error) bool) { dbTraceIds := make([]dbmodel.TraceID, 0, len(params)) for _, id := range params { dbTraceIds = append(dbTraceIds, dbmodel.TraceID(id.TraceID.String())) } dbTraces, err := t.spanReader.GetTraces(ctx, dbTraceIds) if err != nil { yield(nil, err) return } for _, trace := range dbTraces { td, err := FromDBModel(trace.Spans) if err != nil { yield(nil, err) return } if !yield([]ptrace.Traces{td}, nil) { return } } } } func (t *TraceReader) GetServices(ctx context.Context) ([]string, error) { return t.spanReader.GetServices(ctx) } func (t *TraceReader) GetOperations(ctx context.Context, query tracestore.OperationQueryParams) ([]tracestore.Operation, error) { dbOperations, err := t.spanReader.GetOperations(ctx, dbmodel.OperationQueryParameters{ ServiceName: query.ServiceName, SpanKind: query.SpanKind, }) if err != nil { return nil, err } operations := make([]tracestore.Operation, 0, len(dbOperations)) for _, op := range dbOperations { operations = append(operations, tracestore.Operation{ Name: op.Name, SpanKind: op.SpanKind, }) } return operations, nil } func (t *TraceReader) FindTraces(ctx context.Context, query tracestore.TraceQueryParams) iter.Seq2[[]ptrace.Traces, error] { return func(yield func([]ptrace.Traces, error) bool) { traces, err := t.spanReader.FindTraces(ctx, toDBTraceQueryParams(query)) if err != nil { yield(nil, err) return } for _, trace := range traces { td, err := FromDBModel(trace.Spans) if err != nil { yield(nil, err) return } if !yield([]ptrace.Traces{td}, nil) { return } } } } func (t *TraceReader) FindTraceIDs(ctx context.Context, query tracestore.TraceQueryParams) iter.Seq2[[]tracestore.FoundTraceID, error] { return func(yield func([]tracestore.FoundTraceID, error) bool) { traceIds, err := t.spanReader.FindTraceIDs(ctx, toDBTraceQueryParams(query)) if err != nil { yield(nil, err) return } otelTraceIds := make([]tracestore.FoundTraceID, 0, len(traceIds)) for _, traceId := range traceIds { dbTraceId, err := convertTraceIDFromDB(traceId) if err != nil { yield(nil, err) return } otelTraceIds = append(otelTraceIds, tracestore.FoundTraceID{ TraceID: dbTraceId, }) } yield(otelTraceIds, nil) } } func toDBTraceQueryParams(query tracestore.TraceQueryParams) dbmodel.TraceQueryParameters { tags := make(map[string]string) for key, val := range query.Attributes.All() { tags[key] = val.AsString() } return dbmodel.TraceQueryParameters{ ServiceName: query.ServiceName, OperationName: query.OperationName, StartTimeMin: query.StartTimeMin, StartTimeMax: query.StartTimeMax, Tags: tags, NumTraces: query.SearchDepth, DurationMin: query.DurationMin, DurationMax: query.DurationMax, } } ================================================ FILE: internal/storage/v2/elasticsearch/tracestore/reader_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "context" "encoding/json" "errors" "iter" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" "github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch/spanstore/mocks" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" ) func TestTraceReader_GetServices(t *testing.T) { coreReader := &mocks.CoreSpanReader{} reader := TraceReader{spanReader: coreReader} services := []string{"service1", "service2"} coreReader.On("GetServices", mock.Anything).Return(services, nil) actual, err := reader.GetServices(context.Background()) require.NoError(t, err) require.Equal(t, services, actual) } func TestTraceReader_GetOperations(t *testing.T) { coreReader := &mocks.CoreSpanReader{} reader := TraceReader{spanReader: coreReader} operations := []dbmodel.Operation{ { Name: "op-1", SpanKind: "kind--1", }, { Name: "op-2", SpanKind: "kind--2", }, } coreReader.On("GetOperations", mock.Anything, mock.Anything).Return(operations, nil) expected := []tracestore.Operation{ { Name: "op-1", SpanKind: "kind--1", }, { Name: "op-2", SpanKind: "kind--2", }, } actual, err := reader.GetOperations(context.Background(), tracestore.OperationQueryParams{}) require.NoError(t, err) require.Equal(t, expected, actual) } func TestTraceReader_GetOperations_Error(t *testing.T) { coreReader := &mocks.CoreSpanReader{} reader := TraceReader{spanReader: coreReader} coreReader.On("GetOperations", mock.Anything, mock.Anything).Return(nil, errors.New("error")) operations, err := reader.GetOperations(context.Background(), tracestore.OperationQueryParams{}) require.EqualError(t, err, "error") require.Nil(t, operations) } func TestTraceReader_GetTraces(t *testing.T) { coreReader := &mocks.CoreSpanReader{} reader := TraceReader{spanReader: coreReader} tracesStr, spanStr := loadFixtures(t, 1) var span dbmodel.Span require.NoError(t, json.Unmarshal(spanStr, &span)) dbTrace := dbmodel.Trace{Spans: []dbmodel.Span{span}} span.TraceID = "00000000000000020000000000000000" dbTrace2 := dbmodel.Trace{Spans: []dbmodel.Span{span}} coreReader.On("GetTraces", mock.Anything, mock.Anything).Return([]dbmodel.Trace{dbTrace, dbTrace2}, nil) traces := reader.GetTraces(context.Background(), tracestore.GetTraceParams{}) for td, err := range traces { require.NoError(t, err) assert.Len(t, td, 1) testTraces(t, tracesStr, td[0]) break } } func testTraceReaderGetTracesAndFindTracesErrors(t *testing.T, fxnName string, actualTraces func(r TraceReader) iter.Seq2[[]ptrace.Traces, error]) { tests := []struct { name string expectedErr string mockFxn func(m *mocks.CoreSpanReader) }{ { name: "some error from core reader", expectedErr: "some error", mockFxn: func(m *mocks.CoreSpanReader) { m.On(fxnName, mock.Anything, mock.Anything).Return(nil, errors.New("some error")) }, }, { name: "conversion error", mockFxn: func(m *mocks.CoreSpanReader) { dbTraces := []dbmodel.Trace{ { Spans: []dbmodel.Span{ { TraceID: "wrong-trace-id", }, }, }, } m.On(fxnName, mock.Anything, mock.Anything).Return(dbTraces, nil) }, expectedErr: "encoding/hex: invalid byte: U+0077 'w'", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { coreReader := &mocks.CoreSpanReader{} reader := TraceReader{spanReader: coreReader} tt.mockFxn(coreReader) traces := actualTraces(reader) for trace, err := range traces { require.Nil(t, trace) require.ErrorContains(t, err, tt.expectedErr) } }) } } func TestTraceReader_GetTraces_Errors(t *testing.T) { testTraceReaderGetTracesAndFindTracesErrors(t, "GetTraces", func(r TraceReader) iter.Seq2[[]ptrace.Traces, error] { return r.GetTraces(context.Background(), tracestore.GetTraceParams{}) }) } func TestTraceReader_FindTraces(t *testing.T) { coreReader := &mocks.CoreSpanReader{} reader := TraceReader{spanReader: coreReader} tracesStr, spanStr := loadFixtures(t, 1) var span dbmodel.Span require.NoError(t, json.Unmarshal(spanStr, &span)) dbTrace := dbmodel.Trace{Spans: []dbmodel.Span{span}} span.TraceID = "00000000000000020000000000000000" dbTrace2 := dbmodel.Trace{Spans: []dbmodel.Span{span}} coreReader.On("FindTraces", mock.Anything, mock.Anything).Return([]dbmodel.Trace{dbTrace, dbTrace2}, nil) traces := reader.FindTraces(context.Background(), tracestore.TraceQueryParams{ Attributes: pcommon.NewMap(), }) for td, err := range traces { require.NoError(t, err) assert.Len(t, td, 1) testTraces(t, tracesStr, td[0]) break } } func TestTraceReader_FindTraces_Errors(t *testing.T) { testTraceReaderGetTracesAndFindTracesErrors(t, "FindTraces", func(r TraceReader) iter.Seq2[[]ptrace.Traces, error] { return r.FindTraces(context.Background(), tracestore.TraceQueryParams{ Attributes: pcommon.NewMap(), }) }) } func TestTraceReader_FindTraceIDs(t *testing.T) { coreReader := &mocks.CoreSpanReader{} reader := TraceReader{spanReader: coreReader} dbTraceIDs := []dbmodel.TraceID{ "00000000000000010000000000000000", "00000000000000020000000000000000", "00000000000000030000000000000000", } expected := make([]tracestore.FoundTraceID, 0, len(dbTraceIDs)) for _, dbTraceID := range dbTraceIDs { expected = append(expected, fromDBTraceId(t, dbTraceID)) } coreReader.On("FindTraceIDs", mock.Anything, mock.Anything).Return(dbTraceIDs, nil) for traceIds, err := range reader.FindTraceIDs(context.Background(), tracestore.TraceQueryParams{ Attributes: pcommon.NewMap(), }) { require.NoError(t, err) require.Equal(t, expected, traceIds) } } func TestTraceReader_FindTraceIDs_Error(t *testing.T) { tests := []struct { name string errFromCoreReader error traceIdsFromCoreReader []dbmodel.TraceID expectedErr string }{ { name: "some error from core reader", errFromCoreReader: errors.New("some error from core reader"), expectedErr: "some error from core reader", }, { name: "wrong trace id sent from core reader", traceIdsFromCoreReader: []dbmodel.TraceID{"wrong-id"}, expectedErr: "encoding/hex: invalid byte: U+0077 'w'", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { coreReader := &mocks.CoreSpanReader{} attrs := pcommon.NewMap() attrs.PutStr("key1", "val1") ts := time.Now() traceQueryParams := tracestore.TraceQueryParams{ Attributes: attrs, StartTimeMin: ts, ServiceName: "testing-service-name", OperationName: "testing-operation-name", StartTimeMax: ts.Add(1 * time.Hour), DurationMin: 1 * time.Hour, DurationMax: 1 * time.Hour, SearchDepth: 10, } dbTraceQueryParams := dbmodel.TraceQueryParameters{ Tags: map[string]string{"key1": "val1"}, StartTimeMin: ts, ServiceName: "testing-service-name", OperationName: "testing-operation-name", StartTimeMax: ts.Add(1 * time.Hour), DurationMin: 1 * time.Hour, DurationMax: 1 * time.Hour, NumTraces: 10, } coreReader.On("FindTraceIDs", mock.Anything, dbTraceQueryParams).Return(test.traceIdsFromCoreReader, test.errFromCoreReader) reader := TraceReader{spanReader: coreReader} for traceIds, err := range reader.FindTraceIDs(context.Background(), traceQueryParams) { require.ErrorContains(t, err, test.expectedErr) require.Nil(t, traceIds) } }) } } func Test_NewTraceReader(t *testing.T) { reader := NewTraceReader(spanstore.SpanReaderParams{ Logger: zap.NewNop(), }) assert.IsType(t, &spanstore.SpanReader{}, reader.spanReader) } func fromDBTraceId(t *testing.T, traceID dbmodel.TraceID) tracestore.FoundTraceID { traceId, err := convertTraceIDFromDB(traceID) require.NoError(t, err) return tracestore.FoundTraceID{ TraceID: traceId, } } ================================================ FILE: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 // Code originally copied from https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/e49500a9b68447cbbe237fa29526ba99e4963f39/pkg/translator/jaeger/traces_to_jaegerproto.go package tracestore import ( "encoding/hex" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" conventions "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) const ( noServiceName = "OTLPResourceNoServiceName" eventNameAttr = "event" statusError = "ERROR" statusOk = "OK" tagW3CTraceState = "w3c.tracestate" tagHTTPStatusMsg = "http.status_message" tagError = "error" ) // ToDBModel translates internal trace data into the DB Spans. // Returns slice of translated DB Spans and error if translation failed. func ToDBModel(td ptrace.Traces) []dbmodel.Span { resourceSpans := td.ResourceSpans() if resourceSpans.Len() == 0 { return nil } batches := make([]dbmodel.Span, 0, resourceSpans.Len()) for i := 0; i < resourceSpans.Len(); i++ { rs := resourceSpans.At(i) batch := resourceSpansToDbSpans(rs) if batch != nil { batches = append(batches, batch...) } } return batches } func resourceSpansToDbSpans(resourceSpans ptrace.ResourceSpans) []dbmodel.Span { resource := resourceSpans.Resource() scopeSpans := resourceSpans.ScopeSpans() if scopeSpans.Len() == 0 { return []dbmodel.Span{} } process := resourceToDbProcess(resource) // Approximate the number of the spans as the number of the spans in the first // instrumentation library info. dbSpans := make([]dbmodel.Span, 0, scopeSpans.At(0).Spans().Len()) for _, scopeSpan := range scopeSpans.All() { for _, span := range scopeSpan.Spans().All() { dbSpan := spanToDbSpan(span, scopeSpan.Scope(), process) dbSpans = append(dbSpans, dbSpan) } } return dbSpans } func resourceToDbProcess(resource pcommon.Resource) dbmodel.Process { process := dbmodel.Process{} attrs := resource.Attributes() if attrs.Len() == 0 { process.ServiceName = noServiceName return process } tags := make([]dbmodel.KeyValue, 0, attrs.Len()) for key, attr := range attrs.All() { if key == conventions.ServiceNameKey { process.ServiceName = attr.AsString() continue } tags = append(tags, attributeToDbTag(key, attr)) } process.Tags = tags return process } func appendTagsFromAttributes(dest []dbmodel.KeyValue, attrs pcommon.Map) []dbmodel.KeyValue { for key, attr := range attrs.All() { dest = append(dest, attributeToDbTag(key, attr)) } return dest } func attributeToDbTag(key string, attr pcommon.Value) dbmodel.KeyValue { var tag dbmodel.KeyValue switch attr.Type() { case pcommon.ValueTypeBytes: tag = dbmodel.KeyValue{Key: key, Value: hex.EncodeToString(attr.Bytes().AsRaw())} case pcommon.ValueTypeMap, pcommon.ValueTypeSlice: tag = dbmodel.KeyValue{Key: key, Value: attr.AsString()} default: tag = dbmodel.KeyValue{Key: key, Value: attr.AsRaw()} } switch attr.Type() { case pcommon.ValueTypeInt: tag.Type = dbmodel.Int64Type case pcommon.ValueTypeBool: tag.Type = dbmodel.BoolType case pcommon.ValueTypeDouble: tag.Type = dbmodel.Float64Type case pcommon.ValueTypeBytes: tag.Type = dbmodel.BinaryType default: tag.Type = dbmodel.StringType } return tag } func spanToDbSpan(span ptrace.Span, libraryTags pcommon.InstrumentationScope, process dbmodel.Process) dbmodel.Span { traceID := dbmodel.TraceID(span.TraceID().String()) parentSpanID := dbmodel.SpanID(span.ParentSpanID().String()) startTime := span.StartTimestamp().AsTime() return dbmodel.Span{ TraceID: traceID, SpanID: dbmodel.SpanID(span.SpanID().String()), OperationName: span.Name(), References: linksToDbSpanRefs(span.Links(), parentSpanID, traceID), StartTime: model.TimeAsEpochMicroseconds(startTime), StartTimeMillis: model.TimeAsEpochMicroseconds(startTime) / 1000, Duration: model.DurationAsMicroseconds(span.EndTimestamp().AsTime().Sub(startTime)), Tags: getDbSpanTags(span, libraryTags), Logs: spanEventsToDbSpanLogs(span.Events()), Process: process, Flags: span.Flags(), } } func getDbSpanTags(span ptrace.Span, scope pcommon.InstrumentationScope) []dbmodel.KeyValue { var spanKindTag, statusCodeTag, statusMsgTag dbmodel.KeyValue var spanKindTagFound, statusCodeTagFound, statusMsgTagFound bool libraryTags, libraryTagsFound := getTagsFromInstrumentationLibrary(scope) tagsCount := span.Attributes().Len() + len(libraryTags) spanKindTag, spanKindTagFound = getTagFromSpanKind(span.Kind()) if spanKindTagFound { tagsCount++ } status := span.Status() statusCodeTag, statusCodeTagFound = getTagFromStatusCode(status.Code()) if statusCodeTagFound { tagsCount++ } statusMsgTag, statusMsgTagFound = getTagFromStatusMsg(status.Message()) if statusMsgTagFound { tagsCount++ } traceStateTags, traceStateTagsFound := getTagsFromTraceState(span.TraceState().AsRaw()) if traceStateTagsFound { tagsCount += len(traceStateTags) } if tagsCount == 0 { return nil } tags := make([]dbmodel.KeyValue, 0, tagsCount) if libraryTagsFound { tags = append(tags, libraryTags...) } tags = appendTagsFromAttributes(tags, span.Attributes()) if spanKindTagFound { tags = append(tags, spanKindTag) } if statusCodeTagFound { tags = append(tags, statusCodeTag) } if statusMsgTagFound { tags = append(tags, statusMsgTag) } if traceStateTagsFound { tags = append(tags, traceStateTags...) } return tags } // linksToDbSpanRefs constructs jaeger span references based on parent span ID and span links. // The parent span ID is used to add a CHILD_OF reference, _unless_ it is referenced from one of the links. func linksToDbSpanRefs(links ptrace.SpanLinkSlice, parentSpanID dbmodel.SpanID, traceID dbmodel.TraceID) []dbmodel.Reference { refsCount := links.Len() if parentSpanID != "" { refsCount++ } if refsCount == 0 { return nil } refs := make([]dbmodel.Reference, 0, refsCount) // Put parent span ID at the first place because usually backends look for it // as the first CHILD_OF item in the model.SpanRef slice. if parentSpanID != "" { refs = append(refs, dbmodel.Reference{ TraceID: traceID, SpanID: parentSpanID, RefType: dbmodel.ChildOf, }) } for i := 0; i < links.Len(); i++ { link := links.At(i) linkTraceID := dbmodel.TraceID(link.TraceID().String()) linkSpanID := dbmodel.SpanID(link.SpanID().String()) linkRefType := refTypeFromLink(link) if parentSpanID != "" && linkTraceID == traceID && linkSpanID == parentSpanID { // We already added a reference to this span, but maybe with the wrong type, so override. refs[0].RefType = linkRefType continue } refs = append(refs, dbmodel.Reference{ TraceID: linkTraceID, SpanID: linkSpanID, RefType: linkRefType, }) } return refs } func spanEventsToDbSpanLogs(events ptrace.SpanEventSlice) []dbmodel.Log { if events.Len() == 0 { return nil } logs := make([]dbmodel.Log, 0, events.Len()) for i := 0; i < events.Len(); i++ { event := events.At(i) fields := make([]dbmodel.KeyValue, 0, event.Attributes().Len()+1) _, eventAttrFound := event.Attributes().Get(eventNameAttr) if event.Name() != "" && !eventAttrFound { fields = append(fields, dbmodel.KeyValue{ Key: eventNameAttr, Type: dbmodel.StringType, Value: event.Name(), }) } fields = appendTagsFromAttributes(fields, event.Attributes()) logs = append(logs, dbmodel.Log{ Timestamp: model.TimeAsEpochMicroseconds(event.Timestamp().AsTime()), Fields: fields, }) } return logs } func getTagFromSpanKind(spanKind ptrace.SpanKind) (dbmodel.KeyValue, bool) { var tagStr string switch spanKind { case ptrace.SpanKindClient: tagStr = string(model.SpanKindClient) case ptrace.SpanKindServer: tagStr = string(model.SpanKindServer) case ptrace.SpanKindProducer: tagStr = string(model.SpanKindProducer) case ptrace.SpanKindConsumer: tagStr = string(model.SpanKindConsumer) case ptrace.SpanKindInternal: tagStr = string(model.SpanKindInternal) default: return dbmodel.KeyValue{}, false } return dbmodel.KeyValue{ Key: model.SpanKindKey, Type: dbmodel.StringType, Value: tagStr, }, true } func getTagFromStatusCode(statusCode ptrace.StatusCode) (dbmodel.KeyValue, bool) { switch statusCode { case ptrace.StatusCodeOk: return dbmodel.KeyValue{ Key: conventions.OtelStatusCode, Type: dbmodel.StringType, Value: statusOk, }, true case ptrace.StatusCodeError: // For backward compatibility, we also include the error tag // which was previously used in the test fixtures return dbmodel.KeyValue{ Key: "error", Type: dbmodel.BoolType, Value: true, }, true default: return dbmodel.KeyValue{}, false } } func getTagFromStatusMsg(statusMsg string) (dbmodel.KeyValue, bool) { if statusMsg == "" { return dbmodel.KeyValue{}, false } return dbmodel.KeyValue{ Key: conventions.OtelStatusDescription, Type: dbmodel.StringType, Value: statusMsg, }, true } func getTagsFromTraceState(traceState string) ([]dbmodel.KeyValue, bool) { var keyValues []dbmodel.KeyValue exists := traceState != "" if exists { // TODO Bring this inline with solution for jaegertracing/jaeger-client-java #702 once available kv := dbmodel.KeyValue{ Key: tagW3CTraceState, Value: traceState, Type: dbmodel.StringType, } keyValues = append(keyValues, kv) } return keyValues, exists } func getTagsFromInstrumentationLibrary(il pcommon.InstrumentationScope) ([]dbmodel.KeyValue, bool) { var keyValues []dbmodel.KeyValue if ilName := il.Name(); ilName != "" { kv := dbmodel.KeyValue{ Key: conventions.AttributeOtelScopeName, Type: dbmodel.StringType, Value: ilName, } keyValues = append(keyValues, kv) } if ilVersion := il.Version(); ilVersion != "" { kv := dbmodel.KeyValue{ Key: conventions.AttributeOtelScopeVersion, Type: dbmodel.StringType, Value: ilVersion, } keyValues = append(keyValues, kv) } return keyValues, len(keyValues) > 0 } func refTypeFromLink(link ptrace.SpanLink) dbmodel.ReferenceType { refTypeAttr, ok := link.Attributes().Get(conventions.AttributeOpentracingRefType) if !ok { return dbmodel.FollowsFrom } return strToDbSpanRefType(refTypeAttr.Str()) } func strToDbSpanRefType(attr string) dbmodel.ReferenceType { if attr == conventions.AttributeOpentracingRefTypeChildOf { return dbmodel.ChildOf } // There are only 2 types of SpanRefType we assume that everything // that's not a model.ChildOf is a model.FollowsFrom return dbmodel.FollowsFrom } ================================================ FILE: internal/storage/v2/elasticsearch/tracestore/to_dbmodel_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 // Code originally copied from https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/e49500a9b68447cbbe237fa29526ba99e4963f39/pkg/translator/jaeger/traces_to_jaegerproto_test.go package tracestore import ( "bytes" "encoding/json" "fmt" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel" conventions "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" ) func TestGetTagFromStatusCode(t *testing.T) { tests := []struct { name string code ptrace.StatusCode tag dbmodel.KeyValue }{ { name: "ok", code: ptrace.StatusCodeOk, tag: dbmodel.KeyValue{ Key: conventions.OtelStatusCode, Type: dbmodel.StringType, Value: statusOk, }, }, { name: "error", code: ptrace.StatusCodeError, tag: dbmodel.KeyValue{ Key: tagError, Type: dbmodel.BoolType, Value: true, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { got, ok := getTagFromStatusCode(test.code) assert.True(t, ok) assert.Equal(t, test.tag, got) }) } } func TestGetTagFromSpanKind(t *testing.T) { tests := []struct { name string kind ptrace.SpanKind tag dbmodel.KeyValue ok bool }{ { name: "unspecified", kind: ptrace.SpanKindUnspecified, tag: dbmodel.KeyValue{}, ok: false, }, { name: "client", kind: ptrace.SpanKindClient, tag: dbmodel.KeyValue{ Key: model.SpanKindKey, Type: dbmodel.StringType, Value: string(model.SpanKindClient), }, ok: true, }, { name: "server", kind: ptrace.SpanKindServer, tag: dbmodel.KeyValue{ Key: model.SpanKindKey, Type: dbmodel.StringType, Value: string(model.SpanKindServer), }, ok: true, }, { name: "producer", kind: ptrace.SpanKindProducer, tag: dbmodel.KeyValue{ Key: model.SpanKindKey, Type: dbmodel.StringType, Value: string(model.SpanKindProducer), }, ok: true, }, { name: "consumer", kind: ptrace.SpanKindConsumer, tag: dbmodel.KeyValue{ Key: model.SpanKindKey, Type: dbmodel.StringType, Value: string(model.SpanKindConsumer), }, ok: true, }, { name: "internal", kind: ptrace.SpanKindInternal, tag: dbmodel.KeyValue{ Key: model.SpanKindKey, Type: dbmodel.StringType, Value: string(model.SpanKindInternal), }, ok: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { got, ok := getTagFromSpanKind(test.kind) assert.Equal(t, test.ok, ok) assert.Equal(t, test.tag, got) }) } } func TestLinksToDbSpanRefs(t *testing.T) { tests := []struct { name string setupSpan func() ptrace.Span setupLinks func(ptrace.SpanLinkSlice) expectedRefs int expectedRefType dbmodel.ReferenceType description string }{ { name: "empty links with follows from attribute", setupSpan: func() ptrace.Span { traces := ptrace.NewTraces() spans := traces.ResourceSpans().AppendEmpty() scopeSpans := spans.ScopeSpans().AppendEmpty() return scopeSpans.Spans().AppendEmpty() }, setupLinks: func(links ptrace.SpanLinkSlice) { link := links.AppendEmpty() link.Attributes().PutStr("testing-key", "testing-inputValue") link.Attributes().PutStr(conventions.AttributeOpentracingRefType, conventions.AttributeOpentracingRefTypeFollowsFrom) }, expectedRefs: 1, expectedRefType: dbmodel.FollowsFrom, description: "Links with explicit follows-from reference type", }, { name: "links without ref type defaults to FollowsFrom", setupSpan: func() ptrace.Span { traces := ptrace.NewTraces() spans := traces.ResourceSpans().AppendEmpty() scopeSpans := spans.ScopeSpans().AppendEmpty() return scopeSpans.Spans().AppendEmpty() }, setupLinks: func(links ptrace.SpanLinkSlice) { link := links.AppendEmpty() link.Attributes().PutStr("testing-key", "testing-inputValue") }, expectedRefs: 1, expectedRefType: dbmodel.FollowsFrom, description: "Links without reference type should default to FollowsFrom", }, { name: "parent reference with follows from link", setupSpan: func() ptrace.Span { traces := ptrace.NewTraces() resourceSpans := traces.ResourceSpans().AppendEmpty() scopeSpans := resourceSpans.ScopeSpans().AppendEmpty() span := scopeSpans.Spans().AppendEmpty() span.SetTraceID([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}) span.SetSpanID([8]byte{1, 2, 3, 4, 5, 6, 7, 8}) span.SetParentSpanID([8]byte{8, 7, 6, 5, 4, 3, 2, 1}) return span }, setupLinks: func(links ptrace.SpanLinkSlice) { link := links.AppendEmpty() link.SetTraceID([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}) link.SetSpanID([8]byte{8, 7, 6, 5, 4, 3, 2, 1}) link.Attributes().PutStr(conventions.AttributeOpentracingRefType, conventions.AttributeOpentracingRefTypeFollowsFrom) }, expectedRefs: 1, expectedRefType: dbmodel.FollowsFrom, description: "Parent reference should be overridden by link with follows-from type", }, { name: "empty span no links", setupSpan: func() ptrace.Span { traces := ptrace.NewTraces() spans := traces.ResourceSpans().AppendEmpty() scopeSpans := spans.ScopeSpans().AppendEmpty() return scopeSpans.Spans().AppendEmpty() }, setupLinks: func(_ ptrace.SpanLinkSlice) { // No links added }, expectedRefs: 0, expectedRefType: dbmodel.ChildOf, // Not used in this case description: "Span with no links should have no references", }, { name: "span with client kind", setupSpan: func() ptrace.Span { traces := ptrace.NewTraces() spans := traces.ResourceSpans().AppendEmpty() scopeSpans := spans.ScopeSpans().AppendEmpty() span := scopeSpans.Spans().AppendEmpty() span.SetKind(ptrace.SpanKindClient) // This triggers spanKindTagFound = true return span }, setupLinks: func(_ ptrace.SpanLinkSlice) { // No links needed for this test }, expectedRefs: 0, expectedRefType: dbmodel.ChildOf, description: "Span with client kind should have span kind tag", }, { name: "span with unspecified kind", setupSpan: func() ptrace.Span { traces := ptrace.NewTraces() spans := traces.ResourceSpans().AppendEmpty() scopeSpans := spans.ScopeSpans().AppendEmpty() span := scopeSpans.Spans().AppendEmpty() span.SetKind(ptrace.SpanKindUnspecified) // This triggers spanKindTagFound = false return span }, setupLinks: func(_ ptrace.SpanLinkSlice) { // No links needed for this test }, expectedRefs: 0, expectedRefType: dbmodel.ChildOf, description: "Span with unspecified kind should not have span kind tag", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { span := tt.setupSpan() tt.setupLinks(span.Links()) scopeSpans := ptrace.NewScopeSpans() modelSpan := spanToDbSpan(span, scopeSpans.Scope(), dbmodel.Process{}) assert.Len(t, modelSpan.References, tt.expectedRefs, tt.description) if tt.expectedRefs > 0 { assert.Equal(t, tt.expectedRefType, modelSpan.References[0].RefType) } }) } } func TestResourceToDbProcess(t *testing.T) { tests := []struct { name string setupResource func() pcommon.Resource expectedService string expectedTags []dbmodel.KeyValue description string }{ { name: "resource with service name and attributes", setupResource: func() pcommon.Resource { traces := ptrace.NewTraces() resource := traces.ResourceSpans().AppendEmpty().Resource() resource.Attributes().PutStr(conventions.ServiceNameKey, "service") resource.Attributes().PutStr("foo", "bar") return resource }, expectedService: "service", expectedTags: []dbmodel.KeyValue{ { Key: "foo", Value: "bar", Type: dbmodel.StringType, }, }, description: "Resource with service name and additional attributes", }, { name: "resource with only service name", setupResource: func() pcommon.Resource { traces := ptrace.NewTraces() resource := traces.ResourceSpans().AppendEmpty().Resource() resource.Attributes().PutStr(conventions.ServiceNameKey, "service") return resource }, expectedService: "service", expectedTags: []dbmodel.KeyValue{}, description: "Resource with only service name should have empty tags", }, { name: "resource with no attributes", setupResource: func() pcommon.Resource { traces := ptrace.NewTraces() return traces.ResourceSpans().AppendEmpty().Resource() }, expectedService: noServiceName, expectedTags: nil, description: "Resource with no attributes should use default service name", }, { name: "resource with empty service name", setupResource: func() pcommon.Resource { traces := ptrace.NewTraces() resource := traces.ResourceSpans().AppendEmpty().Resource() resource.Attributes().PutStr(conventions.ServiceNameKey, "") // Explicitly empty string resource.Attributes().PutStr("foo", "bar") return resource }, expectedService: "", expectedTags: []dbmodel.KeyValue{ { Key: "foo", Value: "bar", Type: dbmodel.StringType, }, }, description: "Resource with empty service name should use empty string", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { resource := tt.setupResource() process := resourceToDbProcess(resource) assert.Equal(t, tt.expectedService, process.ServiceName, tt.description) assert.Equal(t, tt.expectedTags, process.Tags) }) } } func TestAttributeConversion(t *testing.T) { tests := []struct { name string setupAttr func() pcommon.Map expected []dbmodel.KeyValue }{ { name: "mixed attribute types", setupAttr: func() pcommon.Map { attributes := pcommon.NewMap() attributes.PutBool("bool-val", true) attributes.PutInt("int-val", 123) attributes.PutStr("string-val", "abc") attributes.PutDouble("double-val", 1.23) attributes.PutEmptyBytes("bytes-val").FromRaw([]byte{1, 2, 3, 4}) attributes.PutStr(conventions.ServiceNameKey, "service-name") return attributes }, expected: []dbmodel.KeyValue{ { Key: "bool-val", Type: dbmodel.BoolType, Value: true, }, { Key: "int-val", Type: dbmodel.Int64Type, Value: int64(123), }, { Key: "string-val", Type: dbmodel.StringType, Value: "abc", }, { Key: "double-val", Type: dbmodel.Float64Type, Value: 1.23, }, { Key: "bytes-val", Type: dbmodel.BinaryType, Value: "01020304", }, { Key: conventions.ServiceNameKey, Type: dbmodel.StringType, Value: "service-name", }, }, }, { name: "empty attributes", setupAttr: pcommon.NewMap, expected: []dbmodel.KeyValue{}, }, { name: "map type attributes", setupAttr: func() pcommon.Map { attributes := pcommon.NewMap() attributes.PutEmptyMap("empty-map") return attributes }, expected: []dbmodel.KeyValue{ { Key: "empty-map", Type: dbmodel.StringType, Value: "{}", }, }, }, { name: "slice type attributes", setupAttr: func() pcommon.Map { attributes := pcommon.NewMap() slice := attributes.PutEmptySlice("blockers") slice.AppendEmpty().SetStr("2804-5") slice.AppendEmpty().SetStr("1234-6") return attributes }, expected: []dbmodel.KeyValue{ { Key: "blockers", Type: dbmodel.StringType, Value: `["2804-5","1234-6"]`, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { attrs := tt.setupAttr() result := appendTagsFromAttributes(make([]dbmodel.KeyValue, 0, len(tt.expected)), attrs) assert.Equal(t, tt.expected, result) }) } } func TestStatusMessageHandling(t *testing.T) { tests := []struct { name string statusMsg string expectFound bool expected dbmodel.KeyValue }{ { name: "empty status message", statusMsg: "", expectFound: false, expected: dbmodel.KeyValue{}, }, { name: "non-empty status message", statusMsg: "test-error", expectFound: true, expected: dbmodel.KeyValue{ Key: conventions.OtelStatusDescription, Value: "test-error", Type: dbmodel.StringType, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, ok := getTagFromStatusMsg(tt.statusMsg) assert.Equal(t, tt.expectFound, ok) if tt.expectFound { assert.Equal(t, tt.expected, got) } }) } } func TestRefTypeFromLink(t *testing.T) { tests := []struct { name string setupLink func() ptrace.SpanLink expected dbmodel.ReferenceType }{ { name: "link with child_of reference type", setupLink: func() ptrace.SpanLink { link := ptrace.NewSpanLink() link.Attributes().PutStr(conventions.AttributeOpentracingRefType, conventions.AttributeOpentracingRefTypeChildOf) return link }, expected: dbmodel.ChildOf, }, { name: "link with follows_from reference type", setupLink: func() ptrace.SpanLink { link := ptrace.NewSpanLink() link.Attributes().PutStr(conventions.AttributeOpentracingRefType, conventions.AttributeOpentracingRefTypeFollowsFrom) return link }, expected: dbmodel.FollowsFrom, }, { name: "link without reference type defaults to follows_from", setupLink: ptrace.NewSpanLink, expected: dbmodel.FollowsFrom, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { link := tt.setupLink() result := refTypeFromLink(link) assert.Equal(t, tt.expected, result) }) } } func TestTraceStateHandling(t *testing.T) { tests := []struct { name string traceState string expectFound bool expected []dbmodel.KeyValue }{ { name: "empty trace state", traceState: "", expectFound: false, expected: nil, }, { name: "non-empty trace state", traceState: "key1=value1,key2=value2", expectFound: true, expected: []dbmodel.KeyValue{ { Key: tagW3CTraceState, Value: "key1=value1,key2=value2", Type: dbmodel.StringType, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { keyValues, exists := getTagsFromTraceState(tt.traceState) assert.Equal(t, tt.expectFound, exists) assert.Equal(t, tt.expected, keyValues) }) } } func TestReferenceTypeConversion(t *testing.T) { tests := []struct { name string refType string expected dbmodel.ReferenceType }{ { name: "child of reference", refType: conventions.AttributeOpentracingRefTypeChildOf, expected: dbmodel.ChildOf, }, { name: "follows from reference", refType: conventions.AttributeOpentracingRefTypeFollowsFrom, expected: dbmodel.FollowsFrom, }, { name: "unknown reference type defaults to follows from", refType: "any other string", expected: dbmodel.FollowsFrom, }, { name: "empty reference type defaults to follows from", refType: "", expected: dbmodel.FollowsFrom, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := strToDbSpanRefType(tt.refType) assert.Equal(t, tt.expected, result) }) } } func TestEdgeCases(t *testing.T) { tests := []struct { name string setupTraces func() ptrace.Traces expected any testFunc func(ptrace.Traces) any description string }{ { name: "empty span attributes", setupTraces: func() ptrace.Traces { traces := ptrace.NewTraces() spans := traces.ResourceSpans().AppendEmpty() scopeSpans := spans.ScopeSpans().AppendEmpty() scopeSpans.Spans().AppendEmpty() return traces }, expected: true, testFunc: func(traces ptrace.Traces) any { spans := traces.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0) spanScope := traces.ResourceSpans().At(0).ScopeSpans().At(0).Scope() modelSpan := spanToDbSpan(spans, spanScope, dbmodel.Process{}) return len(modelSpan.Tags) == 0 }, description: "Empty span attributes should result in no tags", }, { name: "resource spans with no scope spans", setupTraces: func() ptrace.Traces { traces := ptrace.NewTraces() traces.ResourceSpans().AppendEmpty() return traces }, expected: true, testFunc: func(traces ptrace.Traces) any { resourceSpans := traces.ResourceSpans().At(0) dbSpans := resourceSpansToDbSpans(resourceSpans) return len(dbSpans) == 0 }, description: "Resource spans with no scope spans should return empty slice", }, { name: "traces with no resource spans", setupTraces: ptrace.NewTraces, expected: true, testFunc: func(traces ptrace.Traces) any { dbSpans := ToDBModel(traces) return dbSpans == nil }, description: "Traces with no resource spans should return nil", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { traces := tt.setupTraces() result := tt.testFunc(traces) assert.Equal(t, tt.expected, result, tt.description) }) } } func TestDbSpanTagsWithStatusAndTraceState(t *testing.T) { tests := []struct { name string setupSpan func() ptrace.Span expectedTagKeys []string description string }{ { name: "span with status message and trace state", setupSpan: func() ptrace.Span { span := ptrace.NewSpan() span.Status().SetMessage("test-error") span.TraceState().FromRaw("key1=value1,key2=value2") return span }, expectedTagKeys: []string{conventions.OtelStatusDescription, tagW3CTraceState}, description: "Span with both status message and trace state should have both tags", }, { name: "span with only status message", setupSpan: func() ptrace.Span { span := ptrace.NewSpan() span.Status().SetMessage("test-error") return span }, expectedTagKeys: []string{conventions.OtelStatusDescription}, description: "Span with only status message should have only status tag", }, { name: "span with only trace state", setupSpan: func() ptrace.Span { span := ptrace.NewSpan() span.TraceState().FromRaw("key1=value1,key2=value2") return span }, expectedTagKeys: []string{tagW3CTraceState}, description: "Span with only trace state should have only trace state tag", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { span := tt.setupSpan() tags := getDbSpanTags(span, pcommon.NewInstrumentationScope()) assert.Len(t, tags, len(tt.expectedTagKeys), tt.description) for i, expectedKey := range tt.expectedTagKeys { assert.Equal(t, expectedKey, tags[i].Key) } }) } } func TestToDbModel_Fixtures(t *testing.T) { tracesStr, spansStr := loadFixtures(t, 1) var span dbmodel.Span err := json.Unmarshal(spansStr, &span) require.NoError(t, err) td, err := FromDBModel([]dbmodel.Span{span}) require.NoError(t, err) testTraces(t, tracesStr, td) spans := ToDBModel(td) assert.Len(t, spans, 1) testSpans(t, spansStr, spans[0]) } func BenchmarkInternalTracesToDbSpans(b *testing.B) { unmarshaller := ptrace.JSONUnmarshaler{} data, err := os.ReadFile("fixtures/otel_traces_01.json") require.NoError(b, err) td, err := unmarshaller.UnmarshalTraces(data) require.NoError(b, err) for b.Loop() { batches := ToDBModel(td) assert.NotEmpty(b, batches) } } func writeActualData(t *testing.T, name string, data []byte) { var prettyJson bytes.Buffer err := json.Indent(&prettyJson, data, "", " ") require.NoError(t, err) path := "fixtures/actual_" + name + ".json" err = os.WriteFile(path, prettyJson.Bytes(), 0o644) require.NoError(t, err) t.Log("Saved the actual " + name + " to " + path) } // Loads and returns domain model and JSON model fixtures with given number i. func loadFixtures(t *testing.T, i int) (tracesData []byte, spansData []byte) { tracesData = loadTraces(t, i) spansData = loadSpans(t, i) return tracesData, spansData } func loadTraces(t *testing.T, i int) []byte { inTraces := fmt.Sprintf("fixtures/otel_traces_%02d.json", i) tracesData, err := os.ReadFile(inTraces) require.NoError(t, err) return tracesData } func loadSpans(t *testing.T, i int) []byte { inSpans := fmt.Sprintf("fixtures/es_%02d.json", i) spansData, err := os.ReadFile(inSpans) require.NoError(t, err) return spansData } func testTraces(t *testing.T, expectedTraces []byte, actualTraces ptrace.Traces) { unmarshaller := ptrace.JSONUnmarshaler{} expectedTd, err := unmarshaller.UnmarshalTraces(expectedTraces) require.NoError(t, err) if !assert.Equal(t, expectedTd, actualTraces) { marshaller := ptrace.JSONMarshaler{} actualTd, err := marshaller.MarshalTraces(actualTraces) require.NoError(t, err) writeActualData(t, "traces", actualTd) } } func testSpans(t *testing.T, expectedSpan []byte, actualSpan dbmodel.Span) { buf := &bytes.Buffer{} enc := json.NewEncoder(buf) enc.SetIndent("", " ") require.NoError(t, enc.Encode(actualSpan)) if !assert.Equal(t, string(expectedSpan), buf.String()) { writeActualData(t, "spans", buf.Bytes()) } } func TestAttributeToDbTag_DefaultCase(t *testing.T) { attr := pcommon.NewValueEmpty() tag := attributeToDbTag("test-key", attr) assert.Equal(t, "test-key", tag.Key) assert.Equal(t, dbmodel.StringType, tag.Type) assert.Nil(t, tag.Value) } func TestGetTagFromStatusCode_DefaultCase(t *testing.T) { tag, shouldInclude := getTagFromStatusCode(ptrace.StatusCodeUnset) assert.False(t, shouldInclude) assert.Equal(t, dbmodel.KeyValue{}, tag) } ================================================ FILE: internal/storage/v2/elasticsearch/tracestore/writer.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "context" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch/spanstore" ) type TraceWriter struct { spanWriter spanstore.CoreSpanWriter } // NewTraceWriter returns the TraceWriter for use func NewTraceWriter(p spanstore.SpanWriterParams) *TraceWriter { return &TraceWriter{ spanWriter: spanstore.NewSpanWriter(p), } } // WriteTraces convert the traces to ES Span model and write into the database func (t *TraceWriter) WriteTraces(_ context.Context, td ptrace.Traces) error { dbSpans := ToDBModel(td) for i := range dbSpans { span := &dbSpans[i] t.spanWriter.WriteSpan(model.EpochMicrosecondsAsTime(span.StartTime), span) } return nil } func (t *TraceWriter) Close() error { return t.spanWriter.Close() } ================================================ FILE: internal/storage/v2/elasticsearch/tracestore/writer_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracestore import ( "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/ptrace" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v1/elasticsearch/spanstore/mocks" ) func TestTraceWriter_WriteTraces(t *testing.T) { coreWriter := &mocks.CoreSpanWriter{} td := ptrace.NewTraces() resourceSpans := td.ResourceSpans().AppendEmpty() resourceSpans.Resource().Attributes().PutStr("service.name", "testing-service") span := resourceSpans.ScopeSpans().AppendEmpty().Spans().AppendEmpty() span.SetName("op-1") dbSpan := ToDBModel(td) writer := TraceWriter{spanWriter: coreWriter} coreWriter.On("WriteSpan", model.EpochMicrosecondsAsTime(dbSpan[0].StartTime), &dbSpan[0]) err := writer.WriteTraces(context.Background(), td) require.NoError(t, err) } func TestTraceWriter_Close(t *testing.T) { coreWriter := &mocks.CoreSpanWriter{} coreWriter.On("Close").Return(nil) writer := TraceWriter{spanWriter: coreWriter} err := writer.Close() require.NoError(t, err) } func Test_NewTraceWriter(t *testing.T) { params := spanstore.SpanWriterParams{ Logger: zap.NewNop(), MetricsFactory: metrics.NullFactory, } writer := NewTraceWriter(params) assert.NotNil(t, writer) } ================================================ FILE: internal/storage/v2/grpc/README.md ================================================ # gRPC Remote Storage Jaeger supports a gRPC-based Remote Storage API that enables integration with custom storage backends not natively supported by Jaeger. A remote storage backend must implement a gRPC server with the following services: - **[TraceReader](https://github.com/jaegertracing/jaeger-idl/tree/main/proto/storage/v2/trace_storage.proto)** Enables Jaeger to read traces from the storage backend. - **[DependencyReader](https://github.com/jaegertracing/jaeger-idl/tree/main/proto/storage/v2/dependency_storage.proto)** Used to load service dependency graphs from storage. - **[TraceService](https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/collector/trace/v1/trace_service.proto)** Allows trace data to be pushed to the storage. This service can run on a separate port if needed. An example configuration for setting up a remote storage backend is available [here](../../../../cmd/jaeger/config-remote-storage.yaml). Note: In this example, the `TraceService` is configured to run on a different port (0.0.0.0:4316), which is overridden in the config file. The integration tests also require a POST HTTP endpoint that can be called to purge the storage backend, ensuring a clean state before each test run. ## Certifying compliance To verify that your remote storage backend works correctly with Jaeger, you can run the integration tests provided by the Jaeger project. ### Step 1: Clone the Jaeger Repository Begin by cloning the Jaeger repository to your local machine: ```bash git clone https://github.com/jaegertracing/jaeger.git cd jaeger ``` ### Step 2: Run the Integration Tests Run the integration tests for the gRPC storage backend using the following command: ```bash STORAGE=grpc \ CUSTOM_STORAGE=true \ REMOTE_STORAGE_ENDPOINT=${MY_REMOTE_STORAGE_ENDPOINT} \ REMOTE_STORAGE_WRITER_ENDPOINT=${MY_REMOTE_STORAGE_WRITER_ENDPOINT} \ PURGER_ENDPOINT=${MY_PURGER_ENDPOINT} \ make jaeger-v2-storage-integration-test ``` The diagram below demonstrates the architecture of the gRPC storage integration test. ``` mermaid flowchart LR Test --> |writeSpan| SpanWriter Test --> |http:$PURGER_ENDPOINT| Purger SpanWriter --> |0.0.0.0:4317| OTLP_Receiver1 OTLP_Receiver1 --> GRPCStorage GRPCStorage --> |grpc:$REMOTE_STORAGE_WRITER_ENDPOINT| TraceService Test --> |readSpan| SpanReader SpanReader --> |0.0.0.0:16685| QueryExtension QueryExtension --> GRPCStorage GRPCStorage --> |grpc:$REMOTE_STORAGE_ENDPOINT| TraceReader GRPCStorage --> |grpc:$REMOTE_STORAGE_ENDPOINT| DependencyReader subgraph Integration Test Executable Test SpanWriter SpanReader end subgraph Jaeger Collector OTLP_Receiver1[OTLP Receiver] QueryExtension[Query Extension] GRPCStorage[gRPC Storage] end subgraph Custom Storage Backend TraceService TraceReader DependencyReader Purger[HTTP/Purger] end ``` ================================================ FILE: internal/storage/v2/grpc/config.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package grpc import ( "time" "go.opentelemetry.io/collector/config/configgrpc" "go.opentelemetry.io/collector/exporter/exporterhelper" "github.com/jaegertracing/jaeger/internal/tenancy" ) type Config struct { configgrpc.ClientConfig `mapstructure:",squash"` // Writer allows overriding the endpoint for writes, e.g. to an OTLP receiver. // If not defined the main endpoint is used for reads and writes. Writer configgrpc.ClientConfig `mapstructure:"writer"` Tenancy tenancy.Options `mapstructure:"multi_tenancy"` exporterhelper.TimeoutConfig `mapstructure:",squash"` } func DefaultConfig() Config { return Config{ TimeoutConfig: exporterhelper.TimeoutConfig{ Timeout: time.Duration(5 * time.Second), }, } } ================================================ FILE: internal/storage/v2/grpc/config_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package grpc import ( "testing" "github.com/stretchr/testify/require" ) func TestDefaultConfig(t *testing.T) { cfg := DefaultConfig() require.NotEmpty(t, cfg.Timeout) } ================================================ FILE: internal/storage/v2/grpc/depreader.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package grpc import ( "context" "fmt" "google.golang.org/grpc" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/proto-gen/storage/v2" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" ) var _ depstore.Reader = (*DependencyReader)(nil) type DependencyReader struct { client storage.DependencyReaderClient } // NewDependencyReader creates a DependencyReader that communicates with a remote gRPC storage server. // The provided gRPC connection is used exclusively for reading dependencies, meaning it is safe // to enable instrumentation on the connection. func NewDependencyReader(conn *grpc.ClientConn) *DependencyReader { return &DependencyReader{ client: storage.NewDependencyReaderClient(conn), } } func (dr *DependencyReader) GetDependencies( ctx context.Context, query depstore.QueryParameters, ) ([]model.DependencyLink, error) { resp, err := dr.client.GetDependencies(ctx, &storage.GetDependenciesRequest{ StartTime: query.StartTime, EndTime: query.EndTime, }) if err != nil { return nil, fmt.Errorf("failed to get dependencies: %w", err) } dependencies := make([]model.DependencyLink, len(resp.Dependencies)) for i, dep := range resp.Dependencies { dependencies[i] = model.DependencyLink{ Parent: dep.Parent, Child: dep.Child, CallCount: dep.CallCount, Source: dep.Source, } } return dependencies, nil } ================================================ FILE: internal/storage/v2/grpc/depreader_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package grpc import ( "context" "net" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/proto-gen/storage/v2" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" ) // testDependenciesServer implements the storage.DependencyReaderServer interface // to simulate responses for testing. type testDependenciesServer struct { storage.UnimplementedDependencyReaderServer dependencies []*storage.Dependency err error } func (t *testDependenciesServer) GetDependencies( context.Context, *storage.GetDependenciesRequest, ) (*storage.GetDependenciesResponse, error) { return &storage.GetDependenciesResponse{ Dependencies: t.dependencies, }, t.err } func startTestDependenciesServer(t *testing.T, testServer *testDependenciesServer) *grpc.ClientConn { listener, err := net.Listen("tcp", ":0") require.NoError(t, err) server := grpc.NewServer() storage.RegisterDependencyReaderServer(server, testServer) return startServer(t, server, listener) } func TestDependencyReader_GetDependencies(t *testing.T) { tests := []struct { name string testServer *testDependenciesServer expectedDependencies []model.DependencyLink expectedError string }{ { name: "success", testServer: &testDependenciesServer{ dependencies: []*storage.Dependency{ { Parent: "service-a", Child: "service-b", CallCount: 42, Source: "source", }, { Parent: "service-c", Child: "service-d", CallCount: 24, Source: "source", }, }, }, expectedDependencies: []model.DependencyLink{ { Parent: "service-a", Child: "service-b", CallCount: 42, Source: "source", }, { Parent: "service-c", Child: "service-d", CallCount: 24, Source: "source", }, }, }, { name: "empty", testServer: &testDependenciesServer{ dependencies: []*storage.Dependency{}, }, expectedDependencies: []model.DependencyLink{}, }, { name: "error", testServer: &testDependenciesServer{ err: assert.AnError, }, expectedError: "failed to get dependencies", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { conn := startTestDependenciesServer(t, test.testServer) reader := NewDependencyReader(conn) dependencies, err := reader.GetDependencies(context.Background(), depstore.QueryParameters{}) if test.expectedError != "" { require.ErrorContains(t, err, test.expectedError) } else { require.Equal(t, test.expectedDependencies, dependencies) } }) } } ================================================ FILE: internal/storage/v2/grpc/factory.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package grpc import ( "context" "errors" "fmt" "io" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/config/configgrpc" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace/noop" "google.golang.org/grpc" "github.com/jaegertracing/jaeger/internal/auth/bearertoken" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" "github.com/jaegertracing/jaeger/internal/telemetry" "github.com/jaegertracing/jaeger/internal/tenancy" ) var ( _ io.Closer = (*Factory)(nil) _ tracestore.Factory = (*Factory)(nil) _ depstore.Factory = (*Factory)(nil) ) type Factory struct { telset telemetry.Settings config Config // readerConn is the gRPC connection used for reading data from the remote storage backend. // It is safe for this connection to have instrumentation enabled without // the risk of recursively generating traces. readerConn *grpc.ClientConn // writerConn is the gRPC connection used for writing data to the remote storage backend. // This connection should not have instrumentation enabled to avoid recursively generating traces. writerConn *grpc.ClientConn } // NewFactory initializes a new gRPC (remote) storage backend. func NewFactory( ctx context.Context, cfg Config, telset telemetry.Settings, ) (*Factory, error) { f := &Factory{ telset: telset, config: cfg, } var writerConfig configgrpc.ClientConfig if cfg.Writer.Endpoint != "" { writerConfig = cfg.Writer } else { writerConfig = cfg.ClientConfig } readerTelset := getTelset(f.telset, f.telset.TracerProvider) writerTelset := getTelset(f.telset, noop.NewTracerProvider()) newClientFn := func(telset component.TelemetrySettings, gcs *configgrpc.ClientConfig, opts ...grpc.DialOption) (conn *grpc.ClientConn, err error) { clientOpts := make([]configgrpc.ToClientConnOption, 0) for _, opt := range opts { clientOpts = append(clientOpts, configgrpc.WithGrpcDialOption(opt)) } return gcs.ToClientConn(ctx, f.telset.Host.GetExtensions(), telset, clientOpts...) } if err := f.initializeConnections(readerTelset, writerTelset, &cfg.ClientConfig, &writerConfig, newClientFn); err != nil { return nil, err } return f, nil } func (f *Factory) CreateTraceReader() (tracestore.Reader, error) { return NewTraceReader(f.readerConn), nil } func (f *Factory) CreateTraceWriter() (tracestore.Writer, error) { return NewTraceWriter(f.writerConn), nil } func (f *Factory) CreateDependencyReader() (depstore.Reader, error) { return NewDependencyReader(f.readerConn), nil } func (f *Factory) Close() error { var errs []error if f.readerConn != nil { errs = append(errs, f.readerConn.Close()) } if f.writerConn != nil { errs = append(errs, f.writerConn.Close()) } return errors.Join(errs...) } func getTelset( telset telemetry.Settings, tracerProvider trace.TracerProvider, ) component.TelemetrySettings { return component.TelemetrySettings{ Logger: telset.Logger, TracerProvider: tracerProvider, MeterProvider: telset.MeterProvider, } } type newClientFn func(telset component.TelemetrySettings, gcs *configgrpc.ClientConfig, opts ...grpc.DialOption) (*grpc.ClientConn, error) func (f *Factory) initializeConnections( readerTelset, writerTelset component.TelemetrySettings, readerConfig, writerConfig *configgrpc.ClientConfig, newClient newClientFn, ) error { if f.config.Auth.HasValue() { return errors.New("authenticator is not supported") } unaryInterceptors := []grpc.UnaryClientInterceptor{bearertoken.NewUnaryClientInterceptor()} streamInterceptors := []grpc.StreamClientInterceptor{bearertoken.NewStreamClientInterceptor()} if tenancyMgr := tenancy.NewManager(&f.config.Tenancy); tenancyMgr.Enabled { unaryInterceptors = append(unaryInterceptors, tenancy.NewClientUnaryInterceptor(tenancyMgr)) streamInterceptors = append(streamInterceptors, tenancy.NewClientStreamInterceptor(tenancyMgr)) } baseOpts := []grpc.DialOption{ grpc.WithChainUnaryInterceptor(unaryInterceptors...), grpc.WithChainStreamInterceptor(streamInterceptors...), } createConn := func(telset component.TelemetrySettings, gcs *configgrpc.ClientConfig) (*grpc.ClientConn, error) { opts := append(baseOpts, grpc.WithStatsHandler( otelgrpc.NewClientHandler( otelgrpc.WithTracerProvider(telset.TracerProvider), otelgrpc.WithMeterProvider(telset.MeterProvider), ), )) return newClient(telset, gcs, opts...) } readerConn, err := createConn(readerTelset, readerConfig) if err != nil { return fmt.Errorf("error creating reader client connection: %w", err) } writerConn, err := createConn(writerTelset, writerConfig) if err != nil { _ = readerConn.Close() return fmt.Errorf("error creating writer client connection: %w", err) } f.readerConn, f.writerConn = readerConn, writerConn return nil } ================================================ FILE: internal/storage/v2/grpc/factory_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package grpc import ( "context" "net" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/config/configauth" "go.opentelemetry.io/collector/config/configgrpc" "go.opentelemetry.io/collector/config/configoptional" "go.opentelemetry.io/collector/exporter/exporterhelper" "google.golang.org/grpc" "github.com/jaegertracing/jaeger/internal/telemetry" "github.com/jaegertracing/jaeger/internal/tenancy" ) func TestNewFactory_NonEmptyAuthenticator(t *testing.T) { cfg := &Config{ ClientConfig: configgrpc.ClientConfig{ Auth: configoptional.Some(configauth.Config{}), }, } _, err := NewFactory(context.Background(), *cfg, telemetry.NoopSettings()) require.ErrorContains(t, err, "authenticator is not supported") } func TestNewFactory(t *testing.T) { lis, err := net.Listen("tcp", ":0") require.NoError(t, err, "failed to listen") t.Cleanup(func() { require.NoError(t, lis.Close()) }) cfg := Config{ ClientConfig: configgrpc.ClientConfig{ Endpoint: lis.Addr().String(), }, TimeoutConfig: exporterhelper.TimeoutConfig{ Timeout: 1 * time.Second, }, Tenancy: tenancy.Options{ Enabled: true, }, } telset := telemetry.NoopSettings() f, err := NewFactory(context.Background(), cfg, telset) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, f.Close()) }) require.Equal(t, lis.Addr().String(), f.readerConn.Target()) require.Equal(t, lis.Addr().String(), f.writerConn.Target()) } func TestNewFactory_WriteEndpointOverride(t *testing.T) { readListener, err := net.Listen("tcp", ":0") require.NoError(t, err, "failed to listen") t.Cleanup(func() { require.NoError(t, readListener.Close()) }) writeListener, err := net.Listen("tcp", ":0") require.NoError(t, err, "failed to listen") t.Cleanup(func() { require.NoError(t, writeListener.Close()) }) cfg := Config{ ClientConfig: configgrpc.ClientConfig{ Endpoint: readListener.Addr().String(), }, Writer: configgrpc.ClientConfig{ Endpoint: writeListener.Addr().String(), }, TimeoutConfig: exporterhelper.TimeoutConfig{ Timeout: 1 * time.Second, }, Tenancy: tenancy.Options{ Enabled: true, }, } telset := telemetry.NoopSettings() f, err := NewFactory(context.Background(), cfg, telset) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, f.Close()) }) require.Equal(t, readListener.Addr().String(), f.readerConn.Target()) require.Equal(t, writeListener.Addr().String(), f.writerConn.Target()) } func TestFactory(t *testing.T) { lis, err := net.Listen("tcp", ":0") require.NoError(t, err, "failed to listen") s := grpc.NewServer() conn := startServer(t, s, lis) f := &Factory{ readerConn: conn, } t.Run("CreateTraceReader", func(t *testing.T) { tr, err := f.CreateTraceReader() require.NoError(t, err) require.NotNil(t, tr) }) t.Run("CreateTraceWriter", func(t *testing.T) { tr, err := f.CreateTraceWriter() require.NoError(t, err) require.NotNil(t, tr) }) t.Run("CreateDependencyReader", func(t *testing.T) { tr, err := f.CreateDependencyReader() require.NoError(t, err) require.NotNil(t, tr) }) } func TestInitializeConnections_ClientError(t *testing.T) { f, err := NewFactory( context.Background(), Config{ ClientConfig: configgrpc.ClientConfig{ Endpoint: ":0", }, }, telemetry.NoopSettings()) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, f.Close()) }) newClientFn := func(_ component.TelemetrySettings, _ *configgrpc.ClientConfig, _ ...grpc.DialOption) (conn *grpc.ClientConn, err error) { return nil, assert.AnError } noopTelset := telemetry.NoopSettings().ToOtelComponent() err = f.initializeConnections( noopTelset, noopTelset, &configgrpc.ClientConfig{}, &configgrpc.ClientConfig{}, newClientFn, ) assert.ErrorContains(t, err, "error creating reader client connection") } ================================================ FILE: internal/storage/v2/grpc/handler.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package grpc import ( "context" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace/ptraceotlp" "google.golang.org/grpc" "google.golang.org/grpc/health" "google.golang.org/grpc/health/grpc_health_v1" "github.com/jaegertracing/jaeger/internal/jptrace" "github.com/jaegertracing/jaeger/internal/proto-gen/storage/v2" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" ) var ( _ storage.TraceReaderServer = (*Handler)(nil) _ storage.DependencyReaderServer = (*Handler)(nil) _ ptraceotlp.GRPCServer = (*Handler)(nil) ) type Handler struct { storage.UnimplementedTraceReaderServer storage.UnimplementedDependencyReaderServer ptraceotlp.UnimplementedGRPCServer traceReader tracestore.Reader traceWriter tracestore.Writer depReader depstore.Reader } func NewHandler( traceReader tracestore.Reader, traceWriter tracestore.Writer, depReader depstore.Reader, ) *Handler { return &Handler{ traceReader: traceReader, traceWriter: traceWriter, depReader: depReader, } } func (h *Handler) GetTraces( req *storage.GetTracesRequest, srv storage.TraceReader_GetTracesServer, ) error { traceIDs := make([]tracestore.GetTraceParams, len(req.Query)) for i, query := range req.Query { var sizedTraceID [16]byte copy(sizedTraceID[:], query.TraceId) traceIDs[i] = tracestore.GetTraceParams{ TraceID: pcommon.TraceID(sizedTraceID), Start: query.StartTime, End: query.EndTime, } } for traces, err := range h.traceReader.GetTraces(srv.Context(), traceIDs...) { if err != nil { return err } for _, trace := range traces { td := jptrace.TracesData(trace) err = srv.Send(&td) if err != nil { return err } } } return nil } func (h *Handler) GetServices( ctx context.Context, _ *storage.GetServicesRequest, ) (*storage.GetServicesResponse, error) { services, err := h.traceReader.GetServices(ctx) if err != nil { return nil, err } return &storage.GetServicesResponse{ Services: services, }, nil } func (h *Handler) GetOperations( ctx context.Context, req *storage.GetOperationsRequest, ) (*storage.GetOperationsResponse, error) { operations, err := h.traceReader.GetOperations(ctx, tracestore.OperationQueryParams{ ServiceName: req.Service, SpanKind: req.SpanKind, }) if err != nil { return nil, err } grpcOperations := make([]*storage.Operation, len(operations)) for i, operation := range operations { grpcOperations[i] = &storage.Operation{ Name: operation.Name, SpanKind: operation.SpanKind, } } return &storage.GetOperationsResponse{ Operations: grpcOperations, }, nil } func (h *Handler) FindTraces( req *storage.FindTracesRequest, srv storage.TraceReader_FindTracesServer, ) error { for traces, err := range h.traceReader.FindTraces(srv.Context(), toTraceQueryParams(req.Query)) { if err != nil { return err } for _, trace := range traces { td := jptrace.TracesData(trace) err = srv.Send(&td) if err != nil { return err } } } return nil } func (h *Handler) FindTraceIDs( ctx context.Context, req *storage.FindTracesRequest, ) (*storage.FindTraceIDsResponse, error) { foundTraceIDs := []*storage.FoundTraceID{} for traceIDs, err := range h.traceReader.FindTraceIDs(ctx, toTraceQueryParams(req.Query)) { if err != nil { return nil, err } for _, traceID := range traceIDs { foundTraceIDs = append(foundTraceIDs, &storage.FoundTraceID{ TraceId: traceID.TraceID[:], Start: traceID.Start, End: traceID.End, }) } } return &storage.FindTraceIDsResponse{ TraceIds: foundTraceIDs, }, nil } func (h *Handler) Export(ctx context.Context, request ptraceotlp.ExportRequest) ( ptraceotlp.ExportResponse, error, ) { err := h.traceWriter.WriteTraces(ctx, request.Traces()) if err != nil { return ptraceotlp.NewExportResponse(), err } return ptraceotlp.NewExportResponse(), nil } func (h *Handler) GetDependencies( ctx context.Context, req *storage.GetDependenciesRequest, ) (*storage.GetDependenciesResponse, error) { dependencies, err := h.depReader.GetDependencies(ctx, depstore.QueryParameters{ StartTime: req.StartTime, EndTime: req.EndTime, }) if err != nil { return nil, err } grpcDependencies := make([]*storage.Dependency, len(dependencies)) for i, dependency := range dependencies { grpcDependencies[i] = &storage.Dependency{ Parent: dependency.Parent, Child: dependency.Child, CallCount: dependency.CallCount, Source: dependency.Source, } } return &storage.GetDependenciesResponse{ Dependencies: grpcDependencies, }, nil } func (h *Handler) Register(ss *grpc.Server, hs *health.Server) { storage.RegisterTraceReaderServer(ss, h) storage.RegisterDependencyReaderServer(ss, h) ptraceotlp.RegisterGRPCServer(ss, h) hs.SetServingStatus("jaeger.storage.v2.TraceReader", grpc_health_v1.HealthCheckResponse_SERVING) hs.SetServingStatus("jaeger.storage.v2.DependencyReader", grpc_health_v1.HealthCheckResponse_SERVING) hs.SetServingStatus("jaeger.storage.v2.TraceWriter", grpc_health_v1.HealthCheckResponse_SERVING) } func toTraceQueryParams(t *storage.TraceQueryParameters) tracestore.TraceQueryParams { return tracestore.TraceQueryParams{ ServiceName: t.ServiceName, OperationName: t.OperationName, Attributes: convertKeyValueListToMap(t.Attributes), StartTimeMin: t.StartTimeMin, StartTimeMax: t.StartTimeMax, DurationMin: t.DurationMin, DurationMax: t.DurationMax, SearchDepth: int(t.SearchDepth), } } func convertKeyValueListToMap(kvList []*storage.KeyValue) pcommon.Map { m := pcommon.NewMap() for _, kv := range kvList { if kv == nil || kv.Value == nil { continue } setValueToMap(m, kv.Key, kv.Value) } return m } func setValueToMap(m pcommon.Map, key string, av *storage.AnyValue) { switch v := av.Value.(type) { case *storage.AnyValue_StringValue: m.PutStr(key, v.StringValue) case *storage.AnyValue_BoolValue: m.PutBool(key, v.BoolValue) case *storage.AnyValue_IntValue: m.PutInt(key, v.IntValue) case *storage.AnyValue_DoubleValue: m.PutDouble(key, v.DoubleValue) case *storage.AnyValue_BytesValue: m.PutEmptyBytes(key).FromRaw(v.BytesValue) case *storage.AnyValue_ArrayValue: sliceVal := m.PutEmptySlice(key) for _, elem := range v.ArrayValue.Values { if elem == nil { sliceVal.AppendEmpty() continue } setValueToSlice(sliceVal, elem) } case *storage.AnyValue_KvlistValue: mapVal := m.PutEmptyMap(key) for _, kv := range v.KvlistValue.Values { if kv == nil || kv.Value == nil { continue } setValueToMap(mapVal, kv.Key, kv.Value) } default: return // unreachable } } func setValueToSlice(slice pcommon.Slice, av *storage.AnyValue) { switch v := av.Value.(type) { case *storage.AnyValue_StringValue: slice.AppendEmpty().SetStr(v.StringValue) case *storage.AnyValue_BoolValue: slice.AppendEmpty().SetBool(v.BoolValue) case *storage.AnyValue_IntValue: slice.AppendEmpty().SetInt(v.IntValue) case *storage.AnyValue_DoubleValue: slice.AppendEmpty().SetDouble(v.DoubleValue) case *storage.AnyValue_BytesValue: slice.AppendEmpty().SetEmptyBytes().FromRaw(v.BytesValue) case *storage.AnyValue_ArrayValue: newSlice := slice.AppendEmpty().SetEmptySlice() for _, subElem := range v.ArrayValue.Values { if subElem == nil { newSlice.AppendEmpty() continue } setValueToSlice(newSlice, subElem) } case *storage.AnyValue_KvlistValue: newMap := slice.AppendEmpty().SetEmptyMap() for _, kv := range v.KvlistValue.Values { if kv == nil || kv.Value == nil { continue } setValueToMap(newMap, kv.Key, kv.Value) } default: return // unreachable } } ================================================ FILE: internal/storage/v2/grpc/handler_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package grpc import ( "context" "iter" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "go.opentelemetry.io/collector/pdata/ptrace/ptraceotlp" "google.golang.org/grpc" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/jptrace" "github.com/jaegertracing/jaeger/internal/proto-gen/storage/v2" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" depstoremocks "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore/mocks" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" tracestoremocks "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore/mocks" ) type testStream struct { grpc.ServerStream sent []*jptrace.TracesData sendErr error } func (*testStream) Context() context.Context { return context.Background() } func (f *testStream) Send(td *jptrace.TracesData) error { if f.sendErr != nil { return f.sendErr } f.sent = append(f.sent, td) return nil } func TestHandler_GetTraces(t *testing.T) { start := time.Now() end := start.Add(time.Minute) query := []tracestore.GetTraceParams{ { TraceID: pcommon.TraceID([16]byte{1}), Start: start, End: end, }, } trace := makeTestTrace() td := jptrace.TracesData(trace) tests := []struct { name string traces [][]ptrace.Traces expectedSent []*jptrace.TracesData sendErr error getTraceErr error expectedErr error }{ { name: "single trace", traces: [][]ptrace.Traces{{trace}}, expectedSent: []*jptrace.TracesData{ &td, }, }, { name: "multiple traces", traces: [][]ptrace.Traces{{trace, trace}}, expectedSent: []*jptrace.TracesData{&td, &td}, }, { name: "multiple chunks", traces: [][]ptrace.Traces{{trace, trace}, {trace, trace}}, expectedSent: []*jptrace.TracesData{&td, &td, &td, &td}, }, { name: "storage error", getTraceErr: assert.AnError, expectedErr: assert.AnError, }, { name: "send error", traces: [][]ptrace.Traces{{trace, trace}}, sendErr: assert.AnError, expectedErr: assert.AnError, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { reader := new(tracestoremocks.Reader) writer := new(tracestoremocks.Writer) depReader := new(depstoremocks.Reader) reader.On("GetTraces", mock.Anything, query). Return(iter.Seq2[[]ptrace.Traces, error](func(yield func([]ptrace.Traces, error) bool) { if test.getTraceErr != nil { yield(nil, test.getTraceErr) return } for _, traces := range test.traces { if !yield(traces, nil) { return } } })).Once() server := NewHandler(reader, writer, depReader) stream := &testStream{ sendErr: test.sendErr, } err := server.GetTraces(&storage.GetTracesRequest{ Query: []*storage.GetTraceParams{ { TraceId: []byte{1}, StartTime: start, EndTime: end, }, }, }, stream) if test.expectedErr != nil { require.ErrorIs(t, err, test.expectedErr) } else { require.NoError(t, err) require.Equal(t, test.expectedSent, stream.sent) } }) } } func TestHandler_GetServices(t *testing.T) { tests := []struct { name string services []string err error expectedServices []string expectedErr error }{ { name: "success", services: []string{"service1", "service2"}, expectedServices: []string{"service1", "service2"}, }, { name: "empty", services: []string{}, expectedServices: []string{}, }, { name: "error", err: assert.AnError, expectedErr: assert.AnError, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { reader := new(tracestoremocks.Reader) writer := new(tracestoremocks.Writer) depReader := new(depstoremocks.Reader) reader.On("GetServices", mock.Anything). Return(test.services, test.err).Once() server := NewHandler(reader, writer, depReader) resp, err := server.GetServices(context.Background(), &storage.GetServicesRequest{}) if test.expectedErr == nil { require.NoError(t, err) require.Equal(t, test.expectedServices, resp.Services) } else { require.ErrorIs(t, err, test.expectedErr) } }) } } func TestHandler_GetOperations(t *testing.T) { params := tracestore.OperationQueryParams{ ServiceName: "service", SpanKind: "kind", } req := &storage.GetOperationsRequest{ Service: "service", SpanKind: "kind", } tests := []struct { name string operations []tracestore.Operation err error expectedOperations []*storage.Operation expectedErr error }{ { name: "success", operations: []tracestore.Operation{ {Name: "operation1", SpanKind: "kind"}, {Name: "operation2", SpanKind: "kind"}, }, expectedOperations: []*storage.Operation{ {Name: "operation1", SpanKind: "kind"}, {Name: "operation2", SpanKind: "kind"}, }, }, { name: "empty", operations: []tracestore.Operation{}, expectedOperations: []*storage.Operation{}, }, { name: "error", err: assert.AnError, expectedErr: assert.AnError, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { reader := new(tracestoremocks.Reader) writer := new(tracestoremocks.Writer) depReader := new(depstoremocks.Reader) reader.On("GetOperations", mock.Anything, params). Return(test.operations, test.err).Once() server := NewHandler(reader, writer, depReader) resp, err := server.GetOperations(context.Background(), req) if test.expectedErr == nil { require.NoError(t, err) require.Equal(t, test.expectedOperations, resp.Operations) } else { require.ErrorIs(t, err, test.expectedErr) } }) } } func TestHandler_FindTraces(t *testing.T) { query := tracestore.TraceQueryParams{ ServiceName: "service", OperationName: "operation", Attributes: pcommon.NewMap(), } trace := makeTestTrace() td := jptrace.TracesData(trace) tests := []struct { name string traces [][]ptrace.Traces expectedSent []*jptrace.TracesData sendErr error getTraceErr error expectedErr error }{ { name: "single trace", traces: [][]ptrace.Traces{{trace}}, expectedSent: []*jptrace.TracesData{ &td, }, }, { name: "multiple traces", traces: [][]ptrace.Traces{{trace, trace}}, expectedSent: []*jptrace.TracesData{&td, &td}, }, { name: "multiple chunks", traces: [][]ptrace.Traces{{trace, trace}, {trace, trace}}, expectedSent: []*jptrace.TracesData{&td, &td, &td, &td}, }, { name: "storage error", getTraceErr: assert.AnError, expectedErr: assert.AnError, }, { name: "send error", traces: [][]ptrace.Traces{{trace, trace}}, sendErr: assert.AnError, expectedErr: assert.AnError, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { reader := new(tracestoremocks.Reader) writer := new(tracestoremocks.Writer) depReader := new(depstoremocks.Reader) reader.On("FindTraces", mock.Anything, query). Return(iter.Seq2[[]ptrace.Traces, error](func(yield func([]ptrace.Traces, error) bool) { if test.getTraceErr != nil { yield(nil, test.getTraceErr) return } for _, traces := range test.traces { if !yield(traces, nil) { return } } })).Once() server := NewHandler(reader, writer, depReader) stream := &testStream{ sendErr: test.sendErr, } err := server.FindTraces(&storage.FindTracesRequest{ Query: &storage.TraceQueryParameters{ ServiceName: "service", OperationName: "operation", }, }, stream) if test.expectedErr != nil { require.ErrorIs(t, err, test.expectedErr) } else { require.NoError(t, err) require.Equal(t, test.expectedSent, stream.sent) } }) } } func TestHandler_FindTraceIDs(t *testing.T) { query := tracestore.TraceQueryParams{ ServiceName: "service", OperationName: "operation", Attributes: pcommon.NewMap(), } now := time.Now() traceIDA := [16]byte{1} traceIDB := [16]byte{2} tests := []struct { name string traceIDs []tracestore.FoundTraceID expectedTraceIDs []*storage.FoundTraceID findTraceIDsErr error expectedErr error }{ { name: "success", traceIDs: []tracestore.FoundTraceID{ { TraceID: traceIDA, Start: now, End: now.Add(time.Minute), }, { TraceID: traceIDB, Start: now, End: now.Add(time.Hour), }, }, expectedTraceIDs: []*storage.FoundTraceID{ { TraceId: traceIDA[:], Start: now, End: now.Add(time.Minute), }, { TraceId: traceIDB[:], Start: now, End: now.Add(time.Hour), }, }, }, { name: "empty", traceIDs: []tracestore.FoundTraceID{}, expectedTraceIDs: []*storage.FoundTraceID{}, }, { name: "error", findTraceIDsErr: assert.AnError, expectedErr: assert.AnError, }, } for _, test := range tests { reader := new(tracestoremocks.Reader) writer := new(tracestoremocks.Writer) depReader := new(depstoremocks.Reader) reader.On("FindTraceIDs", mock.Anything, query). Return(iter.Seq2[[]tracestore.FoundTraceID, error](func(yield func([]tracestore.FoundTraceID, error) bool) { yield(test.traceIDs, test.findTraceIDsErr) })).Once() server := NewHandler(reader, writer, depReader) response, err := server.FindTraceIDs(context.Background(), &storage.FindTracesRequest{ Query: &storage.TraceQueryParameters{ ServiceName: "service", OperationName: "operation", }, }) if test.expectedErr != nil { require.ErrorIs(t, err, test.expectedErr) } else { require.NoError(t, err) require.Equal(t, test.expectedTraceIDs, response.TraceIds) } } } func TestHandler_Export(t *testing.T) { tests := []struct { name string writeTracesErr error expectedErr error }{ { name: "success", }, { name: "write error", expectedErr: assert.AnError, writeTracesErr: assert.AnError, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { reader := new(tracestoremocks.Reader) writer := new(tracestoremocks.Writer) depReader := new(depstoremocks.Reader) writer.On("WriteTraces", mock.Anything, makeTestTrace()).Return(test.writeTracesErr).Once() server := NewHandler(reader, writer, depReader) response, err := server.Export(context.Background(), ptraceotlp.NewExportRequestFromTraces(makeTestTrace())) if test.expectedErr != nil { require.ErrorIs(t, err, test.expectedErr) } else { require.NoError(t, err) } require.Equal(t, ptraceotlp.NewExportResponse(), response) }) } } func TestHandler_GetDependencies(t *testing.T) { now := time.Now() tests := []struct { name string dependencies []model.DependencyLink expectedDependencies []*storage.Dependency err error expectedError error }{ { name: "success", dependencies: []model.DependencyLink{ { Parent: "serviceA", Child: "serviceB", CallCount: 10, Source: "sourceA", }, { Parent: "serviceC", Child: "serviceD", CallCount: 20, Source: "sourceB", }, }, expectedDependencies: []*storage.Dependency{ { Parent: "serviceA", Child: "serviceB", CallCount: 10, Source: "sourceA", }, { Parent: "serviceC", Child: "serviceD", CallCount: 20, Source: "sourceB", }, }, }, { name: "empty", dependencies: []model.DependencyLink{}, expectedDependencies: []*storage.Dependency{}, }, { name: "error", err: assert.AnError, expectedError: assert.AnError, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { reader := new(tracestoremocks.Reader) writer := new(tracestoremocks.Writer) depReader := new(depstoremocks.Reader) depReader.On("GetDependencies", mock.Anything, depstore.QueryParameters{ StartTime: now, EndTime: now.Add(time.Minute), }). Return(test.dependencies, test.err).Once() server := NewHandler(reader, writer, depReader) response, err := server.GetDependencies(context.Background(), &storage.GetDependenciesRequest{ StartTime: now, EndTime: now.Add(time.Minute), }) if test.expectedError != nil { require.ErrorIs(t, err, test.expectedError) } else { require.NoError(t, err) require.Equal(t, test.expectedDependencies, response.Dependencies) } }) } } func TestConvertKeyValueListToMap(t *testing.T) { tests := []struct { name string input []*storage.KeyValue expected pcommon.Map }{ { name: "empty list", input: []*storage.KeyValue{}, expected: pcommon.NewMap(), }, { name: "nil entry", input: []*storage.KeyValue{nil}, expected: pcommon.NewMap(), }, { name: "nil value", input: []*storage.KeyValue{ { Key: "key1", Value: nil, }, }, expected: pcommon.NewMap(), }, { name: "primitive types", input: []*storage.KeyValue{ { Key: "key1", Value: &storage.AnyValue{ Value: &storage.AnyValue_StringValue{StringValue: "value1"}, }, }, { Key: "key2", Value: &storage.AnyValue{ Value: &storage.AnyValue_IntValue{IntValue: 42}, }, }, { Key: "key3", Value: &storage.AnyValue{ Value: &storage.AnyValue_DoubleValue{DoubleValue: 3.14}, }, }, { Key: "key4", Value: &storage.AnyValue{ Value: &storage.AnyValue_BoolValue{BoolValue: true}, }, }, { Key: "key5", Value: &storage.AnyValue{ Value: &storage.AnyValue_BytesValue{BytesValue: []byte{1, 2}}, }, }, }, expected: func() pcommon.Map { m := pcommon.NewMap() m.PutStr("key1", "value1") m.PutInt("key2", 42) m.PutDouble("key3", 3.14) m.PutBool("key4", true) m.PutEmptyBytes("key5").FromRaw([]byte{1, 2}) return m }(), }, { name: "nested map", input: []*storage.KeyValue{ { Key: "key1", Value: &storage.AnyValue{ Value: &storage.AnyValue_KvlistValue{ KvlistValue: &storage.KeyValueList{ Values: []*storage.KeyValue{ { Key: "nestedKey", Value: &storage.AnyValue{ Value: &storage.AnyValue_StringValue{StringValue: "nestedValue"}, }, }, { Key: "nilValueKey", Value: nil, // should be skipped }, }, }, }, }, }, nil, // should be skipped }, expected: func() pcommon.Map { m := pcommon.NewMap() nested := m.PutEmptyMap("key1") nested.PutStr("nestedKey", "nestedValue") return m }(), }, { name: "array", input: []*storage.KeyValue{ { Key: "key1", Value: &storage.AnyValue{ Value: &storage.AnyValue_ArrayValue{ ArrayValue: &storage.ArrayValue{ Values: []*storage.AnyValue{ { Value: &storage.AnyValue_StringValue{StringValue: "value1"}, }, { Value: &storage.AnyValue_IntValue{IntValue: 42}, }, { Value: &storage.AnyValue_DoubleValue{DoubleValue: 3.14}, }, { Value: &storage.AnyValue_BoolValue{BoolValue: true}, }, { Value: &storage.AnyValue_BytesValue{BytesValue: []byte{1, 2}}, }, { Value: &storage.AnyValue_KvlistValue{ KvlistValue: &storage.KeyValueList{ Values: []*storage.KeyValue{ { Key: "nestedKey", Value: &storage.AnyValue{ Value: &storage.AnyValue_StringValue{StringValue: "nestedValue"}, }, }, { Key: "nilValueKey", Value: nil, // should be skipped }, nil, // should be skipped }, }, }, }, nil, }, }, }, }, }, }, expected: func() pcommon.Map { m := pcommon.NewMap() slice := m.PutEmptySlice("key1") slice.AppendEmpty().SetStr("value1") slice.AppendEmpty().SetInt(42) slice.AppendEmpty().SetDouble(3.14) slice.AppendEmpty().SetBool(true) slice.AppendEmpty().SetEmptyBytes().FromRaw([]byte{1, 2}) nested := slice.AppendEmpty().SetEmptyMap() nested.PutStr("nestedKey", "nestedValue") slice.AppendEmpty() // for the nil entry return m }(), }, { name: "nested array", input: []*storage.KeyValue{ { Key: "key1", Value: &storage.AnyValue{ Value: &storage.AnyValue_ArrayValue{ ArrayValue: &storage.ArrayValue{ Values: []*storage.AnyValue{ { Value: &storage.AnyValue_ArrayValue{ ArrayValue: &storage.ArrayValue{ Values: []*storage.AnyValue{ { Value: &storage.AnyValue_StringValue{StringValue: "inner1"}, }, nil, }, }, }, }, nil, }, }, }, }, }, }, expected: func() pcommon.Map { m := pcommon.NewMap() slice := m.PutEmptySlice("key1") nestedSlice := slice.AppendEmpty().SetEmptySlice() nestedSlice.AppendEmpty().SetStr("inner1") nestedSlice.AppendEmpty() // for the nil entry slice.AppendEmpty() // for the nil entry return m }(), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { result := convertKeyValueListToMap(test.input) assert.Equal(t, test.expected, result) }) } } ================================================ FILE: internal/storage/v2/grpc/package_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package grpc import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v2/grpc/tracereader.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package grpc import ( "context" "errors" "fmt" "io" "iter" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "google.golang.org/grpc" "github.com/jaegertracing/jaeger/internal/proto-gen/storage/v2" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" ) var _ tracestore.Reader = (*TraceReader)(nil) type TraceReader struct { client storage.TraceReaderClient } // NewTraceReader creates a TraceReader that communicates with a remote gRPC storage server. // The provided gRPC connection is used exclusively for reading traces, meaning it is safe // to enable instrumentation on the connection without risk of recursively generating traces. func NewTraceReader(conn *grpc.ClientConn) *TraceReader { return &TraceReader{ client: storage.NewTraceReaderClient(conn), } } func (tr *TraceReader) GetTraces( ctx context.Context, traceIDs ...tracestore.GetTraceParams, ) iter.Seq2[[]ptrace.Traces, error] { return func(yield func([]ptrace.Traces, error) bool) { query := []*storage.GetTraceParams{} for _, traceID := range traceIDs { query = append(query, &storage.GetTraceParams{ TraceId: traceID.TraceID[:], StartTime: traceID.Start, EndTime: traceID.End, }) } stream, err := tr.client.GetTraces(ctx, &storage.GetTracesRequest{ Query: query, }) if err != nil { yield(nil, fmt.Errorf("failed to execute GetTraces: %w", err)) return } for received, err := stream.Recv(); !errors.Is(err, io.EOF); received, err = stream.Recv() { if err != nil { yield(nil, fmt.Errorf("received error from grpc stream: %w", err)) return } if !yield([]ptrace.Traces{received.ToTraces()}, nil) { return } } } } func (tr *TraceReader) GetServices(ctx context.Context) ([]string, error) { resp, err := tr.client.GetServices(ctx, &storage.GetServicesRequest{}) if err != nil { return nil, fmt.Errorf("failed to execute GetServices: %w", err) } return resp.Services, nil } func (tr *TraceReader) GetOperations( ctx context.Context, params tracestore.OperationQueryParams, ) ([]tracestore.Operation, error) { resp, err := tr.client.GetOperations(ctx, &storage.GetOperationsRequest{ Service: params.ServiceName, SpanKind: params.SpanKind, }) if err != nil { return nil, fmt.Errorf("failed to execute GetOperations: %w", err) } operations := make([]tracestore.Operation, len(resp.Operations)) for i, op := range resp.Operations { operations[i] = tracestore.Operation{ Name: op.Name, SpanKind: op.SpanKind, } } return operations, nil } func (tr *TraceReader) FindTraces( ctx context.Context, params tracestore.TraceQueryParams, ) iter.Seq2[[]ptrace.Traces, error] { return func(yield func([]ptrace.Traces, error) bool) { stream, err := tr.client.FindTraces(ctx, &storage.FindTracesRequest{ Query: toProtoQueryParameters(params), }) if err != nil { yield(nil, fmt.Errorf("failed to execute FindTraces: %w", err)) return } for received, err := stream.Recv(); !errors.Is(err, io.EOF); received, err = stream.Recv() { if err != nil { yield(nil, fmt.Errorf("received error from grpc stream: %w", err)) return } if !yield([]ptrace.Traces{received.ToTraces()}, nil) { return } } } } func (tr *TraceReader) FindTraceIDs( ctx context.Context, params tracestore.TraceQueryParams, ) iter.Seq2[[]tracestore.FoundTraceID, error] { return func(yield func([]tracestore.FoundTraceID, error) bool) { resp, err := tr.client.FindTraceIDs(ctx, &storage.FindTracesRequest{ Query: toProtoQueryParameters(params), }) if err != nil { yield(nil, fmt.Errorf("failed to execute FindTraceIDs: %w", err)) return } foundTraceIDs := make([]tracestore.FoundTraceID, len(resp.TraceIds)) for i, foundTraceID := range resp.TraceIds { var sizedTraceID [16]byte copy(sizedTraceID[:], foundTraceID.TraceId) foundTraceIDs[i] = tracestore.FoundTraceID{ TraceID: pcommon.TraceID(sizedTraceID), Start: foundTraceID.Start, End: foundTraceID.End, } } yield(foundTraceIDs, nil) } } func toProtoQueryParameters(t tracestore.TraceQueryParams) *storage.TraceQueryParameters { return &storage.TraceQueryParameters{ ServiceName: t.ServiceName, OperationName: t.OperationName, Attributes: convertMapToKeyValueList(t.Attributes), StartTimeMin: t.StartTimeMin, StartTimeMax: t.StartTimeMax, DurationMin: t.DurationMin, DurationMax: t.DurationMax, SearchDepth: int32(t.SearchDepth), //nolint:gosec // G115 } } func convertMapToKeyValueList(m pcommon.Map) []*storage.KeyValue { keyValues := make([]*storage.KeyValue, 0, m.Len()) m.Range(func(k string, v pcommon.Value) bool { keyValues = append(keyValues, &storage.KeyValue{ Key: k, Value: convertValueToAnyValue(v), }) return true }) return keyValues } func convertValueToAnyValue(v pcommon.Value) *storage.AnyValue { switch v.Type() { case pcommon.ValueTypeStr: return &storage.AnyValue{ Value: &storage.AnyValue_StringValue{ StringValue: v.Str(), }, } case pcommon.ValueTypeBool: return &storage.AnyValue{ Value: &storage.AnyValue_BoolValue{ BoolValue: v.Bool(), }, } case pcommon.ValueTypeInt: return &storage.AnyValue{ Value: &storage.AnyValue_IntValue{ IntValue: v.Int(), }, } case pcommon.ValueTypeDouble: return &storage.AnyValue{ Value: &storage.AnyValue_DoubleValue{ DoubleValue: v.Double(), }, } case pcommon.ValueTypeBytes: return &storage.AnyValue{ Value: &storage.AnyValue_BytesValue{ BytesValue: v.Bytes().AsRaw(), }, } case pcommon.ValueTypeSlice: arr := v.Slice() arrayValues := make([]*storage.AnyValue, 0, arr.Len()) for i := 0; i < arr.Len(); i++ { arrayValues = append(arrayValues, convertValueToAnyValue(arr.At(i))) } return &storage.AnyValue{ Value: &storage.AnyValue_ArrayValue{ ArrayValue: &storage.ArrayValue{ Values: arrayValues, }, }, } case pcommon.ValueTypeMap: kvList := &storage.KeyValueList{} v.Map().Range(func(k string, val pcommon.Value) bool { kvList.Values = append(kvList.Values, &storage.KeyValue{ Key: k, Value: convertValueToAnyValue(val), }) return true }) return &storage.AnyValue{Value: &storage.AnyValue_KvlistValue{KvlistValue: kvList}} default: return nil } } ================================================ FILE: internal/storage/v2/grpc/tracereader_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package grpc import ( "context" "net" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "github.com/jaegertracing/jaeger/internal/jiter" "github.com/jaegertracing/jaeger/internal/jptrace" "github.com/jaegertracing/jaeger/internal/proto-gen/storage/v2" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" ) // testServer implements the storage.TraceReaderServer interface // to simulate responses for testing. type testServer struct { storage.UnimplementedTraceReaderServer traces []*jptrace.TracesData services []string operations []*storage.Operation traceIDs []*storage.FoundTraceID err error } func (ts *testServer) GetTraces(_ *storage.GetTracesRequest, s storage.TraceReader_GetTracesServer) error { for _, trace := range ts.traces { s.Send(trace) } return ts.err } func (ts *testServer) GetServices( context.Context, *storage.GetServicesRequest, ) (*storage.GetServicesResponse, error) { return &storage.GetServicesResponse{ Services: ts.services, }, ts.err } func (ts *testServer) GetOperations( context.Context, *storage.GetOperationsRequest, ) (*storage.GetOperationsResponse, error) { return &storage.GetOperationsResponse{ Operations: ts.operations, }, ts.err } func (ts *testServer) FindTraces( _ *storage.FindTracesRequest, s storage.TraceReader_FindTracesServer, ) error { for _, trace := range ts.traces { s.Send(trace) } return ts.err } func (ts *testServer) FindTraceIDs( context.Context, *storage.FindTracesRequest, ) (*storage.FindTraceIDsResponse, error) { return &storage.FindTraceIDsResponse{ TraceIds: ts.traceIDs, }, ts.err } func startTestServer(t *testing.T, testServer *testServer) *grpc.ClientConn { listener, err := net.Listen("tcp", ":0") require.NoError(t, err) server := grpc.NewServer() storage.RegisterTraceReaderServer(server, testServer) return startServer(t, server, listener) } func startServer(t *testing.T, server *grpc.Server, listener net.Listener) *grpc.ClientConn { go func() { server.Serve(listener) }() conn, err := grpc.NewClient( listener.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()), ) require.NoError(t, err) t.Cleanup( func() { conn.Close() server.Stop() listener.Close() }, ) return conn } func makeTestTrace() ptrace.Traces { trace := ptrace.NewTraces() resources := trace.ResourceSpans().AppendEmpty() scopes := resources.ScopeSpans().AppendEmpty() spanA := scopes.Spans().AppendEmpty() spanA.SetName("foobar") spanA.SetTraceID(pcommon.TraceID([16]byte{1})) spanA.SetSpanID(pcommon.SpanID([8]byte{2})) spanA.SetKind(ptrace.SpanKindServer) spanA.Status().SetCode(ptrace.StatusCodeError) return trace } func TestTraceReader_GetTraces(t *testing.T) { tests := []struct { name string testServer *testServer traces []*jptrace.TracesData expectedTraces []ptrace.Traces expectedError string }{ { name: "single trace", testServer: &testServer{ traces: func() []*jptrace.TracesData { trace := makeTestTrace() traces := []*jptrace.TracesData{(*jptrace.TracesData)(&trace)} return traces }(), }, expectedTraces: []ptrace.Traces{makeTestTrace()}, }, { name: "multiple traces", testServer: &testServer{ traces: func() []*jptrace.TracesData { traceA := makeTestTrace() traceB := makeTestTrace() traces := []*jptrace.TracesData{ (*jptrace.TracesData)(&traceA), (*jptrace.TracesData)(&traceB), } return traces }(), }, expectedTraces: []ptrace.Traces{makeTestTrace(), makeTestTrace()}, }, { name: "error", testServer: &testServer{ traces: func() []*jptrace.TracesData { trace := ptrace.NewTraces() traces := []*jptrace.TracesData{(*jptrace.TracesData)(&trace)} return traces }(), err: assert.AnError, }, expectedError: "received error from grpc stream", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { conn := startTestServer(t, test.testServer) reader := NewTraceReader(conn) getTracesIter := reader.GetTraces(context.Background(), tracestore.GetTraceParams{}) traces, err := jiter.FlattenWithErrors(getTracesIter) if test.expectedError != "" { require.ErrorContains(t, err, test.expectedError) } else { require.NoError(t, err) require.Equal(t, test.expectedTraces, traces) } }) } } func TestTraceReader_GetTraces_YieldStopsIteration(t *testing.T) { traceA := makeTestTrace() traceB := makeTestTrace() testServer := &testServer{ traces: []*jptrace.TracesData{ (*jptrace.TracesData)(&traceA), (*jptrace.TracesData)(&traceB), }, } conn := startTestServer(t, testServer) reader := NewTraceReader(conn) getTracesIter := reader.GetTraces(context.Background(), tracestore.GetTraceParams{}) var gotTraces []ptrace.Traces getTracesIter(func(traces []ptrace.Traces, _ error) bool { gotTraces = append(gotTraces, traces...) return false }) require.Len(t, gotTraces, 1) } func TestTraceReader_GetTraces_GRPCClientError(t *testing.T) { conn, err := grpc.NewClient(":0", grpc.WithTransportCredentials(insecure.NewCredentials()), ) // create client without a started server require.NoError(t, err) t.Cleanup(func() { conn.Close() }) reader := NewTraceReader(conn) getTracesIter := reader.GetTraces(context.Background(), tracestore.GetTraceParams{}) _, err = jiter.FlattenWithErrors(getTracesIter) require.ErrorContains(t, err, "failed to execute GetTraces") } func TestTraceReader_GetServices(t *testing.T) { tests := []struct { name string testServer *testServer expectedServices []string expectedError string }{ { name: "success", testServer: &testServer{ services: []string{"service-a", "service-b"}, }, expectedServices: []string{"service-a", "service-b"}, }, { name: "error", testServer: &testServer{ err: assert.AnError, }, expectedError: "failed to execute GetServices", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { conn := startTestServer(t, test.testServer) reader := NewTraceReader(conn) services, err := reader.GetServices(context.Background()) if test.expectedError != "" { require.ErrorContains(t, err, test.expectedError) } else { require.Equal(t, test.expectedServices, services) } }) } } func TestTraceReader_GetOperations(t *testing.T) { tests := []struct { name string testServer *testServer expectedOps []tracestore.Operation expectedError string }{ { name: "success", testServer: &testServer{ operations: []*storage.Operation{ {Name: "operation-a", SpanKind: "kind"}, {Name: "operation-b", SpanKind: "kind"}, }, }, expectedOps: []tracestore.Operation{ {Name: "operation-a", SpanKind: "kind"}, {Name: "operation-b", SpanKind: "kind"}, }, }, { name: "error", testServer: &testServer{ err: assert.AnError, }, expectedError: "failed to execute GetOperations", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { conn := startTestServer(t, test.testServer) reader := NewTraceReader(conn) ops, err := reader.GetOperations(context.Background(), tracestore.OperationQueryParams{ ServiceName: "service-a", SpanKind: "kind", }) if test.expectedError != "" { require.ErrorContains(t, err, test.expectedError) } else { require.Equal(t, test.expectedOps, ops) } }) } } func TestTraceReader_FindTraces(t *testing.T) { queryParams := tracestore.TraceQueryParams{ ServiceName: "service-a", OperationName: "operation-a", Attributes: pcommon.NewMap(), } tests := []struct { name string testServer *testServer traces []*jptrace.TracesData expectedTraces []ptrace.Traces expectedError string }{ { name: "single trace", testServer: &testServer{ traces: func() []*jptrace.TracesData { trace := makeTestTrace() traces := []*jptrace.TracesData{(*jptrace.TracesData)(&trace)} return traces }(), }, expectedTraces: []ptrace.Traces{makeTestTrace()}, }, { name: "multiple traces", testServer: &testServer{ traces: func() []*jptrace.TracesData { traceA := makeTestTrace() traceB := makeTestTrace() traces := []*jptrace.TracesData{ (*jptrace.TracesData)(&traceA), (*jptrace.TracesData)(&traceB), } return traces }(), }, expectedTraces: []ptrace.Traces{makeTestTrace(), makeTestTrace()}, }, { name: "error", testServer: &testServer{ traces: func() []*jptrace.TracesData { trace := ptrace.NewTraces() traces := []*jptrace.TracesData{(*jptrace.TracesData)(&trace)} return traces }(), err: assert.AnError, }, expectedError: "received error from grpc stream", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { conn := startTestServer(t, test.testServer) reader := NewTraceReader(conn) getTracesIter := reader.FindTraces(context.Background(), queryParams) traces, err := jiter.FlattenWithErrors(getTracesIter) if test.expectedError != "" { require.ErrorContains(t, err, test.expectedError) } else { require.NoError(t, err) require.Equal(t, test.expectedTraces, traces) } }) } } func TestTraceReader_FindTraces_YieldStopsIteration(t *testing.T) { queryParams := tracestore.TraceQueryParams{ ServiceName: "service-a", OperationName: "operation-a", Attributes: pcommon.NewMap(), } traceA := makeTestTrace() traceB := makeTestTrace() testServer := &testServer{ traces: []*jptrace.TracesData{ (*jptrace.TracesData)(&traceA), (*jptrace.TracesData)(&traceB), }, } conn := startTestServer(t, testServer) reader := NewTraceReader(conn) getTracesIter := reader.FindTraces(context.Background(), queryParams) var gotTraces []ptrace.Traces getTracesIter(func(traces []ptrace.Traces, _ error) bool { gotTraces = append(gotTraces, traces...) return false }) require.Len(t, gotTraces, 1) } func TestTraceReader_FindTraces_GRPCClientError(t *testing.T) { queryParams := tracestore.TraceQueryParams{ ServiceName: "service-a", OperationName: "operation-a", Attributes: pcommon.NewMap(), } conn, err := grpc.NewClient(":0", grpc.WithTransportCredentials(insecure.NewCredentials()), ) // create client without a started server require.NoError(t, err) t.Cleanup(func() { conn.Close() }) reader := NewTraceReader(conn) getTracesIter := reader.FindTraces(context.Background(), queryParams) _, err = jiter.FlattenWithErrors(getTracesIter) require.ErrorContains(t, err, "failed to execute FindTraces") } func TestTraceReader_FindTraceIDs(t *testing.T) { queryParams := tracestore.TraceQueryParams{ ServiceName: "service-a", OperationName: "operation-a", Attributes: pcommon.NewMap(), } now := time.Now().UTC() tests := []struct { name string testServer *testServer queryParams tracestore.TraceQueryParams expectedIDs []tracestore.FoundTraceID expectedError string }{ { name: "success", testServer: &testServer{ traceIDs: []*storage.FoundTraceID{ { TraceId: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, Start: now, End: now.Add(1 * time.Second), }, { TraceId: []byte{2, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, Start: now, End: now.Add(1 * time.Minute), }, }, }, queryParams: queryParams, expectedIDs: []tracestore.FoundTraceID{ { TraceID: pcommon.TraceID([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}), Start: now, End: now.Add(1 * time.Second), }, { TraceID: pcommon.TraceID([16]byte{2, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}), Start: now, End: now.Add(1 * time.Minute), }, }, }, { name: "trace ID with less than 16 bytes", testServer: &testServer{ traceIDs: []*storage.FoundTraceID{ { TraceId: []byte{1, 2, 3, 4, 5, 6, 7, 8}, Start: now, End: now.Add(1 * time.Second), }, }, }, queryParams: queryParams, expectedIDs: []tracestore.FoundTraceID{ { TraceID: pcommon.TraceID([16]byte{1, 2, 3, 4, 5, 6, 7, 8}), Start: now, End: now.Add(1 * time.Second), }, }, }, { name: "trace ID with more than 16 bytes", testServer: &testServer{ traceIDs: []*storage.FoundTraceID{ { TraceId: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17}, Start: now, End: now.Add(1 * time.Second), }, }, }, queryParams: queryParams, expectedIDs: []tracestore.FoundTraceID{ { TraceID: pcommon.TraceID([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}), Start: now, End: now.Add(1 * time.Second), }, }, }, { name: "error", testServer: &testServer{ err: assert.AnError, }, queryParams: queryParams, expectedError: "failed to execute FindTraceIDs", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { conn := startTestServer(t, test.testServer) reader := NewTraceReader(conn) foundIDsIter := reader.FindTraceIDs(context.Background(), test.queryParams) foundIDs, err := jiter.FlattenWithErrors(foundIDsIter) if test.expectedError != "" { require.ErrorContains(t, err, test.expectedError) } else { require.NoError(t, err) require.Equal(t, test.expectedIDs, foundIDs) } }) } } func TestConvertMapToKeyValueList(t *testing.T) { tests := []struct { name string attributes pcommon.Map expected []*storage.KeyValue }{ { name: "empty map", attributes: pcommon.NewMap(), expected: []*storage.KeyValue{}, }, { name: "empty value", attributes: func() pcommon.Map { m := pcommon.NewMap() m.PutEmpty("key1") return m }(), expected: []*storage.KeyValue{ { Key: "key1", Value: nil, }, }, }, { name: "primitive types", attributes: func() pcommon.Map { m := pcommon.NewMap() m.PutStr("key1", "value1") m.PutInt("key2", 42) m.PutDouble("key3", 3.14) m.PutBool("key4", true) m.PutEmptyBytes("key5").Append(1, 2) return m }(), expected: []*storage.KeyValue{ { Key: "key1", Value: &storage.AnyValue{ Value: &storage.AnyValue_StringValue{ StringValue: "value1", }, }, }, { Key: "key2", Value: &storage.AnyValue{ Value: &storage.AnyValue_IntValue{ IntValue: 42, }, }, }, { Key: "key3", Value: &storage.AnyValue{ Value: &storage.AnyValue_DoubleValue{ DoubleValue: 3.14, }, }, }, { Key: "key4", Value: &storage.AnyValue{ Value: &storage.AnyValue_BoolValue{ BoolValue: true, }, }, }, { Key: "key5", Value: &storage.AnyValue{ Value: &storage.AnyValue_BytesValue{ BytesValue: []byte{1, 2}, }, }, }, }, }, { name: "nested map", attributes: func() pcommon.Map { m := pcommon.NewMap() nested := pcommon.NewMap() nested.PutStr("nestedKey", "nestedValue") nested.CopyTo(m.PutEmptyMap("key1")) return m }(), expected: []*storage.KeyValue{ { Key: "key1", Value: &storage.AnyValue{ Value: &storage.AnyValue_KvlistValue{ KvlistValue: &storage.KeyValueList{ Values: []*storage.KeyValue{ { Key: "nestedKey", Value: &storage.AnyValue{ Value: &storage.AnyValue_StringValue{ StringValue: "nestedValue", }, }, }, }, }, }, }, }, }, }, { name: "array attribute", attributes: func() pcommon.Map { m := pcommon.NewMap() arr := pcommon.NewValueSlice() arr.Slice().AppendEmpty().SetStr("value1") arr.Slice().AppendEmpty().SetInt(42) arr.Slice().CopyTo(m.PutEmptySlice("key1")) return m }(), expected: []*storage.KeyValue{ { Key: "key1", Value: &storage.AnyValue{ Value: &storage.AnyValue_ArrayValue{ ArrayValue: &storage.ArrayValue{ Values: []*storage.AnyValue{ { Value: &storage.AnyValue_StringValue{ StringValue: "value1", }, }, { Value: &storage.AnyValue_IntValue{ IntValue: 42, }, }, }, }, }, }, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { result := convertMapToKeyValueList(test.attributes) require.Equal(t, test.expected, result) }) } } ================================================ FILE: internal/storage/v2/grpc/tracewriter.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package grpc import ( "context" "fmt" "go.opentelemetry.io/collector/pdata/ptrace" "go.opentelemetry.io/collector/pdata/ptrace/ptraceotlp" "google.golang.org/grpc" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" ) var _ tracestore.Writer = (*TraceWriter)(nil) type TraceWriter struct { client ptraceotlp.GRPCClient } // NewTraceWriter creates a TraceWriter that exports traces to a remote gRPC storage server. // // The provided gRPC connection is used exclusively for sending trace data to the backend. // To prevent recursive trace generation, this connection should not have instrumentation enabled. func NewTraceWriter(conn *grpc.ClientConn) *TraceWriter { return &TraceWriter{ client: ptraceotlp.NewGRPCClient(conn), } } func (tw *TraceWriter) WriteTraces(ctx context.Context, td ptrace.Traces) error { req := ptraceotlp.NewExportRequestFromTraces(td) _, err := tw.client.Export(ctx, req) if err != nil { return fmt.Errorf("failed to export traces: %w", err) } return nil } ================================================ FILE: internal/storage/v2/grpc/tracewriter_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package grpc import ( "context" "net" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/ptrace" "go.opentelemetry.io/collector/pdata/ptrace/ptraceotlp" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) type testWriterServer struct { ptraceotlp.UnimplementedGRPCServer err error } func (s *testWriterServer) Export( context.Context, ptraceotlp.ExportRequest, ) (ptraceotlp.ExportResponse, error) { return ptraceotlp.NewExportResponse(), s.err } func startWriterServer(t *testing.T, testServer *testWriterServer) *grpc.ClientConn { listener, err := net.Listen("tcp", ":0") require.NoError(t, err) server := grpc.NewServer() ptraceotlp.RegisterGRPCServer(server, testServer) go func() { server.Serve(listener) }() conn, err := grpc.NewClient( listener.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()), ) require.NoError(t, err) t.Cleanup( func() { conn.Close() server.Stop() listener.Close() }, ) return conn } func TestTraceWriter_WriteTraces(t *testing.T) { tests := []struct { name string serverErr error expectedErr string }{ { name: "no error", }, { name: "server error", serverErr: assert.AnError, expectedErr: "failed to export traces", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { server := &testWriterServer{err: test.serverErr} conn := startWriterServer(t, server) writer := NewTraceWriter(conn) err := writer.WriteTraces(context.Background(), ptrace.NewTraces()) if test.expectedErr == "" { require.NoError(t, err) } else { require.ErrorContains(t, err, test.expectedErr) } }) } } ================================================ FILE: internal/storage/v2/memory/config.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package memory import "github.com/asaskevich/govalidator" // Configuration describes the options to customize the storage behavior. type Configuration struct { // MaxTraces is the maximum amount of traces to store in memory. // If multi-tenancy is enabled, this limit applies per tenant. // Zero value (default) means no limit (Warning: memory usage will be unbounded). MaxTraces int `mapstructure:"max_traces"` } func (c *Configuration) Validate() error { _, err := govalidator.ValidateStruct(c) return err } ================================================ FILE: internal/storage/v2/memory/config_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package memory import ( "testing" "github.com/stretchr/testify/require" ) func TestValidate_DoesNotReturnErrorWhenValid(t *testing.T) { tests := []struct { name string config *Configuration }{ { name: "non-required fields not set", config: &Configuration{}, }, { name: "all fields are set", config: &Configuration{ MaxTraces: 100, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { err := test.config.Validate() require.NoError(t, err) }) } } ================================================ FILE: internal/storage/v2/memory/factory.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package memory import ( "context" "github.com/jaegertracing/jaeger/internal/distributedlock" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/storage/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore/tracestoremetrics" "github.com/jaegertracing/jaeger/internal/telemetry" ) var ( _ tracestore.Factory = (*Factory)(nil) _ storage.SamplingStoreFactory = (*Factory)(nil) _ storage.Purger = (*Factory)(nil) ) type Factory struct { store *Store metricsFactory metrics.Factory } func NewFactory(cfg Configuration, telset telemetry.Settings) (*Factory, error) { store, err := NewStore(cfg) if err != nil { return nil, err } return &Factory{ store: store, metricsFactory: telset.Metrics, }, nil } func (f *Factory) CreateTraceReader() (tracestore.Reader, error) { return tracestoremetrics.NewReaderDecorator(f.store, f.metricsFactory), nil } func (f *Factory) CreateTraceWriter() (tracestore.Writer, error) { return f.store, nil } func (f *Factory) CreateDependencyReader() (depstore.Reader, error) { return f.store, nil } func (*Factory) CreateSamplingStore(buckets int) (samplingstore.Store, error) { return NewSamplingStore(buckets), nil } func (*Factory) CreateLock() (distributedlock.Lock, error) { return &Lock{}, nil } func (f *Factory) Purge(_ context.Context) error { return f.store.Purge() } ================================================ FILE: internal/storage/v2/memory/factory_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package memory import ( "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/telemetry" ) func TestNewFactory(t *testing.T) { f, err := NewFactory(Configuration{MaxTraces: 10}, telemetry.NoopSettings()) require.NoError(t, err) _, err = f.CreateTraceWriter() require.NoError(t, err) _, err = f.CreateTraceReader() require.NoError(t, err) _, err = f.CreateDependencyReader() require.NoError(t, err) _, err = f.CreateSamplingStore(5) require.NoError(t, err) _, err = f.CreateLock() require.NoError(t, err) require.NoError(t, f.Purge(context.Background())) } func TestNewFactoryErr(t *testing.T) { f, err := NewFactory(Configuration{}, telemetry.NoopSettings()) require.ErrorContains(t, err, "max traces must be greater than zero") assert.Nil(t, f) } ================================================ FILE: internal/storage/v2/memory/fixtures/db_traces_01.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "service-x" } } ] }, "scopeSpans": [ { "scope": { "name": "testing-library-2", "version": "1.1.1", "attributes": [ { "key": "scope.attributes.2", "value": { "stringValue": "attribute-y" } } ] }, "spans": [ { "traceId": "00000000000000010000000000000000", "spanId": "0000000000000002", "parentSpanId": "0000000000000003", "kind": 2, "traceState": "state.2", "flags": 1, "name": "test-general-conversion-2", "startTimeUnixNano": "1485467191639875000", "endTimeUnixNano": "1485467191639880000", "attributes": [ { "key": "peer.service", "value": { "stringValue": "service-y" } }, { "key": "peer.ipv4", "value": { "intValue": "23456" } }, { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } }, { "key": "temperature", "value": { "doubleValue": 72.5 } } ], "events": [ { "timeUnixNano": "1485467191639875000", "name": "testing-event", "attributes": [ { "key": "event-x", "value": { "stringValue": "event-y" } } ] }, { "timeUnixNano": "1485467191639875000", "attributes": [ { "key": "x", "value": { "stringValue": "y" } } ] } ], "links": [ { "traceId": "00000000000000010000000000000000", "spanId": "0000000000000004", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "follows_from" } } ] }, { "traceId": "00000000000000ff0000000000000000", "spanId": "00000000000000ff", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "child_of" } } ] } ], "status": { "code": 2 } } ] }, { "scope": { "name": "testing-library-3", "version": "1.1.2", "attributes": [ { "key": "scope.attributes.3", "value": { "stringValue": "attribute-y" } } ] }, "spans": [ { "traceId": "00000000000000010000000000000000", "spanId": "0000000000000004", "parentSpanId": "0000000000000011", "kind": 4, "traceState": "state.4", "flags": 1, "name": "test-general-conversion-4", "startTimeUnixNano": "1485467191639875000", "endTimeUnixNano": "1485467191639880000", "attributes": [ { "key": "peer.service", "value": { "stringValue": "service-y" } }, { "key": "peer.ipv4", "value": { "intValue": "23456" } }, { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } }, { "key": "temperature", "value": { "doubleValue": 72.5 } } ], "events": [ { "timeUnixNano": "1485467191639875000", "name": "testing-event", "attributes": [ { "key": "event-x", "value": { "stringValue": "event-y" } } ] }, { "timeUnixNano": "1485467191639875000", "attributes": [ { "key": "x", "value": { "stringValue": "y" } } ] } ], "links": [ { "traceId": "00000000000000010000000000000000", "spanId": "0000000000000004", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "follows_from" } } ] }, { "traceId": "00000000000000ff0000000000000000", "spanId": "00000000000000ff", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "child_of" } } ] } ], "status": { "code": 2 } } ] } ] } ] } ================================================ FILE: internal/storage/v2/memory/fixtures/db_traces_02.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "service-x" } } ] }, "scopeSpans": [ { "scope": { "name": "testing-library-2", "version": "1.1.1", "attributes": [ { "key": "scope.attributes.2", "value": { "stringValue": "attribute-y" } } ] }, "spans": [ { "traceId": "00000000000000020000000000000000", "spanId": "0000000000000003", "parentSpanId": "0000000000000010", "kind": 3, "traceState": "state.3", "flags": 1, "name": "test-general-conversion-3", "startTimeUnixNano": "1485467191639875000", "endTimeUnixNano": "1485467191639880000", "attributes": [ { "key": "peer.service", "value": { "stringValue": "service-y" } }, { "key": "peer.ipv4", "value": { "intValue": "23456" } }, { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } }, { "key": "temperature", "value": { "doubleValue": 72.5 } } ], "events": [ { "timeUnixNano": "1485467191639875000", "name": "testing-event", "attributes": [ { "key": "event-x", "value": { "stringValue": "event-y" } } ] }, { "timeUnixNano": "1485467191639875000", "attributes": [ { "key": "x", "value": { "stringValue": "y" } } ] } ], "links": [ { "traceId": "00000000000000010000000000000000", "spanId": "0000000000000004", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "follows_from" } } ] }, { "traceId": "00000000000000ff0000000000000000", "spanId": "00000000000000ff", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "child_of" } } ] } ], "status": { "code": 2 } } ] }, { "scope": { "name": "testing-library-3", "version": "1.1.2", "attributes": [ { "key": "scope.attributes.3", "value": { "stringValue": "attribute-y" } } ] }, "spans": [ { "traceId": "00000000000000020000000000000000", "spanId": "0000000000000005", "parentSpanId": "0000000000000010", "kind": 5, "traceState": "state.5", "flags": 1, "name": "test-general-conversion-5", "startTimeUnixNano": "1485467191639875000", "endTimeUnixNano": "1485467191639880000", "attributes": [ { "key": "peer.service", "value": { "stringValue": "service-y" } }, { "key": "peer.ipv4", "value": { "intValue": "23456" } }, { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } }, { "key": "temperature", "value": { "doubleValue": 72.5 } } ], "events": [ { "timeUnixNano": "1485467191639875000", "name": "testing-event", "attributes": [ { "key": "event-x", "value": { "stringValue": "event-y" } } ] }, { "timeUnixNano": "1485467191639875000", "attributes": [ { "key": "x", "value": { "stringValue": "y" } } ] } ], "links": [ { "traceId": "00000000000000010000000000000000", "spanId": "0000000000000004", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "follows_from" } } ] }, { "traceId": "00000000000000ff0000000000000000", "spanId": "00000000000000ff", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "child_of" } } ] } ], "status": { "code": 2 } } ] } ] } ] } ================================================ FILE: internal/storage/v2/memory/fixtures/otel_traces_01.json ================================================ { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "service-x" } } ] }, "scopeSpans": [ { "scope": { "name": "testing-library-2", "version": "1.1.1", "attributes": [ { "key": "scope.attributes.2", "value": { "stringValue": "attribute-y" } } ] }, "spans": [ { "traceId": "00000000000000010000000000000000", "spanId": "0000000000000002", "parentSpanId": "0000000000000003", "kind": 2, "traceState": "state.2", "flags": 1, "name": "test-general-conversion-2", "startTimeUnixNano": "1485467191639875000", "endTimeUnixNano": "1485467191639880000", "attributes": [ { "key": "peer.service", "value": { "stringValue": "service-y" } }, { "key": "peer.ipv4", "value": { "intValue": "23456" } }, { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } }, { "key": "temperature", "value": { "doubleValue": 72.5 } } ], "events": [ { "timeUnixNano": "1485467191639875000", "name": "testing-event", "attributes": [ { "key": "event-x", "value": { "stringValue": "event-y" } } ] }, { "timeUnixNano": "1485467191639875000", "attributes": [ { "key": "x", "value": { "stringValue": "y" } } ] } ], "links": [ { "traceId": "00000000000000010000000000000000", "spanId": "0000000000000004", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "follows_from" } } ] }, { "traceId": "00000000000000ff0000000000000000", "spanId": "00000000000000ff", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "child_of" } } ] } ], "status": { "code": 2 } }, { "traceId": "00000000000000020000000000000000", "spanId": "0000000000000003", "parentSpanId": "0000000000000010", "kind": 3, "traceState": "state.3", "flags": 1, "name": "test-general-conversion-3", "startTimeUnixNano": "1485467191639875000", "endTimeUnixNano": "1485467191639880000", "attributes": [ { "key": "peer.service", "value": { "stringValue": "service-y" } }, { "key": "peer.ipv4", "value": { "intValue": "23456" } }, { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } }, { "key": "temperature", "value": { "doubleValue": 72.5 } } ], "events": [ { "timeUnixNano": "1485467191639875000", "name": "testing-event", "attributes": [ { "key": "event-x", "value": { "stringValue": "event-y" } } ] }, { "timeUnixNano": "1485467191639875000", "attributes": [ { "key": "x", "value": { "stringValue": "y" } } ] } ], "links": [ { "traceId": "00000000000000010000000000000000", "spanId": "0000000000000004", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "follows_from" } } ] }, { "traceId": "00000000000000ff0000000000000000", "spanId": "00000000000000ff", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "child_of" } } ] } ], "status": { "code": 2 } } ] }, { "scope": { "name": "testing-library-3", "version": "1.1.2", "attributes": [ { "key": "scope.attributes.3", "value": { "stringValue": "attribute-y" } } ] }, "spans": [ { "traceId": "00000000000000010000000000000000", "spanId": "0000000000000004", "parentSpanId": "0000000000000011", "kind": 4, "traceState": "state.4", "flags": 1, "name": "test-general-conversion-4", "startTimeUnixNano": "1485467191639875000", "endTimeUnixNano": "1485467191639880000", "attributes": [ { "key": "peer.service", "value": { "stringValue": "service-y" } }, { "key": "peer.ipv4", "value": { "intValue": "23456" } }, { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } }, { "key": "temperature", "value": { "doubleValue": 72.5 } } ], "events": [ { "timeUnixNano": "1485467191639875000", "name": "testing-event", "attributes": [ { "key": "event-x", "value": { "stringValue": "event-y" } } ] }, { "timeUnixNano": "1485467191639875000", "attributes": [ { "key": "x", "value": { "stringValue": "y" } } ] } ], "links": [ { "traceId": "00000000000000010000000000000000", "spanId": "0000000000000004", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "follows_from" } } ] }, { "traceId": "00000000000000ff0000000000000000", "spanId": "00000000000000ff", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "child_of" } } ] } ], "status": { "code": 2 } }, { "traceId": "00000000000000020000000000000000", "spanId": "0000000000000005", "parentSpanId": "0000000000000010", "kind": 5, "traceState": "state.5", "flags": 1, "name": "test-general-conversion-5", "startTimeUnixNano": "1485467191639875000", "endTimeUnixNano": "1485467191639880000", "attributes": [ { "key": "peer.service", "value": { "stringValue": "service-y" } }, { "key": "peer.ipv4", "value": { "intValue": "23456" } }, { "key": "blob", "value": { "bytesValue": "AAAwOQ==" } }, { "key": "temperature", "value": { "doubleValue": 72.5 } } ], "events": [ { "timeUnixNano": "1485467191639875000", "name": "testing-event", "attributes": [ { "key": "event-x", "value": { "stringValue": "event-y" } } ] }, { "timeUnixNano": "1485467191639875000", "attributes": [ { "key": "x", "value": { "stringValue": "y" } } ] } ], "links": [ { "traceId": "00000000000000010000000000000000", "spanId": "0000000000000004", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "follows_from" } } ] }, { "traceId": "00000000000000ff0000000000000000", "spanId": "00000000000000ff", "attributes": [ { "key": "opentracing.ref_type", "value": { "stringValue": "child_of" } } ] } ], "status": { "code": 2 } } ] } ] } ] } ================================================ FILE: internal/storage/v2/memory/lock.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package memory import "time" type Lock struct{} // Acquire always returns true for memory storage because it's a single-node func (*Lock) Acquire(string /* resource */, time.Duration /* ttl */) (bool, error) { return true, nil } // Forfeit always returns true for memory storage func (*Lock) Forfeit(string /* resource */) (bool, error) { return true, nil } ================================================ FILE: internal/storage/v2/memory/lock_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package memory import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestAcquire(t *testing.T) { l := &Lock{} ok, err := l.Acquire("resource", time.Duration(1)) assert.True(t, ok) require.NoError(t, err) } func TestForfeit(t *testing.T) { l := &Lock{} ok, err := l.Forfeit("resource") assert.True(t, ok) require.NoError(t, err) } ================================================ FILE: internal/storage/v2/memory/memory.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package memory import ( "context" "errors" "iter" "sync" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" conventions "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" "github.com/jaegertracing/jaeger/internal/tenancy" ) const errorAttribute = "error" var errInvalidSearchDepth = errors.New("search depth must be greater than 0 and less than max traces") // Store is an in-memory store of traces type Store struct { mu sync.RWMutex // Each tenant gets a copy of default config. // In the future this can be extended to contain per-tenant configuration. cfg Configuration perTenant map[string]*Tenant } // NewStore creates an in-memory store func NewStore(cfg Configuration) (*Store, error) { if cfg.MaxTraces <= 0 { return nil, errInvalidMaxTraces } return &Store{ cfg: cfg, perTenant: make(map[string]*Tenant), }, nil } // getTenant returns the per-tenant storage. Note that tenantID has already been checked for by the collector or query func (st *Store) getTenant(tenantID string) *Tenant { st.mu.RLock() tenant, ok := st.perTenant[tenantID] st.mu.RUnlock() if !ok { st.mu.Lock() defer st.mu.Unlock() tenant, ok = st.perTenant[tenantID] if !ok { tenant = newTenant(&st.cfg) st.perTenant[tenantID] = tenant } } return tenant } // WriteTraces write the traces into the tenant by grouping all the spans with same trace id together. // The traces will not be saved as they are coming, rather they would be reshuffled. func (st *Store) WriteTraces(ctx context.Context, td ptrace.Traces) error { resourceSpansByTraceId := reshuffleResourceSpans(td.ResourceSpans()) m := st.getTenant(tenancy.GetTenant(ctx)) m.storeTraces(resourceSpansByTraceId) return nil } // GetOperations returns operations based on the service name and span kind func (st *Store) GetOperations(ctx context.Context, query tracestore.OperationQueryParams) ([]tracestore.Operation, error) { m := st.getTenant(tenancy.GetTenant(ctx)) m.mu.RLock() defer m.mu.RUnlock() var retMe []tracestore.Operation if operations, ok := m.operations[query.ServiceName]; ok { for operation := range operations { if query.SpanKind == "" || query.SpanKind == operation.SpanKind { retMe = append(retMe, operation) } } } return retMe, nil } // GetServices returns a list of all known services func (st *Store) GetServices(ctx context.Context) ([]string, error) { m := st.getTenant(tenancy.GetTenant(ctx)) m.mu.RLock() defer m.mu.RUnlock() var retMe []string for k := range m.services { retMe = append(retMe, k) } return retMe, nil } func (st *Store) FindTraces(ctx context.Context, query tracestore.TraceQueryParams) iter.Seq2[[]ptrace.Traces, error] { m := st.getTenant(tenancy.GetTenant(ctx)) return func(yield func([]ptrace.Traces, error) bool) { traceAndIds, err := m.findTraceAndIds(query) if err != nil { yield(nil, err) return } for i := range traceAndIds { if !yield([]ptrace.Traces{traceAndIds[i].trace}, nil) { return } } } } func (st *Store) FindTraceIDs(ctx context.Context, query tracestore.TraceQueryParams) iter.Seq2[[]tracestore.FoundTraceID, error] { m := st.getTenant(tenancy.GetTenant(ctx)) return func(yield func([]tracestore.FoundTraceID, error) bool) { traceAndIds, err := m.findTraceAndIds(query) if err != nil { yield(nil, err) return } ids := make([]tracestore.FoundTraceID, len(traceAndIds)) for i := range traceAndIds { ids[i] = tracestore.FoundTraceID{TraceID: traceAndIds[i].id} } yield(ids, nil) } } func (st *Store) GetTraces(ctx context.Context, traceIDs ...tracestore.GetTraceParams) iter.Seq2[[]ptrace.Traces, error] { m := st.getTenant(tenancy.GetTenant(ctx)) return func(yield func([]ptrace.Traces, error) bool) { traces := m.getTraces(traceIDs...) for i := range traces { if !yield([]ptrace.Traces{traces[i]}, nil) { return } } } } func (st *Store) GetDependencies(ctx context.Context, query depstore.QueryParameters) ([]model.DependencyLink, error) { m := st.getTenant(tenancy.GetTenant(ctx)) return m.getDependencies(query) } func (st *Store) Purge() error { st.mu.Lock() st.perTenant = make(map[string]*Tenant) st.mu.Unlock() return nil } // reshuffleResourceSpans reshuffles the resource spans so as to group the spans from same traces together. To understand this reshuffling // take an example of 2 resource spans, then these two resource spans have 2 scope spans each. // Every scope span consists of 2 spans with trace ids: 1 and 2. Now the final structure should look like: // For TraceID1: [ResourceSpan1:[ScopeSpan1:[Span(TraceID1)],ScopeSpan2:[Span(TraceID1)], ResourceSpan2:[ScopeSpan1:[Span(TraceID1)],ScopeSpan2:[Span(TraceID1)] // A similar structure will be there for TraceID2 func reshuffleResourceSpans(resourceSpanSlice ptrace.ResourceSpansSlice) map[pcommon.TraceID]ptrace.ResourceSpansSlice { resourceSpansByTraceId := make(map[pcommon.TraceID]ptrace.ResourceSpansSlice) for _, resourceSpan := range resourceSpanSlice.All() { scopeSpansByTraceId := reshuffleScopeSpans(resourceSpan.ScopeSpans()) // All the scope spans here will have the same resource as of resourceSpan. Therefore: // Copy the resource to an empty resourceSpan. After this, append the scope spans with same // trace id to this empty resource span. Finally move this resource span to the resourceSpanSlice // containing other resource spans and having same trace id. for traceId, scopeSpansSlice := range scopeSpansByTraceId { resourceSpanByTraceId := ptrace.NewResourceSpans() resourceSpan.Resource().CopyTo(resourceSpanByTraceId.Resource()) scopeSpansSlice.MoveAndAppendTo(resourceSpanByTraceId.ScopeSpans()) resourceSpansSlice, ok := resourceSpansByTraceId[traceId] if !ok { resourceSpansSlice = ptrace.NewResourceSpansSlice() resourceSpansByTraceId[traceId] = resourceSpansSlice } resourceSpanByTraceId.MoveTo(resourceSpansSlice.AppendEmpty()) } } return resourceSpansByTraceId } // reshuffleScopeSpans reshuffles all the scope spans of a resource span to group // spans of same trace ids together. The first step is to iterate the scope spans and then. // copy the scope to an empty scopeSpan. After this, append the spans with same // trace id to this empty scope span. Finally move this scope span to the scope span // slice containing other scope spans and having same trace id. func reshuffleScopeSpans(scopeSpanSlice ptrace.ScopeSpansSlice) map[pcommon.TraceID]ptrace.ScopeSpansSlice { scopeSpansByTraceId := make(map[pcommon.TraceID]ptrace.ScopeSpansSlice) for _, scopeSpan := range scopeSpanSlice.All() { spansByTraceId := reshuffleSpans(scopeSpan.Spans()) for traceId, spansSlice := range spansByTraceId { scopeSpanByTraceId := ptrace.NewScopeSpans() scopeSpan.Scope().CopyTo(scopeSpanByTraceId.Scope()) spansSlice.MoveAndAppendTo(scopeSpanByTraceId.Spans()) scopeSpansSlice, ok := scopeSpansByTraceId[traceId] if !ok { scopeSpansSlice = ptrace.NewScopeSpansSlice() scopeSpansByTraceId[traceId] = scopeSpansSlice } scopeSpanByTraceId.MoveTo(scopeSpansSlice.AppendEmpty()) } } return scopeSpansByTraceId } func reshuffleSpans(spanSlice ptrace.SpanSlice) map[pcommon.TraceID]ptrace.SpanSlice { spansByTraceId := make(map[pcommon.TraceID]ptrace.SpanSlice) for _, span := range spanSlice.All() { spansSlice, ok := spansByTraceId[span.TraceID()] if !ok { spansSlice = ptrace.NewSpanSlice() spansByTraceId[span.TraceID()] = spansSlice } span.CopyTo(spansSlice.AppendEmpty()) } return spansByTraceId } func getServiceNameFromResource(resource pcommon.Resource) string { val, ok := resource.Attributes().Get(conventions.ServiceNameKey) if !ok { return "" } return val.Str() } ================================================ FILE: internal/storage/v2/memory/memory_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package memory import ( "bytes" "context" "encoding/hex" "encoding/json" "fmt" "os" "sort" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" conventions "github.com/jaegertracing/jaeger/internal/telemetry/otelsemconv" "github.com/jaegertracing/jaeger/internal/tenancy" ) func TestNewStore_DefaultConfig(t *testing.T) { store, err := NewStore(Configuration{ MaxTraces: 10, }) require.NoError(t, err) td := loadInputTraces(t, 1) err = store.WriteTraces(context.Background(), td) require.NoError(t, err) traceID1 := fromString(t, "00000000000000010000000000000000") traceID2 := fromString(t, "00000000000000020000000000000000") getTracesIter := store.GetTraces(context.Background(), tracestore.GetTraceParams{TraceID: traceID1}, tracestore.GetTraceParams{TraceID: traceID2}) var traces []ptrace.Traces for gottd, err := range getTracesIter { require.NoError(t, err) assert.Len(t, gottd, 1) traces = append(traces, gottd[0]) } expected := loadOutputTraces(t, 1) testTraces(t, expected, traces[0]) expected2 := loadOutputTraces(t, 2) testTraces(t, expected2, traces[1]) operations, err := store.GetOperations(context.Background(), tracestore.OperationQueryParams{ServiceName: "service-x"}) require.NoError(t, err) expectedOperations := []tracestore.Operation{ { Name: "test-general-conversion-2", SpanKind: "server", }, { Name: "test-general-conversion-3", SpanKind: "client", }, { Name: "test-general-conversion-4", SpanKind: "producer", }, { Name: "test-general-conversion-5", SpanKind: "consumer", }, } sort.Slice(operations, func(i, j int) bool { return operations[i].Name < operations[j].Name }) assert.Equal(t, expectedOperations, operations) expectedServices := []string{"service-x"} services, err := store.GetServices(context.Background()) require.NoError(t, err) assert.Equal(t, expectedServices, services) queryAttrs := getQueryAttributes() findTracesParams := tracestore.TraceQueryParams{ ServiceName: "service-x", OperationName: "test-general-conversion-2", Attributes: queryAttrs, SearchDepth: 5, } idsIter := store.FindTraceIDs(context.Background(), findTracesParams) i := 0 for foundTraceIds, err := range idsIter { i++ require.NoError(t, err) assert.Len(t, foundTraceIds, 1) assert.Equal(t, traceID1, foundTraceIds[0].TraceID) } assert.Equal(t, 1, i) gotIter := store.FindTraces(context.Background(), findTracesParams) i = 0 for foundTraces, err := range gotIter { i++ require.NoError(t, err) assert.Len(t, foundTraces, 1) testTraces(t, expected, foundTraces[0]) } assert.Equal(t, 1, i) } func getQueryAttributes() pcommon.Map { queryAttrs := pcommon.NewMap() queryAttrs.PutStr("peer.service", "service-y") queryAttrs.PutDouble("temperature", 72.5) queryAttrs.PutBool(errorAttribute, true) queryAttrs.PutStr("event-x", "event-y") queryAttrs.PutStr("scope.attributes.2", "attribute-y") return queryAttrs } func TestFindTraces_WrongQuery(t *testing.T) { wrongStringValue := "wrongStringValue" startTime := time.Unix(0, int64(1485467191639875000)) endTime := time.Unix(0, int64(1485467191639880000)) duration := endTime.Sub(startTime) tests := []struct { name string modifyQueryFxn func(query *tracestore.TraceQueryParams) }{ { name: "wrong service-name", modifyQueryFxn: func(query *tracestore.TraceQueryParams) { query.ServiceName = wrongStringValue }, }, { name: "wrong tag", modifyQueryFxn: func(query *tracestore.TraceQueryParams) { attrs := pcommon.NewMap() attrs.PutStr(wrongStringValue, wrongStringValue) attrs.MoveTo(query.Attributes) }, }, { name: "wrong operation name", modifyQueryFxn: func(query *tracestore.TraceQueryParams) { query.OperationName = wrongStringValue }, }, { name: "wrong status code", modifyQueryFxn: func(query *tracestore.TraceQueryParams) { query.Attributes.PutStr(errorAttribute, wrongStringValue) }, }, { name: "wrong min start time", modifyQueryFxn: func(query *tracestore.TraceQueryParams) { query.StartTimeMin = startTime.Add(1 * time.Hour) }, }, { name: "wrong max start time", modifyQueryFxn: func(query *tracestore.TraceQueryParams) { query.StartTimeMax = startTime.Add(-1 * time.Hour) }, }, { name: "wrong min duration", modifyQueryFxn: func(query *tracestore.TraceQueryParams) { query.DurationMin = duration + 1*time.Hour }, }, { name: "wrong max duration", modifyQueryFxn: func(query *tracestore.TraceQueryParams) { query.DurationMax = duration - 1*time.Hour }, }, } store, err := NewStore(Configuration{ MaxTraces: 10, }) require.NoError(t, err) td := loadInputTraces(t, 1) err = store.WriteTraces(context.Background(), td) require.NoError(t, err) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { query := tracestore.TraceQueryParams{ ServiceName: "service-x", OperationName: "test-general-conversion-2", Attributes: getQueryAttributes(), SearchDepth: 10, } tt.modifyQueryFxn(&query) gotIter := store.FindTraces(context.Background(), query) iterLength := 0 for _, err := range gotIter { require.NoError(t, err) iterLength++ } assert.Equal(t, 0, iterLength) }) } } func TestFindTracesAttributesMatching(t *testing.T) { stringVal := "val" tests := []struct { name string attributes func(td ptrace.Traces) pcommon.Map }{ { name: "resource-attributes", attributes: func(td ptrace.Traces) pcommon.Map { return td.ResourceSpans().At(0).Resource().Attributes() }, }, { name: "scope-attributes", attributes: func(td ptrace.Traces) pcommon.Map { return td.ResourceSpans().At(0).ScopeSpans().At(0).Scope().Attributes() }, }, { name: "span-attributes", attributes: func(td ptrace.Traces) pcommon.Map { return td.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0).Attributes() }, }, { name: "event-attributes", attributes: func(td ptrace.Traces) pcommon.Map { return td.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0).Events().AppendEmpty().Attributes() }, }, { name: "link-attributes", attributes: func(td ptrace.Traces) pcommon.Map { return td.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0).Links().AppendEmpty().Attributes() }, }, } store, err := NewStore(Configuration{ MaxTraces: 10, }) require.NoError(t, err) for i, tt := range tests { t.Run(tt.name, func(t *testing.T) { td := ptrace.NewTraces() td.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty().SetTraceID(fromString(t, fmt.Sprintf("000000000000000%d0000000000000000", i+1))) attrs := tt.attributes(td) attrs.PutStr(tt.name, stringVal) err := store.WriteTraces(context.Background(), td) require.NoError(t, err) iter := store.FindTraces(context.Background(), tracestore.TraceQueryParams{ Attributes: attrs, SearchDepth: 10, }) iterLength := 0 for traces, err := range iter { require.NoError(t, err) iterLength++ assert.Len(t, traces, 1) assert.Equal(t, traces[0], td) } assert.Equal(t, 1, iterLength) }) } } func TestFindTraces_MaxTraces(t *testing.T) { store, err := NewStore(Configuration{ MaxTraces: 10, }) require.NoError(t, err) for i := 1; i < 9; i++ { td := ptrace.NewTraces() span := td.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty() span.SetTraceID(fromString(t, fmt.Sprintf("000000000000000%d0000000000000000", i))) span.SetStartTimestamp(pcommon.NewTimestampFromTime(time.Now())) span.Attributes().PutBool("key", true) err := store.WriteTraces(context.Background(), td) require.NoError(t, err) } attrs := pcommon.NewMap() attrs.PutBool("key", true) params := tracestore.TraceQueryParams{ SearchDepth: 5, Attributes: attrs, } gotIter := store.FindTraces(context.Background(), params) iterLength := 0 for traces, err := range gotIter { require.NoError(t, err) assert.Len(t, traces, 1) iterLength++ } assert.Equal(t, 5, iterLength) newIter := store.FindTraces(context.Background(), params) iterLength = 0 for _, err := range newIter { require.NoError(t, err) iterLength++ if iterLength > 3 { break } } assert.Equal(t, 4, iterLength) } func TestFindTraces_AttributesFoundInEvents(t *testing.T) { store, err := NewStore(Configuration{ MaxTraces: 10, }) require.NoError(t, err) td := ptrace.NewTraces() span := td.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty() span.SetTraceID(fromString(t, "00000000000000010000000000000000")) span.SetStartTimestamp(pcommon.NewTimestampFromTime(time.Now())) span.Events().AppendEmpty().Attributes().PutBool("key", true) err = store.WriteTraces(context.Background(), td) require.NoError(t, err) queryAttributes := pcommon.NewMap() queryAttributes.PutBool("key", true) params := tracestore.TraceQueryParams{ Attributes: queryAttributes, SearchDepth: 10, } gotIter := store.FindTraces(context.Background(), params) iterLength := 0 for traces, err := range gotIter { iterLength++ require.NoError(t, err) assert.Len(t, traces, 1) assert.Equal(t, td, traces[0]) } assert.Equal(t, 1, iterLength) } func TestFindTraces_ErrorStatusNotMatched(t *testing.T) { store, err := NewStore(Configuration{ MaxTraces: 10, }) require.NoError(t, err) td := ptrace.NewTraces() span := td.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty() span.SetTraceID(fromString(t, "00000000000000010000000000000000")) span.SetStartTimestamp(pcommon.NewTimestampFromTime(time.Now())) span.Status().SetCode(ptrace.StatusCodeOk) err = store.WriteTraces(context.Background(), td) require.NoError(t, err) queryAttributes := pcommon.NewMap() queryAttributes.PutBool(errorAttribute, true) params := tracestore.TraceQueryParams{ Attributes: queryAttributes, SearchDepth: 10, } gotIter := store.FindTraces(context.Background(), params) iterLength := 0 for _, err := range gotIter { require.NoError(t, err) iterLength++ } assert.Equal(t, 0, iterLength) } func TestFindTraces_NegativeSearchDepthErr(t *testing.T) { testInvalidSearchDepth(t, func(store *Store, params tracestore.TraceQueryParams) { gotIter := store.FindTraces(context.Background(), params) iterLength := 0 for traces, err := range gotIter { iterLength++ require.ErrorContains(t, err, errInvalidSearchDepth.Error()) assert.Nil(t, traces) } assert.Equal(t, 1, iterLength) }) } func TestFindTraceIds_NegativeSearchDepth(t *testing.T) { testInvalidSearchDepth(t, func(store *Store, params tracestore.TraceQueryParams) { gotIter := store.FindTraceIDs(context.Background(), params) iterLength := 0 for traces, err := range gotIter { iterLength++ require.ErrorContains(t, err, errInvalidSearchDepth.Error()) assert.Nil(t, traces) } assert.Equal(t, 1, iterLength) }) } func testInvalidSearchDepth(t *testing.T, fxn func(store *Store, params tracestore.TraceQueryParams)) { tests := []struct { name string searchDepth int }{ { name: "negative search depth", searchDepth: -1, }, { name: "zero search depth", searchDepth: 0, }, { name: "search depth greater than max traces", searchDepth: 11, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { store, err := NewStore(Configuration{ MaxTraces: 10, }) require.NoError(t, err) params := tracestore.TraceQueryParams{ SearchDepth: test.searchDepth, } fxn(store, params) }) } } func TestFindTraces_StatusCode(t *testing.T) { store, err := NewStore(Configuration{ MaxTraces: 10, }) traceId1 := fromString(t, "00000000000000010000000000000000") traceId2 := fromString(t, "00000000000000020000000000000000") require.NoError(t, err) td := ptrace.NewTraces() spans := td.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans() span1 := spans.AppendEmpty() span2 := spans.AppendEmpty() span1.SetTraceID(traceId1) span1.Status().SetCode(ptrace.StatusCodeOk) span2.SetTraceID(traceId2) span2.Status().SetCode(ptrace.StatusCodeError) err = store.WriteTraces(context.Background(), td) require.NoError(t, err) queryAttributes := pcommon.NewMap() queryAttributes.PutBool(errorAttribute, true) iter1 := store.FindTraces(context.Background(), tracestore.TraceQueryParams{ Attributes: queryAttributes, SearchDepth: 10, }) iterLength := 0 for traces, err := range iter1 { require.NoError(t, err) iterLength++ assert.Len(t, traces, 1) assert.Equal(t, traceId2, traces[0].ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0).TraceID()) } assert.Equal(t, 1, iterLength) iterLength = 0 queryAttributes.PutBool(errorAttribute, false) iter2 := store.FindTraces(context.Background(), tracestore.TraceQueryParams{ Attributes: queryAttributes, SearchDepth: 10, }) for traces, err := range iter2 { require.NoError(t, err) iterLength++ assert.Len(t, traces, 1) assert.Equal(t, traceId1, traces[0].ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0).TraceID()) } assert.Equal(t, 1, iterLength) } func TestGetOperationsWithKind(t *testing.T) { tests := []struct { spanKind ptrace.SpanKind expectedKind string }{ { spanKind: ptrace.SpanKindClient, expectedKind: "client", }, { spanKind: ptrace.SpanKindServer, expectedKind: "server", }, { spanKind: ptrace.SpanKindProducer, expectedKind: "producer", }, { spanKind: ptrace.SpanKindUnspecified, expectedKind: "", }, { spanKind: ptrace.SpanKindInternal, expectedKind: "internal", }, { spanKind: ptrace.SpanKindConsumer, expectedKind: "consumer", }, } for _, test := range tests { t.Run(test.spanKind.String(), func(t *testing.T) { store, err := NewStore(Configuration{ MaxTraces: 10, }) require.NoError(t, err) td := ptrace.NewTraces() resourceSpan := td.ResourceSpans().AppendEmpty() resourceSpan.Resource().Attributes().PutStr(conventions.ServiceNameKey, "service-z") span := resourceSpan.ScopeSpans().AppendEmpty().Spans().AppendEmpty() span.SetTraceID(fromString(t, "00000000000000010000000000000000")) span.SetName("span") span.SetKind(test.spanKind) err = store.WriteTraces(context.Background(), td) require.NoError(t, err) operations, err := store.GetOperations(context.Background(), tracestore.OperationQueryParams{ ServiceName: "service-z", SpanKind: test.expectedKind, }) require.NoError(t, err) assert.Len(t, operations, 1) assert.Equal(t, operations[0].SpanKind, string(test.expectedKind)) }) } } func TestGetTraces_IterBreak(t *testing.T) { store, err := NewStore(Configuration{ MaxTraces: 10, }) require.NoError(t, err) td := loadInputTraces(t, 1) err = store.WriteTraces(context.Background(), td) require.NoError(t, err) traceID1 := fromString(t, "00000000000000010000000000000000") traceID2 := fromString(t, "00000000000000020000000000000000") iter := store.GetTraces(context.Background(), tracestore.GetTraceParams{TraceID: traceID1}, tracestore.GetTraceParams{TraceID: traceID2}) expected := loadOutputTraces(t, 1) iterLength := 1 for traces, err := range iter { require.NoError(t, err) assert.Len(t, traces, 1) assert.Equal(t, expected, traces[0]) break } assert.Equal(t, 1, iterLength) } func TestWriteTraces_WriteTwoBatches(t *testing.T) { store, err := NewStore(Configuration{ MaxTraces: 10, }) require.NoError(t, err) traceId := fromString(t, "00000000000000010000000000000000") td1 := ptrace.NewTraces() td1.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty().SetTraceID(traceId) err = store.WriteTraces(context.Background(), td1) require.NoError(t, err) td2 := ptrace.NewTraces() td2.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty().SetTraceID(traceId) err = store.WriteTraces(context.Background(), td2) require.NoError(t, err) tenant := store.getTenant(tenancy.GetTenant(context.Background())) require.NoError(t, err) traceIndex := tenant.ids[traceId] assert.Equal(t, 2, tenant.traces[traceIndex].trace.ResourceSpans().Len()) } func TestWriteTraces_WriteTraceWithTwoResourceSpans(t *testing.T) { store, err := NewStore(Configuration{ MaxTraces: 10, }) require.NoError(t, err) traceId := fromString(t, "00000000000000010000000000000000") td := ptrace.NewTraces() resourceSpans := td.ResourceSpans() scopeSpan1 := resourceSpans.AppendEmpty().ScopeSpans().AppendEmpty() scopeSpan1.Spans().AppendEmpty().SetTraceID(traceId) scopeSpan1.Spans().AppendEmpty().SetTraceID(traceId) scopeSpan2 := resourceSpans.AppendEmpty().ScopeSpans().AppendEmpty() scopeSpan2.Spans().AppendEmpty().SetTraceID(traceId) scopeSpan2.Spans().AppendEmpty().SetTraceID(traceId) err = store.WriteTraces(context.Background(), td) require.NoError(t, err) tenant := store.getTenant(tenancy.GetTenant(context.Background())) require.NoError(t, err) traceIndex := tenant.ids[traceId] // All spans have same trace id, so output should be same as input (that is no reshuffling, effectively) assert.Equal(t, td, tenant.traces[traceIndex].trace) } func TestNewStore_TracesLimit(t *testing.T) { maxTraces := 8 store, err := NewStore(Configuration{ MaxTraces: maxTraces, }) require.NoError(t, err) writeTenTraces(t, store) tenant := store.getTenant(tenancy.GetTenant(context.Background())) require.NoError(t, err) assert.Len(t, tenant.traces, maxTraces) assert.Len(t, tenant.ids, maxTraces) } func TestNewStore_ReverseChronologicalOrder(t *testing.T) { maxTraces := 8 store, err := NewStore(Configuration{ MaxTraces: maxTraces, }) require.NoError(t, err) writeTenTraces(t, store) iter := store.FindTraces(context.Background(), tracestore.TraceQueryParams{ SearchDepth: 5, Attributes: pcommon.NewMap(), }) // This test whether the traces are fetched in Reverse Chronological Order iterLength := 0 for traces, err := range iter { require.NoError(t, err) assert.Len(t, traces, 1) actualTraceId := traces[0].ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0).TraceID() assert.Equal(t, fromString(t, fmt.Sprintf("000000000000000%d0000000000000000", 9-iterLength)), actualTraceId) iterLength++ } assert.Equal(t, 5, iterLength) } func TestInvalidMaxTracesErr(t *testing.T) { store, err := NewStore(Configuration{}) require.ErrorContains(t, err, errInvalidMaxTraces.Error()) assert.Nil(t, store) } func TestGetDependencies(t *testing.T) { store, err := NewStore(Configuration{ MaxTraces: 10, }) require.NoError(t, err) traceId := fromString(t, "00000000000000010000000000000000") td := ptrace.NewTraces() resourceSpans := td.ResourceSpans().AppendEmpty() resourceSpans.Resource().Attributes().PutStr(conventions.ServiceNameKey, "service-x") span1StartTime := time.Now() span1 := resourceSpans.ScopeSpans().AppendEmpty().Spans().AppendEmpty() span1.SetTraceID(traceId) span1.SetSpanID(spanIdFromString(t, "0000000000000001")) span1.SetParentSpanID(spanIdFromString(t, "0000000000000003")) span1.SetStartTimestamp(pcommon.NewTimestampFromTime(span1StartTime)) span1.SetEndTimestamp(pcommon.NewTimestampFromTime(span1StartTime.Add(1 * time.Second))) span2 := resourceSpans.ScopeSpans().AppendEmpty().Spans().AppendEmpty() span2.SetTraceID(traceId) span2.SetSpanID(spanIdFromString(t, "0000000000000002")) span2.SetParentSpanID(spanIdFromString(t, "0000000000000003")) span2.SetStartTimestamp(pcommon.NewTimestampFromTime(span1StartTime.Add(1 * time.Second))) span2.SetEndTimestamp(pcommon.NewTimestampFromTime(span1StartTime.Add(2 * time.Second))) newResourceSpan := td.ResourceSpans().AppendEmpty() newResourceSpan.Resource().Attributes().PutStr(string(conventions.ServiceNameKey), "service-y") span3 := newResourceSpan.ScopeSpans().AppendEmpty().Spans().AppendEmpty() span3.SetTraceID(traceId) span3.SetSpanID(spanIdFromString(t, "0000000000000003")) span3.SetStartTimestamp(pcommon.NewTimestampFromTime(span1StartTime.Add(-2 * time.Second))) span3.SetEndTimestamp(pcommon.NewTimestampFromTime(span1StartTime.Add(3 * time.Second))) span3.SetParentSpanID(spanIdFromString(t, "0000000000000004")) err = store.WriteTraces(context.Background(), td) require.NoError(t, err) deps, err := store.GetDependencies(context.Background(), depstore.QueryParameters{ StartTime: span1StartTime.Add(-4 * time.Second), EndTime: span1StartTime.Add(5 * time.Second), }) require.NoError(t, err) assert.Len(t, deps, 1) assert.Equal(t, model.DependencyLink{ Parent: "service-y", Child: "service-x", CallCount: 2, }, deps[0]) td2 := ptrace.NewTraces() resourceSpan2 := td2.ResourceSpans().AppendEmpty() resourceSpan2.Resource().Attributes().PutStr(string(conventions.ServiceNameKey), "service-z") span4 := resourceSpan2.ScopeSpans().AppendEmpty().Spans().AppendEmpty() span4.SetTraceID(traceId) span4.SetSpanID(spanIdFromString(t, "0000000000000004")) span4.SetStartTimestamp(pcommon.NewTimestampFromTime(span1StartTime.Add(-4 * time.Second))) span4.SetEndTimestamp(pcommon.NewTimestampFromTime(span1StartTime.Add(5 * time.Second))) err = store.WriteTraces(context.Background(), td2) require.NoError(t, err) newDeps, err := store.GetDependencies(context.Background(), depstore.QueryParameters{ StartTime: span1StartTime.Add(-5 * time.Second), EndTime: span1StartTime.Add(6 * time.Second), }) require.NoError(t, err) assert.Len(t, newDeps, 2) newDeps2, err := store.GetDependencies(context.Background(), depstore.QueryParameters{ StartTime: span1StartTime.Add(-5 * time.Second), }) require.NoError(t, err) assert.Len(t, newDeps2, 2) emptyDeps, err := store.GetDependencies(context.Background(), depstore.QueryParameters{ StartTime: span1StartTime.Add(-4 * time.Second), EndTime: span1StartTime.Add(5 * time.Second), }) require.NoError(t, err) assert.Empty(t, emptyDeps) } func TestGetDependencies_Err(t *testing.T) { store, err := NewStore(Configuration{ MaxTraces: 10, }) require.NoError(t, err) startTime := time.Now() deps, err := store.GetDependencies(context.Background(), depstore.QueryParameters{ StartTime: startTime, EndTime: startTime.Add(-1 * time.Second), }) require.ErrorContains(t, err, "end time must be greater than start time") assert.Nil(t, deps) deps, err = store.GetDependencies(context.Background(), depstore.QueryParameters{}) require.ErrorContains(t, err, "start time is required") assert.Nil(t, deps) } func TestGetDependencies_EmptyParentSpanId(t *testing.T) { store, err := NewStore(Configuration{ MaxTraces: 10, }) require.NoError(t, err) td := ptrace.NewTraces() span := td.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty() span.SetTraceID(fromString(t, "00000000000000010000000000000000")) startTime := time.Now() span.SetStartTimestamp(pcommon.NewTimestampFromTime(startTime)) span.SetEndTimestamp(pcommon.NewTimestampFromTime(startTime.Add(1 * time.Second))) err = store.WriteTraces(context.Background(), td) require.NoError(t, err) deps, err := store.GetDependencies(context.Background(), depstore.QueryParameters{ StartTime: startTime.Add(-1 * time.Second), EndTime: startTime.Add(2 * time.Second), }) require.NoError(t, err) assert.Empty(t, deps) } func TestGetDependencies_WrongSpanId(t *testing.T) { store, err := NewStore(Configuration{ MaxTraces: 10, }) require.NoError(t, err) td := ptrace.NewTraces() span := td.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty() span.SetTraceID(fromString(t, "00000000000000010000000000000000")) startTime := time.Now() span.SetStartTimestamp(pcommon.NewTimestampFromTime(startTime)) span.SetEndTimestamp(pcommon.NewTimestampFromTime(startTime.Add(1 * time.Second))) span.SetSpanID(spanIdFromString(t, "0000000000000002")) err = store.WriteTraces(context.Background(), td) require.NoError(t, err) deps, err := store.GetDependencies(context.Background(), depstore.QueryParameters{ StartTime: startTime.Add(-1 * time.Second), EndTime: startTime.Add(2 * time.Second), }) require.NoError(t, err) assert.Empty(t, deps) } func writeTenTraces(t *testing.T, store *Store) { for i := 1; i < 10; i++ { traceID := fromString(t, fmt.Sprintf("000000000000000%d0000000000000000", i)) traces := ptrace.NewTraces() traces.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty().SetTraceID(traceID) err := store.WriteTraces(context.Background(), traces) require.NoError(t, err) } } func fromString(t *testing.T, dbTraceId string) pcommon.TraceID { var traceId [16]byte traceBytes, err := hex.DecodeString(dbTraceId) require.NoError(t, err) copy(traceId[:], traceBytes) return traceId } func spanIdFromString(t *testing.T, dbTraceId string) pcommon.SpanID { var spanId [8]byte spanIdBytes, err := hex.DecodeString(dbTraceId) require.NoError(t, err) copy(spanId[:], spanIdBytes) return spanId } func testTraces(t *testing.T, expectedTraces ptrace.Traces, actualTraces ptrace.Traces) { if !assert.Equal(t, expectedTraces, actualTraces) { marshaller := ptrace.JSONMarshaler{} actualTd, err := marshaller.MarshalTraces(actualTraces) require.NoError(t, err) writeActualData(t, "traces", actualTd) } } func writeActualData(t *testing.T, name string, data []byte) { var prettyJson bytes.Buffer err := json.Indent(&prettyJson, data, "", " ") require.NoError(t, err) path := "fixtures/actual_" + name + ".json" err = os.WriteFile(path, prettyJson.Bytes(), 0o644) require.NoError(t, err) t.Log("Saved the actual " + name + " to " + path) } func loadInputTraces(t *testing.T, i int) ptrace.Traces { return loadTraces(t, fmt.Sprintf("fixtures/otel_traces_%02d.json", i)) } func loadOutputTraces(t *testing.T, i int) ptrace.Traces { return loadTraces(t, fmt.Sprintf("fixtures/db_traces_%02d.json", i)) } func loadTraces(t *testing.T, name string) ptrace.Traces { unmarshller := ptrace.JSONUnmarshaler{} data, err := os.ReadFile(name) require.NoError(t, err) td, err := unmarshller.UnmarshalTraces(data) require.NoError(t, err) return td } func TestFindTraces_OTLPFields(t *testing.T) { store, err := NewStore(Configuration{ MaxTraces: 100, }) require.NoError(t, err) traceID1 := fromString(t, "00000000000000010000000000000000") traceID2 := fromString(t, "00000000000000020000000000000000") traceID3 := fromString(t, "00000000000000030000000000000000") traceID4 := fromString(t, "00000000000000040000000000000000") traceID5 := fromString(t, "00000000000000050000000000000000") // Trace 1: ERROR status, SERVER kind, scope "my-scope" v1.0.0, resource.deployment.environment=production td1 := ptrace.NewTraces() rs1 := td1.ResourceSpans().AppendEmpty() rs1.Resource().Attributes().PutStr(conventions.ServiceNameKey, "service1") rs1.Resource().Attributes().PutStr("deployment.environment", "production") ss1 := rs1.ScopeSpans().AppendEmpty() ss1.Scope().SetName("my-scope") ss1.Scope().SetVersion("1.0.0") span1 := ss1.Spans().AppendEmpty() span1.SetTraceID(traceID1) span1.SetSpanID(spanIdFromString(t, "0000000000000001")) span1.SetName("operation1") span1.SetKind(ptrace.SpanKindServer) span1.Status().SetCode(ptrace.StatusCodeError) span1.SetStartTimestamp(pcommon.NewTimestampFromTime(time.Now())) span1.SetEndTimestamp(pcommon.NewTimestampFromTime(time.Now().Add(time.Second))) // Trace 2: OK status, CLIENT kind, scope "other-scope" v2.0.0, resource.deployment.environment=staging td2 := ptrace.NewTraces() rs2 := td2.ResourceSpans().AppendEmpty() rs2.Resource().Attributes().PutStr(conventions.ServiceNameKey, "service2") rs2.Resource().Attributes().PutStr("deployment.environment", "staging") ss2 := rs2.ScopeSpans().AppendEmpty() ss2.Scope().SetName("other-scope") ss2.Scope().SetVersion("2.0.0") span2 := ss2.Spans().AppendEmpty() span2.SetTraceID(traceID2) span2.SetSpanID(spanIdFromString(t, "0000000000000002")) span2.SetName("operation2") span2.SetKind(ptrace.SpanKindClient) span2.Status().SetCode(ptrace.StatusCodeOk) span2.SetStartTimestamp(pcommon.NewTimestampFromTime(time.Now())) span2.SetEndTimestamp(pcommon.NewTimestampFromTime(time.Now().Add(time.Second))) // Trace 3: PRODUCER kind with UNSET status td3 := ptrace.NewTraces() rs3 := td3.ResourceSpans().AppendEmpty() rs3.Resource().Attributes().PutStr(conventions.ServiceNameKey, "service3") ss3 := rs3.ScopeSpans().AppendEmpty() span3 := ss3.Spans().AppendEmpty() span3.SetTraceID(traceID3) span3.SetSpanID(spanIdFromString(t, "0000000000000003")) span3.SetName("operation3") span3.SetKind(ptrace.SpanKindProducer) span3.Status().SetCode(ptrace.StatusCodeUnset) span3.SetStartTimestamp(pcommon.NewTimestampFromTime(time.Now())) span3.SetEndTimestamp(pcommon.NewTimestampFromTime(time.Now().Add(time.Second))) // Trace 4: CONSUMER kind with UNSET status td4 := ptrace.NewTraces() rs4 := td4.ResourceSpans().AppendEmpty() rs4.Resource().Attributes().PutStr(conventions.ServiceNameKey, "service4") ss4 := rs4.ScopeSpans().AppendEmpty() span4 := ss4.Spans().AppendEmpty() span4.SetTraceID(traceID4) span4.SetSpanID(spanIdFromString(t, "0000000000000004")) span4.SetName("operation4") span4.SetKind(ptrace.SpanKindConsumer) span4.Status().SetCode(ptrace.StatusCodeUnset) span4.SetStartTimestamp(pcommon.NewTimestampFromTime(time.Now())) span4.SetEndTimestamp(pcommon.NewTimestampFromTime(time.Now().Add(time.Second))) // Trace 5: INTERNAL kind with UNSET status td5 := ptrace.NewTraces() rs5 := td5.ResourceSpans().AppendEmpty() rs5.Resource().Attributes().PutStr(conventions.ServiceNameKey, "service5") ss5 := rs5.ScopeSpans().AppendEmpty() span5 := ss5.Spans().AppendEmpty() span5.SetTraceID(traceID5) span5.SetSpanID(spanIdFromString(t, "0000000000000005")) span5.SetName("operation5") span5.SetKind(ptrace.SpanKindInternal) span5.Status().SetCode(ptrace.StatusCodeUnset) span5.SetStartTimestamp(pcommon.NewTimestampFromTime(time.Now())) span5.SetEndTimestamp(pcommon.NewTimestampFromTime(time.Now().Add(time.Second))) // Write traces err = store.WriteTraces(context.Background(), td1) require.NoError(t, err) err = store.WriteTraces(context.Background(), td2) require.NoError(t, err) err = store.WriteTraces(context.Background(), td3) require.NoError(t, err) err = store.WriteTraces(context.Background(), td4) require.NoError(t, err) err = store.WriteTraces(context.Background(), td5) require.NoError(t, err) tests := []struct { name string queryAttrs map[string]string expectedTraces int expectedIDs []pcommon.TraceID }{ { name: "Filter by span.status=ERROR", queryAttrs: map[string]string{"span.status": "ERROR"}, expectedTraces: 1, expectedIDs: []pcommon.TraceID{traceID1}, }, { name: "Filter by span.status=OK", queryAttrs: map[string]string{"span.status": "OK"}, expectedTraces: 1, expectedIDs: []pcommon.TraceID{traceID2}, }, { name: "Filter by span.status=UNSET", queryAttrs: map[string]string{"span.status": "UNSET"}, expectedTraces: 3, expectedIDs: []pcommon.TraceID{traceID5, traceID4, traceID3}, }, { name: "Filter by span.kind=SERVER", queryAttrs: map[string]string{"span.kind": "SERVER"}, expectedTraces: 1, expectedIDs: []pcommon.TraceID{traceID1}, }, { name: "Filter by span.kind=CLIENT", queryAttrs: map[string]string{"span.kind": "CLIENT"}, expectedTraces: 1, expectedIDs: []pcommon.TraceID{traceID2}, }, { name: "Filter by span.kind=PRODUCER", queryAttrs: map[string]string{"span.kind": "PRODUCER"}, expectedTraces: 1, expectedIDs: []pcommon.TraceID{traceID3}, }, { name: "Filter by span.kind=CONSUMER", queryAttrs: map[string]string{"span.kind": "CONSUMER"}, expectedTraces: 1, expectedIDs: []pcommon.TraceID{traceID4}, }, { name: "Filter by span.kind=INTERNAL", queryAttrs: map[string]string{"span.kind": "INTERNAL"}, expectedTraces: 1, expectedIDs: []pcommon.TraceID{traceID5}, }, { name: "Filter by span.kind=UNSPECIFIED (no match)", queryAttrs: map[string]string{"span.kind": "UNSPECIFIED"}, expectedTraces: 0, expectedIDs: []pcommon.TraceID{}, }, { name: "Filter by span.kind=INVALID (default/unknown)", queryAttrs: map[string]string{"span.kind": "INVALID"}, expectedTraces: 0, expectedIDs: []pcommon.TraceID{}, }, { name: "Filter by scope.name=my-scope", queryAttrs: map[string]string{"scope.name": "my-scope"}, expectedTraces: 1, expectedIDs: []pcommon.TraceID{traceID1}, }, { name: "Filter by scope.name=other-scope", queryAttrs: map[string]string{"scope.name": "other-scope"}, expectedTraces: 1, expectedIDs: []pcommon.TraceID{traceID2}, }, { name: "Filter by scope.name (no match)", queryAttrs: map[string]string{"scope.name": "nonexistent"}, expectedTraces: 0, expectedIDs: []pcommon.TraceID{}, }, { name: "Filter by scope.version=1.0.0", queryAttrs: map[string]string{"scope.version": "1.0.0"}, expectedTraces: 1, expectedIDs: []pcommon.TraceID{traceID1}, }, { name: "Filter by scope.version=2.0.0", queryAttrs: map[string]string{"scope.version": "2.0.0"}, expectedTraces: 1, expectedIDs: []pcommon.TraceID{traceID2}, }, { name: "Filter by scope.version (no match)", queryAttrs: map[string]string{"scope.version": "99.0.0"}, expectedTraces: 0, expectedIDs: []pcommon.TraceID{}, }, { name: "Filter by resource.deployment.environment=production", queryAttrs: map[string]string{"resource.deployment.environment": "production"}, expectedTraces: 1, expectedIDs: []pcommon.TraceID{traceID1}, }, { name: "Filter by resource.deployment.environment=staging", queryAttrs: map[string]string{"resource.deployment.environment": "staging"}, expectedTraces: 1, expectedIDs: []pcommon.TraceID{traceID2}, }, { name: "Filter by resource.deployment.environment (no match)", queryAttrs: map[string]string{"resource.deployment.environment": "development"}, expectedTraces: 0, expectedIDs: []pcommon.TraceID{}, }, { name: "Combined: span.status=ERROR AND span.kind=SERVER", queryAttrs: map[string]string{"span.status": "ERROR", "span.kind": "SERVER"}, expectedTraces: 1, expectedIDs: []pcommon.TraceID{traceID1}, }, { name: "No match: span.status=ERROR AND span.kind=CLIENT", queryAttrs: map[string]string{"span.status": "ERROR", "span.kind": "CLIENT"}, expectedTraces: 0, expectedIDs: []pcommon.TraceID{}, }, { name: "Combined: scope.name AND scope.version", queryAttrs: map[string]string{"scope.name": "my-scope", "scope.version": "1.0.0"}, expectedTraces: 1, expectedIDs: []pcommon.TraceID{traceID1}, }, { name: "No OTLP filters (backward compatibility)", queryAttrs: map[string]string{}, expectedTraces: 5, expectedIDs: []pcommon.TraceID{traceID5, traceID4, traceID3, traceID2, traceID1}, // Reverse chronological }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { attrs := pcommon.NewMap() for k, v := range tt.queryAttrs { attrs.PutStr(k, v) } query := tracestore.TraceQueryParams{ Attributes: attrs, SearchDepth: 100, } iter := store.FindTraces(context.Background(), query) var foundTraces []ptrace.Traces for traces, err := range iter { require.NoError(t, err) foundTraces = append(foundTraces, traces...) } assert.Len(t, foundTraces, tt.expectedTraces, "query: %v", tt.queryAttrs) if tt.expectedTraces > 0 { for i, expectedID := range tt.expectedIDs { actualID := foundTraces[i].ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0).TraceID() assert.Equal(t, expectedID, actualID) } } }) } } ================================================ FILE: internal/storage/v2/memory/package_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package memory import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v2/memory/sampling.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package memory import ( "sync" "time" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore/model" ) // SamplingStore is an in-memory store for sampling data type SamplingStore struct { mu sync.RWMutex throughputs []*storedThroughput probabilitiesAndQPS *storedServiceOperationProbabilitiesAndQPS maxBuckets int } type storedThroughput struct { throughput []*model.Throughput time time.Time } type storedServiceOperationProbabilitiesAndQPS struct { hostname string probabilities model.ServiceOperationProbabilities qps model.ServiceOperationQPS time time.Time } // NewSamplingStore creates an in-memory sampling store. func NewSamplingStore(maxBuckets int) *SamplingStore { return &SamplingStore{maxBuckets: maxBuckets} } // InsertThroughput implements samplingstore.Store#InsertThroughput. func (ss *SamplingStore) InsertThroughput(throughput []*model.Throughput) error { ss.mu.Lock() defer ss.mu.Unlock() now := time.Now() ss.preprendThroughput(&storedThroughput{throughput, now}) return nil } // GetThroughput implements samplingstore.Store#GetThroughput. func (ss *SamplingStore) GetThroughput(start, end time.Time) ([]*model.Throughput, error) { ss.mu.Lock() defer ss.mu.Unlock() var retSlice []*model.Throughput for _, t := range ss.throughputs { if t.time.After(start) && (t.time.Before(end) || t.time.Equal(end)) { retSlice = append(retSlice, t.throughput...) } } return retSlice, nil } // InsertProbabilitiesAndQPS implements samplingstore.Store#InsertProbabilitiesAndQPS. func (ss *SamplingStore) InsertProbabilitiesAndQPS( hostname string, probabilities model.ServiceOperationProbabilities, qps model.ServiceOperationQPS, ) error { ss.mu.Lock() defer ss.mu.Unlock() ss.probabilitiesAndQPS = &storedServiceOperationProbabilitiesAndQPS{hostname, probabilities, qps, time.Now()} return nil } // GetLatestProbabilities implements samplingstore.Store#GetLatestProbabilities. func (ss *SamplingStore) GetLatestProbabilities() (model.ServiceOperationProbabilities, error) { ss.mu.Lock() defer ss.mu.Unlock() if ss.probabilitiesAndQPS != nil { return ss.probabilitiesAndQPS.probabilities, nil } return model.ServiceOperationProbabilities{}, nil } func (ss *SamplingStore) preprendThroughput(throughput *storedThroughput) { ss.throughputs = append([]*storedThroughput{throughput}, ss.throughputs...) if len(ss.throughputs) > ss.maxBuckets { ss.throughputs = ss.throughputs[0:ss.maxBuckets] } } ================================================ FILE: internal/storage/v2/memory/sampling_test.go ================================================ // Copyright (c) 2021 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package memory import ( "fmt" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger/internal/storage/v1/api/samplingstore/model" ) func withPopulatedSamplingStore(f func(samplingStore *SamplingStore)) { now := time.Now() millisAfter := now.Add(time.Millisecond * time.Duration(100)) secondsAfter := now.Add(time.Second * time.Duration(2)) throughputs := []*storedThroughput{ {[]*model.Throughput{{Service: "svc-1", Operation: "op-1", Count: 1}}, now}, {[]*model.Throughput{{Service: "svc-1", Operation: "op-2", Count: 1}}, millisAfter}, {[]*model.Throughput{{Service: "svc-2", Operation: "op-3", Count: 1}}, secondsAfter}, } pQPS := &storedServiceOperationProbabilitiesAndQPS{ hostname: "guntur38ab8928", probabilities: model.ServiceOperationProbabilities{"svc-1": {"op-1": 0.01}}, qps: model.ServiceOperationQPS{"svc-1": {"op-1": 10.0}}, time: now, } samplingStore := &SamplingStore{throughputs: throughputs, probabilitiesAndQPS: pQPS} f(samplingStore) } func withMemorySamplingStore(f func(samplingStore *SamplingStore)) { f(NewSamplingStore(5)) } func TestInsertThroughtput(t *testing.T) { withMemorySamplingStore(func(samplingStore *SamplingStore) { start := time.Now() throughputs := []*model.Throughput{ {Service: "my-svc", Operation: "op"}, {Service: "our-svc", Operation: "op2"}, } require.NoError(t, samplingStore.InsertThroughput(throughputs)) ret, _ := samplingStore.GetThroughput(start, start.Add(time.Second*time.Duration(1))) assert.Len(t, ret, 2) for i := range 10 { in := []*model.Throughput{ {Service: fmt.Sprint("svc-", i), Operation: fmt.Sprint("op-", i)}, } samplingStore.InsertThroughput(in) } assert.Len(t, samplingStore.throughputs, 5) }) } func TestGetThroughput(t *testing.T) { withPopulatedSamplingStore(func(samplingStore *SamplingStore) { start := time.Now() ret, err := samplingStore.GetThroughput(start, start.Add(time.Second*time.Duration(1))) require.NoError(t, err) assert.Len(t, ret, 1) ret1, _ := samplingStore.GetThroughput(start, start) assert.Empty(t, ret1) ret2, _ := samplingStore.GetThroughput(start, start.Add(time.Hour*time.Duration(1))) assert.Len(t, ret2, 2) }) } func TestInsertProbabilitiesAndQPS(t *testing.T) { withMemorySamplingStore(func(samplingStore *SamplingStore) { require.NoError(t, samplingStore.InsertProbabilitiesAndQPS("dell11eg843d", model.ServiceOperationProbabilities{"new-srv": {"op": 0.1}}, model.ServiceOperationQPS{"new-srv": {"op": 4}})) assert.NotEmpty(t, 1, samplingStore.probabilitiesAndQPS) // Only latest one is kept in memory require.NoError(t, samplingStore.InsertProbabilitiesAndQPS("lncol73", model.ServiceOperationProbabilities{"my-app": {"hello": 0.3}}, model.ServiceOperationQPS{"new-srv": {"op": 7}})) assert.InDelta(t, 0.3, samplingStore.probabilitiesAndQPS.probabilities["my-app"]["hello"], 0.01) }) } func TestGetLatestProbability(t *testing.T) { withMemorySamplingStore(func(samplingStore *SamplingStore) { // No priod data ret, err := samplingStore.GetLatestProbabilities() require.NoError(t, err) assert.Empty(t, ret) }) withPopulatedSamplingStore(func(samplingStore *SamplingStore) { // With some pregenerated data ret, err := samplingStore.GetLatestProbabilities() require.NoError(t, err) assert.Equal(t, model.ServiceOperationProbabilities{"svc-1": {"op-1": 0.01}}, ret) require.NoError(t, samplingStore.InsertProbabilitiesAndQPS("utfhyolf", model.ServiceOperationProbabilities{"another-service": {"hello": 0.009}}, model.ServiceOperationQPS{"new-srv": {"op": 5}})) ret, _ = samplingStore.GetLatestProbabilities() assert.NotEqual(t, model.ServiceOperationProbabilities{"svc-1": {"op-1": 0.01}}, ret) }) } ================================================ FILE: internal/storage/v2/memory/tenant.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package memory import ( "errors" "strings" "sync" "time" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" ) var errInvalidMaxTraces = errors.New("max traces must be greater than zero") // Tenant is an in-memory store of traces for a single tenant type Tenant struct { mu sync.RWMutex config *Configuration ids map[pcommon.TraceID]int // maps trace id to index in traces[] traces []traceAndId // ring buffer to store traces mostRecent int // position in traces[] of the most recently added trace services map[string]struct{} operations map[string]map[tracestore.Operation]struct{} } type traceAndId struct { id pcommon.TraceID trace ptrace.Traces startTime time.Time endTime time.Time } func (t traceAndId) traceIsBetweenStartAndEnd(startTime time.Time, endTime time.Time) bool { if endTime.IsZero() { return t.startTime.After(startTime) } return t.startTime.After(startTime) && t.endTime.Before(endTime) } func newTenant(cfg *Configuration) *Tenant { return &Tenant{ config: cfg, ids: make(map[pcommon.TraceID]int), traces: make([]traceAndId, cfg.MaxTraces), mostRecent: -1, services: map[string]struct{}{}, operations: map[string]map[tracestore.Operation]struct{}{}, } } func (t *Tenant) storeTraces(tracesById map[pcommon.TraceID]ptrace.ResourceSpansSlice) { t.mu.Lock() defer t.mu.Unlock() for traceId, sameTraceIDResourceSpan := range tracesById { var startTime time.Time var endTime time.Time for _, resourceSpan := range sameTraceIDResourceSpan.All() { serviceName := getServiceNameFromResource(resourceSpan.Resource()) if serviceName != "" { t.services[serviceName] = struct{}{} } for _, scopeSpan := range resourceSpan.ScopeSpans().All() { for _, span := range scopeSpan.Spans().All() { if serviceName != "" { operation := tracestore.Operation{ Name: span.Name(), SpanKind: fromOTELSpanKind(span.Kind()), } if _, ok := t.operations[serviceName]; !ok { t.operations[serviceName] = make(map[tracestore.Operation]struct{}) } t.operations[serviceName][operation] = struct{}{} } if startTime.IsZero() || span.StartTimestamp().AsTime().Before(startTime) { startTime = span.StartTimestamp().AsTime() } if endTime.IsZero() || span.EndTimestamp().AsTime().After(endTime) { endTime = span.EndTimestamp().AsTime() } } } } if index, ok := t.ids[traceId]; ok { sameTraceIDResourceSpan.MoveAndAppendTo(t.traces[index].trace.ResourceSpans()) if startTime.Before(t.traces[index].startTime) { t.traces[index].startTime = startTime } if endTime.After(t.traces[index].endTime) { t.traces[index].endTime = endTime } continue } traces := ptrace.NewTraces() sameTraceIDResourceSpan.MoveAndAppendTo(traces.ResourceSpans()) t.mostRecent = (t.mostRecent + 1) % t.config.MaxTraces // if there is already a trace in lastEvicted position, remove its ID from ids map if !t.traces[t.mostRecent].id.IsEmpty() { delete(t.ids, t.traces[t.mostRecent].id) } // update the ring with the trace id t.ids[traceId] = t.mostRecent t.traces[t.mostRecent] = traceAndId{ id: traceId, trace: traces, startTime: startTime, endTime: endTime, } } } func (t *Tenant) findTraceAndIds(query tracestore.TraceQueryParams) ([]traceAndId, error) { if query.SearchDepth <= 0 || query.SearchDepth > t.config.MaxTraces { return nil, errInvalidSearchDepth } t.mu.RLock() defer t.mu.RUnlock() traceAndIds := make([]traceAndId, 0, query.SearchDepth) n := len(t.traces) for i := range t.traces { if len(traceAndIds) == query.SearchDepth { break } index := (t.mostRecent - i + n) % n traceById := t.traces[index] if traceById.id.IsEmpty() { // Finding an empty ID means we reached a gap in the ring buffer // that has not yet been filled with traces. break } if validTrace(traceById.trace, query) { traceAndIds = append(traceAndIds, traceById) } } return traceAndIds, nil } func (t *Tenant) getTraces(traceIds ...tracestore.GetTraceParams) []ptrace.Traces { t.mu.RLock() defer t.mu.RUnlock() traces := make([]ptrace.Traces, 0) for i := range traceIds { index, ok := t.ids[traceIds[i].TraceID] if ok { traces = append(traces, t.traces[index].trace) } } return traces } func (t *Tenant) getDependencies(query depstore.QueryParameters) ([]model.DependencyLink, error) { if query.StartTime.IsZero() { return nil, errors.New("start time is required") } if !query.EndTime.IsZero() && query.EndTime.Before(query.StartTime) { return nil, errors.New("end time must be greater than start time") } t.mu.RLock() defer t.mu.RUnlock() deps := map[string]*model.DependencyLink{} for _, index := range t.ids { traceWithTime := t.traces[index] if !traceWithTime.traceIsBetweenStartAndEnd(query.StartTime, query.EndTime) { continue } for _, resourceSpan := range traceWithTime.trace.ResourceSpans().All() { for _, scopeSpan := range resourceSpan.ScopeSpans().All() { for _, span := range scopeSpan.Spans().All() { if span.ParentSpanID().IsEmpty() { continue } spanServiceName := getServiceNameFromResource(resourceSpan.Resource()) parentSpanServiceName, found := findServiceNameWithSpanId(traceWithTime.trace, span.ParentSpanID()) if !found { continue } depKey := parentSpanServiceName + "&&&" + spanServiceName if _, ok := deps[depKey]; !ok { deps[depKey] = &model.DependencyLink{ Parent: parentSpanServiceName, Child: spanServiceName, CallCount: 1, } } else { deps[depKey].CallCount++ } } } } } retMe := make([]model.DependencyLink, 0, len(deps)) for _, dep := range deps { retMe = append(retMe, *dep) } return retMe, nil } func findServiceNameWithSpanId(trace ptrace.Traces, spanId pcommon.SpanID) (string, bool) { for _, resourceSpan := range trace.ResourceSpans().All() { for _, scopeSpan := range resourceSpan.ScopeSpans().All() { for _, span := range scopeSpan.Spans().All() { if span.SpanID() == spanId { return getServiceNameFromResource(resourceSpan.Resource()), true } } } } return "", false } func validTrace(td ptrace.Traces, query tracestore.TraceQueryParams) bool { for _, resourceSpan := range td.ResourceSpans().All() { if !validResource(resourceSpan.Resource(), query) { continue } for _, scopeSpan := range resourceSpan.ScopeSpans().All() { for _, span := range scopeSpan.Spans().All() { if validSpan(resourceSpan.Resource().Attributes(), scopeSpan.Scope(), span, query) { return true } } } } return false } func validResource(resource pcommon.Resource, query tracestore.TraceQueryParams) bool { return query.ServiceName == "" || query.ServiceName == getServiceNameFromResource(resource) } func validSpan(resourceAttributes pcommon.Map, scope pcommon.InstrumentationScope, span ptrace.Span, query tracestore.TraceQueryParams) bool { if query.OperationName != "" && query.OperationName != span.Name() { return false } startTime := span.StartTimestamp().AsTime() if !query.StartTimeMin.IsZero() && startTime.Before(query.StartTimeMin) { return false } if !query.StartTimeMax.IsZero() && startTime.After(query.StartTimeMax) { return false } duration := span.EndTimestamp().AsTime().Sub(startTime) if query.DurationMin != 0 && duration < query.DurationMin { return false } if query.DurationMax != 0 && duration > query.DurationMax { return false } if errAttribute, ok := query.Attributes.Get(errorAttribute); ok { if errAttribute.Bool() && span.Status().Code() != ptrace.StatusCodeError { return false } if !errAttribute.Bool() && span.Status().Code() != ptrace.StatusCodeOk { return false } } if statusAttr, ok := query.Attributes.Get("span.status"); ok { expectedStatus := spanStatusFromString(statusAttr.AsString()) if expectedStatus != span.Status().Code() { return false } } if kindAttr, ok := query.Attributes.Get("span.kind"); ok { expectedKind := spanKindFromString(kindAttr.AsString()) if expectedKind != span.Kind() { return false } } if scopeNameAttr, ok := query.Attributes.Get("scope.name"); ok { if scopeNameAttr.AsString() != scope.Name() { return false } } if scopeVersionAttr, ok := query.Attributes.Get("scope.version"); ok { if scopeVersionAttr.AsString() != scope.Version() { return false } } for key, val := range query.Attributes.All() { if key == errorAttribute || key == "span.status" || key == "span.kind" || key == "scope.name" || key == "scope.version" { continue } if resourceKey, ok := strings.CutPrefix(key, "resource."); ok { if !matchAttributes(resourceKey, val, resourceAttributes) { return false } continue } if !findKeyValInTrace(key, val, resourceAttributes, scope.Attributes(), span) { return false } } return true } func matchAttributes(key string, val pcommon.Value, attrs pcommon.Map) bool { if queryValue, ok := attrs.Get(key); ok { return queryValue.AsString() == val.AsString() } return false } func findKeyValInTrace(key string, val pcommon.Value, resourceAttributes pcommon.Map, scopeAttributes pcommon.Map, span ptrace.Span) bool { tagsMatched := matchAttributes(key, val, span.Attributes()) || matchAttributes(key, val, scopeAttributes) || matchAttributes(key, val, resourceAttributes) if tagsMatched { return true } for _, event := range span.Events().All() { if matchAttributes(key, val, event.Attributes()) { return true } } for _, link := range span.Links().All() { if matchAttributes(key, val, link.Attributes()) { return true } } return tagsMatched } func fromOTELSpanKind(kind ptrace.SpanKind) string { if kind == ptrace.SpanKindUnspecified { return "" } return strings.ToLower(kind.String()) } func spanStatusFromString(statusStr string) ptrace.StatusCode { switch strings.ToUpper(statusStr) { case "OK": return ptrace.StatusCodeOk case "ERROR": return ptrace.StatusCodeError default: return ptrace.StatusCodeUnset } } func spanKindFromString(kindStr string) ptrace.SpanKind { switch strings.ToUpper(kindStr) { case "CLIENT": return ptrace.SpanKindClient case "SERVER": return ptrace.SpanKindServer case "PRODUCER": return ptrace.SpanKindProducer case "CONSUMER": return ptrace.SpanKindConsumer case "INTERNAL": return ptrace.SpanKindInternal default: return ptrace.SpanKindUnspecified } } ================================================ FILE: internal/storage/v2/v1adapter/README.md ================================================ # Storage Factory Converter A temporary v1 storage factory wrapper to implement v2 storage APIs. This way, the existing v1 storage factories declared in `jaegerstorageextension` can act as v2 storage while we migrate to v2 storage APIs. ================================================ FILE: internal/storage/v2/v1adapter/depreader.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package v1adapter import ( "context" "time" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/api/dependencystore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" ) type DependencyReader struct { reader dependencystore.Reader } func GetV1DependencyReader(reader depstore.Reader) dependencystore.Reader { if dr, ok := reader.(*DependencyReader); ok { return dr.reader } return &DowngradedDependencyReader{ reader: reader, } } func NewDependencyReader(reader dependencystore.Reader) *DependencyReader { return &DependencyReader{ reader: reader, } } func (dr *DependencyReader) GetDependencies( ctx context.Context, query depstore.QueryParameters, ) ([]model.DependencyLink, error) { return dr.reader.GetDependencies(ctx, query.EndTime, query.EndTime.Sub(query.StartTime)) } type DowngradedDependencyReader struct { reader depstore.Reader } func (dr *DowngradedDependencyReader) GetDependencies( ctx context.Context, endTs time.Time, lookback time.Duration, ) ([]model.DependencyLink, error) { return dr.reader.GetDependencies(ctx, depstore.QueryParameters{ StartTime: endTs.Add(-lookback), EndTime: endTs, }) } ================================================ FILE: internal/storage/v2/v1adapter/depreader_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package v1adapter import ( "context" "testing" "time" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger-idl/model/v1" dependencystoremocks "github.com/jaegertracing/jaeger/internal/storage/v1/api/dependencystore/mocks" "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore" depstoremocks "github.com/jaegertracing/jaeger/internal/storage/v2/api/depstore/mocks" ) func TestGetV1DependencyReader(t *testing.T) { t.Run("wrapped v1 reader", func(t *testing.T) { reader := new(dependencystoremocks.Reader) traceReader := &DependencyReader{ reader: reader, } v1Reader := GetV1DependencyReader(traceReader) require.Equal(t, reader, v1Reader) }) t.Run("native v2 reader", func(t *testing.T) { reader := new(depstoremocks.Reader) v1Reader := GetV1DependencyReader(reader) require.IsType(t, &DowngradedDependencyReader{}, v1Reader) require.Equal(t, reader, v1Reader.(*DowngradedDependencyReader).reader) }) } func TestDependencyReader_GetDependencies(t *testing.T) { end := time.Now() start := end.Add(-1 * time.Minute) query := depstore.QueryParameters{ StartTime: start, EndTime: end, } expectedDeps := []model.DependencyLink{{Parent: "parent", Child: "child", CallCount: 12}} mr := new(dependencystoremocks.Reader) mr.On("GetDependencies", mock.Anything, end, time.Minute).Return(expectedDeps, nil) dr := NewDependencyReader(mr) deps, err := dr.GetDependencies(context.Background(), query) require.NoError(t, err) require.Equal(t, expectedDeps, deps) } func TestDowngradedDependencyReader_GetDependencies(t *testing.T) { end := time.Now() start := end.Add(-1 * time.Minute) query := depstore.QueryParameters{ StartTime: start, EndTime: end, } expectedDeps := []model.DependencyLink{{Parent: "parent", Child: "child", CallCount: 12}} mr := new(depstoremocks.Reader) mr.On("GetDependencies", mock.Anything, query).Return(expectedDeps, nil) dr := &DowngradedDependencyReader{ reader: mr, } deps, err := dr.GetDependencies(context.Background(), end, time.Minute) require.NoError(t, err) require.Equal(t, expectedDeps, deps) } ================================================ FILE: internal/storage/v2/v1adapter/factory.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package v1adapter ================================================ FILE: internal/storage/v2/v1adapter/factory_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package v1adapter ================================================ FILE: internal/storage/v2/v1adapter/otelids.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2018 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package v1adapter import ( "encoding/binary" "go.opentelemetry.io/collector/pdata/pcommon" "github.com/jaegertracing/jaeger-idl/model/v1" ) // FromV1TraceID converts the TraceID to OTEL's representation of a trace identitfier. // This was taken from // https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/internal/coreinternal/idutils/big_endian_converter.go. func FromV1TraceID(t model.TraceID) pcommon.TraceID { traceID := [16]byte{} binary.BigEndian.PutUint64(traceID[:8], t.High) binary.BigEndian.PutUint64(traceID[8:], t.Low) return traceID } func ToV1TraceID(traceID pcommon.TraceID) model.TraceID { // traceIDShortBytesLen indicates length of 64bit traceID when represented as list of bytes const traceIDShortBytesLen = 8 return model.TraceID{ High: binary.BigEndian.Uint64(traceID[:traceIDShortBytesLen]), Low: binary.BigEndian.Uint64(traceID[traceIDShortBytesLen:]), } } // FromV1SpanID converts the SpanID to OTEL's representation of a span identitfier. // This was taken from // https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/internal/coreinternal/idutils/big_endian_converter.go. func FromV1SpanID(s model.SpanID) pcommon.SpanID { spanID := [8]byte{} binary.BigEndian.PutUint64(spanID[:], uint64(s)) return pcommon.SpanID(spanID) } // ToV1SpanID converts OTEL's SpanID to the model representation of a span identitfier. // This was taken from // https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/internal/coreinternal/idutils/big_endian_converter.go. func ToV1SpanID(spanID pcommon.SpanID) model.SpanID { return model.SpanID(binary.BigEndian.Uint64(spanID[:])) } ================================================ FILE: internal/storage/v2/v1adapter/otelids_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2018 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package v1adapter import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "github.com/jaegertracing/jaeger-idl/model/v1" ) func TestToOTELTraceID(t *testing.T) { modelTraceID := model.TraceID{ Low: 3, High: 2, } otelTraceID := FromV1TraceID(modelTraceID) expected := []byte{0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3} require.Equal(t, pcommon.TraceID(expected), otelTraceID) } func TestTraceIDFromOTEL(t *testing.T) { otelTraceID := pcommon.TraceID([]byte{0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3}) expected := model.TraceID{ Low: 3, High: 2, } require.Equal(t, expected, ToV1TraceID(otelTraceID)) } func TestToOTELSpanID(t *testing.T) { tests := []struct { name string spanID model.SpanID expected pcommon.SpanID }{ { name: "zero span ID", spanID: model.NewSpanID(0), expected: pcommon.NewSpanIDEmpty(), }, { name: "non-zero span ID", spanID: model.NewSpanID(1), expected: pcommon.SpanID([8]byte{0, 0, 0, 0, 0, 0, 0, 1}), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { actual := FromV1SpanID(test.spanID) assert.Equal(t, test.expected, actual) }) } } func TestSpanIDFromOTEL(t *testing.T) { tests := []struct { name string otelSpanID pcommon.SpanID expected model.SpanID }{ { name: "zero span ID", otelSpanID: pcommon.NewSpanIDEmpty(), expected: model.NewSpanID(0), }, { name: "non-zero span ID", otelSpanID: pcommon.SpanID([8]byte{0, 0, 0, 0, 0, 0, 0, 1}), expected: model.NewSpanID(1), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { actual := ToV1SpanID(test.otelSpanID) assert.Equal(t, test.expected, actual) }) } } ================================================ FILE: internal/storage/v2/v1adapter/package_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package v1adapter import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/storage/v2/v1adapter/spanreader.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package v1adapter import ( "context" "errors" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/jptrace" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" ) var _ spanstore.Reader = (*SpanReader)(nil) var errTooManyTracesFound = errors.New("too many traces found") // SpanReader adapts a v2 tracestore.Reader to the v1 spanstore.Reader interface. type SpanReader struct { traceReader tracestore.Reader } // GetV1Reader adapts a v2 tracestore.Reader to the v1 spanstore.Reader interface. // If the passed reader is already a v1 adapter, it returns the underlying v1 spanstore.Reader. func GetV1Reader(reader tracestore.Reader) spanstore.Reader { if tr, ok := reader.(*TraceReader); ok { return tr.spanReader } return &SpanReader{ traceReader: reader, } } func (sr *SpanReader) GetTrace(ctx context.Context, query spanstore.GetTraceParameters) (*model.Trace, error) { getTracesIter := sr.traceReader.GetTraces(ctx, tracestore.GetTraceParams{ TraceID: FromV1TraceID(query.TraceID), Start: query.StartTime, End: query.EndTime, }) traces, err := V1TracesFromSeq2(getTracesIter) if err != nil { return nil, err } if len(traces) == 0 { return nil, spanstore.ErrTraceNotFound } else if len(traces) > 1 { return nil, errTooManyTracesFound } return traces[0], nil } func (sr *SpanReader) GetServices(ctx context.Context) ([]string, error) { return sr.traceReader.GetServices(ctx) } func (sr *SpanReader) GetOperations( ctx context.Context, query spanstore.OperationQueryParameters, ) ([]spanstore.Operation, error) { o, err := sr.traceReader.GetOperations(ctx, tracestore.OperationQueryParams{ ServiceName: query.ServiceName, SpanKind: query.SpanKind, }) if err != nil || o == nil { return nil, err } operations := []spanstore.Operation{} for _, operation := range o { operations = append(operations, spanstore.Operation{ Name: operation.Name, SpanKind: operation.SpanKind, }) } return operations, nil } func (sr *SpanReader) FindTraces( ctx context.Context, query *spanstore.TraceQueryParameters, ) ([]*model.Trace, error) { getTracesIter := sr.traceReader.FindTraces(ctx, tracestore.TraceQueryParams{ ServiceName: query.ServiceName, OperationName: query.OperationName, Attributes: jptrace.PlainMapToPcommonMap(query.Tags), StartTimeMin: query.StartTimeMin, StartTimeMax: query.StartTimeMax, DurationMin: query.DurationMin, DurationMax: query.DurationMax, SearchDepth: query.NumTraces, }) return V1TracesFromSeq2(getTracesIter) } func (sr *SpanReader) FindTraceIDs( ctx context.Context, query *spanstore.TraceQueryParameters, ) ([]model.TraceID, error) { traceIDsIter := sr.traceReader.FindTraceIDs(ctx, tracestore.TraceQueryParams{ ServiceName: query.ServiceName, OperationName: query.OperationName, Attributes: jptrace.PlainMapToPcommonMap(query.Tags), StartTimeMin: query.StartTimeMin, StartTimeMax: query.StartTimeMax, DurationMin: query.DurationMin, DurationMax: query.DurationMax, SearchDepth: query.NumTraces, }) return V1TraceIDsFromSeq2(traceIDsIter) } ================================================ FILE: internal/storage/v2/v1adapter/spanreader_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package v1adapter import ( "context" "iter" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" tracestoremocks "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore/mocks" ) func TestSpanReader_GetTrace(t *testing.T) { tests := []struct { name string query spanstore.GetTraceParameters expectedQuery tracestore.GetTraceParams traces []ptrace.Traces expectedTrace *model.Trace err error expectedErr error }{ { name: "error getting trace", query: spanstore.GetTraceParameters{ TraceID: model.NewTraceID(1, 2), }, expectedQuery: tracestore.GetTraceParams{ TraceID: [16]byte{0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2}, }, err: assert.AnError, expectedErr: assert.AnError, }, { name: "empty traces", query: spanstore.GetTraceParameters{ TraceID: model.NewTraceID(1, 2), }, expectedQuery: tracestore.GetTraceParams{ TraceID: [16]byte{0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2}, }, traces: []ptrace.Traces{}, expectedErr: spanstore.ErrTraceNotFound, }, { name: "too many traces found", query: spanstore.GetTraceParameters{ TraceID: model.NewTraceID(1, 2), }, expectedQuery: tracestore.GetTraceParams{ TraceID: [16]byte{0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2}, }, traces: func() []ptrace.Traces { traces1 := ptrace.NewTraces() resources1 := traces1.ResourceSpans().AppendEmpty() resources1.Resource().Attributes().PutStr("service.name", "service1") scopes1 := resources1.ScopeSpans().AppendEmpty() span1 := scopes1.Spans().AppendEmpty() span1.SetTraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2}) traces2 := ptrace.NewTraces() resources2 := traces2.ResourceSpans().AppendEmpty() resources2.Resource().Attributes().PutStr("service.name", "service1") scopes2 := resources2.ScopeSpans().AppendEmpty() span2 := scopes2.Spans().AppendEmpty() span2.SetTraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3}) return []ptrace.Traces{traces1, traces2} }(), expectedErr: errTooManyTracesFound, }, { name: "success", query: spanstore.GetTraceParameters{ TraceID: model.NewTraceID(1, 2), }, expectedQuery: tracestore.GetTraceParams{ TraceID: [16]byte{0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2}, }, traces: func() []ptrace.Traces { traces := ptrace.NewTraces() resources := traces.ResourceSpans().AppendEmpty() resources.Resource().Attributes().PutStr("service.name", "service") scopes := resources.ScopeSpans().AppendEmpty() span := scopes.Spans().AppendEmpty() span.SetTraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2}) span.SetSpanID([8]byte{0, 0, 0, 0, 0, 0, 0, 3}) span.SetName("span") span.SetStartTimestamp(pcommon.NewTimestampFromTime(time.Unix(0, 0).UTC())) return []ptrace.Traces{traces} }(), expectedTrace: &model.Trace{ Spans: []*model.Span{ { TraceID: model.NewTraceID(1, 2), SpanID: model.NewSpanID(3), OperationName: "span", References: []model.SpanRef{}, Tags: make([]model.KeyValue, 0), Process: model.NewProcess("service", make([]model.KeyValue, 0)), StartTime: time.Unix(0, 0).UTC(), }, }, }, }, } for _, test := range tests { tr := tracestoremocks.Reader{} tr.On("GetTraces", mock.Anything, mock.Anything). Return(iter.Seq2[[]ptrace.Traces, error](func(yield func([]ptrace.Traces, error) bool) { yield(test.traces, test.err) })).Once() sr := SpanReader{ traceReader: &tr, } trace, err := sr.GetTrace(context.Background(), test.query) require.ErrorIs(t, err, test.expectedErr) require.Equal(t, test.expectedTrace, trace) } } func TestSpanReader_GetServices(t *testing.T) { tests := []struct { name string services []string expectedServices []string err error expectedErr error }{ { name: "error getting services", err: assert.AnError, expectedErr: assert.AnError, }, { name: "no services", services: []string{}, expectedServices: []string{}, }, { name: "multiple services", services: []string{"service1", "service2"}, expectedServices: []string{"service1", "service2"}, }, } for _, test := range tests { tr := tracestoremocks.Reader{} tr.On("GetServices", mock.Anything). Return(test.services, test.err).Once() sr := SpanReader{ traceReader: &tr, } services, err := sr.GetServices(context.Background()) require.ErrorIs(t, err, test.expectedErr) require.Equal(t, test.expectedServices, services) } } func TestSpanReader_GetOperations(t *testing.T) { tests := []struct { name string query spanstore.OperationQueryParameters expectedQuery tracestore.OperationQueryParams operations []tracestore.Operation expectedOperations []spanstore.Operation err error expectedErr error }{ { name: "error getting operations", query: spanstore.OperationQueryParameters{ ServiceName: "service1", }, expectedQuery: tracestore.OperationQueryParams{ ServiceName: "service1", }, err: assert.AnError, expectedErr: assert.AnError, }, { name: "no operations", query: spanstore.OperationQueryParameters{ ServiceName: "service1", }, expectedQuery: tracestore.OperationQueryParams{ ServiceName: "service1", }, operations: []tracestore.Operation{}, expectedOperations: []spanstore.Operation{}, }, { name: "multiple operations", query: spanstore.OperationQueryParameters{ ServiceName: "service1", }, expectedQuery: tracestore.OperationQueryParams{ ServiceName: "service1", }, operations: []tracestore.Operation{ {Name: "operation1", SpanKind: "kind1"}, {Name: "operation2", SpanKind: "kind2"}, }, expectedOperations: []spanstore.Operation{ {Name: "operation1", SpanKind: "kind1"}, {Name: "operation2", SpanKind: "kind2"}, }, }, } for _, test := range tests { tr := tracestoremocks.Reader{} tr.On("GetOperations", mock.Anything, test.expectedQuery). Return(test.operations, test.err).Once() sr := SpanReader{ traceReader: &tr, } ops, err := sr.GetOperations(context.Background(), test.query) require.ErrorIs(t, err, test.expectedErr) require.Equal(t, test.expectedOperations, ops) } } func TestSpanReader_FindTraces(t *testing.T) { tests := []struct { name string query *spanstore.TraceQueryParameters expectedQuery tracestore.TraceQueryParams traces []ptrace.Traces expectedTraces []*model.Trace err error expectedErr error }{ { name: "error finding traces", query: &spanstore.TraceQueryParameters{ ServiceName: "service1", }, expectedQuery: tracestore.TraceQueryParams{ ServiceName: "service1", Attributes: pcommon.NewMap(), }, err: assert.AnError, expectedErr: assert.AnError, }, { name: "no traces found", query: &spanstore.TraceQueryParameters{ ServiceName: "service1", }, expectedQuery: tracestore.TraceQueryParams{ ServiceName: "service1", Attributes: pcommon.NewMap(), }, traces: []ptrace.Traces{}, expectedTraces: nil, }, { name: "multiple traces found", query: &spanstore.TraceQueryParameters{ ServiceName: "service1", }, expectedQuery: tracestore.TraceQueryParams{ ServiceName: "service1", Attributes: pcommon.NewMap(), }, traces: func() []ptrace.Traces { traces1 := ptrace.NewTraces() resources1 := traces1.ResourceSpans().AppendEmpty() resources1.Resource().Attributes().PutStr("service.name", "service1") scopes1 := resources1.ScopeSpans().AppendEmpty() span1 := scopes1.Spans().AppendEmpty() span1.SetTraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2}) span1.SetSpanID([8]byte{0, 0, 0, 0, 0, 0, 0, 3}) span1.SetName("span1") span1.SetStartTimestamp(pcommon.NewTimestampFromTime(time.Unix(0, 0).UTC())) traces2 := ptrace.NewTraces() resources2 := traces2.ResourceSpans().AppendEmpty() resources2.Resource().Attributes().PutStr("service.name", "service1") scopes2 := resources2.ScopeSpans().AppendEmpty() span2 := scopes2.Spans().AppendEmpty() span2.SetTraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 5}) span2.SetSpanID([8]byte{0, 0, 0, 0, 0, 0, 0, 6}) span2.SetName("span2") span2.SetStartTimestamp(pcommon.NewTimestampFromTime(time.Unix(0, 0).UTC())) return []ptrace.Traces{traces1, traces2} }(), expectedTraces: []*model.Trace{ { Spans: []*model.Span{ { TraceID: model.NewTraceID(1, 2), SpanID: model.NewSpanID(3), OperationName: "span1", References: make([]model.SpanRef, 0), Tags: model.KeyValues{}, Process: model.NewProcess("service1", make([]model.KeyValue, 0)), StartTime: time.Unix(0, 0).UTC(), }, }, }, { Spans: []*model.Span{ { TraceID: model.NewTraceID(4, 5), SpanID: model.NewSpanID(6), OperationName: "span2", References: make([]model.SpanRef, 0), Tags: model.KeyValues{}, Process: model.NewProcess("service1", make([]model.KeyValue, 0)), StartTime: time.Unix(0, 0).UTC(), }, }, }, }, }, } for _, test := range tests { tr := tracestoremocks.Reader{} tr.On("FindTraces", mock.Anything, test.expectedQuery). Return(iter.Seq2[[]ptrace.Traces, error](func(yield func([]ptrace.Traces, error) bool) { yield(test.traces, test.err) })).Once() sr := SpanReader{ traceReader: &tr, } traces, err := sr.FindTraces(context.Background(), test.query) require.ErrorIs(t, err, test.expectedErr) require.Equal(t, test.expectedTraces, traces) } } func TestSpanReader_FindTraceIDs(t *testing.T) { tests := []struct { name string query *spanstore.TraceQueryParameters expectedQuery tracestore.TraceQueryParams traceIDs []tracestore.FoundTraceID expectedTraceIDs []model.TraceID err error expectedErr error }{ { name: "error finding trace IDs", query: &spanstore.TraceQueryParameters{ ServiceName: "service1", }, expectedQuery: tracestore.TraceQueryParams{ ServiceName: "service1", Attributes: pcommon.NewMap(), }, err: assert.AnError, expectedErr: assert.AnError, }, { name: "no trace IDs found", query: &spanstore.TraceQueryParameters{ ServiceName: "service1", }, expectedQuery: tracestore.TraceQueryParams{ ServiceName: "service1", Attributes: pcommon.NewMap(), }, traceIDs: []tracestore.FoundTraceID{}, expectedTraceIDs: nil, }, { name: "multiple trace IDs found", query: &spanstore.TraceQueryParameters{ ServiceName: "service1", }, expectedQuery: tracestore.TraceQueryParams{ ServiceName: "service1", Attributes: pcommon.NewMap(), }, traceIDs: []tracestore.FoundTraceID{ { TraceID: [16]byte{0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2}, }, { TraceID: [16]byte{0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 4}, }, }, expectedTraceIDs: []model.TraceID{ model.NewTraceID(1, 2), model.NewTraceID(3, 4), }, }, } for _, test := range tests { tr := tracestoremocks.Reader{} tr.On("FindTraceIDs", mock.Anything, test.expectedQuery). Return(iter.Seq2[[]tracestore.FoundTraceID, error](func(yield func([]tracestore.FoundTraceID, error) bool) { yield(test.traceIDs, test.err) })).Once() sr := SpanReader{ traceReader: &tr, } traceIDs, err := sr.FindTraceIDs(context.Background(), test.query) require.ErrorIs(t, err, test.expectedErr) require.Equal(t, test.expectedTraceIDs, traceIDs) } } ================================================ FILE: internal/storage/v2/v1adapter/spanwriter.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package v1adapter import ( "context" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" ) var _ spanstore.Writer = (*SpanWriter)(nil) // SpanReader wraps a tracestore.Writer so that it can be downgraded to implement // the v1 spanstore.Writer interface. type SpanWriter struct { traceWriter tracestore.Writer } func (sw *SpanWriter) WriteSpan(ctx context.Context, span *model.Span) error { traces := V1BatchesToTraces([]*model.Batch{{Spans: []*model.Span{span}}}) return sw.traceWriter.WriteTraces(ctx, traces) } ================================================ FILE: internal/storage/v2/v1adapter/spanwriter_test.go ================================================ // Copyright (c) 2025 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package v1adapter import ( "context" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger-idl/model/v1" spanstoremocks "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore/mocks" tracestoremocks "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore/mocks" ) func TestGetV1Reader(t *testing.T) { t.Run("wrapped v1 reader", func(t *testing.T) { reader := new(spanstoremocks.Reader) traceReader := &TraceReader{ spanReader: reader, } v1Reader := GetV1Reader(traceReader) require.Equal(t, reader, v1Reader) }) t.Run("native v2 reader", func(t *testing.T) { reader := new(tracestoremocks.Reader) v1Reader := GetV1Reader(reader) require.IsType(t, &SpanReader{}, v1Reader) require.Equal(t, reader, v1Reader.(*SpanReader).traceReader) }) } func TestSpanWriter_WriteSpan(t *testing.T) { tests := []struct { name string mockReturnErr error expectedErr error }{ { name: "success", mockReturnErr: nil, expectedErr: nil, }, { name: "error in translator", mockReturnErr: assert.AnError, expectedErr: assert.AnError, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { mockTraceWriter := &tracestoremocks.Writer{} spanWriter := &SpanWriter{ traceWriter: mockTraceWriter, } now := time.Now().UTC() testSpan := &model.Span{ TraceID: model.NewTraceID(0, 1), SpanID: model.NewSpanID(1), StartTime: now, Duration: time.Second, } traces := ptrace.NewTraces() resources := traces.ResourceSpans().AppendEmpty() scopes := resources.ScopeSpans().AppendEmpty() span := scopes.Spans().AppendEmpty() span.SetTraceID(pcommon.TraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})) span.SetSpanID(pcommon.SpanID([8]byte{0, 0, 0, 0, 0, 0, 0, 1})) span.SetStartTimestamp(pcommon.NewTimestampFromTime(now)) span.SetEndTimestamp(pcommon.NewTimestampFromTime(now.Add(time.Second))) mockTraceWriter.On("WriteTraces", mock.Anything, traces).Return(test.mockReturnErr) err := spanWriter.WriteSpan(context.Background(), testSpan) require.ErrorIs(t, err, test.expectedErr) }) } } ================================================ FILE: internal/storage/v2/v1adapter/tracereader.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package v1adapter import ( "context" "errors" "iter" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" ) var _ tracestore.Reader = (*TraceReader)(nil) // TraceReader adapts a v1 spanstore.Reader to the v2 tracestore.Reader interface. type TraceReader struct { spanReader spanstore.Reader } func NewTraceReader(spanReader spanstore.Reader) *TraceReader { return &TraceReader{ spanReader: spanReader, } } func (tr *TraceReader) GetTraces( ctx context.Context, traceIDs ...tracestore.GetTraceParams, ) iter.Seq2[[]ptrace.Traces, error] { return func(yield func([]ptrace.Traces, error) bool) { for _, idParams := range traceIDs { query := spanstore.GetTraceParameters{ TraceID: ToV1TraceID(idParams.TraceID), StartTime: idParams.Start, EndTime: idParams.End, } t, err := tr.spanReader.GetTrace(ctx, query) if err != nil { if errors.Is(err, spanstore.ErrTraceNotFound) { continue } yield(nil, err) return } batch := &model.Batch{Spans: t.GetSpans()} tr := V1BatchesToTraces([]*model.Batch{batch}) if !yield([]ptrace.Traces{tr}, nil) { return } } } } func (tr *TraceReader) GetServices(ctx context.Context) ([]string, error) { return tr.spanReader.GetServices(ctx) } func (tr *TraceReader) GetOperations( ctx context.Context, query tracestore.OperationQueryParams, ) ([]tracestore.Operation, error) { o, err := tr.spanReader.GetOperations(ctx, spanstore.OperationQueryParameters{ ServiceName: query.ServiceName, SpanKind: query.SpanKind, }) if err != nil || o == nil { return nil, err } operations := []tracestore.Operation{} for _, operation := range o { operations = append(operations, tracestore.Operation{ Name: operation.Name, SpanKind: operation.SpanKind, }) } return operations, nil } func (tr *TraceReader) FindTraces( ctx context.Context, query tracestore.TraceQueryParams, ) iter.Seq2[[]ptrace.Traces, error] { return func(yield func([]ptrace.Traces, error) bool) { traces, err := tr.spanReader.FindTraces(ctx, query.ToSpanStoreQueryParameters()) if err != nil { yield(nil, err) return } for _, trace := range traces { batch := &model.Batch{Spans: trace.GetSpans()} otelTrace := V1BatchesToTraces([]*model.Batch{batch}) if !yield([]ptrace.Traces{otelTrace}, nil) { return } } } } func (tr *TraceReader) FindTraceIDs( ctx context.Context, query tracestore.TraceQueryParams, ) iter.Seq2[[]tracestore.FoundTraceID, error] { return func(yield func([]tracestore.FoundTraceID, error) bool) { traceIDs, err := tr.spanReader.FindTraceIDs(ctx, query.ToSpanStoreQueryParameters()) if err != nil { yield(nil, err) return } otelIDs := make([]tracestore.FoundTraceID, 0, len(traceIDs)) for _, traceID := range traceIDs { otelIDs = append(otelIDs, tracestore.FoundTraceID{ TraceID: FromV1TraceID(traceID), }) } yield(otelIDs, nil) } } ================================================ FILE: internal/storage/v2/v1adapter/tracereader_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package v1adapter import ( "context" "errors" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/jiter" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" spanstoremocks "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore/mocks" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" ) func TestTraceReader_GetTracesDelegatesSuccessResponse(t *testing.T) { sr := new(spanstoremocks.Reader) modelTrace := &model.Trace{ Spans: []*model.Span{ { TraceID: model.NewTraceID(2, 3), SpanID: model.SpanID(1), OperationName: "operation-a", }, { TraceID: model.NewTraceID(2, 3), SpanID: model.SpanID(2), OperationName: "operation-b", }, }, } expectedQuery := spanstore.GetTraceParameters{TraceID: model.NewTraceID(2, 3)} sr.On("GetTrace", mock.Anything, expectedQuery).Return(modelTrace, nil) traceReader := &TraceReader{ spanReader: sr, } traces, err := jiter.FlattenWithErrors(traceReader.GetTraces( context.Background(), tracestore.GetTraceParams{ TraceID: pcommon.TraceID([]byte{0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3}), }, )) require.NoError(t, err) require.Len(t, traces, 1) trace := traces[0] traceSpans := trace.ResourceSpans().At(0).ScopeSpans().At(0).Spans() require.EqualValues(t, []byte{0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3}, traceSpans.At(0).TraceID()) require.EqualValues(t, []byte{0, 0, 0, 0, 0, 0, 0, 1}, traceSpans.At(0).SpanID()) require.Equal(t, "operation-a", traceSpans.At(0).Name()) require.EqualValues(t, []byte{0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3}, traceSpans.At(1).TraceID()) require.EqualValues(t, []byte{0, 0, 0, 0, 0, 0, 0, 2}, traceSpans.At(1).SpanID()) require.Equal(t, "operation-b", traceSpans.At(1).Name()) } func TestTraceReader_GetTracesErrorResponse(t *testing.T) { testCases := []struct { name string firstErr error expectedErr error expectedIters int }{ { name: "real error aborts iterator", firstErr: assert.AnError, expectedErr: assert.AnError, expectedIters: 0, // technically 1 but FlattenWithErrors makes it 0 }, { name: "trace not found error skips iteration", firstErr: spanstore.ErrTraceNotFound, expectedErr: nil, expectedIters: 1, }, { name: "no error produces two iterations", firstErr: nil, expectedErr: nil, expectedIters: 2, }, } traceID := func(i byte) tracestore.GetTraceParams { return tracestore.GetTraceParams{ TraceID: pcommon.TraceID([]byte{0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, i}), } } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { sr := new(spanstoremocks.Reader) sr.On("GetTrace", mock.Anything, mock.Anything).Return(&model.Trace{}, test.firstErr).Once() sr.On("GetTrace", mock.Anything, mock.Anything).Return(&model.Trace{}, nil).Once() traceReader := &TraceReader{ spanReader: sr, } traces, err := jiter.FlattenWithErrors(traceReader.GetTraces( context.Background(), traceID(1), traceID(2), )) require.ErrorIs(t, err, test.expectedErr) assert.Len(t, traces, test.expectedIters) }) } } func TestTraceReader_GetTracesEarlyStop(t *testing.T) { sr := new(spanstoremocks.Reader) sr.On( "GetTrace", mock.Anything, mock.Anything, ).Return(&model.Trace{}, nil) traceReader := &TraceReader{ spanReader: sr, } traceID := func(i byte) tracestore.GetTraceParams { return tracestore.GetTraceParams{ TraceID: pcommon.TraceID([]byte{0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, i}), } } called := 0 traceReader.GetTraces( context.Background(), traceID(1), traceID(2), traceID(3), )(func(tr []ptrace.Traces, err error) bool { require.NoError(t, err) require.Len(t, tr, 1) called++ return true }) assert.Equal(t, 3, called) called = 0 traceReader.GetTraces( context.Background(), traceID(1), traceID(2), traceID(3), )(func(tr []ptrace.Traces, err error) bool { require.NoError(t, err) require.Len(t, tr, 1) called++ return false // early return }) assert.Equal(t, 1, called) } func TestTraceReader_GetServicesDelegatesToSpanReader(t *testing.T) { sr := new(spanstoremocks.Reader) expectedServices := []string{"service-a", "service-b"} sr.On("GetServices", mock.Anything).Return(expectedServices, nil) traceReader := &TraceReader{ spanReader: sr, } services, err := traceReader.GetServices(context.Background()) require.NoError(t, err) require.Equal(t, expectedServices, services) } func TestTraceReader_GetOperationsDelegatesResponse(t *testing.T) { tests := []struct { name string operations []spanstore.Operation expectedOperations []tracestore.Operation err error }{ { name: "successful response", operations: []spanstore.Operation{ { Name: "operation-a", SpanKind: "server", }, { Name: "operation-b", SpanKind: "server", }, }, expectedOperations: []tracestore.Operation{ { Name: "operation-a", SpanKind: "server", }, { Name: "operation-b", SpanKind: "server", }, }, }, { name: "nil response", operations: nil, expectedOperations: nil, }, { name: "empty response", operations: []spanstore.Operation{}, expectedOperations: []tracestore.Operation{}, }, { name: "error response", operations: nil, expectedOperations: nil, err: errors.New("test error"), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { sr := new(spanstoremocks.Reader) sr.On("GetOperations", mock.Anything, spanstore.OperationQueryParameters{ ServiceName: "service-a", SpanKind: "server", }).Return(test.operations, test.err) traceReader := &TraceReader{ spanReader: sr, } operations, err := traceReader.GetOperations( context.Background(), tracestore.OperationQueryParams{ ServiceName: "service-a", SpanKind: "server", }) require.ErrorIs(t, err, test.err) require.Equal(t, test.expectedOperations, operations) }) } } func TestTraceReader_FindTracesDelegatesSuccessResponse(t *testing.T) { modelTraces := []*model.Trace{ { Spans: []*model.Span{ { TraceID: model.NewTraceID(2, 3), SpanID: model.SpanID(1), OperationName: "operation-a", }, { TraceID: model.NewTraceID(4, 5), SpanID: model.SpanID(2), OperationName: "operation-b", }, }, }, { Spans: []*model.Span{ { TraceID: model.NewTraceID(6, 7), SpanID: model.SpanID(3), OperationName: "operation-c", }, }, }, } sr := new(spanstoremocks.Reader) now := time.Now() sr.On( "FindTraces", mock.Anything, &spanstore.TraceQueryParameters{ ServiceName: "service", OperationName: "operation", Tags: map[string]string{"tag-a": "val-a"}, StartTimeMin: now, StartTimeMax: now.Add(time.Minute), DurationMin: time.Minute, DurationMax: time.Hour, NumTraces: 10, }, ).Return(modelTraces, nil) traceReader := &TraceReader{ spanReader: sr, } attributes := pcommon.NewMap() attributes.PutStr("tag-a", "val-a") traces, err := jiter.FlattenWithErrors(traceReader.FindTraces( context.Background(), tracestore.TraceQueryParams{ ServiceName: "service", OperationName: "operation", Attributes: attributes, StartTimeMin: now, StartTimeMax: now.Add(time.Minute), DurationMin: time.Minute, DurationMax: time.Hour, SearchDepth: 10, }, )) require.NoError(t, err) require.Len(t, traces, len(modelTraces)) traceASpans := traces[0].ResourceSpans().At(0).ScopeSpans().At(0).Spans() traceBSpans := traces[1].ResourceSpans().At(0).ScopeSpans().At(0).Spans() require.EqualValues(t, []byte{0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3}, traceASpans.At(0).TraceID()) require.EqualValues(t, []byte{0, 0, 0, 0, 0, 0, 0, 1}, traceASpans.At(0).SpanID()) require.Equal(t, "operation-a", traceASpans.At(0).Name()) require.EqualValues(t, []byte{0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 5}, traceASpans.At(1).TraceID()) require.EqualValues(t, []byte{0, 0, 0, 0, 0, 0, 0, 2}, traceASpans.At(1).SpanID()) require.Equal(t, "operation-b", traceASpans.At(1).Name()) require.EqualValues(t, []byte{0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 7}, traceBSpans.At(0).TraceID()) require.EqualValues(t, []byte{0, 0, 0, 0, 0, 0, 0, 3}, traceBSpans.At(0).SpanID()) require.Equal(t, "operation-c", traceBSpans.At(0).Name()) } func TestTraceReader_FindTracesEdgeCases(t *testing.T) { tests := []struct { name string modelTraces []*model.Trace expectedTraces []ptrace.Traces err error }{ { name: "nil response", modelTraces: nil, expectedTraces: nil, }, { name: "empty response", modelTraces: []*model.Trace{}, expectedTraces: nil, }, { name: "error response", modelTraces: nil, expectedTraces: nil, err: errors.New("test error"), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { sr := new(spanstoremocks.Reader) sr.On( "FindTraces", mock.Anything, mock.Anything, ).Return(test.modelTraces, test.err) traceReader := &TraceReader{ spanReader: sr, } traces, err := jiter.FlattenWithErrors(traceReader.FindTraces( context.Background(), tracestore.TraceQueryParams{ Attributes: pcommon.NewMap(), }, )) require.ErrorIs(t, err, test.err) require.Equal(t, test.expectedTraces, traces) }) } } func TestTraceReader_FindTracesEarlyStop(t *testing.T) { sr := new(spanstoremocks.Reader) sr.On( "FindTraces", mock.Anything, mock.Anything, ).Return([]*model.Trace{{}, {}, {}}, nil).Twice() traceReader := &TraceReader{ spanReader: sr, } called := 0 traceReader.FindTraces( context.Background(), tracestore.TraceQueryParams{ Attributes: pcommon.NewMap(), }, )(func(tr []ptrace.Traces, err error) bool { require.NoError(t, err) require.Len(t, tr, 1) called++ return true }) assert.Equal(t, 3, called) called = 0 traceReader.FindTraces( context.Background(), tracestore.TraceQueryParams{ Attributes: pcommon.NewMap(), }, )(func(tr []ptrace.Traces, err error) bool { require.NoError(t, err) require.Len(t, tr, 1) called++ return false // early return }) assert.Equal(t, 1, called) } func TestTraceReader_FindTraceIDsDelegatesResponse(t *testing.T) { tests := []struct { name string modelTraceIDs []model.TraceID expectedTraceIDs []tracestore.FoundTraceID err error }{ { name: "successful response", modelTraceIDs: []model.TraceID{ {Low: 3, High: 2}, {Low: 4, High: 3}, }, expectedTraceIDs: []tracestore.FoundTraceID{ { TraceID: pcommon.TraceID([]byte{0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3}), }, { TraceID: pcommon.TraceID([]byte{0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 4}), }, }, }, { name: "empty response", modelTraceIDs: []model.TraceID{}, expectedTraceIDs: nil, }, { name: "nil response", modelTraceIDs: nil, expectedTraceIDs: nil, }, { name: "error response", modelTraceIDs: nil, expectedTraceIDs: nil, err: errors.New("test error"), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { sr := new(spanstoremocks.Reader) now := time.Now() sr.On( "FindTraceIDs", mock.Anything, &spanstore.TraceQueryParameters{ ServiceName: "service", OperationName: "operation", Tags: map[string]string{"tag-a": "val-a"}, StartTimeMin: now, StartTimeMax: now.Add(time.Minute), DurationMin: time.Minute, DurationMax: time.Hour, NumTraces: 10, }, ).Return(test.modelTraceIDs, test.err) traceReader := &TraceReader{ spanReader: sr, } attributes := pcommon.NewMap() attributes.PutStr("tag-a", "val-a") traceIDs, err := jiter.FlattenWithErrors(traceReader.FindTraceIDs( context.Background(), tracestore.TraceQueryParams{ ServiceName: "service", OperationName: "operation", Attributes: attributes, StartTimeMin: now, StartTimeMax: now.Add(time.Minute), DurationMin: time.Minute, DurationMax: time.Hour, SearchDepth: 10, }, )) require.ErrorIs(t, err, test.err) require.Equal(t, test.expectedTraceIDs, traceIDs) }) } } ================================================ FILE: internal/storage/v2/v1adapter/tracewriter.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package v1adapter import ( "context" "errors" "time" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/storage/v1/api/dependencystore" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" ) type TraceWriter struct { spanWriter spanstore.Writer } func GetV1Writer(writer tracestore.Writer) spanstore.Writer { if tr, ok := writer.(*TraceWriter); ok { return tr.spanWriter } return &SpanWriter{ traceWriter: writer, } } func NewTraceWriter(spanWriter spanstore.Writer) *TraceWriter { return &TraceWriter{ spanWriter: spanWriter, } } // WriteTraces implements tracestore.Writer. func (t *TraceWriter) WriteTraces(ctx context.Context, td ptrace.Traces) error { batches := V1BatchesFromTraces(td) var errs []error for _, batch := range batches { for _, span := range batch.Spans { if span.Process == nil { span.Process = batch.Process } err := t.spanWriter.WriteSpan(ctx, span) if err != nil { errs = append(errs, err) } } } return errors.Join(errs...) } type DependencyWriter struct { writer dependencystore.Writer } func NewDependencyWriter(writer dependencystore.Writer) *DependencyWriter { return &DependencyWriter{ writer: writer, } } func (dw *DependencyWriter) WriteDependencies(ts time.Time, dependencies []model.DependencyLink) error { return dw.writer.WriteDependencies(ts, dependencies) } ================================================ FILE: internal/storage/v2/v1adapter/tracewriter_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package v1adapter import ( "context" "errors" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "go.uber.org/zap" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore" spanstoremocks "github.com/jaegertracing/jaeger/internal/storage/v1/api/spanstore/mocks" "github.com/jaegertracing/jaeger/internal/storage/v1/badger" tracestoremocks "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore/mocks" ) func TestWriteTraces(t *testing.T) { f := badger.NewFactory() err := f.Initialize(metrics.NullFactory, zap.NewNop()) require.NoError(t, err) defer func() { require.NoError(t, f.Close()) }() spanWriter, err := f.CreateSpanWriter() require.NoError(t, err) spanReader, err := f.CreateSpanReader() require.NoError(t, err) traceWriter := &TraceWriter{ spanWriter: spanWriter, } td := makeTraces() err = traceWriter.WriteTraces(context.Background(), td) require.NoError(t, err) tdID := td.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0).TraceID() traceID, err := model.TraceIDFromBytes(tdID[:]) require.NoError(t, err) query := spanstore.GetTraceParameters{TraceID: traceID} trace, err := spanReader.GetTrace(context.Background(), query) require.NoError(t, err) require.NotNil(t, trace) assert.Len(t, trace.Spans, 1) } func TestWriteTracesError(t *testing.T) { mockstore := spanstoremocks.NewWriter(t) mockstore.On( "WriteSpan", mock.AnythingOfType("context.backgroundCtx"), mock.AnythingOfType("*model.Span"), ).Return(errors.New("mocked error")) traceWriter := &TraceWriter{ spanWriter: mockstore, } err := traceWriter.WriteTraces(context.Background(), makeTraces()) require.ErrorContains(t, err, "mocked error") } func TestGetV1Writer(t *testing.T) { t.Run("wrapped v1 writer", func(t *testing.T) { writer := new(spanstoremocks.Writer) traceWriter := &TraceWriter{ spanWriter: writer, } v1Writer := GetV1Writer(traceWriter) require.Equal(t, writer, v1Writer) }) t.Run("native v2 writer", func(t *testing.T) { writer := new(tracestoremocks.Writer) v1Writer := GetV1Writer(writer) require.IsType(t, &SpanWriter{}, v1Writer) require.Equal(t, writer, v1Writer.(*SpanWriter).traceWriter) }) } func makeTraces() ptrace.Traces { traces := ptrace.NewTraces() rSpans := traces.ResourceSpans().AppendEmpty() sSpans := rSpans.ScopeSpans().AppendEmpty() span := sSpans.Spans().AppendEmpty() spanID := pcommon.NewSpanIDEmpty() spanID[5] = 5 // 0000000000050000 span.SetSpanID(spanID) traceID := pcommon.NewTraceIDEmpty() traceID[15] = 1 // 00000000000000000000000000000001 span.SetTraceID(traceID) return traces } ================================================ FILE: internal/storage/v2/v1adapter/translator.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package v1adapter import ( "iter" jaegertranslator "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/jptrace" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" ) // V1BatchesFromTraces converts OpenTelemetry traces (ptrace.Traces) // to Jaeger model batches ([]*model.Batch). func V1BatchesFromTraces(traces ptrace.Traces) []*model.Batch { batches := jaegertranslator.ProtoFromTraces(traces) spanMap := createSpanMapFromBatches(batches) transferWarningsToModelSpans(traces, spanMap) return batches } // V1BatchesToTraces converts Jaeger model batches ([]*model.Batch) // to OpenTelemetry traces (ptrace.Traces). func V1BatchesToTraces(batches []*model.Batch) ptrace.Traces { traces, _ := jaegertranslator.ProtoToTraces(batches) // never returns an error spanMap := jptrace.SpanMap(traces, func(s ptrace.Span) pcommon.SpanID { return s.SpanID() }) transferWarningsToOTLPSpans(batches, spanMap) return traces } // V1TracesFromSeq2 converts an interator of ptrace.Traces chunks into v1 traces. func V1TracesFromSeq2(otelSeq iter.Seq2[[]ptrace.Traces, error]) ([]*model.Trace, error) { var ( jaegerTraces []*model.Trace iterErr error ) jptrace.AggregateTraces(otelSeq)(func(otelTrace ptrace.Traces, err error) bool { if err != nil { iterErr = err return false } jaegerTraces = append(jaegerTraces, modelTraceFromOtelTrace(otelTrace)) return true }) if iterErr != nil { return nil, iterErr } return jaegerTraces, nil } func V1TraceIDsFromSeq2(traceIDsIter iter.Seq2[[]tracestore.FoundTraceID, error]) ([]model.TraceID, error) { var ( iterErr error modelTraceIDs []model.TraceID ) traceIDsIter(func(traceIDs []tracestore.FoundTraceID, err error) bool { if err != nil { iterErr = err return false } for _, traceID := range traceIDs { modelTraceIDs = append(modelTraceIDs, ToV1TraceID(traceID.TraceID)) } return true }) if iterErr != nil { return nil, iterErr } return modelTraceIDs, nil } // V1TraceToOtelTrace converts v1 traces (*model.Trace) to Otel traces (ptrace.Traces) func V1TraceToOtelTrace(jTrace *model.Trace) ptrace.Traces { batches := createBatchesFromModelTrace(jTrace) return V1BatchesToTraces(batches) } func createBatchesFromModelTrace(jTrace *model.Trace) []*model.Batch { spans := jTrace.Spans if len(spans) == 0 { return nil } batch := &model.Batch{ Spans: jTrace.Spans, } return []*model.Batch{batch} } // modelTraceFromOtelTrace extracts spans from otel traces func modelTraceFromOtelTrace(otelTrace ptrace.Traces) *model.Trace { var spans []*model.Span batches := V1BatchesFromTraces(otelTrace) for _, batch := range batches { for _, span := range batch.Spans { if span.Process == nil { proc := *batch.Process // shallow clone span.Process = &proc } spans = append(spans, span) if span.Process.Tags == nil { span.Process.Tags = []model.KeyValue{} } if span.References == nil { span.References = []model.SpanRef{} } if span.Tags == nil { span.Tags = []model.KeyValue{} } } } return &model.Trace{Spans: spans} } func createSpanMapFromBatches(batches []*model.Batch) map[model.SpanID]*model.Span { spanMap := make(map[model.SpanID]*model.Span) for _, batch := range batches { for _, span := range batch.Spans { spanMap[span.SpanID] = span } } return spanMap } func transferWarningsToModelSpans(traces ptrace.Traces, spanMap map[model.SpanID]*model.Span) { resources := traces.ResourceSpans() for i := 0; i < resources.Len(); i++ { scopes := resources.At(i).ScopeSpans() for j := 0; j < scopes.Len(); j++ { spans := scopes.At(j).Spans() for k := 0; k < spans.Len(); k++ { otelSpan := spans.At(k) warnings := jptrace.GetWarnings(otelSpan) if len(warnings) == 0 { continue } if span, ok := spanMap[ToV1SpanID(otelSpan.SpanID())]; ok { span.Warnings = append(span.Warnings, warnings...) // filter out the warning tag span.Tags = filterTags(span.Tags, jptrace.WarningsAttribute) } } } } } func transferWarningsToOTLPSpans(batches []*model.Batch, spanMap map[pcommon.SpanID]ptrace.Span) { for _, batch := range batches { for _, span := range batch.Spans { if len(span.Warnings) == 0 { continue } if otelSpan, ok := spanMap[FromV1SpanID(span.SpanID)]; ok { jptrace.AddWarnings(otelSpan, span.Warnings...) } } } } func filterTags(tags []model.KeyValue, keyToRemove string) []model.KeyValue { var filteredTags []model.KeyValue for _, tag := range tags { if tag.Key != keyToRemove { filteredTags = append(filteredTags, tag) } } return filteredTags } ================================================ FILE: internal/storage/v2/v1adapter/translator_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package v1adapter import ( "errors" "iter" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/jptrace" "github.com/jaegertracing/jaeger/internal/storage/v2/api/tracestore" ) func TestV1BatchesFromTraces_AddsWarnings(t *testing.T) { traces := ptrace.NewTraces() rs1 := traces.ResourceSpans().AppendEmpty() ss1 := rs1.ScopeSpans().AppendEmpty() span1 := ss1.Spans().AppendEmpty() span1.SetName("test-span-1") span1.SetSpanID(pcommon.SpanID([8]byte{1, 2, 3, 4, 5, 6, 7, 8})) jptrace.AddWarnings(span1, "test-warning-1") jptrace.AddWarnings(span1, "test-warning-2") span1.Attributes().PutStr("key", "value") ss2 := rs1.ScopeSpans().AppendEmpty() span2 := ss2.Spans().AppendEmpty() span2.SetName("test-span-2") span2.SetSpanID(pcommon.SpanID([8]byte{9, 10, 11, 12, 13, 14, 15, 16})) rs2 := traces.ResourceSpans().AppendEmpty() ss3 := rs2.ScopeSpans().AppendEmpty() span3 := ss3.Spans().AppendEmpty() span3.SetName("test-span-3") span3.SetSpanID(pcommon.SpanID([8]byte{17, 18, 19, 20, 21, 22, 23, 24})) jptrace.AddWarnings(span3, "test-warning-3") batches := V1BatchesFromTraces(traces) assert.Len(t, batches, 2) assert.Len(t, batches[0].Spans, 2) assert.Equal(t, "test-span-1", batches[0].Spans[0].OperationName) assert.Equal(t, []string{"test-warning-1", "test-warning-2"}, batches[0].Spans[0].Warnings) assert.Equal(t, []model.KeyValue{{Key: "key", VStr: "value"}}, batches[0].Spans[0].Tags) assert.Equal(t, "test-span-2", batches[0].Spans[1].OperationName) assert.Empty(t, batches[0].Spans[1].Warnings) assert.Empty(t, batches[0].Spans[1].Tags) assert.Len(t, batches[1].Spans, 1) assert.Equal(t, "test-span-3", batches[1].Spans[0].OperationName) assert.Equal(t, []string{"test-warning-3"}, batches[1].Spans[0].Warnings) assert.Empty(t, batches[1].Spans[0].Tags) } func TestProtoToTraces_AddsWarnings(t *testing.T) { batch1 := &model.Batch{ Process: &model.Process{ ServiceName: "batch-1", }, Spans: []*model.Span{ { OperationName: "test-span-1", SpanID: model.NewSpanID(1), Warnings: []string{"test-warning-1", "test-warning-2"}, }, { OperationName: "test-span-2", SpanID: model.NewSpanID(2), }, }, } batch2 := &model.Batch{ Process: &model.Process{ ServiceName: "batch-2", }, Spans: []*model.Span{ { OperationName: "test-span-3", SpanID: model.NewSpanID(3), Warnings: []string{"test-warning-3"}, }, }, } batches := []*model.Batch{batch1, batch2} traces := V1BatchesToTraces(batches) assert.Equal(t, 2, traces.ResourceSpans().Len()) spanMap := jptrace.SpanMap(traces, func(s ptrace.Span) string { return s.Name() }) span1 := spanMap["test-span-1"] assert.Equal(t, pcommon.SpanID([8]byte{0, 0, 0, 0, 0, 0, 0, 1}), span1.SpanID()) assert.Equal(t, []string{"test-warning-1", "test-warning-2"}, jptrace.GetWarnings(span1)) span2 := spanMap["test-span-2"] assert.Equal(t, pcommon.SpanID([8]byte{0, 0, 0, 0, 0, 0, 0, 2}), span2.SpanID()) assert.Empty(t, jptrace.GetWarnings(span2)) span3 := spanMap["test-span-3"] assert.Equal(t, pcommon.SpanID([8]byte{0, 0, 0, 0, 0, 0, 0, 3}), span3.SpanID()) assert.Equal(t, []string{"test-warning-3"}, jptrace.GetWarnings(span3)) } func TestV1TracesFromSeq2(t *testing.T) { var ( processNoServiceName = "OTLPResourceNoServiceName" startTime = time.Unix(0, 0) // 1970-01-01T00:00:00Z, matches the default for otel span's start time ) testCases := []struct { name string expectedModelTraces []*model.Trace seqTrace iter.Seq2[[]ptrace.Traces, error] expectedErr error }{ { name: "sequence with one trace", expectedModelTraces: []*model.Trace{ { Spans: []*model.Span{ { TraceID: model.NewTraceID(2, 3), SpanID: model.NewSpanID(1), OperationName: "op-success-a", Process: model.NewProcess(processNoServiceName, make([]model.KeyValue, 0)), StartTime: startTime, }, }, }, }, seqTrace: func(yield func([]ptrace.Traces, error) bool) { testTrace := ptrace.NewTraces() rSpans := testTrace.ResourceSpans().AppendEmpty() sSpans := rSpans.ScopeSpans().AppendEmpty() spans := sSpans.Spans() // Add a new span and set attributes modelTraceID := model.NewTraceID(2, 3) span1 := spans.AppendEmpty() span1.SetTraceID(FromV1TraceID(modelTraceID)) span1.SetName("op-success-a") span1.SetSpanID(FromV1SpanID(model.NewSpanID(1))) // Yield the test trace yield([]ptrace.Traces{testTrace}, nil) }, expectedErr: nil, }, { name: "sequence with two chunks of a trace", expectedModelTraces: []*model.Trace{ { Spans: []*model.Span{ { TraceID: model.NewTraceID(2, 3), SpanID: model.NewSpanID(1), OperationName: "op-two-chunks-a", Process: model.NewProcess(processNoServiceName, make([]model.KeyValue, 0)), StartTime: startTime, }, { TraceID: model.NewTraceID(2, 3), SpanID: model.NewSpanID(2), OperationName: "op-two-chunks-b", Process: model.NewProcess(processNoServiceName, make([]model.KeyValue, 0)), StartTime: startTime, }, }, }, }, seqTrace: func(yield func([]ptrace.Traces, error) bool) { traceChunk1 := ptrace.NewTraces() rSpans1 := traceChunk1.ResourceSpans().AppendEmpty() sSpans1 := rSpans1.ScopeSpans().AppendEmpty() spans1 := sSpans1.Spans() modelTraceID := model.NewTraceID(2, 3) span1 := spans1.AppendEmpty() span1.SetTraceID(FromV1TraceID(modelTraceID)) span1.SetName("op-two-chunks-a") span1.SetSpanID(FromV1SpanID(model.NewSpanID(1))) traceChunk2 := ptrace.NewTraces() rSpans2 := traceChunk2.ResourceSpans().AppendEmpty() sSpans2 := rSpans2.ScopeSpans().AppendEmpty() spans2 := sSpans2.Spans() span2 := spans2.AppendEmpty() span2.SetTraceID(FromV1TraceID(modelTraceID)) span2.SetName("op-two-chunks-b") span2.SetSpanID(FromV1SpanID(model.NewSpanID(2))) // Yield the test trace yield([]ptrace.Traces{traceChunk1, traceChunk2}, nil) }, expectedErr: nil, }, { // a case that occurs when no trace is contained in the iterator name: "empty sequence", expectedModelTraces: nil, seqTrace: func(_ func([]ptrace.Traces, error) bool) {}, expectedErr: nil, }, { name: "sequence containing error", expectedModelTraces: nil, seqTrace: func(yield func([]ptrace.Traces, error) bool) { testTrace := ptrace.NewTraces() rSpans := testTrace.ResourceSpans().AppendEmpty() sSpans := rSpans.ScopeSpans().AppendEmpty() spans := sSpans.Spans() modelTraceID := model.NewTraceID(2, 3) span1 := spans.AppendEmpty() span1.SetTraceID(FromV1TraceID(modelTraceID)) span1.SetName("op-error-a") span1.SetSpanID(FromV1SpanID(model.NewSpanID(1))) // Yield the test trace if !yield([]ptrace.Traces{testTrace}, nil) { return } yield(nil, errors.New("unexpected-op-err")) }, expectedErr: errors.New("unexpected-op-err"), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { actualTraces, err := V1TracesFromSeq2(tc.seqTrace) require.Equal(t, tc.expectedErr, err) require.Len(t, actualTraces, len(tc.expectedModelTraces)) if len(tc.expectedModelTraces) < 1 { return } for i, etrace := range tc.expectedModelTraces { eSpans := etrace.Spans aSpans := actualTraces[i].Spans require.Len(t, aSpans, len(eSpans)) for j, espan := range eSpans { assert.Equal(t, espan.TraceID, aSpans[j].TraceID) assert.Equal(t, espan.OperationName, aSpans[j].OperationName) assert.Equal(t, espan.Process, aSpans[j].Process) } } }) } } func TestV1TraceToOtelTrace_ReturnsExptectedOtelTrace(t *testing.T) { jTrace := &model.Trace{ Spans: []*model.Span{ { TraceID: model.NewTraceID(2, 3), SpanID: model.NewSpanID(1), Process: model.NewProcess("Service1", nil), OperationName: "two-resources-1", }, { TraceID: model.NewTraceID(2, 3), SpanID: model.NewSpanID(2), Process: model.NewProcess("service2", nil), OperationName: "two-resources-2", }, }, } actualTrace := V1TraceToOtelTrace(jTrace) require.NotEmpty(t, actualTrace) require.Equal(t, 2, actualTrace.ResourceSpans().Len()) } func TestV1TraceToOtelTrace_ReturnEmptyOtelTrace(t *testing.T) { jTrace := &model.Trace{} eTrace := ptrace.NewTraces() aTrace := V1TraceToOtelTrace(jTrace) require.Equal(t, eTrace.SpanCount(), aTrace.SpanCount(), 0) } func TestV1TraceIDsFromSeq2(t *testing.T) { testCases := []struct { name string seqTraceIDs iter.Seq2[[]tracestore.FoundTraceID, error] expectedIDs []model.TraceID expectedError error }{ { name: "empty sequence", seqTraceIDs: func(func([]tracestore.FoundTraceID, error) bool) {}, expectedIDs: nil, expectedError: nil, }, { name: "sequence with error", seqTraceIDs: func(yield func([]tracestore.FoundTraceID, error) bool) { yield(nil, assert.AnError) }, expectedIDs: nil, expectedError: assert.AnError, }, { name: "sequence with one chunk of trace IDs", seqTraceIDs: func(yield func([]tracestore.FoundTraceID, error) bool) { yield([]tracestore.FoundTraceID{ { TraceID: pcommon.TraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3}), }, { TraceID: pcommon.TraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 5}), }, }, nil) }, expectedIDs: []model.TraceID{ model.NewTraceID(2, 3), model.NewTraceID(4, 5), }, expectedError: nil, }, { name: "sequence with multiple chunks of trace IDs", seqTraceIDs: func(yield func([]tracestore.FoundTraceID, error) bool) { traceID1 := pcommon.TraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3}) traceID2 := pcommon.TraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 5}) traceID3 := pcommon.TraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 7}) yield([]tracestore.FoundTraceID{{TraceID: traceID1}}, nil) yield([]tracestore.FoundTraceID{{TraceID: traceID2}, {TraceID: traceID3}}, nil) }, expectedIDs: []model.TraceID{ model.NewTraceID(2, 3), model.NewTraceID(4, 5), model.NewTraceID(6, 7), }, expectedError: nil, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { actualIDs, err := V1TraceIDsFromSeq2(tc.seqTraceIDs) require.Equal(t, tc.expectedError, err) require.Equal(t, tc.expectedIDs, actualIDs) }) } } ================================================ FILE: internal/telemetry/otelsemconv/empty_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package otelsemconv import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/telemetry/otelsemconv/semconv.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package otelsemconv import ( "go.opentelemetry.io/otel/attribute" semconv "go.opentelemetry.io/otel/semconv/v1.40.0" ) // We do not use a lot of semconv constants, and its annoying to keep // the semver of the imports the same. This package serves as a // one stop shop replacement / alias. const ( SchemaURL = semconv.SchemaURL // Telemetry SDK TelemetrySDKLanguageKey = string(semconv.TelemetrySDKLanguageKey) TelemetrySDKNameKey = string(semconv.TelemetrySDKNameKey) TelemetrySDKVersionKey = string(semconv.TelemetrySDKVersionKey) TelemetryDistroNameKey = string(semconv.TelemetryDistroNameKey) TelemetryDistroVersionKey = string(semconv.TelemetryDistroVersionKey) // Service ServiceNameKey = string(semconv.ServiceNameKey) // Database DBQueryTextKey = string(semconv.DBQueryTextKey) DBSystemKey = "db.system" // Network PeerServiceKey = string(semconv.ServicePeerNameKey) // HTTP HTTPResponseStatusCodeKey = string(semconv.HTTPResponseStatusCodeKey) // Host HostIDKey = string(semconv.HostIDKey) HostIPKey = string(semconv.HostIPKey) HostNameKey = string(semconv.HostNameKey) // Status OtelStatusCode = "otel.status_code" OtelStatusDescription = "otel.status_description" // OpenTracing AttributeOpentracingRefType = "opentracing.ref_type" AttributeOpentracingRefTypeChildOf = "child_of" AttributeOpentracingRefTypeFollowsFrom = "follows_from" // OTel Scope AttributeOtelScopeName = "otel.scope.name" AttributeOtelScopeVersion = "otel.scope.version" ) // Helper functions for creating typed attributes for the OpenTelemetry SDK. // ServiceName creates a key-value pair for the service name attribute. func ServiceNameAttribute(value string) attribute.KeyValue { return semconv.ServiceNameKey.String(value) } // PeerService creates a key-value pair for the peer service attribute. func PeerServiceAttribute(value string) attribute.KeyValue { return semconv.ServicePeerNameKey.String(value) } // DBSystem creates a key-value pair for the DB system attribute. func DBSystemAttribute(value string) attribute.KeyValue { return semconv.DBSystemNameKey.String(value) } // HTTPStatusCode creates a key-value pair for the HTTP status code attribute. func HTTPStatusCodeAttribute(value int) attribute.KeyValue { return semconv.HTTPResponseStatusCodeKey.Int(value) } // This var provides the original semconv function variable for creating an int attribute. var HTTPResponseStatusCode = semconv.HTTPResponseStatusCode ================================================ FILE: internal/telemetry/otelsemconv/semconv_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package otelsemconv import ( "testing" "go.opentelemetry.io/otel/attribute" semconv "go.opentelemetry.io/otel/semconv/v1.40.0" ) func TestServiceNameAttribute(t *testing.T) { tests := []struct { name string value string expected attribute.KeyValue }{ { name: "valid service name", value: "my-service", expected: semconv.ServiceNameKey.String("my-service"), }, { name: "empty service name", value: "", expected: semconv.ServiceNameKey.String(""), }, { name: "service name with spaces", value: "my service name", expected: semconv.ServiceNameKey.String("my service name"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ServiceNameAttribute(tt.value) if result.Key != tt.expected.Key { t.Errorf("Expected key %v, got %v", tt.expected.Key, result.Key) } if result.Value != tt.expected.Value { t.Errorf("Expected value %v, got %v", tt.expected.Value, result.Value) } }) } } func TestPeerServiceAttribute(t *testing.T) { tests := []struct { name string value string expected attribute.KeyValue }{ { name: "valid peer service", value: "external-api", expected: semconv.ServicePeerNameKey.String("external-api"), }, { name: "empty peer service", value: "", expected: semconv.ServicePeerNameKey.String(""), }, { name: "peer service with special characters", value: "api-service_v1.2", expected: semconv.ServicePeerNameKey.String("api-service_v1.2"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := PeerServiceAttribute(tt.value) if result.Key != tt.expected.Key { t.Errorf("Expected key %v, got %v", tt.expected.Key, result.Key) } if result.Value != tt.expected.Value { t.Errorf("Expected value %v, got %v", tt.expected.Value, result.Value) } }) } } func TestDBSystemAttribute(t *testing.T) { tests := []struct { name string value string expected attribute.KeyValue }{ { name: "postgresql database", value: "postgresql", expected: semconv.DBSystemNameKey.String("postgresql"), }, { name: "mysql database", value: "mysql", expected: semconv.DBSystemNameKey.String("mysql"), }, { name: "empty database system", value: "", expected: semconv.DBSystemNameKey.String(""), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := DBSystemAttribute(tt.value) if result.Key != tt.expected.Key { t.Errorf("Expected key %v, got %v", tt.expected.Key, result.Key) } if result.Value != tt.expected.Value { t.Errorf("Expected value %v, got %v", tt.expected.Value, result.Value) } }) } } func TestHTTPStatusCodeAttribute(t *testing.T) { tests := []struct { name string value int expected attribute.KeyValue }{ { name: "success status code", value: 200, expected: semconv.HTTPResponseStatusCodeKey.Int(200), }, { name: "client error status code", value: 404, expected: semconv.HTTPResponseStatusCodeKey.Int(404), }, { name: "server error status code", value: 500, expected: semconv.HTTPResponseStatusCodeKey.Int(500), }, { name: "zero status code", value: 0, expected: semconv.HTTPResponseStatusCodeKey.Int(0), }, { name: "negative status code", value: -1, expected: semconv.HTTPResponseStatusCodeKey.Int(-1), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := HTTPStatusCodeAttribute(tt.value) if result.Key != tt.expected.Key { t.Errorf("Expected key %v, got %v", tt.expected.Key, result.Key) } if result.Value != tt.expected.Value { t.Errorf("Expected value %v, got %v", tt.expected.Value, result.Value) } }) } } func TestAttributeTypes(t *testing.T) { // Test that all helper functions return the correct attribute types serviceAttr := ServiceNameAttribute("test") if serviceAttr.Value.Type() != attribute.STRING { t.Errorf("ServiceNameAttribute should return STRING type, got %v", serviceAttr.Value.Type()) } peerAttr := PeerServiceAttribute("test") if peerAttr.Value.Type() != attribute.STRING { t.Errorf("PeerServiceAttribute should return STRING type, got %v", peerAttr.Value.Type()) } dbAttr := DBSystemAttribute("test") if dbAttr.Value.Type() != attribute.STRING { t.Errorf("DBSystemAttribute should return STRING type, got %v", dbAttr.Value.Type()) } httpAttr := HTTPStatusCodeAttribute(200) if httpAttr.Value.Type() != attribute.INT64 { t.Errorf("HTTPStatusCodeAttribute should return INT64 type, got %v", httpAttr.Value.Type()) } } ================================================ FILE: internal/telemetry/settings.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package telemetry import ( "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/component/componentstatus" "go.opentelemetry.io/collector/component/componenttest" "go.opentelemetry.io/otel/metric" noopmetric "go.opentelemetry.io/otel/metric/noop" "go.opentelemetry.io/otel/trace" nooptrace "go.opentelemetry.io/otel/trace/noop" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/metrics" "github.com/jaegertracing/jaeger/internal/metrics/otelmetrics" ) type Settings struct { Logger *zap.Logger Metrics metrics.Factory MeterProvider metric.MeterProvider TracerProvider trace.TracerProvider Host component.Host } // ReportStatus reports a component status event. // If Host is set, it delegates to componentstatus.ReportStatus. // Otherwise, it logs the status as an info message. func (s Settings) ReportStatus(event *componentstatus.Event) { if s.Host != nil { componentstatus.ReportStatus(s.Host, event) } else if s.Logger != nil { s.Logger.Info("status", zap.Stringer("status", event.Status())) } } func NoopSettings() Settings { return Settings{ Logger: zap.NewNop(), Metrics: metrics.NullFactory, MeterProvider: noopmetric.NewMeterProvider(), TracerProvider: nooptrace.NewTracerProvider(), Host: componenttest.NewNopHost(), } } func FromOtelComponent(telset component.TelemetrySettings, host component.Host) Settings { return Settings{ Logger: telset.Logger, Metrics: otelmetrics.NewFactory(telset.MeterProvider), MeterProvider: telset.MeterProvider, TracerProvider: telset.TracerProvider, Host: host, } } func (s Settings) ToOtelComponent() component.TelemetrySettings { return component.TelemetrySettings{ Logger: s.Logger, MeterProvider: s.MeterProvider, TracerProvider: s.TracerProvider, } } ================================================ FILE: internal/telemetry/settings_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package telemetry_test import ( "errors" "testing" "github.com/stretchr/testify/assert" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/component/componentstatus" "go.opentelemetry.io/collector/component/componenttest" noopmetric "go.opentelemetry.io/otel/metric/noop" nooptrace "go.opentelemetry.io/otel/trace/noop" "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/telemetry" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestNoopSettings(t *testing.T) { telset := telemetry.NoopSettings() assert.NotNil(t, telset.Logger) assert.NotNil(t, telset.Metrics) assert.NotNil(t, telset.MeterProvider) assert.NotNil(t, telset.TracerProvider) assert.NotNil(t, telset.Host) // ReportStatus is now a method, not a field - just verify it doesn't panic telset.ReportStatus(componentstatus.NewFatalErrorEvent(errors.New("foobar"))) } func TestFromOtelComponent(t *testing.T) { otelTelset := component.TelemetrySettings{ Logger: zap.NewNop(), MeterProvider: noopmetric.NewMeterProvider(), TracerProvider: nooptrace.NewTracerProvider(), } host := componenttest.NewNopHost() telset := telemetry.FromOtelComponent(otelTelset, host) assert.Equal(t, otelTelset.Logger, telset.Logger) assert.Equal(t, otelTelset.MeterProvider, telset.MeterProvider) assert.Equal(t, otelTelset.TracerProvider, telset.TracerProvider) assert.Equal(t, host, telset.Host) // ReportStatus is now a method - just verify it doesn't panic telset.ReportStatus(componentstatus.NewFatalErrorEvent(errors.New("foobar"))) } func TestReportStatus_NilHost(_ *testing.T) { telset := telemetry.Settings{ Logger: zap.NewNop(), } // Should not panic, just log telset.ReportStatus(componentstatus.NewEvent(componentstatus.StatusOK)) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/tenancy/context.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tenancy import "context" // tenantKeyType is a custom type for the key "tenant", following context.Context convention type tenantKeyType string const ( // tenantKey holds tenancy for spans tenantKey = tenantKeyType("tenant") ) // WithTenant creates a Context with a tenant association func WithTenant(ctx context.Context, tenant string) context.Context { return context.WithValue(ctx, tenantKey, tenant) } // GetTenant retrieves a tenant associated with a Context func GetTenant(ctx context.Context) string { tenant := ctx.Value(tenantKey) if tenant == nil { return "" } if s, ok := tenant.(string); ok { return s } return "" } ================================================ FILE: internal/tenancy/context_test.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tenancy import ( "context" "testing" "github.com/stretchr/testify/assert" ) type testContextKey string func TestContextTenantHandling(t *testing.T) { ctxWithTenant := WithTenant(context.Background(), "tenant1") assert.Equal(t, "tenant1", GetTenant(ctxWithTenant)) } func TestContextPreserved(t *testing.T) { key := testContextKey("expected-key") val := "expected-value" ctxWithValue := context.WithValue(context.Background(), key, val) ctxWithTenant := WithTenant(ctxWithValue, "tenant1") assert.Equal(t, "tenant1", GetTenant(ctxWithTenant)) assert.Equal(t, val, ctxWithTenant.Value(key)) } func TestNoTenant(t *testing.T) { // If no tenant in context, GetTenant should return the empty string assert.Empty(t, GetTenant(context.Background())) } func TestImpossibleTenantType(t *testing.T) { // If the tenant is not a string, GetTenant should return the empty string ctxWithIntTenant := context.WithValue(context.Background(), tenantKey, -1) assert.Empty(t, GetTenant(ctxWithIntTenant)) } ================================================ FILE: internal/tenancy/flags.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tenancy import ( "flag" "fmt" "strings" "github.com/spf13/viper" ) const ( flagPrefix = "multi-tenancy" flagTenancyEnabled = flagPrefix + ".enabled" flagTenancyHeader = flagPrefix + ".header" flagValidTenants = flagPrefix + ".tenants" ) // AddFlags adds flags for tenancy to the FlagSet. func AddFlags(flags *flag.FlagSet) { flags.Bool(flagTenancyEnabled, false, "Enable tenancy header when receiving or querying") flags.String(flagTenancyHeader, "x-tenant", "HTTP header carrying tenant") flags.String(flagValidTenants, "", fmt.Sprintf("comma-separated list of allowed values for --%s header. (If not supplied, tenants are not restricted)", flagTenancyHeader)) } // InitFromViper creates tenancy.Options populated with values retrieved from Viper. func InitFromViper(v *viper.Viper) Options { var p Options p.Enabled = v.GetBool(flagTenancyEnabled) p.Header = v.GetString(flagTenancyHeader) tenants := v.GetString(flagValidTenants) if tenants != "" { p.Tenants = strings.Split(tenants, ",") } else { p.Tenants = []string{} } return p } ================================================ FILE: internal/tenancy/flags_test.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tenancy import ( "flag" "testing" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestTenancyFlags(t *testing.T) { tests := []struct { name string cmd []string expected Options }{ { name: "one tenant", cmd: []string{ "--multi-tenancy.enabled=true", "--multi-tenancy.tenants=acme", }, expected: Options{ Enabled: true, Header: "x-tenant", Tenants: []string{"acme"}, }, }, { name: "two tenants", cmd: []string{ "--multi-tenancy.enabled=true", "--multi-tenancy.tenants=acme,country-store", }, expected: Options{ Enabled: true, Header: "x-tenant", Tenants: []string{"acme", "country-store"}, }, }, { name: "custom header", cmd: []string{ "--multi-tenancy.enabled=true", "--multi-tenancy.header=jaeger-tenant", "--multi-tenancy.tenants=acme", }, expected: Options{ Enabled: true, Header: "jaeger-tenant", Tenants: []string{"acme"}, }, }, { // Not supplying a list of tenants will mean // "tenant header required, but any value will pass" name: "no_tenants", cmd: []string{ "--multi-tenancy.enabled=true", }, expected: Options{ Enabled: true, Header: "x-tenant", Tenants: []string{}, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { v := viper.New() command := cobra.Command{} flagSet := &flag.FlagSet{} AddFlags(flagSet) command.PersistentFlags().AddGoFlagSet(flagSet) v.BindPFlags(command.PersistentFlags()) err := command.ParseFlags(test.cmd) require.NoError(t, err) tenancyCfg := InitFromViper(v) assert.Equal(t, test.expected, tenancyCfg) }) } } ================================================ FILE: internal/tenancy/grpc.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tenancy import ( "context" "go.opentelemetry.io/collector/client" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) // tenantedServerStream is a wrapper for ServerStream providing settable context type tenantedServerStream struct { grpc.ServerStream context context.Context } func (tss *tenantedServerStream) Context() context.Context { return tss.context } func GetValidTenant(ctx context.Context, tm *Manager) (string, error) { tenant, err := extractTenantFromSources(ctx, tm.Header) if err != nil { return "", err } if !tm.Valid(tenant) { return "", status.Errorf(codes.PermissionDenied, "unknown tenant") } return tenant, nil } // helper function to extract tenant from different sources func extractTenantFromSources(ctx context.Context, header string) (string, error) { if tenant := GetTenant(ctx); tenant != "" { return tenant, nil } if cli := client.FromContext(ctx); cli.Metadata.Get(header) != nil { if tenants := cli.Metadata.Get(header); len(tenants) > 0 { return extractSingleTenant(tenants) } } md, ok := metadata.FromIncomingContext(ctx) if !ok { return "", status.Errorf(codes.PermissionDenied, "missing tenant header") } return extractSingleTenant(md.Get(header)) } // Helper function for metadata extraction func tenantFromMetadata(md metadata.MD, header string) (string, error) { tenants := md.Get(header) return extractSingleTenant(tenants) } // Ensures single tenant value exists func extractSingleTenant(tenants []string) (string, error) { switch len(tenants) { case 0: return "", status.Errorf(codes.Unauthenticated, "missing tenant header") case 1: return tenants[0], nil default: return "", status.Errorf(codes.PermissionDenied, "extra tenant header") } } func directlyAttachedTenant(ctx context.Context) bool { return GetTenant(ctx) != "" } // NewGuardingStreamInterceptor blocks handling of streams whose tenancy header doesn't meet tenancy requirements. // It also ensures the tenant is directly in the context, rather than context metadata. func NewGuardingStreamInterceptor(tc *Manager) grpc.StreamServerInterceptor { return func(srv any, ss grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error { tenant, err := GetValidTenant(ss.Context(), tc) if err != nil { return err } if directlyAttachedTenant(ss.Context()) { return handler(srv, ss) } // "upgrade" the tenant to be part of the context, rather than just incoming metadata return handler(srv, &tenantedServerStream{ ServerStream: ss, context: WithTenant(ss.Context(), tenant), }) } } // NewGuardingUnaryInterceptor blocks handling of RPCs whose tenancy header doesn't meet tenancy requirements. // It also ensures the tenant is directly in the context, rather than context metadata. func NewGuardingUnaryInterceptor(tc *Manager) grpc.UnaryServerInterceptor { return func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { tenant, err := GetValidTenant(ctx, tc) if err != nil { return nil, err } if directlyAttachedTenant(ctx) { return handler(ctx, req) } return handler(WithTenant(ctx, tenant), req) } } // NewClientUnaryInterceptor injects tenant header into gRPC request metadata. func NewClientUnaryInterceptor(tc *Manager) grpc.UnaryClientInterceptor { return grpc.UnaryClientInterceptor(func( ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption, ) error { if tenant := GetTenant(ctx); tenant != "" { ctx = metadata.AppendToOutgoingContext(ctx, tc.Header, tenant) } return invoker(ctx, method, req, reply, cc, opts...) }) } // NewClientStreamInterceptor injects tenant header into gRPC request metadata. func NewClientStreamInterceptor(tc *Manager) grpc.StreamClientInterceptor { return grpc.StreamClientInterceptor(func( ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption, ) (grpc.ClientStream, error) { if tenant := GetTenant(ctx); tenant != "" { ctx = metadata.AppendToOutgoingContext(ctx, tc.Header, tenant) } return streamer(ctx, desc, cc, method, opts...) }) } ================================================ FILE: internal/tenancy/grpc_test.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tenancy import ( "context" "errors" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/client" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) func TestTenancyInterceptors(t *testing.T) { tests := []struct { name string tenancyMgr *Manager ctx context.Context errMsg string }{ { name: "missing tenant context", tenancyMgr: NewManager(&Options{Enabled: true}), ctx: context.Background(), errMsg: "rpc error: code = PermissionDenied desc = missing tenant header", }, { name: "invalid tenant context", tenancyMgr: NewManager(&Options{Enabled: true, Tenants: []string{"megacorp"}}), ctx: WithTenant(context.Background(), "acme"), errMsg: "rpc error: code = PermissionDenied desc = unknown tenant", }, { name: "valid tenant context", tenancyMgr: NewManager(&Options{Enabled: true, Tenants: []string{"acme"}}), ctx: WithTenant(context.Background(), "acme"), errMsg: "", }, { name: "invalid tenant header", tenancyMgr: NewManager(&Options{Enabled: true, Tenants: []string{"megacorp"}}), ctx: metadata.NewIncomingContext(context.Background(), map[string][]string{"x-tenant": {"acme"}}), errMsg: "rpc error: code = PermissionDenied desc = unknown tenant", }, { name: "missing tenant header", tenancyMgr: NewManager(&Options{Enabled: true, Tenants: []string{"megacorp"}}), ctx: metadata.NewIncomingContext(context.Background(), map[string][]string{}), errMsg: "rpc error: code = Unauthenticated desc = missing tenant header", }, { name: "valid tenant header", tenancyMgr: NewManager(&Options{Enabled: true, Tenants: []string{"acme"}}), ctx: metadata.NewIncomingContext(context.Background(), map[string][]string{"x-tenant": {"acme"}}), errMsg: "", }, { name: "extra tenant header", tenancyMgr: NewManager(&Options{Enabled: true, Tenants: []string{"acme"}}), ctx: metadata.NewIncomingContext(context.Background(), map[string][]string{"x-tenant": {"acme", "megacorp"}}), errMsg: "rpc error: code = PermissionDenied desc = extra tenant header", }, { name: "missing tenant context", tenancyMgr: NewManager(&Options{Enabled: true}), ctx: client.NewContext(context.Background(), client.Info{ Metadata: client.NewMetadata(map[string][]string{}), }), errMsg: "rpc error: code = PermissionDenied desc = missing tenant header", }, { name: "invalid tenant context", tenancyMgr: NewManager(&Options{Enabled: true, Tenants: []string{"megacorp"}}), ctx: client.NewContext(context.Background(), client.Info{ Metadata: client.NewMetadata(map[string][]string{"x-tenant": {"acme"}}), }), errMsg: "rpc error: code = PermissionDenied desc = unknown tenant", }, { name: "valid tenant context", tenancyMgr: NewManager(&Options{Enabled: true, Tenants: []string{"acme"}}), ctx: client.NewContext(context.Background(), client.Info{ Metadata: client.NewMetadata(map[string][]string{"x-tenant": {"acme"}}), }), errMsg: "", }, { name: "extra tenant context", tenancyMgr: NewManager(&Options{Enabled: true, Tenants: []string{"acme"}}), ctx: client.NewContext(context.Background(), client.Info{ Metadata: client.NewMetadata(map[string][]string{"x-tenant": {"acme", "megacorp"}}), }), errMsg: "rpc error: code = PermissionDenied desc = extra tenant header", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { interceptor := NewGuardingStreamInterceptor(test.tenancyMgr) ss := tenantedServerStream{ context: test.ctx, } ssi := grpc.StreamServerInfo{} handler := func(any, grpc.ServerStream) error { // do nothing return nil } err := interceptor(0, &ss, &ssi, handler) if test.errMsg == "" { require.NoError(t, err) } else { require.Error(t, err) assert.Equal(t, test.errMsg, err.Error()) } uinterceptor := NewGuardingUnaryInterceptor(test.tenancyMgr) usi := &grpc.UnaryServerInfo{} iface := 0 uhandler := func(_ context.Context, req any) (any, error) { // do nothing return req, nil } _, err = uinterceptor(test.ctx, iface, usi, uhandler) if test.errMsg == "" { require.NoError(t, err) } else { require.Error(t, err) assert.Equal(t, test.errMsg, err.Error()) } }) } } func TestClientUnaryInterceptor(t *testing.T) { tm := NewManager(&Options{Enabled: true, Tenants: []string{"acme"}}) interceptor := NewClientUnaryInterceptor(tm) var tenant string fakeErr := errors.New("foo") invoker := func(ctx context.Context, _ /* method */ string, _ /* req */, _ /* reply */ any, _ *grpc.ClientConn, _ ...grpc.CallOption) error { md, ok := metadata.FromOutgoingContext(ctx) assert.True(t, ok) ten, err := tenantFromMetadata(md, tm.Header) require.NoError(t, err) tenant = ten return fakeErr } ctx := WithTenant(context.Background(), "acme") err := interceptor(ctx, "method", "request", "response", nil, invoker) assert.Equal(t, "acme", tenant) assert.Same(t, fakeErr, err) } func TestClientStreamInterceptor(t *testing.T) { tm := NewManager(&Options{Enabled: true, Tenants: []string{"acme"}}) interceptor := NewClientStreamInterceptor(tm) var tenant string fakeErr := errors.New("foo") ctx := WithTenant(context.Background(), "acme") streamer := func(ctx context.Context, _ *grpc.StreamDesc, _ *grpc.ClientConn, _ /* method */ string, _ ...grpc.CallOption) (grpc.ClientStream, error) { md, ok := metadata.FromOutgoingContext(ctx) assert.True(t, ok) ten, err := tenantFromMetadata(md, tm.Header) require.NoError(t, err) tenant = ten return nil, fakeErr } stream, err := interceptor(ctx, &grpc.StreamDesc{}, nil, "", streamer) assert.Same(t, fakeErr, err) require.Nil(t, stream) assert.Equal(t, "acme", tenant) } ================================================ FILE: internal/tenancy/http.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tenancy import ( "context" "net/http" "google.golang.org/grpc/metadata" ) // PropagationHandler returns a http.Handler containing the logic to extract // the tenancy header of the http.Request and insert the tenant into request.Context // for propagation. The token can be accessed via tenancy.GetTenant(). func ExtractTenantHTTPHandler(tc *Manager, h http.Handler) http.Handler { if !tc.Enabled { return h } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tenant := r.Header.Get(tc.Header) if tenant == "" { w.WriteHeader(http.StatusUnauthorized) w.Write([]byte("missing tenant header")) return } if !tc.Valid(tenant) { w.WriteHeader(http.StatusUnauthorized) w.Write([]byte("unknown tenant")) return } ctx := WithTenant(r.Context(), tenant) h.ServeHTTP(w, r.WithContext(ctx)) }) } // MetadataAnnotator returns a function suitable for propagating tenancy // via github.com/grpc-ecosystem/grpc-gateway/runtime.NewServeMux func (tc *Manager) MetadataAnnotator() func(context.Context, *http.Request) metadata.MD { return func(_ context.Context, req *http.Request) metadata.MD { tenant := req.Header.Get(tc.Header) if tenant == "" { // The HTTP request lacked the tenancy header. Pass along // empty metadata -- the gRPC query service will reject later. return metadata.Pairs() } return metadata.New(map[string]string{ tc.Header: tenant, }) } } ================================================ FILE: internal/tenancy/http_test.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tenancy import ( "context" "net/http" "net/http/httptest" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type testHttpHandler struct { reached bool } func (thh *testHttpHandler) ServeHTTP(http.ResponseWriter, *http.Request) { thh.reached = true } func TestProgationHandler(t *testing.T) { tests := []struct { name string tenancyMgr *Manager shouldReach bool requestHeaders map[string][]string }{ { name: "untenanted", tenancyMgr: NewManager(&Options{}), requestHeaders: map[string][]string{}, shouldReach: true, }, { name: "missing tenant header", tenancyMgr: NewManager(&Options{Enabled: true}), requestHeaders: map[string][]string{}, shouldReach: false, }, { name: "valid tenant header", tenancyMgr: NewManager(&Options{Enabled: true}), requestHeaders: map[string][]string{"x-tenant": {"acme"}}, shouldReach: true, }, { name: "unauthorized tenant", tenancyMgr: NewManager(&Options{Enabled: true, Tenants: []string{"megacorp"}}), requestHeaders: map[string][]string{"x-tenant": {"acme"}}, shouldReach: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { handler := &testHttpHandler{} propH := ExtractTenantHTTPHandler(test.tenancyMgr, handler) req, err := http.NewRequest(http.MethodGet, "/", strings.NewReader("")) for k, vs := range test.requestHeaders { for _, v := range vs { req.Header.Add(k, v) } } require.NoError(t, err) writer := httptest.NewRecorder() propH.ServeHTTP(writer, req) assert.Equal(t, test.shouldReach, handler.reached) }) } } func TestMetadataAnnotator(t *testing.T) { tests := []struct { name string tenancyMgr *Manager requestHeaders map[string][]string }{ { name: "missing tenant", tenancyMgr: NewManager(&Options{Enabled: true}), requestHeaders: map[string][]string{}, }, { name: "tenanted", tenancyMgr: NewManager(&Options{Enabled: true}), requestHeaders: map[string][]string{"x-tenant": {"acme"}}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { req, err := http.NewRequest(http.MethodGet, "/", strings.NewReader("")) for k, vs := range test.requestHeaders { for _, v := range vs { req.Header.Add(k, v) } } require.NoError(t, err) annotator := test.tenancyMgr.MetadataAnnotator() md := annotator(context.Background(), req) assert.Len(t, md, len(test.requestHeaders)) }) } } ================================================ FILE: internal/tenancy/manage_test.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tenancy import ( "testing" "github.com/stretchr/testify/assert" ) func TestTenancyValidity(t *testing.T) { tests := []struct { name string options Options tenant string valid bool }{ { name: "valid single tenant", options: Options{ Enabled: true, Header: "x-tenant", Tenants: []string{"acme"}, }, tenant: "acme", valid: true, }, { name: "valid tenant in multi-tenant setup", options: Options{ Enabled: true, Header: "x-tenant", Tenants: []string{"acme", "country-store"}, }, tenant: "acme", valid: true, }, { name: "invalid tenant", options: Options{ Enabled: true, Header: "x-tenant", Tenants: []string{"acme", "country-store"}, }, tenant: "auto-repair", valid: false, }, { // Not supplying a list of tenants will mean // "tenant header required, but any value will pass" name: "any tenant", options: Options{ Enabled: true, Header: "x-tenant", Tenants: []string{}, }, tenant: "convenience-store", valid: true, }, { name: "ignore tenant", options: Options{ Enabled: false, Header: "", Tenants: []string{"acme"}, }, tenant: "country-store", // If tenancy not enabled, any tenant is valid valid: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { tc := NewManager(&test.options) assert.Equal(t, test.valid, tc.Valid(test.tenant)) }) } } ================================================ FILE: internal/tenancy/manager.go ================================================ // Copyright (c) 2022 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tenancy // Options describes the configuration properties for multitenancy type Options struct { Enabled bool Header string Tenants []string } // Manager can check tenant usage for multi-tenant Jaeger configurations type Manager struct { Enabled bool Header string guard guard } // Guard verifies a valid tenant when tenancy is enabled type guard interface { Valid(candidate string) bool } // NewManager creates a tenancy.Manager for given tenancy.Options. func NewManager(options *Options) *Manager { // Default header value (although set by CLI flags, this helps tests and API users) header := options.Header if header == "" && options.Enabled { header = "x-tenant" } return &Manager{ Enabled: options.Enabled, Header: header, guard: tenancyGuardFactory(options), } } func (tc *Manager) Valid(tenant string) bool { return tc.guard.Valid(tenant) } type tenantDontCare bool func (tenantDontCare) Valid(string /* candidate */) bool { return true } type tenantList struct { tenants map[string]bool } func (tl *tenantList) Valid(candidate string) bool { _, ok := tl.tenants[candidate] return ok } func newTenantList(tenants []string) *tenantList { tenantMap := make(map[string]bool) for _, tenant := range tenants { tenantMap[tenant] = true } return &tenantList{ tenants: tenantMap, } } func tenancyGuardFactory(options *Options) guard { // Three cases // - no tenancy // - tenancy, but no guarding by tenant // - tenancy, with guarding by a list if !options.Enabled || len(options.Tenants) == 0 { return tenantDontCare(true) } return newTenantList(options.Tenants) } ================================================ FILE: internal/tenancy/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tenancy import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/testutils/leakcheck.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package testutils import ( "testing" "go.uber.org/goleak" ) // IgnoreGlogFlushDaemonLeak returns a goleak.Option that ignores the flushDaemon function // from the glog package that can cause false positives in leak detection. // This is necessary because glog starts a goroutine in the background that may not // be stopped when the test finishes, leading to a detected but expected leak. func IgnoreGlogFlushDaemonLeak() goleak.Option { return goleak.IgnoreTopFunction("github.com/golang/glog.(*fileSink).flushDaemon") } // IgnoreOpenCensusWorkerLeak This prevent catching the leak generated by opencensus defaultWorker.Start which at the time is part of the package's init call. // See https://github.com/jaegertracing/jaeger/pull/5055#discussion_r1438702168 for more context. func IgnoreOpenCensusWorkerLeak() goleak.Option { return goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start") } // IgnoreGoMetricsMeterLeak prevents the leak created by go-metrics which is // used by Sarama (Kafka Client) in Jaeger v1. This reason of this leak is // not Jaeger but the go-metrics used by Samara. // See these issues for the context // - https://github.com/IBM/sarama/issues/1321 // - https://github.com/IBM/sarama/issues/1340 // - https://github.com/IBM/sarama/issues/2832 func IgnoreGoMetricsMeterLeak() goleak.Option { return goleak.IgnoreTopFunction("github.com/rcrowley/go-metrics.(*meterArbiter).tick") } // Don't use this in any other method other than leaks for ElasticSearch and OpenSearch // These leaks are from olivere client not from the jaeger // See this PR for context: https://github.com/jaegertracing/jaeger/pull/6339 func ignoreHttpTransportWriteLoopLeak() goleak.Option { return goleak.IgnoreTopFunction("net/http.(*persistConn).writeLoop") } // Don't use this in any other method other than leaks for ElasticSearch and OpenSearch // These leaks are from olivere client not from the jaeger // See this PR for context: https://github.com/jaegertracing/jaeger/pull/6339 func ignoreHttpTransportPollRuntimeLeak() goleak.Option { return goleak.IgnoreTopFunction("internal/poll.runtime_pollWait") } // Don't use this in any other method other than leaks for ElasticSearch and OpenSearch // These leaks are from olivere client not from the jaeger // See this PR for context: https://github.com/jaegertracing/jaeger/pull/6339 func ignoreHttpTransportReadLoopLeak() goleak.Option { return goleak.IgnoreTopFunction("net/http.(*persistConn).readLoop") } // VerifyGoLeaks verifies that unit tests do not leak any goroutines. // It should be called in TestMain. func VerifyGoLeaks(m *testing.M) { goleak.VerifyTestMain(m, IgnoreGlogFlushDaemonLeak(), IgnoreOpenCensusWorkerLeak(), IgnoreGoMetricsMeterLeak()) } // VerifyGoLeaksOnce verifies that a given unit test does not leak any goroutines. // Occasionally useful to troubleshoot specific tests that are flaky due to leaks, // since VerifyGoLeaks cannot distiguish which specific test caused the leak. // It should be called via defer or from Cleanup: // // defer testutils.VerifyGoLeaksOnce(t) func VerifyGoLeaksOnce(t *testing.T) { goleak.VerifyNone(t, IgnoreGlogFlushDaemonLeak(), IgnoreOpenCensusWorkerLeak(), IgnoreGoMetricsMeterLeak()) } // VerifyGoLeaksOnceForES is go leak check for ElasticSearch integration tests (v1) // This must not be used anywhere else other than integration package for v1 func VerifyGoLeaksOnceForES(t *testing.T) { goleak.VerifyNone(t, ignoreHttpTransportWriteLoopLeak(), ignoreHttpTransportPollRuntimeLeak(), ignoreHttpTransportReadLoopLeak()) } // VerifyGoLeaksForES is go leak check for integration package in ElasticSearch Environment // This must not be used anywhere else other than integration package in ES environment for v1 func VerifyGoLeaksForES(m *testing.M) { goleak.VerifyTestMain(m, ignoreHttpTransportWriteLoopLeak(), ignoreHttpTransportPollRuntimeLeak(), ignoreHttpTransportReadLoopLeak()) } ================================================ FILE: internal/testutils/leakcheck_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package testutils_test import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestVerifyGoLeaksOnce(t *testing.T) { testutils.VerifyGoLeaksOnce(t) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/testutils/logger.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package testutils import ( "encoding/json" "fmt" "strings" "sync" "testing" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest" ) // NewLogger creates a new zap.Logger backed by a zaptest.Buffer, which is also returned. func NewLogger() (*zap.Logger, *Buffer) { core, buf := newRecordingCore() logger := zap.New(core, zap.WithFatalHook(zapcore.WriteThenPanic)) return logger, buf } func newRecordingCore() (zapcore.Core, *Buffer) { encoder := zapcore.NewJSONEncoder(zapcore.EncoderConfig{ MessageKey: "msg", LevelKey: "level", EncodeLevel: zapcore.LowercaseLevelEncoder, EncodeTime: zapcore.ISO8601TimeEncoder, EncodeDuration: zapcore.StringDurationEncoder, }) buf := &Buffer{} return zapcore.NewCore(encoder, buf, zapcore.DebugLevel), buf } // NewEchoLogger is similar to NewLogger, but the logs are also echoed to t.Log. func NewEchoLogger(t *testing.T) (*zap.Logger, *Buffer) { core, buf := newRecordingCore() echo := zaptest.NewLogger(t).Core() logger := zap.New(zapcore.NewTee(core, echo)) return logger, buf } // Buffer wraps zaptest.Buffer and provides convenience method JSONLine(n) type Buffer struct { mu sync.RWMutex zaptest.Buffer } // JSONLine reads n-th line from the buffer and converts it to JSON. func (b *Buffer) JSONLine(n int) map[string]string { data := make(map[string]string) line := b.Lines()[n] if err := json.Unmarshal([]byte(line), &data); err != nil { return map[string]string{ "error": err.Error(), } } return data } // NB. the below functions overwrite the existing functions so that logger is threadsafe. // This is not that fragile given how if the API were to change underneath in zap, the overwritten // function will fail to compile. // Lines overwrites zaptest.Buffer.Lines() to make it thread safe func (b *Buffer) Lines() []string { b.mu.RLock() defer b.mu.RUnlock() return b.Buffer.Lines() } // Stripped overwrites zaptest.Buffer.Stripped() to make it thread safe func (b *Buffer) Stripped() string { b.mu.RLock() defer b.mu.RUnlock() return b.Buffer.Stripped() } // String overwrites zaptest.Buffer.String() to make it thread safe func (b *Buffer) String() string { b.mu.RLock() defer b.mu.RUnlock() return b.Buffer.String() } // Write overwrites zaptest.Buffer.bytes.Buffer.Write() to make it thread safe func (b *Buffer) Write(p []byte) (int, error) { b.mu.Lock() defer b.mu.Unlock() return b.Buffer.Write(p) } // LogMatcher is a helper func that returns true if the subStr appears more than 'occurrences' times in the logs. var LogMatcher = func(occurrences int, subStr string, logs []string) (bool, string) { errMsg := fmt.Sprintf("subStr '%s' does not occur %d time(s) in %v", subStr, occurrences, logs) if len(logs) < occurrences { return false, errMsg } var count int for _, log := range logs { if strings.Contains(log, subStr) { count++ } } if count >= occurrences { return true, "" } return false, errMsg } ================================================ FILE: internal/testutils/logger_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package testutils import ( "strconv" "sync" "testing" "github.com/stretchr/testify/assert" "go.uber.org/zap" ) func TestNewLogger(t *testing.T) { logger, log := NewLogger() logger.Warn("hello", zap.String("x", "y")) assert.JSONEq(t, `{"level":"warn","msg":"hello","x":"y"}`, log.Lines()[0]) assert.Equal(t, map[string]string{ "level": "warn", "msg": "hello", "x": "y", }, log.JSONLine(0)) } func TestNewEchoLogger(t *testing.T) { logger, _ := NewEchoLogger(t) logger.Warn("hello", zap.String("x", "y")) } func TestJSONLineError(t *testing.T) { log := &Buffer{} log.WriteString("bad-json\n") _, ok := log.JSONLine(0)["error"] assert.True(t, ok, "must have 'error' key") } // NB. Run with -race to ensure no race condition func TestRaceCondition(*testing.T) { logger, buffer := NewLogger() start := make(chan struct{}) finish := sync.WaitGroup{} finish.Add(2) go func() { <-start logger.Info("test") finish.Done() }() go func() { <-start buffer.Lines() buffer.Stripped() _ = buffer.String() finish.Done() }() close(start) finish.Wait() } func TestLogMatcher(t *testing.T) { tests := []struct { occurrences int subStr string logs []string expected bool errMsg string }{ {occurrences: 1, expected: false, errMsg: "subStr '' does not occur 1 time(s) in []"}, {occurrences: 1, subStr: "hi", logs: []string{"hi"}, expected: true}, {occurrences: 3, subStr: "hi", logs: []string{"hi", "hi"}, expected: false, errMsg: "subStr 'hi' does not occur 3 time(s) in [hi hi]"}, {occurrences: 3, subStr: "hi", logs: []string{"hi", "hi", "hi"}, expected: true}, {occurrences: 1, subStr: "hi", logs: []string{"bye", "bye"}, expected: false, errMsg: "subStr 'hi' does not occur 1 time(s) in [bye bye]"}, } for i, tt := range tests { test := tt t.Run(strconv.Itoa(i), func(t *testing.T) { match, errMsg := LogMatcher(test.occurrences, test.subStr, test.logs) assert.Equal(t, test.expected, match) assert.Equal(t, test.errMsg, errMsg) }) } } ================================================ FILE: internal/tools/empty.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 // Package tools is used to track versions of 3rd party tools used for building / testing CI. // See tools.go for imported tools and go.mod for the versions of those tools. package tools ================================================ FILE: internal/tools/go.mod ================================================ module github.com/jaegertracing/jaeger/internal/tools go 1.26.0 require ( github.com/golangci/golangci-lint/v2 v2.10.1 github.com/josephspurrier/goversioninfo v1.5.0 github.com/open-telemetry/opentelemetry-collector-contrib/cmd/schemagen v0.147.0 github.com/vektra/mockery/v3 v3.6.1 github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad golang.org/x/vuln v1.1.4 mvdan.cc/gofumpt v0.9.2 ) require ( 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect 4d63.com/gochecknoglobals v0.2.2 // indirect codeberg.org/chavacava/garif v0.2.0 // indirect codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect dev.gaijin.team/go/golib v0.6.0 // indirect github.com/4meepo/tagalign v1.4.3 // indirect github.com/Abirdcfly/dupword v0.1.7 // indirect github.com/AdminBenni/iota-mixing v1.0.0 // indirect github.com/AlwxSin/noinlineerr v1.0.5 // indirect github.com/Antonboom/errname v1.1.1 // indirect github.com/Antonboom/nilnil v1.1.1 // indirect github.com/Antonboom/testifylint v1.6.4 // indirect github.com/BurntSushi/toml v1.6.0 // indirect github.com/Djarvur/go-err113 v0.1.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/MirrexOne/unqueryvet v1.5.3 // indirect github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect github.com/akavel/rsrc v0.10.2 // indirect github.com/alecthomas/chroma/v2 v2.23.1 // indirect github.com/alecthomas/go-check-sumtype v0.3.1 // indirect github.com/alexkohler/nakedret/v2 v2.0.6 // indirect github.com/alexkohler/prealloc v1.0.2 // indirect github.com/alfatraining/structtag v1.0.0 // indirect github.com/alingse/asasalint v0.0.11 // indirect github.com/alingse/nilnesserr v0.2.0 // indirect github.com/ashanbrown/forbidigo/v2 v2.3.0 // indirect github.com/ashanbrown/makezero/v2 v2.1.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bkielbasa/cyclop v1.2.3 // indirect github.com/blizzy78/varnamelen v0.8.0 // indirect github.com/bombsimon/wsl/v4 v4.7.0 // indirect github.com/bombsimon/wsl/v5 v5.6.0 // indirect github.com/breml/bidichk v0.3.3 // indirect github.com/breml/errchkjson v0.4.1 // indirect github.com/brunoga/deep v1.2.4 // indirect github.com/butuzov/ireturn v0.4.0 // indirect github.com/butuzov/mirror v1.3.0 // indirect github.com/catenacyber/perfsprint v0.10.1 // indirect github.com/ccojocar/zxcvbn-go v1.0.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charithe/durationcheck v0.0.11 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/lipgloss v1.1.0 // indirect github.com/charmbracelet/x/ansi v0.10.1 // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/ckaznocha/intrange v0.3.1 // indirect github.com/curioswitch/go-reassign v0.3.0 // indirect github.com/daixiang0/gci v0.13.7 // indirect github.com/dave/dst v0.27.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect github.com/ettle/strcase v0.2.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/fatih/structtag v1.2.0 // indirect github.com/firefart/nonamedreturns v1.0.6 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect github.com/ghostiam/protogetter v0.3.20 // indirect github.com/go-critic/go-critic v0.14.3 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect github.com/go-toolsmith/astcopy v1.1.0 // indirect github.com/go-toolsmith/astequal v1.2.0 // indirect github.com/go-toolsmith/astfmt v1.1.0 // indirect github.com/go-toolsmith/astp v1.1.0 // indirect github.com/go-toolsmith/strparse v1.1.0 // indirect github.com/go-toolsmith/typep v1.1.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/godoc-lint/godoc-lint v0.11.2 // indirect github.com/gofrs/flock v0.13.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golangci/asciicheck v0.5.0 // indirect github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect github.com/golangci/go-printf-func-name v0.1.1 // indirect github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect github.com/golangci/golines v0.15.0 // indirect github.com/golangci/misspell v0.8.0 // indirect github.com/golangci/plugin-module-register v0.1.2 // indirect github.com/golangci/revgrep v0.8.0 // indirect github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/gordonklaus/ineffassign v0.2.0 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect github.com/gostaticanalysis/comment v1.5.0 // indirect github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect github.com/gostaticanalysis/nilerr v0.1.2 // indirect github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect github.com/hashicorp/go-version v1.8.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jedib0t/go-pretty/v6 v6.6.7 // indirect github.com/jgautheron/goconst v1.8.2 // indirect github.com/jingyugao/rowserrcheck v1.1.1 // indirect github.com/jjti/go-spancheck v0.6.5 // indirect github.com/julz/importas v0.2.0 // indirect github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect github.com/kisielk/errcheck v1.9.0 // indirect github.com/kkHAIKE/contextcheck v1.1.6 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect github.com/knadh/koanf/parsers/yaml v0.1.0 // indirect github.com/knadh/koanf/providers/env v1.0.0 // indirect github.com/knadh/koanf/providers/file v1.1.2 // indirect github.com/knadh/koanf/providers/posflag v0.1.0 // indirect github.com/knadh/koanf/providers/structs v0.1.0 // indirect github.com/knadh/koanf/v2 v2.3.0 // indirect github.com/kulti/thelper v0.7.1 // indirect github.com/kunwardeep/paralleltest v1.0.15 // indirect github.com/lasiar/canonicalheader v1.1.2 // indirect github.com/ldez/exptostd v0.4.5 // indirect github.com/ldez/gomoddirectives v0.8.0 // indirect github.com/ldez/grignotin v0.10.1 // indirect github.com/ldez/structtags v0.6.1 // indirect github.com/ldez/tagliatelle v0.7.2 // indirect github.com/ldez/usetesting v0.5.0 // indirect github.com/leonklingele/grouper v1.1.2 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/macabu/inamedparam v0.2.0 // indirect github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect github.com/manuelarte/funcorder v0.5.0 // indirect github.com/maratori/testableexamples v1.0.1 // indirect github.com/maratori/testpackage v1.1.2 // indirect github.com/matoous/godox v1.1.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mgechev/revive v1.14.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moricho/tparallel v0.3.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/nakabonne/nestif v0.3.1 // indirect github.com/nishanths/exhaustive v0.12.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect github.com/nunnatsa/ginkgolinter v0.23.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.12.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect github.com/quasilyte/go-ruleguard v0.4.5 // indirect github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect github.com/quasilyte/gogrep v0.5.0 // indirect github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect github.com/raeperd/recvcheck v0.2.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/zerolog v1.33.0 // indirect github.com/ryancurrah/gomodguard v1.4.1 // indirect github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/sashamelentyev/interfacebloat v1.1.0 // indirect github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect github.com/securego/gosec/v2 v2.23.0 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/sivchari/containedctx v1.0.3 // indirect github.com/sonatard/noctx v0.4.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/go-diff v0.7.0 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.20.0 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tetafro/godot v1.5.4 // indirect github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect github.com/timonwong/loggercheck v0.11.0 // indirect github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect github.com/ultraware/funlen v0.2.0 // indirect github.com/ultraware/whitespace v0.2.0 // indirect github.com/uudashr/gocognit v1.2.0 // indirect github.com/uudashr/iface v1.4.1 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xen0n/gosmopolitan v1.3.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yagipy/maintidx v1.0.0 // indirect github.com/yeya24/promlinter v0.3.0 // indirect github.com/ykadowak/zerologlint v0.1.5 // indirect gitlab.com/bosi/decorder v0.4.2 // indirect go-simpler.org/musttag v0.14.0 // indirect go-simpler.org/sloglint v0.11.1 // indirect go.augendre.info/arangolint v0.4.0 // indirect go.augendre.info/fatcontext v0.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect golang.org/x/mod v0.33.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 // indirect golang.org/x/term v0.39.0 // indirect golang.org/x/text v0.34.0 // indirect golang.org/x/tools v0.42.0 // indirect google.golang.org/protobuf v1.36.8 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect honnef.co/go/tools v0.7.0 // indirect mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect ) ================================================ FILE: internal/tools/go.sum ================================================ 4d63.com/gocheckcompilerdirectives v1.3.0 h1:Ew5y5CtcAAQeTVKUVFrE7EwHMrTO6BggtEj8BZSjZ3A= 4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY= 4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU= 4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= codeberg.org/chavacava/garif v0.2.0 h1:F0tVjhYbuOCnvNcU3YSpO6b3Waw6Bimy4K0mM8y6MfY= codeberg.org/chavacava/garif v0.2.0/go.mod h1:P2BPbVbT4QcvLZrORc2T29szK3xEOlnl0GiPTJmEqBQ= codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh2an7YmodI= codeberg.org/polyfloyd/go-errorlint v1.9.0/go.mod h1:GPRRu2LzVijNn4YkrZYJfatQIdS+TrcK8rL5Xs24qw8= dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y= dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI= dev.gaijin.team/go/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo= dev.gaijin.team/go/golib v0.6.0/go.mod h1:uY1mShx8Z/aNHWDyAkZTkX+uCi5PdX7KsG1eDQa2AVE= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8= github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c= github.com/Abirdcfly/dupword v0.1.7 h1:2j8sInznrje4I0CMisSL6ipEBkeJUJAmK1/lfoNGWrQ= github.com/Abirdcfly/dupword v0.1.7/go.mod h1:K0DkBeOebJ4VyOICFdppB23Q0YMOgVafM0zYW0n9lF4= github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo= github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY= github.com/AlwxSin/noinlineerr v1.0.5 h1:RUjt63wk1AYWTXtVXbSqemlbVTb23JOSRiNsshj7TbY= github.com/AlwxSin/noinlineerr v1.0.5/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc= github.com/Antonboom/errname v1.1.1 h1:bllB7mlIbTVzO9jmSWVWLjxTEbGBVQ1Ff/ClQgtPw9Q= github.com/Antonboom/errname v1.1.1/go.mod h1:gjhe24xoxXp0ScLtHzjiXp0Exi1RFLKJb0bVBtWKCWQ= github.com/Antonboom/nilnil v1.1.1 h1:9Mdr6BYd8WHCDngQnNVV0b554xyisFioEKi30sksufQ= github.com/Antonboom/nilnil v1.1.1/go.mod h1:yCyAmSw3doopbOWhJlVci+HuyNRuHJKIv6V2oYQa8II= github.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ= github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g= github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/MirrexOne/unqueryvet v1.5.3 h1:LpT3rsH+IY3cQddWF9bg4C7jsbASdGnrOSofY8IPEiw= github.com/MirrexOne/unqueryvet v1.5.3/go.mod h1:fs9Zq6eh1LRIhsDIsxf9PONVUjYdFHdtkHIgZdJnyPU= github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4= github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo= github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw= github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU= github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ= github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q= github.com/alexkohler/prealloc v1.0.2 h1:MPo8cIkGkZytq7WNH9UHv3DIX1mPz1RatPXnZb0zHWQ= github.com/alexkohler/prealloc v1.0.2/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig= github.com/alfatraining/structtag v1.0.0 h1:2qmcUqNcCoyVJ0up879K614L9PazjBSFruTB0GOFjCc= github.com/alfatraining/structtag v1.0.0/go.mod h1:p3Xi5SwzTi+Ryj64DqjLWz7XurHxbGsq6y3ubePJPus= github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= github.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w= github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg= github.com/ashanbrown/forbidigo/v2 v2.3.0 h1:OZZDOchCgsX5gvToVtEBoV2UWbFfI6RKQTir2UZzSxo= github.com/ashanbrown/forbidigo/v2 v2.3.0/go.mod h1:5p6VmsG5/1xx3E785W9fouMxIOkvY2rRV9nMdWadd6c= github.com/ashanbrown/makezero/v2 v2.1.0 h1:snuKYMbqosNokUKm+R6/+vOPs8yVAi46La7Ck6QYSaE= github.com/ashanbrown/makezero/v2 v2.1.0/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w= github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo= github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= github.com/bombsimon/wsl/v4 v4.7.0 h1:1Ilm9JBPRczjyUs6hvOPKvd7VL1Q++PL8M0SXBDf+jQ= github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg= github.com/bombsimon/wsl/v5 v5.6.0 h1:4z+/sBqC5vUmSp1O0mS+czxwH9+LKXtCWtHH9rZGQL8= github.com/bombsimon/wsl/v5 v5.6.0/go.mod h1:Uqt2EfrMj2NV8UGoN1f1Y3m0NpUVCsUdrNCdet+8LvU= github.com/breml/bidichk v0.3.3 h1:WSM67ztRusf1sMoqH6/c4OBCUlRVTKq+CbSeo0R17sE= github.com/breml/bidichk v0.3.3/go.mod h1:ISbsut8OnjB367j5NseXEGGgO/th206dVa427kR8YTE= github.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDwg= github.com/breml/errchkjson v0.4.1/go.mod h1:a23OvR6Qvcl7DG/Z4o0el6BRAjKnaReoPQFciAl9U3s= github.com/brunoga/deep v1.2.4 h1:Aj9E9oUbE+ccbyh35VC/NHlzzjfIVU69BXu2mt2LmL8= github.com/brunoga/deep v1.2.4/go.mod h1:GDV6dnXqn80ezsLSZ5Wlv1PdKAWAO4L5PnKYtv2dgaI= github.com/butuzov/ireturn v0.4.0 h1:+s76bF/PfeKEdbG8b54aCocxXmi0wvYdOVsWxVO7n8E= github.com/butuzov/ireturn v0.4.0/go.mod h1:ghI0FrCmap8pDWZwfPisFD1vEc56VKH4NpQUxDHta70= github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc= github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI= github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ= github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc= github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc= github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charithe/durationcheck v0.0.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1DidBcABctk= github.com/charithe/durationcheck v0.0.11/go.mod h1:x5iZaixRNl8ctbM+3B2RrPG5t856TxRyVQEnbIEM2X4= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs= github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk= 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/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= github.com/daixiang0/gci v0.13.7 h1:+0bG5eK9vlI08J+J/NWGbWPTNiXPG4WhNLJOkSxWITQ= github.com/daixiang0/gci v0.13.7/go.mod h1:812WVN6JLFY9S6Tv76twqmNqevN0pa3SX3nih0brVzQ= github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY= github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 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/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/firefart/nonamedreturns v1.0.6 h1:vmiBcKV/3EqKY3ZiPxCINmpS431OcE1S47AQUwhrg8E= github.com/firefart/nonamedreturns v1.0.6/go.mod h1:R8NisJnSIpvPWheCq0mNRXJok6D8h7fagJTF8EMEwCo= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0= github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI= github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog= github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s= github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw= github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4= github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ= github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw= github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY= github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco= github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4= github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA= github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA= github.com/go-toolsmith/pkgload v1.2.2 h1:0CtmHq/02QhxcF7E9N5LIFcYFsMR5rdovfqTtRKkgIk= github.com/go-toolsmith/pkgload v1.2.2/go.mod h1:R2hxLNRKuAsiXCo2i5J6ZQPhnPMOVtU+f0arbFPWCus= github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw= github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY= github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godoc-lint/godoc-lint v0.11.2 h1:Bp0FkJWoSdNsBikdNgIcgtaoo+xz6I/Y9s5WSBQUeeM= github.com/godoc-lint/godoc-lint v0.11.2/go.mod h1:iVpGdL1JCikNH2gGeAn3Hh+AgN5Gx/I/cxV+91L41jo= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/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.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 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/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0= github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ= github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw= github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E= github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarogrvjO9AfiW3B4U= github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss= github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE= github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY= github.com/golangci/golangci-lint/v2 v2.10.1 h1:flhw5Px6ojbLyEFzXvJn5B2HEdkkRlkhE1SnmCbQBiE= github.com/golangci/golangci-lint/v2 v2.10.1/go.mod h1:dBsrOk6zj0vDhlTv+IiJGqkDokR24IVTS7W3EVfPTQY= github.com/golangci/golines v0.15.0 h1:Qnph25g8Y1c5fdo1X7GaRDGgnMHgnxh4Gk4VfPTtRx0= github.com/golangci/golines v0.15.0/go.mod h1:AZjXd23tbHMpowhtnGlj9KCNsysj72aeZVVHnVcZx10= github.com/golangci/misspell v0.8.0 h1:qvxQhiE2/5z+BVRo1kwYA8yGz+lOlu5Jfvtx2b04Jbg= github.com/golangci/misspell v0.8.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg= github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg= github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw= github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s= github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2bRA5htgAG9r7s3tHsfjIhN98WshBTJ9jM= github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s= github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM= github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU= github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs= github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw= github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= github.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8= github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc= github.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk= github.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY= github.com/gostaticanalysis/nilerr v0.1.2 h1:S6nk8a9N8g062nsx63kUkF6AzbHGw7zzyHMcpu52xQU= github.com/gostaticanalysis/nilerr v0.1.2/go.mod h1:A19UHhoY3y8ahoL7YKz6sdjDtduwTSI4CsymaC2htPA= github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8= github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs= github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo= github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jgautheron/goconst v1.8.2 h1:y0XF7X8CikZ93fSNT6WBTb/NElBu9IjaY7CCYQrCMX4= github.com/jgautheron/goconst v1.8.2/go.mod h1:A0oxgBCHy55NQn6sYpO7UdnA9p+h7cPtoOZUmvNIako= github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8= github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU= github.com/josephspurrier/goversioninfo v1.5.0 h1:9TJtORoyf4YMoWSOo/cXFN9A/lB3PniJ91OxIH6e7Zg= github.com/josephspurrier/goversioninfo v1.5.0/go.mod h1:6MoTvFZ6GKJkzcdLnU5T/RGYUbHQbKpYeNP0AgQLd2o= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ= github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY= github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0= github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY= github.com/kisielk/errcheck v1.9.0 h1:9xt1zI9EBfcYBvdU1nVrzMzzUPUtPKs9bVSIM3TAb3M= github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE= github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= github.com/knadh/koanf/providers/env v1.0.0 h1:ufePaI9BnWH+ajuxGGiJ8pdTG0uLEUWC7/HDDPGLah0= github.com/knadh/koanf/providers/env v1.0.0/go.mod h1:mzFyRZueYhb37oPmC1HAv/oGEEuyvJDA98r3XAa8Gak= github.com/knadh/koanf/providers/file v1.1.2 h1:aCC36YGOgV5lTtAFz2qkgtWdeQsgfxUkxDOe+2nQY3w= github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U= github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0= github.com/knadh/koanf/providers/structs v0.1.0 h1:wJRteCNn1qvLtE5h8KQBvLJovidSdntfdyIbbCzEyE0= github.com/knadh/koanf/providers/structs v0.1.0/go.mod h1:sw2YZ3txUcqA3Z27gPlmmBzWn1h8Nt9O6EP/91MkcWE= github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM= github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 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= 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/kulti/thelper v0.7.1 h1:fI8QITAoFVLx+y+vSyuLBP+rcVIB8jKooNSCT2EiI98= github.com/kulti/thelper v0.7.1/go.mod h1:NsMjfQEy6sd+9Kfw8kCP61W1I0nerGSYSFnGaxQkcbs= github.com/kunwardeep/paralleltest v1.0.15 h1:ZMk4Qt306tHIgKISHWFJAO1IDQJLc6uDyJMLyncOb6w= github.com/kunwardeep/paralleltest v1.0.15/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk= github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4= github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI= github.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ= github.com/ldez/exptostd v0.4.5/go.mod h1:QRjHRMXJrCTIm9WxVNH6VW7oN7KrGSht69bIRwvdFsM= github.com/ldez/gomoddirectives v0.8.0 h1:JqIuTtgvFC2RdH1s357vrE23WJF2cpDCPFgA/TWDGpk= github.com/ldez/gomoddirectives v0.8.0/go.mod h1:jutzamvZR4XYJLr0d5Honycp4Gy6GEg2mS9+2YX3F1Q= github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o= github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas= github.com/ldez/structtags v0.6.1 h1:bUooFLbXx41tW8SvkfwfFkkjPYvFFs59AAMgVg6DUBk= github.com/ldez/structtags v0.6.1/go.mod h1:YDxVSgDy/MON6ariaxLF2X09bh19qL7MtGBN5MrvbdY= github.com/ldez/tagliatelle v0.7.2 h1:KuOlL70/fu9paxuxbeqlicJnCspCRjH0x8FW+NfgYUk= github.com/ldez/tagliatelle v0.7.2/go.mod h1:PtGgm163ZplJfZMZ2sf5nhUT170rSuPgBimoyYtdaSI= github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc= github.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3iRrzvDQ= github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE= github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U= github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww= github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM= github.com/manuelarte/funcorder v0.5.0 h1:llMuHXXbg7tD0i/LNw8vGnkDTHFpTnWqKPI85Rknc+8= github.com/manuelarte/funcorder v0.5.0/go.mod h1:Yt3CiUQthSBMBxjShjdXMexmzpP8YGvGLjrxJNkO2hA= github.com/maratori/testableexamples v1.0.1 h1:HfOQXs+XgfeRBJ+Wz0XfH+FHnoY9TVqL6Fcevpzy4q8= github.com/maratori/testableexamples v1.0.1/go.mod h1:XE2F/nQs7B9N08JgyRmdGjYVGqxWwClLPCGSQhXQSrQ= github.com/maratori/testpackage v1.1.2 h1:ffDSh+AgqluCLMXhM19f/cpvQAKygKAJXFl9aUjmbqs= github.com/maratori/testpackage v1.1.2/go.mod h1:8F24GdVDFW5Ew43Et02jamrVMNXLUNaOynhDssITGfc= github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4= github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgechev/revive v1.14.0 h1:CC2Ulb3kV7JFYt+izwORoS3VT/+Plb8BvslI/l1yZsc= github.com/mgechev/revive v1.14.0/go.mod h1:MvnujelCZBZCaoDv5B3foPo6WWgULSSFxvfxp7GsPfo= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg= github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= github.com/nunnatsa/ginkgolinter v0.23.0 h1:x3o4DGYOWbBMP/VdNQKgSj+25aJKx2Pe6lHr8gBcgf8= github.com/nunnatsa/ginkgolinter v0.23.0/go.mod h1:9qN1+0akwXEccwV1CAcCDfcoBlWXHB+ML9884pL4SZ4= github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/open-telemetry/opentelemetry-collector-contrib/cmd/schemagen v0.147.0 h1:0+UQLw0n243+NSXefYMA+8FDlzRh+drZ14zuGB+fdUQ= github.com/open-telemetry/opentelemetry-collector-contrib/cmd/schemagen v0.147.0/go.mod h1:OINT7fivIXPsH4oMYFjnkreLeHimo7uayD3NZn+SwY8= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA= github.com/quasilyte/go-ruleguard v0.4.5/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE= github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY= github.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng= github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU= github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= github.com/raeperd/recvcheck v0.2.0 h1:GnU+NsbiCqdC2XX5+vMZzP+jAJC5fht7rcVTAhX74UI= github.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryancurrah/gomodguard v1.4.1 h1:eWC8eUMNZ/wM/PWuZBv7JxxqT5fiIKSIyTvjb7Elr+g= github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I= github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU= github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0= github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ= github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8= github.com/securego/gosec/v2 v2.23.0 h1:h4TtF64qFzvnkqvsHC/knT7YC5fqyOCItlVR8+ptEBo= github.com/securego/gosec/v2 v2.23.0/go.mod h1:qRHEgXLFuYUDkI2T7W7NJAmOkxVhkR0x9xyHOIcMNZ0= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= github.com/sonatard/noctx v0.4.0 h1:7MC/5Gg4SQ4lhLYR6mvOP6mQVSxCrdyiExo7atBs27o= github.com/sonatard/noctx v0.4.0/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g= github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ= 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.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA= github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= github.com/tetafro/godot v1.5.4 h1:u1ww+gqpRLiIA16yF2PV1CV1n/X3zhyezbNXC3E14Sg= github.com/tetafro/godot v1.5.4/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU= github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 h1:9LPGD+jzxMlnk5r6+hJnar67cgpDIz/iyD+rfl5r2Vk= github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460= github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M= github.com/timonwong/loggercheck v0.11.0/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8= github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is= github.com/tomarrell/wrapcheck/v2 v2.12.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo= github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI= github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA= github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g= github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= github.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYRA= github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU= github.com/uudashr/iface v1.4.1 h1:J16Xl1wyNX9ofhpHmQ9h9gk5rnv2A6lX/2+APLTo0zU= github.com/uudashr/iface v1.4.1/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg= github.com/vektra/mockery/v3 v3.6.1 h1:YyqAXihdNML8y6SJnvPKYr+2HAHvBjdvqFu/fMYlX8g= github.com/vektra/mockery/v3 v3.6.1/go.mod h1:Oti3Df0WP8wwT31yuVri3QNsDeMUQU5Q4QEg8EabaBw= github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad h1:W0LEBv82YCGEtcmPA3uNZBI33/qF//HAAs3MawDjRa0= github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad/go.mod h1:Hy8o65+MXnS6EwGElrSRjUzQDLXreJlzYLlWiHtt8hM= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM= github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs= github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4= github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw= github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/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= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo= gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8= go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ= go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28= go-simpler.org/musttag v0.14.0 h1:XGySZATqQYSEV3/YTy+iX+aofbZZllJaqwFWs+RTtSo= go-simpler.org/musttag v0.14.0/go.mod h1:uP8EymctQjJ4Z1kUnjX0u2l60WfUdQxCwSNKzE1JEOE= go-simpler.org/sloglint v0.11.1 h1:xRbPepLT/MHPTCA6TS/wNfZrDzkGvCCqUv4Bdwc3H7s= go-simpler.org/sloglint v0.11.1/go.mod h1:2PowwiCOK8mjiF+0KGifVOT8ZsCNiFzvfyJeJOIt8MQ= go.augendre.info/arangolint v0.4.0 h1:xSCZjRoS93nXazBSg5d0OGCi9APPLNMmmLrC995tR50= go.augendre.info/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA= go.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDojE= go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 h1:qWFG1Dj7TBjOjOvhEOkmyGPVoquqUKnIU0lEVLp8xyk= golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 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.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= 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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/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-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/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-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 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-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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-20201207232520-09787c993a3a/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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/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-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 h1:bTLqdHv7xrGlFbvf5/TXNxy/iUwwdkjhqQTJDjW7aj0= golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/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-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I= golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 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/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 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.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 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.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 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-20190902080502-41f04d3bba15/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU= honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc= mvdan.cc/gofumpt v0.9.2 h1:zsEMWL8SVKGHNztrx6uZrXdp7AX8r421Vvp23sz7ik4= mvdan.cc/gofumpt v0.9.2/go.mod h1:iB7Hn+ai8lPvofHd9ZFGVg2GOr8sBUw1QUWjNbmIL/s= mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 h1:ssMzja7PDPJV8FStj7hq9IKiuiKhgz9ErWw+m68e7DI= mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15/go.mod h1:4M5MMXl2kW6fivUT6yRGpLLPNfuGtU2Z0cPvFquGDYU= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= ================================================ FILE: internal/tools/tools.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 //go:build tools package tools // This file follows the recommendation at // https://go.dev/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module // on how to pin tooling dependencies to a go.mod file. // This ensures that all systems use the same version of tools in addition to regular dependencies. import ( _ "mvdan.cc/gofumpt" _ "github.com/golangci/golangci-lint/v2/cmd/golangci-lint" _ "github.com/josephspurrier/goversioninfo/cmd/goversioninfo" _ "github.com/open-telemetry/opentelemetry-collector-contrib/cmd/schemagen" _ "github.com/vektra/mockery/v3" _ "github.com/wadey/gocovmerge" _ "golang.org/x/vuln/cmd/govulncheck" ) ================================================ FILE: internal/tracegen/config.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracegen import ( "errors" "flag" "sync" "sync/atomic" "time" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" ) // Config describes the test scenario. type Config struct { Workers int Services int Traces int ChildSpans int Attributes int AttrKeys int AttrValues int Marshal bool Debug bool Firehose bool Pause time.Duration Duration time.Duration Service string TraceExporter string } // Flags registers config flags. func (c *Config) Flags(fs *flag.FlagSet) { fs.IntVar(&c.Workers, "workers", 1, "Number of workers (goroutines) to run") fs.IntVar(&c.Traces, "traces", 1, "Number of traces to generate in each worker (ignored if duration is provided)") fs.IntVar(&c.ChildSpans, "spans", 1, "Number of child spans to generate for each trace") fs.IntVar(&c.Attributes, "attrs", 11, "Number of attributes to generate for each child span") fs.IntVar(&c.AttrKeys, "attr-keys", 97, "Number of distinct attributes keys to use") fs.IntVar(&c.AttrValues, "attr-values", 1000, "Number of distinct values to allow for each attribute") fs.BoolVar(&c.Debug, "debug", false, "Whether to set DEBUG flag on the spans to force sampling") fs.BoolVar(&c.Firehose, "firehose", false, "Whether to set FIREHOSE flag on the spans to skip indexing") fs.DurationVar(&c.Pause, "pause", time.Microsecond, "How long to sleep before finishing each span. If set to 0s then a fake 123µs duration is used.") fs.DurationVar(&c.Duration, "duration", 0, "For how long to run the test if greater than 0s (overrides -traces).") fs.StringVar(&c.Service, "service", "tracegen", "Service name prefix to use") fs.IntVar(&c.Services, "services", 1, "Number of unique suffixes to add to service name when generating traces, e.g. tracegen-01 (but only one service per trace)") fs.StringVar(&c.TraceExporter, "trace-exporter", "otlp-http", "Trace exporter (otlp/otlp-http|otlp-grpc|stdout). Exporters can be additionally configured via environment variables, see https://github.com/jaegertracing/jaeger/blob/main/cmd/tracegen/README.md") } // Run executes the test scenario. func Run(c *Config, tracers []trace.Tracer, logger *zap.Logger) error { if c.Duration > 0 { c.Traces = 0 } else if c.Traces <= 0 { return errors.New("either `traces` or `duration` must be greater than 0") } wg := sync.WaitGroup{} var running uint32 = 1 for i := 0; i < c.Workers; i++ { wg.Add(1) w := worker{ id: i, tracers: tracers, Config: *c, running: &running, wg: &wg, logger: logger.With(zap.Int("worker", i)), } go w.simulateTraces() } if c.Duration > 0 { time.Sleep(c.Duration) atomic.StoreUint32(&running, 0) } wg.Wait() return nil } ================================================ FILE: internal/tracegen/config_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracegen import ( "errors" "flag" "testing" "github.com/stretchr/testify/assert" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" ) func Test_Run(t *testing.T) { logger := zap.NewNop() tp := sdktrace.NewTracerProvider() tests := []struct { name string config *Config expectedErr error }{ { name: "Empty config", config: &Config{}, expectedErr: errors.New("either `traces` or `duration` must be greater than 0"), }, { name: "Non-empty config", config: &Config{ Workers: 2, Traces: 10, ChildSpans: 5, Attributes: 20, AttrKeys: 50, AttrValues: 100, }, expectedErr: nil, }, { name: "Negative traces, positive duration", config: &Config{ Traces: -7, Duration: 7, }, expectedErr: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tracers := []trace.Tracer{tp.Tracer("Test-Tracer")} err := Run(tt.config, tracers, logger) assert.Equal(t, tt.expectedErr, err) }) } } func Test_Flags(t *testing.T) { fs := &flag.FlagSet{} config := &Config{} expectedConfig := &Config{ Workers: 1, Traces: 1, ChildSpans: 1, Attributes: 11, AttrKeys: 97, AttrValues: 1000, Debug: false, Firehose: false, Pause: 1000, Duration: 0, Service: "tracegen", Services: 1, TraceExporter: "otlp-http", } config.Flags(fs) assert.Equal(t, expectedConfig, config) } ================================================ FILE: internal/tracegen/package_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracegen import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/tracegen/worker.go ================================================ // Copyright (c) 2018 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracegen import ( "context" "fmt" "sync" "sync/atomic" "time" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" ) type worker struct { tracers []trace.Tracer running *uint32 // pointer to shared flag that indicates it's time to stop the test id int // worker id Config wg *sync.WaitGroup // notify when done logger *zap.Logger // internal counters traceNo int attrKeyNo int attrValNo int } const ( fakeSpanDuration = 123 * time.Microsecond ) func (w *worker) simulateTraces() { for atomic.LoadUint32(w.running) == 1 { svcNo := w.traceNo % len(w.tracers) w.simulateOneTrace(w.tracers[svcNo]) w.traceNo++ if w.Traces != 0 { if w.traceNo >= w.Traces { break } } } w.logger.Info(fmt.Sprintf("Worker %d generated %d traces", w.id, w.traceNo)) w.wg.Done() } func (w *worker) simulateOneTrace(tracer trace.Tracer) { ctx := context.Background() attrs := []attribute.KeyValue{ attribute.String("peer.service", "tracegen-server"), attribute.String("peer.host.ipv4", "1.1.1.1"), } if w.Debug { attrs = append(attrs, attribute.Bool("jaeger.debug", true)) } if w.Firehose { attrs = append(attrs, attribute.Bool("jaeger.firehose", true)) } start := time.Now() ctx, parent := tracer.Start( ctx, "lets-go", trace.WithSpanKind(trace.SpanKindServer), trace.WithAttributes(attrs...), trace.WithTimestamp(start), ) w.simulateChildSpans(ctx, start, tracer) if w.Pause != 0 { parent.End() } else { totalDuration := time.Duration(w.ChildSpans) * fakeSpanDuration parent.End( trace.WithTimestamp(start.Add(totalDuration)), ) } } func (w *worker) simulateChildSpans(ctx context.Context, start time.Time, tracer trace.Tracer) { for c := 0; c < w.ChildSpans; c++ { var attrs []attribute.KeyValue for a := 0; a < w.Attributes; a++ { key := fmt.Sprintf("attr_%02d", w.attrKeyNo) val := fmt.Sprintf("val_%02d", w.attrValNo) attrs = append(attrs, attribute.String(key, val)) w.attrKeyNo = (w.attrKeyNo + 1) % w.AttrKeys w.attrValNo = (w.attrValNo + 1) % w.AttrValues } opts := []trace.SpanStartOption{ trace.WithSpanKind(trace.SpanKindClient), trace.WithAttributes(attrs...), } childStart := start.Add(time.Duration(c) * fakeSpanDuration) if w.Pause == 0 { opts = append(opts, trace.WithTimestamp(childStart)) } _, child := tracer.Start( ctx, fmt.Sprintf("child-span-%02d", c), opts..., ) if w.Pause != 0 { time.Sleep(w.Pause) child.End() } else { child.End( trace.WithTimestamp(childStart.Add(fakeSpanDuration)), ) } } } ================================================ FILE: internal/tracegen/worker_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package tracegen import ( "sync" "testing" "time" "github.com/stretchr/testify/assert" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace" "github.com/jaegertracing/jaeger/internal/testutils" ) func Test_SimulateTraces(t *testing.T) { tests := []struct { name string pause time.Duration }{ { name: "no pause", pause: 0, }, { name: "with pause", pause: time.Second, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { logger, buf := testutils.NewLogger() tp := sdktrace.NewTracerProvider() tracers := []trace.Tracer{tp.Tracer("stdout")} wg := sync.WaitGroup{} wg.Add(1) var running uint32 = 1 worker := &worker{ logger: logger, tracers: tracers, wg: &wg, id: 7, running: &running, Config: Config{ Traces: 7, Duration: time.Second, Pause: tt.pause, Service: "stdout", Debug: true, Firehose: true, ChildSpans: 1, }, } expectedOutput := `{"level":"info","msg":"Worker 7 generated 7 traces"}` + "\n" worker.simulateTraces() assert.Equal(t, expectedOutput, buf.String()) }) } } ================================================ FILE: internal/uimodel/converter/v1/json/doc.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 // Package json allows converting model.Trace to external JSON data model. package json ================================================ FILE: internal/uimodel/converter/v1/json/fixtures/domain_01.json ================================================ { "spans": [ { "traceId": "AAAAAAAAAAAAAAAAAAAAAQ==", "spanId": "AAAAAAAAAAI=", "operationName": "test-general-conversion", "startTime": "2017-01-26T16:46:31.639875139-05:00", "duration": "5000ns", "process": { "serviceName": "service-x" }, "logs": [ { "timestamp": "2017-01-26T16:46:31.639875139-05:00", "fields": [ { "key": "event", "vStr": "some-event" } ] }, { "timestamp": "2017-01-26T16:46:31.639875139-05:00", "fields": [ { "key": "x", "vStr": "y" } ] } ] }, { "traceId": "AAAAAAAAAAAAAAAAAAAAAQ==", "spanId": "AAAAAAAAAAI=", "operationName": "some-operation", "startTime": "2017-01-26T16:46:31.639875139-05:00", "duration": "5000ns", "tags": [ { "key": "peer.service", "vType": "STRING", "vStr": "service-y" }, { "key": "peer.ipv4", "vType": "INT64", "vInt64": 23456 }, { "key": "error", "vType": "BOOL", "vBool": true }, { "key": "temperature", "vType": "FLOAT64", "vFloat64": 72.5 }, { "key": "javascript_limit", "vType": "INT64", "vInt64": 9223372036854775222 }, { "key": "blob", "vType": "BINARY", "vBinary": "AAAwOQ==" } ], "process": { "serviceName": "service-x" } }, { "traceId": "AAAAAAAAAAAAAAAAAAAAAQ==", "spanId": "AAAAAAAAAAM=", "references": [ { "refType": "CHILD_OF", "traceId": "AAAAAAAAAAAAAAAAAAAAAQ==", "spanId": "AAAAAAAAAAI=" } ], "operationName": "some-operation", "startTime": "2017-01-26T16:46:31.639875139-05:00", "duration": "5000ns", "process": { "serviceName": "service-y" } }, { "traceId": "AAAAAAAAAAAAAAAAAAAAAQ==", "spanId": "AAAAAAAAAAQ=", "operationName": "reference-test", "references": [ { "refType": "CHILD_OF", "traceId": "AAAAAAAAAAAAAAAAAAAA/w==", "spanId": "AAAAAAAAAP8=" }, { "refType": "CHILD_OF", "traceId": "AAAAAAAAAAAAAAAAAAAAAQ==", "spanId": "AAAAAAAAAAI=" }, { "refType": "FOLLOWS_FROM", "traceId": "AAAAAAAAAAAAAAAAAAAAAQ==", "spanId": "AAAAAAAAAAI=" } ], "startTime": "2017-01-26T16:46:31.639875139-05:00", "duration": "5000ns", "process": { "serviceName": "service-y" }, "warnings": [ "some span warning" ] }, { "traceId": "AAAAAAAAAAAAAAAAAAAAAQ==", "spanId": "AAAAAAAAAAU=", "operationName": "preserveParentID-test", "references": [ { "refType": "CHILD_OF", "traceId": "AAAAAAAAAAAAAAAAAAAAAQ==", "spanId": "AAAAAAAAAAQ=" } ], "startTime": "2017-01-26T16:46:31.639875139-05:00", "duration": "4000ns", "process": { "serviceName": "service-y" }, "warnings": [ "some span warning" ] } ], "processMap": [], "warnings": [ "some trace warning" ] } ================================================ FILE: internal/uimodel/converter/v1/json/fixtures/domain_es_01.json ================================================ { "traceId": "AAAAAAAAAAAAAAAAAAAAAQ==", "spanId": "AAAAAAAAAAI=", "operationName": "test-general-conversion", "references": [ { "refType": "CHILD_OF", "traceId": "AAAAAAAAAAAAAAAAAAAAAQ==", "spanId": "AAAAAAAAAAM=" }, { "refType": "FOLLOWS_FROM", "traceId": "AAAAAAAAAAAAAAAAAAAAAQ==", "spanId": "AAAAAAAAAAQ=" }, { "refType": "CHILD_OF", "traceId": "AAAAAAAAAAAAAAAAAAAA/w==", "spanId": "AAAAAAAAAP8=" } ], "flags": 1, "startTime": "2017-01-26T16:46:31.639875-05:00", "duration": "5000ns", "tags": [ { "key": "peer.service", "vType": "STRING", "vStr": "service-y" }, { "key": "peer.ipv4", "vType": "INT64", "vInt64": 23456 }, { "key": "error", "vType": "BOOL", "vBool": true }, { "key": "temperature", "vType": "FLOAT64", "vFloat64": 72.5 }, { "key": "blob", "vType": "BINARY", "vBinary": "AAAwOQ==" } ], "logs": [ { "timestamp": "2017-01-26T16:46:31.639875-05:00", "fields": [ { "key": "event", "vType": "INT64", "vInt64": 123415 } ] }, { "timestamp": "2017-01-26T16:46:31.639875-05:00", "fields": [ { "key": "x", "vType": "STRING", "vStr": "y" } ] } ], "process": { "serviceName": "service-x", "tags": [ { "key": "peer.ipv4", "vType": "INT64", "vInt64": 23456 }, { "key": "error", "vType": "BOOL", "vBool": true } ] } } ================================================ FILE: internal/uimodel/converter/v1/json/fixtures/es_01.json ================================================ { "traceID": "0000000000000001", "spanID": "0000000000000002", "flags": 1, "operationName": "test-general-conversion", "references": [ { "refType": "CHILD_OF", "traceID": "0000000000000001", "spanID": "0000000000000003" }, { "refType": "FOLLOWS_FROM", "traceID": "0000000000000001", "spanID": "0000000000000004" }, { "refType": "CHILD_OF", "traceID": "00000000000000ff", "spanID": "00000000000000ff" } ], "startTime": 1485467191639875, "duration": 5, "tags": [ { "key": "peer.service", "type": "string", "value": "service-y" }, { "key": "peer.ipv4", "type": "int64", "value": "23456" }, { "key": "error", "type": "bool", "value": "true" }, { "key": "temperature", "type": "float64", "value": "72.5" }, { "key": "blob", "type": "binary", "value": "00003039" } ], "logs": [ { "timestamp": 1485467191639875, "fields": [ { "key": "event", "type": "int64", "value": "123415" } ] }, { "timestamp": 1485467191639875, "fields": [ { "key": "x", "type": "string", "value": "y" } ] } ], "process": { "serviceName": "service-x", "tags": [ { "key": "peer.ipv4", "type": "int64", "value": "23456" }, { "key": "error", "type": "bool", "value": "true" } ] }, "warnings": null } ================================================ FILE: internal/uimodel/converter/v1/json/fixtures/ui_01.json ================================================ { "traceID": "0000000000000001", "spans": [ { "traceID": "0000000000000001", "spanID": "0000000000000002", "operationName": "test-general-conversion", "references": [], "startTime": 1485467191639875, "duration": 5, "tags": [], "logs": [ { "timestamp": 1485467191639875, "fields": [ { "key": "event", "type": "string", "value": "some-event" } ] }, { "timestamp": 1485467191639875, "fields": [ { "key": "x", "type": "string", "value": "y" } ] } ], "processID": "p1", "warnings": null }, { "traceID": "0000000000000001", "spanID": "0000000000000002", "operationName": "some-operation", "references": [], "startTime": 1485467191639875, "duration": 5, "tags": [ { "key": "peer.service", "type": "string", "value": "service-y" }, { "key": "peer.ipv4", "type": "int64", "value": 23456 }, { "key": "error", "type": "bool", "value": true }, { "key": "temperature", "type": "float64", "value": 72.5 }, { "key": "javascript_limit", "type": "int64", "value": "9223372036854775222" }, { "key": "blob", "type": "binary", "value": "AAAwOQ==" } ], "logs": [], "processID": "p1", "warnings": null }, { "traceID": "0000000000000001", "spanID": "0000000000000003", "operationName": "some-operation", "references": [ { "refType": "CHILD_OF", "traceID": "0000000000000001", "spanID": "0000000000000002" } ], "startTime": 1485467191639875, "duration": 5, "tags": [], "logs": [], "processID": "p2", "warnings": null }, { "traceID": "0000000000000001", "spanID": "0000000000000004", "operationName": "reference-test", "references": [ { "refType": "CHILD_OF", "traceID": "00000000000000ff", "spanID": "00000000000000ff" }, { "refType": "CHILD_OF", "traceID": "0000000000000001", "spanID": "0000000000000002" }, { "refType": "FOLLOWS_FROM", "traceID": "0000000000000001", "spanID": "0000000000000002" } ], "startTime": 1485467191639875, "duration": 5, "tags": [], "logs": [], "processID": "p2", "warnings": [ "some span warning" ] }, { "traceID": "0000000000000001", "spanID": "0000000000000005", "operationName": "preserveParentID-test", "references": [ { "refType": "CHILD_OF", "traceID": "0000000000000001", "spanID": "0000000000000004" } ], "startTime": 1485467191639875, "duration": 4, "tags": [], "logs": [], "processID": "p2", "warnings": [ "some span warning" ] } ], "processes": { "p1": { "serviceName": "service-x", "tags": [] }, "p2": { "serviceName": "service-y", "tags": [] } }, "warnings": [ "some trace warning" ] } ================================================ FILE: internal/uimodel/converter/v1/json/from_domain.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package json import ( "fmt" "strings" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/uimodel" ) const ( jsMaxSafeInteger = int64(1)<<53 - 1 jsMinSafeInteger = -jsMaxSafeInteger ) // FromDomain converts model.Trace into json.Trace format. // It assumes that the domain model is valid, namely that all enums // have valid values, so that it does not need to check for errors. func FromDomain(trace *model.Trace) *uimodel.Trace { fd := fromDomain{} fd.convertKeyValuesFunc = fd.convertKeyValues return fd.fromDomain(trace) } // FromDomainEmbedProcess converts model.Span into json.Span format. // This format includes a ParentSpanID and an embedded Process. func FromDomainEmbedProcess(span *model.Span) *uimodel.Span { fd := fromDomain{} fd.convertKeyValuesFunc = fd.convertKeyValuesString return fd.convertSpanEmbedProcess(span) } type fromDomain struct { convertKeyValuesFunc func(keyValues model.KeyValues) []uimodel.KeyValue } func (fd fromDomain) fromDomain(trace *model.Trace) *uimodel.Trace { jSpans := make([]uimodel.Span, len(trace.Spans)) processes := &processHashtable{} var traceID uimodel.TraceID for i, span := range trace.Spans { if i == 0 { traceID = uimodel.TraceID(span.TraceID.String()) } processID := uimodel.ProcessID(processes.getKey(span.Process)) jSpans[i] = fd.convertSpan(span, processID) } jTrace := &uimodel.Trace{ TraceID: traceID, Spans: jSpans, Processes: fd.convertProcesses(processes.getMapping()), Warnings: trace.Warnings, } return jTrace } func (fd fromDomain) convertSpanInternal(span *model.Span) uimodel.Span { return uimodel.Span{ TraceID: uimodel.TraceID(span.TraceID.String()), SpanID: uimodel.SpanID(span.SpanID.String()), Flags: uint32(span.Flags), OperationName: span.OperationName, StartTime: model.TimeAsEpochMicroseconds(span.StartTime), Duration: model.DurationAsMicroseconds(span.Duration), Tags: fd.convertKeyValuesFunc(span.Tags), Logs: fd.convertLogs(span.Logs), } } func (fd fromDomain) convertSpan(span *model.Span, processID uimodel.ProcessID) uimodel.Span { s := fd.convertSpanInternal(span) s.ProcessID = processID s.Warnings = span.Warnings s.References = fd.convertReferences(span) return s } func (fd fromDomain) convertSpanEmbedProcess(span *model.Span) *uimodel.Span { s := fd.convertSpanInternal(span) process := fd.convertProcess(span.Process) s.Process = &process s.References = fd.convertReferences(span) return &s } func (fd fromDomain) convertReferences(span *model.Span) []uimodel.Reference { out := make([]uimodel.Reference, 0, len(span.References)) for _, ref := range span.References { out = append(out, uimodel.Reference{ RefType: fd.convertRefType(ref.RefType), TraceID: uimodel.TraceID(ref.TraceID.String()), SpanID: uimodel.SpanID(ref.SpanID.String()), }) } return out } func (fromDomain) convertRefType(refType model.SpanRefType) uimodel.ReferenceType { if refType == model.FollowsFrom { return uimodel.FollowsFrom } return uimodel.ChildOf } func (fromDomain) convertKeyValues(keyValues model.KeyValues) []uimodel.KeyValue { out := make([]uimodel.KeyValue, len(keyValues)) for i, kv := range keyValues { var value any switch kv.VType { case model.StringType: value = kv.VStr case model.BoolType: value = kv.Bool() case model.Int64Type: value = kv.Int64() if kv.Int64() > jsMaxSafeInteger || kv.Int64() < jsMinSafeInteger { value = fmt.Sprintf("%d", value) } case model.Float64Type: value = kv.Float64() case model.BinaryType: value = kv.Binary() default: value = kv.AsString() } out[i] = uimodel.KeyValue{ Key: kv.Key, Type: uimodel.ValueType(strings.ToLower(kv.VType.String())), Value: value, } } return out } func (fromDomain) convertKeyValuesString(keyValues model.KeyValues) []uimodel.KeyValue { out := make([]uimodel.KeyValue, len(keyValues)) for i, kv := range keyValues { out[i] = uimodel.KeyValue{ Key: kv.Key, Type: uimodel.ValueType(strings.ToLower(kv.VType.String())), Value: kv.AsString(), } } return out } func (fd fromDomain) convertLogs(logs []model.Log) []uimodel.Log { out := make([]uimodel.Log, len(logs)) for i, log := range logs { out[i] = uimodel.Log{ Timestamp: model.TimeAsEpochMicroseconds(log.Timestamp), Fields: fd.convertKeyValuesFunc(log.Fields), } } return out } func (fd fromDomain) convertProcesses(processes map[string]*model.Process) map[uimodel.ProcessID]uimodel.Process { out := make(map[uimodel.ProcessID]uimodel.Process) for key, process := range processes { out[uimodel.ProcessID(key)] = fd.convertProcess(process) } return out } func (fd fromDomain) convertProcess(process *model.Process) uimodel.Process { return uimodel.Process{ ServiceName: process.ServiceName, Tags: fd.convertKeyValuesFunc(process.Tags), } } // DependenciesFromDomain converts []model.DependencyLink into []json.DependencyLink format. func DependenciesFromDomain(dependencyLinks []model.DependencyLink) []uimodel.DependencyLink { retMe := make([]uimodel.DependencyLink, 0, len(dependencyLinks)) for _, dependencyLink := range dependencyLinks { retMe = append( retMe, uimodel.DependencyLink{ Parent: dependencyLink.Parent, Child: dependencyLink.Child, CallCount: dependencyLink.CallCount, }, ) } return retMe } ================================================ FILE: internal/uimodel/converter/v1/json/from_domain_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package json import ( "bytes" "encoding/json" "fmt" "os" "testing" "time" "github.com/gogo/protobuf/jsonpb" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger-idl/model/v1" "github.com/jaegertracing/jaeger/internal/uimodel" ) const NumberOfFixtures = 1 func TestMarshalJSON(t *testing.T) { span1 := &model.Span{ TraceID: model.TraceID{Low: 1}, SpanID: model.SpanID(2), OperationName: "span", StartTime: time.Now(), Duration: time.Microsecond, } trace1 := &model.Trace{ Spans: []*model.Span{ span1, }, ProcessMap: []model.Trace_ProcessMapping{ { ProcessID: "p1", Process: model.Process{ ServiceName: "abc", }, }, }, } m := &jsonpb.Marshaler{} out := &bytes.Buffer{} require.NoError(t, m.Marshal(out, trace1)) var trace2 model.Trace bb := bytes.NewReader(out.Bytes()) require.NoError(t, jsonpb.Unmarshal(bb, &trace2)) trace1.NormalizeTimestamps() trace2.NormalizeTimestamps() assert.Equal(t, &trace2, trace1) } func TestFromDomain(t *testing.T) { for i := 1; i <= NumberOfFixtures; i++ { domainStr, jsonStr := loadFixturesUI(t, i) var trace model.Trace require.NoError(t, jsonpb.Unmarshal(bytes.NewReader(domainStr), &trace)) uiTrace := FromDomain(&trace) testJSONEncoding(t, i, jsonStr, uiTrace, false) } } func TestFromDomainEmbedProcess(t *testing.T) { for i := 1; i <= NumberOfFixtures; i++ { domainStr, jsonStr := loadFixturesES(t, i) var span model.Span require.NoError(t, jsonpb.Unmarshal(bytes.NewReader(domainStr), &span)) embeddedSpan := FromDomainEmbedProcess(&span) var expectedSpan uimodel.Span require.NoError(t, json.Unmarshal(jsonStr, &expectedSpan)) testJSONEncoding(t, i, jsonStr, embeddedSpan, true) CompareJSONSpans(t, &expectedSpan, embeddedSpan) } } func loadFixturesUI(t *testing.T, i int) (inStr []byte, outStr []byte) { return loadFixtures(t, i, false) } func loadFixturesES(t *testing.T, i int) (inStr []byte, outStr []byte) { return loadFixtures(t, i, true) } // Loads and returns domain model and JSON model fixtures with given number i. func loadFixtures(t *testing.T, i int, processEmbedded bool) (inStr []byte, outStr []byte) { var in string var err error if processEmbedded { in = fmt.Sprintf("fixtures/domain_es_%02d.json", i) } else { in = fmt.Sprintf("fixtures/domain_%02d.json", i) } inStr, err = os.ReadFile(in) require.NoError(t, err) var out string if processEmbedded { out = fmt.Sprintf("fixtures/es_%02d.json", i) } else { out = fmt.Sprintf("fixtures/ui_%02d.json", i) } outStr, err = os.ReadFile(out) require.NoError(t, err) return inStr, outStr } func testJSONEncoding(t *testing.T, i int, expectedStr []byte, object any, processEmbedded bool) { buf := &bytes.Buffer{} enc := json.NewEncoder(buf) enc.SetIndent("", " ") var outFile string if processEmbedded { outFile = fmt.Sprintf("fixtures/es_%02d", i) } else { outFile = fmt.Sprintf("fixtures/ui_%02d", i) } require.NoError(t, enc.Encode(object)) if !assert.Equal(t, string(expectedStr), buf.String()) { err := os.WriteFile(outFile+"-actual.json", buf.Bytes(), 0o644) require.NoError(t, err) } } func TestDependenciesFromDomain(t *testing.T) { someParent := "someParent" someChild := "someChild" someCallCount := uint64(123) anotherParent := "anotherParent" anotherChild := "anotherChild" anotherCallCount := uint64(456) expected := []uimodel.DependencyLink{ { Parent: someParent, Child: someChild, CallCount: someCallCount, }, { Parent: anotherParent, Child: anotherChild, CallCount: anotherCallCount, }, } input := []model.DependencyLink{ { Parent: someParent, Child: someChild, CallCount: someCallCount, }, { Parent: anotherParent, Child: anotherChild, CallCount: anotherCallCount, }, } actual := DependenciesFromDomain(input) assert.Equal(t, expected, actual) } func TestConvertKeyValues_DefaultValueType(t *testing.T) { // Create a custom ValueType that's not handled by the switch customType := model.ValueType(999) kv := model.KeyValue{ Key: "custom-key", VType: customType, VStr: "custom-value", } fd := fromDomain{} result := fd.convertKeyValues(model.KeyValues{kv}) require.Len(t, result, 1) assert.Equal(t, "custom-key", result[0].Key) assert.Equal(t, "unknown type 999", result[0].Value) assert.Equal(t, uimodel.ValueType("999"), result[0].Type) } ================================================ FILE: internal/uimodel/converter/v1/json/json_span_compare_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package json import ( "encoding/json" "sort" "testing" "github.com/kr/pretty" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" esjson "github.com/jaegertracing/jaeger/internal/uimodel" ) func CompareJSONSpans(t *testing.T, expected *esjson.Span, actual *esjson.Span) { sortJSONSpan(expected) sortJSONSpan(actual) if !assert.Equal(t, expected, actual) { for _, err := range pretty.Diff(expected, actual) { t.Log(err) } out, err := json.Marshal(actual) require.NoError(t, err) t.Logf("Actual trace: %s", string(out)) } } func sortJSONSpan(span *esjson.Span) { sortJSONTags(span.Tags) sortJSONLogs(span.Logs) sortJSONProcess(span.Process) } type JSONTagByKey []esjson.KeyValue func (t JSONTagByKey) Len() int { return len(t) } func (t JSONTagByKey) Swap(i, j int) { t[i], t[j] = t[j], t[i] } func (t JSONTagByKey) Less(i, j int) bool { return t[i].Key < t[j].Key } func sortJSONTags(tags []esjson.KeyValue) { sort.Sort(JSONTagByKey(tags)) } type JSONLogByTimestamp []esjson.Log func (t JSONLogByTimestamp) Len() int { return len(t) } func (t JSONLogByTimestamp) Swap(i, j int) { t[i], t[j] = t[j], t[i] } func (t JSONLogByTimestamp) Less(i, j int) bool { return t[i].Timestamp < t[j].Timestamp } func sortJSONLogs(logs []esjson.Log) { sort.Sort(JSONLogByTimestamp(logs)) for i := range logs { sortJSONTags(logs[i].Fields) } } func sortJSONProcess(process *esjson.Process) { sortJSONTags(process.Tags) } ================================================ FILE: internal/uimodel/converter/v1/json/package_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package json import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/uimodel/converter/v1/json/process_hashtable.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package json import ( "strconv" "github.com/jaegertracing/jaeger-idl/model/v1" ) type processHashtable struct { count int processes map[uint64][]processKey extHash func(*model.Process) uint64 } type processKey struct { process *model.Process key string } // getKey assigns a new unique string key to the process, or returns // a previously assigned value if the process has already been seen. func (ph *processHashtable) getKey(process *model.Process) string { if ph.processes == nil { ph.processes = make(map[uint64][]processKey) } hash := ph.hash(process) if keys, ok := ph.processes[hash]; ok { for _, k := range keys { if k.process.Equal(process) { return k.key } } key := ph.nextKey() keys = append(keys, processKey{process: process, key: key}) ph.processes[hash] = keys return key } key := ph.nextKey() ph.processes[hash] = []processKey{{process: process, key: key}} return key } // getMapping returns the accumulated mapping of string keys to processes. func (ph *processHashtable) getMapping() map[string]*model.Process { out := make(map[string]*model.Process) for _, keys := range ph.processes { for _, key := range keys { out[key.key] = key.process } } return out } func (ph *processHashtable) nextKey() string { ph.count++ key := "p" + strconv.Itoa(ph.count) return key } func (ph processHashtable) hash(process *model.Process) uint64 { if ph.extHash != nil { // for testing collisions return ph.extHash(process) } hc, _ := model.HashCode(process) return hc } ================================================ FILE: internal/uimodel/converter/v1/json/process_hashtable_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package json import ( "testing" "github.com/stretchr/testify/assert" "github.com/jaegertracing/jaeger-idl/model/v1" ) func TestProcessHashtable(t *testing.T) { ht := &processHashtable{} p1 := model.NewProcess("s1", []model.KeyValue{ model.String("ip", "1.2.3.4"), model.String("host", "google.com"), }) // same process but with different order of tags p1dup := model.NewProcess("s1", []model.KeyValue{ model.String("host", "google.com"), model.String("ip", "1.2.3.4"), }) p2 := model.NewProcess("s2", []model.KeyValue{ model.String("host", "facebook.com"), }) assert.Equal(t, "p1", ht.getKey(p1)) assert.Equal(t, "p1", ht.getKey(p1)) assert.Equal(t, "p1", ht.getKey(p1dup)) assert.Equal(t, "p2", ht.getKey(p2)) expectedMapping := map[string]*model.Process{ "p1": p1, "p2": p2, } assert.Equal(t, expectedMapping, ht.getMapping()) } func TestProcessHashtableCollision(t *testing.T) { ht := &processHashtable{} // hash all processes to the same number ht.extHash = func(*model.Process) uint64 { return 42 } p1 := model.NewProcess("s1", []model.KeyValue{ model.String("host", "google.com"), }) p2 := model.NewProcess("s2", []model.KeyValue{ model.String("host", "facebook.com"), }) assert.Equal(t, "p1", ht.getKey(p1)) assert.Equal(t, "p2", ht.getKey(p2)) } ================================================ FILE: internal/uimodel/converter/v1/json/sampling.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package json import ( "bytes" "strings" "github.com/gogo/protobuf/jsonpb" "github.com/jaegertracing/jaeger-idl/proto-gen/api_v2" ) // SamplingStrategyResponseToJSON defines the official way to generate // a JSON response from /sampling endpoints. func SamplingStrategyResponseToJSON(protoObj *api_v2.SamplingStrategyResponse) (string, error) { // For backwards compatibility with Thrift-to-JSON encoding, // we want the output to include "strategyType":"PROBABILISTIC" when appropriate. // However, due to design oversight, the enum value for PROBABILISTIC is 0, so // we need to set EmitDefaults=true. This in turns causes null fields to be emitted too, // so we take care of them below. jsonpbMarshaler := jsonpb.Marshaler{ EmitDefaults: true, } str, err := jsonpbMarshaler.MarshalToString(protoObj) if err != nil { return "", err } // Because we set EmitDefaults, jsonpb will also render null entries, so we remove them here. str = strings.ReplaceAll(str, `"probabilisticSampling":null,`, "") str = strings.ReplaceAll(str, `,"rateLimitingSampling":null`, "") str = strings.ReplaceAll(str, `,"operationSampling":null`, "") return str, nil } // SamplingStrategyResponseFromJSON is the official way to parse strategy in JSON. func SamplingStrategyResponseFromJSON(json []byte) (*api_v2.SamplingStrategyResponse, error) { var obj api_v2.SamplingStrategyResponse if err := jsonpb.Unmarshal(bytes.NewReader(json), &obj); err != nil { return nil, err } return &obj, nil } ================================================ FILE: internal/uimodel/converter/v1/json/sampling_test.go ================================================ // Copyright (c) 2023 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package json import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaegertracing/jaeger-idl/proto-gen/api_v2" apiv1 "github.com/jaegertracing/jaeger-idl/thrift-gen/sampling" thriftconv "github.com/jaegertracing/jaeger/internal/converter/thrift/jaeger" ) func TestSamplingStrategyResponseToJSON_Error(t *testing.T) { _, err := SamplingStrategyResponseToJSON(nil) require.Error(t, err) } // TestSamplingStrategyResponseToJSON verifies that the function outputs // the same string as Thrift-based JSON marshaler. func TestSamplingStrategyResponseToJSON(t *testing.T) { t.Run("probabilistic", func(t *testing.T) { s := &apiv1.SamplingStrategyResponse{ StrategyType: apiv1.SamplingStrategyType_PROBABILISTIC, ProbabilisticSampling: &apiv1.ProbabilisticSamplingStrategy{ SamplingRate: 0.42, }, } compareProtoAndThriftJSON(t, s) }) t.Run("rateLimiting", func(t *testing.T) { s := &apiv1.SamplingStrategyResponse{ StrategyType: apiv1.SamplingStrategyType_RATE_LIMITING, RateLimitingSampling: &apiv1.RateLimitingSamplingStrategy{ MaxTracesPerSecond: 42, }, } compareProtoAndThriftJSON(t, s) }) t.Run("operationSampling", func(t *testing.T) { a := 11.2 // we need a pointer to value s := &apiv1.SamplingStrategyResponse{ OperationSampling: &apiv1.PerOperationSamplingStrategies{ DefaultSamplingProbability: 0.42, DefaultUpperBoundTracesPerSecond: &a, DefaultLowerBoundTracesPerSecond: 2, PerOperationStrategies: []*apiv1.OperationSamplingStrategy{ { Operation: "foo", ProbabilisticSampling: &apiv1.ProbabilisticSamplingStrategy{ SamplingRate: 0.42, }, }, { Operation: "bar", ProbabilisticSampling: &apiv1.ProbabilisticSamplingStrategy{ SamplingRate: 0.42, }, }, }, }, } compareProtoAndThriftJSON(t, s) }) } func compareProtoAndThriftJSON(t *testing.T, thriftObj *apiv1.SamplingStrategyResponse) { protoObj, err := thriftconv.ConvertSamplingResponseToDomain(thriftObj) require.NoError(t, err) s1, err := json.Marshal(thriftObj) require.NoError(t, err) s2, err := SamplingStrategyResponseToJSON(protoObj) require.NoError(t, err) assert.Equal(t, string(s1), s2) } func TestSamplingStrategyResponseFromJSON(t *testing.T) { _, err := SamplingStrategyResponseFromJSON([]byte("broken")) require.Error(t, err) s1 := &api_v2.SamplingStrategyResponse{ StrategyType: api_v2.SamplingStrategyType_PROBABILISTIC, ProbabilisticSampling: &api_v2.ProbabilisticSamplingStrategy{ SamplingRate: 0.42, }, } jsonData, err := SamplingStrategyResponseToJSON(s1) require.NoError(t, err) s2, err := SamplingStrategyResponseFromJSON([]byte(jsonData)) require.NoError(t, err) assert.Equal(t, s1.GetStrategyType(), s2.GetStrategyType()) assert.Equal(t, s1.GetProbabilisticSampling(), s2.GetProbabilisticSampling()) } ================================================ FILE: internal/uimodel/doc.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 // Package json defines the external JSON representation for Jaeger traces. package uimodel ================================================ FILE: internal/uimodel/empty_test.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package uimodel import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: internal/uimodel/model.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // Copyright (c) 2017 Uber Technologies, Inc. // SPDX-License-Identifier: Apache-2.0 package uimodel // ReferenceType is the reference type of one span to another type ReferenceType string // TraceID is the shared trace ID of all spans in the trace. type TraceID string // SpanID is the id of a span type SpanID string // ProcessID is a hashed value of the Process struct that is unique within the trace. type ProcessID string // ValueType is the type of a value stored in KeyValue struct. type ValueType string const ( // ChildOf means a span is the child of another span ChildOf ReferenceType = "CHILD_OF" // FollowsFrom means a span follows from another span FollowsFrom ReferenceType = "FOLLOWS_FROM" // StringType indicates a string value stored in KeyValue StringType ValueType = "string" // BoolType indicates a Boolean value stored in KeyValue BoolType ValueType = "bool" // Int64Type indicates a 64bit signed integer value stored in KeyValue Int64Type ValueType = "int64" // Float64Type indicates a 64bit float value stored in KeyValue Float64Type ValueType = "float64" // BinaryType indicates an arbitrary byte array stored in KeyValue BinaryType ValueType = "binary" ) // Trace is a list of spans type Trace struct { TraceID TraceID `json:"traceID"` Spans []Span `json:"spans"` Processes map[ProcessID]Process `json:"processes"` Warnings []string `json:"warnings"` } // Span is a span denoting a piece of work in some infrastructure // When converting to UI model, ParentSpanID and Process should be dereferenced into // References and ProcessID, respectively. // When converting to ES model, ProcessID and Warnings should be omitted. Even if // included, ES with dynamic settings off will automatically ignore unneeded fields. type Span struct { TraceID TraceID `json:"traceID"` SpanID SpanID `json:"spanID"` ParentSpanID SpanID `json:"parentSpanID,omitempty"` // deprecated Flags uint32 `json:"flags,omitempty"` OperationName string `json:"operationName"` References []Reference `json:"references"` StartTime uint64 `json:"startTime"` // microseconds since Unix epoch Duration uint64 `json:"duration"` // microseconds Tags []KeyValue `json:"tags"` Logs []Log `json:"logs"` ProcessID ProcessID `json:"processID,omitempty"` Process *Process `json:"process,omitempty"` Warnings []string `json:"warnings"` } // Reference is a reference from one span to another type Reference struct { RefType ReferenceType `json:"refType"` TraceID TraceID `json:"traceID"` SpanID SpanID `json:"spanID"` } // Process is the process emitting a set of spans type Process struct { ServiceName string `json:"serviceName"` Tags []KeyValue `json:"tags"` } // Log is a log emitted in a span type Log struct { Timestamp uint64 `json:"timestamp"` Fields []KeyValue `json:"fields"` } // KeyValue is a key-value pair with typed value. type KeyValue struct { Key string `json:"key"` Type ValueType `json:"type,omitempty"` Value any `json:"value"` } // DependencyLink shows dependencies between services type DependencyLink struct { Parent string `json:"parent"` Child string `json:"child"` CallCount uint64 `json:"callCount"` } // Operation defines the data in the operation response when query operation by service and span kind type Operation struct { Name string `json:"name"` SpanKind string `json:"spanKind"` } ================================================ FILE: internal/version/build.go ================================================ // Copyright (c) 2017 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package version import ( "fmt" "github.com/jaegertracing/jaeger/internal/metrics" ) var ( // commitFromGit is a constant representing the source version that // generated this build. It should be set during build via -ldflags. commitSHA string // versionFromGit is a constant representing the version tag that // generated this build. It should be set during build via -ldflags. latestVersion string // build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ') date string ) // Info holds build information. type Info struct { GitCommit string `json:"gitCommit"` GitVersion string `json:"gitVersion"` BuildDate string `json:"buildDate"` } // InfoMetrics hold a gauge whose tags include build information. type InfoMetrics struct { BuildInfo metrics.Gauge `metric:"build_info"` } // Get creates and initialized Info object func Get() Info { return Info{ GitCommit: commitSHA, GitVersion: latestVersion, BuildDate: date, } } // NewInfoMetrics returns a InfoMetrics func NewInfoMetrics(metricsFactory metrics.Factory) *InfoMetrics { var info InfoMetrics buildTags := map[string]string{ "revision": commitSHA, "version": latestVersion, "build_date": date, } metrics.Init(&info, metricsFactory, buildTags) info.BuildInfo.Update(1) return &info } func (i Info) String() string { return fmt.Sprintf( "git-commit=%s, git-version=%s, build-date=%s", i.GitCommit, i.GitVersion, i.BuildDate, ) } ================================================ FILE: internal/version/build_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package version import ( "testing" "github.com/stretchr/testify/assert" ) func TestGet(t *testing.T) { commitSHA = "foobar" latestVersion = "v1.2.3" date = "2024-01-04" info := Get() assert.Equal(t, commitSHA, info.GitCommit) assert.Equal(t, latestVersion, info.GitVersion) assert.Equal(t, date, info.BuildDate) } func TestString(t *testing.T) { commitSHA = "foobar" latestVersion = "v1.2.3" date = "2024-01-04" test := Info{ GitCommit: commitSHA, GitVersion: latestVersion, BuildDate: date, } expectedOutput := "git-commit=foobar, git-version=v1.2.3, build-date=2024-01-04" assert.Equal(t, expectedOutput, test.String()) } ================================================ FILE: internal/version/command.go ================================================ // Copyright (c) 2017 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package version import ( "encoding/json" "fmt" "log" "github.com/spf13/cobra" ) // Command creates version command func Command() *cobra.Command { info := Get() log.Println("application version:", info) return &cobra.Command{ Use: "version", Short: "Print the version.", Long: `Print the version and build information.`, RunE: func(cmd *cobra.Command, _ /* args */ []string) error { json, err := json.Marshal(info) if err != nil { return err } fmt.Fprint(cmd.OutOrStdout(), string(json)) return nil }, } } ================================================ FILE: internal/version/command_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package version import ( "bytes" "io" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewCommand(t *testing.T) { commitSHA = "foobar" latestVersion = "v1.2.3" date = "2024-01-04" cmd := Command() var b bytes.Buffer cmd.SetOut(&b) err := cmd.Execute() require.NoError(t, err) out, err := io.ReadAll(&b) require.NoError(t, err) expectedCommandOutput := `{"gitCommit":"foobar","gitVersion":"v1.2.3","buildDate":"2024-01-04"}` assert.Equal(t, expectedCommandOutput, string(out)) } ================================================ FILE: internal/version/handler.go ================================================ // Copyright (c) 2017 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package version import ( "encoding/json" "net/http" "go.uber.org/zap" ) // RegisterHandler registers version handler to /version func RegisterHandler(mu *http.ServeMux, logger *zap.Logger) { info := Get() jsonData, err := json.Marshal(info) if err != nil { logger.Fatal("Could not get Jaeger version", zap.Error(err)) } mu.HandleFunc("/version", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) w.Write(jsonData) }) } ================================================ FILE: internal/version/handler_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package version import ( "io" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" ) func TestRegisterHandler(t *testing.T) { commitSHA = "foobar" latestVersion = "v1.2.3" date = "2024-01-04" expectedJSON := `{"gitCommit":"foobar","gitVersion":"v1.2.3","buildDate":"2024-01-04"}` mockLogger := zap.NewNop() mux := http.NewServeMux() RegisterHandler(mux, mockLogger) server := httptest.NewServer(mux) defer server.Close() resp, err := http.Get(server.URL + "/version") require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) body, err := io.ReadAll(resp.Body) require.NoError(t, err) resp.Body.Close() assert.JSONEq(t, expectedJSON, string(body)) } ================================================ FILE: internal/version/package_test.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package version import ( "testing" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: monitoring/jaeger-mixin/README.md ================================================ # Prometheus monitoring mixin for Jaeger The Prometheus monitoring mixin for Jaeger provides a starting point for people wanting to monitor Jaeger using Prometheus, Alertmanager, and Grafana. To use it, you'll need [`jsonnet`](https://github.com/google/go-jsonnet) and [`jb` (jsonnet-bundler)](https://github.com/jsonnet-bundler/jsonnet-bundler). They can be installed using `go get`, as follows: ```console mkdir -p ~/bin && curl -sL https://github.com/google/go-jsonnet/releases/download/v0.17.0/go-jsonnet_0.17.0_Linux_x86_64.tar.gz | tar -C ~/bin/ -xzf - curl -sLo ~/bin/jb https://github.com/jsonnet-bundler/jsonnet-bundler/releases/download/v0.4.0/jb-linux-amd64 chmod +x ~/bin/jb ``` Your monitoring mixin can then be initialized as follows: ```console jb init jb install \ github.com/jaegertracing/jaeger/monitoring/jaeger-mixin@main \ github.com/grafana/jsonnet-libs/grafana-builder@master \ github.com/coreos/kube-prometheus/jsonnet/kube-prometheus@main ``` In the directory where your mixin was initialized, create a new `monitoring-setup.jsonnet`, specifying how your monitoring stack should look like: this file is yours, any customizations to Prometheus, Grafana, or Alertmanager should take place here. A simple example providing only the Jaeger dashboard for Grafana would be: ```jsonnet local jaegerDashboard = (import 'jaeger-mixin/mixin.libsonnet').grafanaDashboards; { ['dashboards-jaeger.json']: jaegerDashboard['jaeger.json'] } ``` The manifest files can be generated via the `jsonnet` command below. Once the command finishes, the file `manifests/dashboards-jaeger.json` should be available and can be loaded directly into Grafana. ```console jsonnet -J vendor -cm manifests/ monitoring-setup.jsonnet ``` An example producing the manifests for a complete monitoring stack is located in this directory, as `monitoring-setup.example.jsonnet`. The manifests include Prometheus, Grafana, and Alertmanager managed via the Prometheus Operator for Kubernetes. ```jsonnet local jaegerAlerts = (import 'jaeger-mixin/alerts.libsonnet').prometheusAlerts; local jaegerDashboard = (import 'jaeger-mixin/mixin.libsonnet').grafanaDashboards; local kp = (import 'kube-prometheus/main.libsonnet') + { values+:: { common+: { namespace: 'observability', }, grafana+: { dashboards+:: { 'my-dashboard.json': jaegerDashboard['jaeger.json'], }, }, }, exampleApplication: { prometheusRuleExample: { apiVersion: 'monitoring.coreos.com/v1', kind: 'PrometheusRule', metadata: { name: 'my-prometheus-rule', namespace: $.values.common.namespace, }, spec: jaegerAlerts, }, }, }; { ['00namespace-' + name + '.json']: kp.kubePrometheus[name] for name in std.objectFields(kp.kubePrometheus) } + { ['0prometheus-operator-' + name + '.json']: kp.prometheusOperator[name] for name in std.objectFields(kp.prometheusOperator) } + { ['node-exporter-' + name + '.json']: kp.nodeExporter[name] for name in std.objectFields(kp.nodeExporter) } + { ['kube-state-metrics-' + name + '.json']: kp.kubeStateMetrics[name] for name in std.objectFields(kp.kubeStateMetrics) } + { ['alertmanager-' + name + '.json']: kp.alertmanager[name] for name in std.objectFields(kp.alertmanager) } + { ['prometheus-' + name + '.json']: kp.prometheus[name] for name in std.objectFields(kp.prometheus) } + { ['prometheus-adapter-' + name + '.json']: kp.prometheusAdapter[name] for name in std.objectFields(kp.prometheusAdapter) } + { ['grafana-' + name + '.json']: kp.grafana[name] for name in std.objectFields(kp.grafana) } + { ['my-application-' + name + '.json']: kp.exampleApplication[name] for name in std.objectFields(kp.exampleApplication) } ``` The manifest files can be generated via `jsonnet` and passed directly to `kubectl`: ```console jsonnet -J vendor -cm manifests/ monitoring-setup.jsonnet kubectl apply -f manifests/ ``` The resulting manifests will include everything that is needed to have a Prometheus, Alertmanager, and Grafana instances. Whenever a new alert rule is needed, or a new dashboard has to be defined, change your `monitoring-setup.jsonnet`, re-generate and re-apply the manifests. Make sure your Prometheus setup is properly scraping the Jaeger components, either by creating a `ServiceMonitor` (and the backing `Service` objects), or via `PodMonitor` resources, like: ```console kubectl apply -f - < 1', 'for': '15m', labels: { severity: 'warning', }, annotations: { message: ||| {{ $labels.job }} {{ $labels.instance }} is experiencing {{ printf "%.2f" $value }}% HTTP errors. |||, }, }, { alert: 'JaegerRPCRequestsErrors', expr: percentErrs('jaeger_client_jaeger_rpc_http_requests', 'status_code=~"4xx|5xx"') + '> 1', 'for': '15m', labels: { severity: 'warning', }, annotations: { message: ||| {{ $labels.job }} {{ $labels.instance }} is experiencing {{ printf "%.2f" $value }}% RPC HTTP errors. |||, }, }, { alert: 'JaegerClientSpansDropped', expr: percentErrs('jaeger_reporter_spans', 'result=~"dropped|err"') + '> 1', 'for': '15m', labels: { severity: 'warning', }, annotations: { message: ||| service {{ $labels.job }} {{ $labels.instance }} is dropping {{ printf "%.2f" $value }}% spans. |||, }, }, { alert: 'JaegerAgentSpansDropped', expr: percentErrsWithTotal('jaeger_agent_reporter_batches_failures_total', 'jaeger_agent_reporter_batches_submitted_total') + '> 1', 'for': '15m', labels: { severity: 'warning', }, annotations: { message: ||| agent {{ $labels.job }} {{ $labels.instance }} is dropping {{ printf "%.2f" $value }}% spans. |||, }, }, { alert: 'JaegerCollectorDroppingSpans', expr: percentErrsWithTotal('jaeger_collector_spans_dropped_total', 'jaeger_collector_spans_received_total') + '> 1', 'for': '15m', labels: { severity: 'warning', }, annotations: { message: ||| collector {{ $labels.job }} {{ $labels.instance }} is dropping {{ printf "%.2f" $value }}% spans. |||, }, }, { alert: 'JaegerSamplingUpdateFailing', expr: percentErrs('jaeger_sampler_queries', 'result="err"') + '> 1', 'for': '15m', labels: { severity: 'warning', }, annotations: { message: ||| {{ $labels.job }} {{ $labels.instance }} is failing {{ printf "%.2f" $value }}% in updating sampling policies. |||, }, }, { alert: 'JaegerThrottlingUpdateFailing', expr: percentErrs('jaeger_throttler_updates', 'result="err"') + '> 1', 'for': '15m', labels: { severity: 'warning', }, annotations: { message: ||| {{ $labels.job }} {{ $labels.instance }} is failing {{ printf "%.2f" $value }}% in updating throttling policies. |||, }, }, { alert: 'JaegerQueryReqsFailing', expr: percentErrs('jaeger_query_requests_total', 'result="err"') + '> 1', 'for': '15m', labels: { severity: 'warning', }, annotations: { message: ||| {{ $labels.job }} {{ $labels.instance }} is seeing {{ printf "%.2f" $value }}% query errors on {{ $labels.operation }}. |||, }, }], }, ], }, } ================================================ FILE: monitoring/jaeger-mixin/dashboard-for-grafana.json ================================================ { "annotations": { "list": [ ] }, "editable": true, "gnetId": null, "graphTooltip": 0, "hideControls": false, "links": [ ], "refresh": "10s", "rows": [ { "collapse": false, "height": "250px", "panels": [ { "aliasColors": { }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "fill": 10, "id": 1, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 0, "links": [ ], "nullPointMode": "null as zero", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ ], "spaceLength": 10, "span": 6, "stack": true, "steppedLine": false, "targets": [ { "expr": "sum(rate(otelcol_receiver_refused_spans_total[1m])) or vector(0)", "format": "time_series", "legendFormat": "error", "legendLink": null }, { "expr": "sum(rate(otelcol_receiver_accepted_spans_total[1m]))", "format": "time_series", "legendFormat": "success", "legendLink": null } ], "thresholds": [ ], "timeFrom": null, "timeShift": null, "title": "Span Ingest Rate", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [ ] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false } ] }, { "aliasColors": { }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "fill": 10, "id": 2, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 0, "links": [ ], "nullPointMode": "null as zero", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ ], "spaceLength": 10, "span": 6, "stack": true, "steppedLine": false, "targets": [ { "expr": "sum(rate(otelcol_receiver_refused_spans_total[1m])) by (receiver, transport) / (sum(rate(otelcol_receiver_accepted_spans_total[1m])) by (receiver, transport) + sum(rate(otelcol_receiver_refused_spans_total[1m])) by (receiver, transport)) or vector(0)", "format": "time_series", "legendFormat": "{{receiver}}-{{transport}}", "legendLink": null } ], "thresholds": [ ], "timeFrom": null, "timeShift": null, "title": "% Spans Refused", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [ ] }, "yaxes": [ { "format": "percentunit", "label": null, "logBase": 1, "max": 1, "min": 0, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false } ] } ], "repeat": null, "repeatIteration": null, "repeatRowId": null, "showTitle": true, "title": "Collector - Ingestion", "titleSize": "h6" }, { "collapse": false, "height": "250px", "panels": [ { "aliasColors": { }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "fill": 10, "id": 3, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 0, "links": [ ], "nullPointMode": "null as zero", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ ], "spaceLength": 10, "span": 6, "stack": true, "steppedLine": false, "targets": [ { "expr": "sum(rate(otelcol_exporter_send_failed_spans_total[1m])) or vector(0)", "format": "time_series", "legendFormat": "error", "legendLink": null }, { "expr": "sum(rate(otelcol_exporter_sent_spans_total[1m]))", "format": "time_series", "legendFormat": "success", "legendLink": null } ], "thresholds": [ ], "timeFrom": null, "timeShift": null, "title": "Span Export Rate", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [ ] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false } ] }, { "aliasColors": { }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "fill": 10, "id": 4, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 0, "links": [ ], "nullPointMode": "null as zero", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ ], "spaceLength": 10, "span": 6, "stack": true, "steppedLine": false, "targets": [ { "expr": "(sum(rate(otelcol_exporter_sent_spans_total[1m])) by (exporter) / (sum(rate(otelcol_exporter_sent_spans_total[1m])) by (exporter) + sum(rate(otelcol_exporter_send_failed_spans_total[1m])) by (exporter))) * 100 or vector(0)", "format": "time_series", "legendFormat": "{{exporter}}", "legendLink": null } ], "thresholds": [ ], "timeFrom": null, "timeShift": null, "title": "Export Success Rate %", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [ ] }, "yaxes": [ { "format": "percent", "label": null, "logBase": 1, "max": 100, "min": 0, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false } ] } ], "repeat": null, "repeatIteration": null, "repeatRowId": null, "showTitle": true, "title": "Collector - Export", "titleSize": "h6" }, { "collapse": false, "height": "250px", "panels": [ { "aliasColors": { }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "fill": 10, "id": 5, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 0, "links": [ ], "nullPointMode": "null as zero", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ ], "spaceLength": 10, "span": 6, "stack": true, "steppedLine": false, "targets": [ { "expr": "sum(rate(jaeger_storage_requests_total[1m])) by (operation, result)", "format": "time_series", "legendFormat": "{{operation}} - {{result}}", "legendLink": null } ], "thresholds": [ ], "timeFrom": null, "timeShift": null, "title": "Storage Request Rate", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [ ] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false } ] }, { "aliasColors": { }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "fill": 10, "id": 6, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 0, "links": [ ], "nullPointMode": "null as zero", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ ], "spaceLength": 10, "span": 6, "stack": true, "steppedLine": false, "targets": [ { "expr": "histogram_quantile(0.99, sum(rate(jaeger_storage_latency_seconds_bucket[1m])) by (le, operation))", "format": "time_series", "legendFormat": "{{operation}}", "legendLink": null } ], "thresholds": [ ], "timeFrom": null, "timeShift": null, "title": "Storage Latency - P99", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [ ] }, "yaxes": [ { "format": "s", "label": null, "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false } ] } ], "repeat": null, "repeatIteration": null, "repeatRowId": null, "showTitle": true, "title": "Storage", "titleSize": "h6" }, { "collapse": false, "height": "250px", "panels": [ { "aliasColors": { }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "fill": 10, "id": 7, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 0, "links": [ ], "nullPointMode": "null as zero", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ ], "spaceLength": 10, "span": 6, "stack": true, "steppedLine": false, "targets": [ { "expr": "sum(rate(http_server_request_duration_seconds_count{http_route=\"/api/traces\"}[1m])) by (http_response_status_code)", "format": "time_series", "legendFormat": "status {{http_response_status_code}}", "legendLink": null } ], "thresholds": [ ], "timeFrom": null, "timeShift": null, "title": "Query Request Rate", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [ ] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false } ] }, { "aliasColors": { }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "fill": 10, "id": 8, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 0, "links": [ ], "nullPointMode": "null as zero", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ ], "spaceLength": 10, "span": 6, "stack": true, "steppedLine": false, "targets": [ { "expr": "histogram_quantile(0.99, sum(rate(http_server_request_duration_seconds_bucket{http_route=\"/api/traces\"}[1m])) by (le))", "format": "time_series", "legendFormat": "P99", "legendLink": null } ], "thresholds": [ ], "timeFrom": null, "timeShift": null, "title": "Query Latency - P99", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [ ] }, "yaxes": [ { "format": "s", "label": null, "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false } ] } ], "repeat": null, "repeatIteration": null, "repeatRowId": null, "showTitle": true, "title": "Query", "titleSize": "h6" }, { "collapse": false, "height": "250px", "panels": [ { "aliasColors": { }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "fill": 10, "id": 9, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 0, "links": [ ], "nullPointMode": "null as zero", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ ], "spaceLength": 10, "span": 6, "stack": true, "steppedLine": false, "targets": [ { "expr": "rate(otelcol_process_cpu_seconds_total[1m])", "format": "time_series", "legendFormat": "CPU", "legendLink": null } ], "thresholds": [ ], "timeFrom": null, "timeShift": null, "title": "CPU Usage", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [ ] }, "yaxes": [ { "format": "percentunit", "label": null, "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false } ] }, { "aliasColors": { }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "fill": 10, "id": 10, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 0, "links": [ ], "nullPointMode": "null as zero", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ ], "spaceLength": 10, "span": 6, "stack": true, "steppedLine": false, "targets": [ { "expr": "otelcol_process_memory_rss_bytes", "format": "time_series", "legendFormat": "Memory", "legendLink": null } ], "thresholds": [ ], "timeFrom": null, "timeShift": null, "title": "Memory RSS", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [ ] }, "yaxes": [ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false } ] } ], "repeat": null, "repeatIteration": null, "repeatRowId": null, "showTitle": true, "title": "System", "titleSize": "h6" } ], "schemaVersion": 14, "style": "dark", "tags": [ ], "templating": { "list": [ { "current": { "text": "default", "value": "default" }, "hide": 0, "label": "Data source", "name": "datasource", "options": [ ], "query": "prometheus", "refresh": 1, "regex": "", "type": "datasource" } ] }, "time": { "from": "now-1h", "to": "now" }, "timepicker": { "refresh_intervals": [ "5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d" ], "time_options": [ "5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d" ] }, "timezone": "utc", "title": "Jaeger v2", "uid": "", "version": 0 } ================================================ FILE: monitoring/jaeger-mixin/dashboards.libsonnet ================================================ local g = (import 'grafana-builder/grafana.libsonnet'); { grafanaDashboards+: { 'jaeger.json': g.dashboard('Jaeger v2') .addRow( g.row('Collector - Ingestion') .addPanel( g.panel('Span Ingest Rate') + g.queryPanel( [ 'sum(rate(otelcol_receiver_refused_spans_total[1m])) or vector(0)', 'sum(rate(otelcol_receiver_accepted_spans_total[1m]))', ], [ 'error', 'success', ] ) + g.stack ) .addPanel( g.panel('% Spans Refused') + g.queryPanel( 'sum(rate(otelcol_receiver_refused_spans_total[1m])) by (receiver, transport) / (sum(rate(otelcol_receiver_accepted_spans_total[1m])) by (receiver, transport) + sum(rate(otelcol_receiver_refused_spans_total[1m])) by (receiver, transport)) or vector(0)', '{{receiver}}-{{transport}}' ) + { yaxes: g.yaxes({ format: 'percentunit', max: 1 }) } + g.stack ) ) .addRow( g.row('Collector - Export') .addPanel( g.panel('Span Export Rate') + g.queryPanel( [ 'sum(rate(otelcol_exporter_send_failed_spans_total[1m])) or vector(0)', 'sum(rate(otelcol_exporter_sent_spans_total[1m]))', ], [ 'error', 'success', ] ) + g.stack ) .addPanel( g.panel('Export Success Rate %') + g.queryPanel( '(sum(rate(otelcol_exporter_sent_spans_total[1m])) by (exporter) / (sum(rate(otelcol_exporter_sent_spans_total[1m])) by (exporter) + sum(rate(otelcol_exporter_send_failed_spans_total[1m])) by (exporter))) * 100 or vector(0)', '{{exporter}}' ) + { yaxes: g.yaxes({ format: 'percent', max: 100 }) } + g.stack ) ) .addRow( g.row('Storage') .addPanel( g.panel('Storage Request Rate') + g.queryPanel( 'sum(rate(jaeger_storage_requests_total[1m])) by (operation, result)', '{{operation}} - {{result}}' ) + g.stack ) .addPanel( g.panel('Storage Latency - P99') + g.queryPanel( 'histogram_quantile(0.99, sum(rate(jaeger_storage_latency_seconds_bucket[1m])) by (le, operation))', '{{operation}}' ) + { yaxes: g.yaxes({ format: 's' }) } + g.stack ) ) .addRow( g.row('Query') .addPanel( g.panel('Query Request Rate') + g.queryPanel( 'sum(rate(http_server_request_duration_seconds_count{http_route="/api/traces"}[1m])) by (http_response_status_code)', 'status {{http_response_status_code}}' ) + g.stack ) .addPanel( g.panel('Query Latency - P99') + g.queryPanel( 'histogram_quantile(0.99, sum(rate(http_server_request_duration_seconds_bucket{http_route="/api/traces"}[1m])) by (le))', 'P99' ) + { yaxes: g.yaxes({ format: 's' }) } + g.stack ) ) .addRow( g.row('System') .addPanel( g.panel('CPU Usage') + g.queryPanel( 'rate(otelcol_process_cpu_seconds_total[1m])', 'CPU' ) + { yaxes: g.yaxes({ format: 'percentunit' }) } + g.stack ) .addPanel( g.panel('Memory RSS') + g.queryPanel( 'otelcol_process_memory_rss_bytes', 'Memory' ) + { yaxes: g.yaxes({ format: 'bytes' }) } + g.stack ) ), }, } ================================================ FILE: monitoring/jaeger-mixin/jsonnetfile.json ================================================ { "dependencies": [ { "name": "grafana-builder", "source": { "git": { "remote": "https://github.com/grafana/jsonnet-libs", "subdir": "grafana-builder" } }, "version": "master" } ] } ================================================ FILE: monitoring/jaeger-mixin/mixin.libsonnet ================================================ (import 'dashboards.libsonnet') + (import 'alerts.libsonnet') ================================================ FILE: monitoring/jaeger-mixin/monitoring-setup.example.jsonnet ================================================ local jaegerAlerts = (import 'jaeger-mixin/alerts.libsonnet').prometheusAlerts; local jaegerDashboard = (import 'jaeger-mixin/mixin.libsonnet').grafanaDashboards; local kp = (import 'kube-prometheus/main.libsonnet') + { values+:: { common+: { namespace: 'observability', }, grafana+: { dashboards+:: { 'my-dashboard.json': jaegerDashboard['jaeger.json'], }, }, }, exampleApplication: { prometheusRuleExample: { apiVersion: 'monitoring.coreos.com/v1', kind: 'PrometheusRule', metadata: { name: 'my-prometheus-rule', namespace: $.values.common.namespace, }, spec: jaegerAlerts, }, }, }; { ['00namespace-' + name + '.json']: kp.kubePrometheus[name] for name in std.objectFields(kp.kubePrometheus) } + { ['0prometheus-operator-' + name + '.json']: kp.prometheusOperator[name] for name in std.objectFields(kp.prometheusOperator) } + { ['node-exporter-' + name + '.json']: kp.nodeExporter[name] for name in std.objectFields(kp.nodeExporter) } + { ['kube-state-metrics-' + name + '.json']: kp.kubeStateMetrics[name] for name in std.objectFields(kp.kubeStateMetrics) } + { ['alertmanager-' + name + '.json']: kp.alertmanager[name] for name in std.objectFields(kp.alertmanager) } + { ['prometheus-' + name + '.json']: kp.prometheus[name] for name in std.objectFields(kp.prometheus) } + { ['prometheus-adapter-' + name + '.json']: kp.prometheusAdapter[name] for name in std.objectFields(kp.prometheusAdapter) } + { ['grafana-' + name + '.json']: kp.grafana[name] for name in std.objectFields(kp.grafana) } + { ['my-application-' + name + '.json']: kp.exampleApplication[name] for name in std.objectFields(kp.exampleApplication) } ================================================ FILE: monitoring/jaeger-mixin/prometheus_alerts.yml ================================================ "groups": - "name": "jaeger_alerts" "rules": - "alert": "JaegerHTTPServerErrs" "annotations": "message": | {{ $labels.job }} {{ $labels.instance }} is experiencing {{ printf "%.2f" $value }}% HTTP errors. "expr": "100 * sum(rate(jaeger_agent_http_server_errors_total[1m])) by (instance, job, namespace) / sum(rate(jaeger_agent_http_server_total[1m])) by (instance, job, namespace)> 1" "for": "15m" "labels": "severity": "warning" - "alert": "JaegerRPCRequestsErrors" "annotations": "message": | {{ $labels.job }} {{ $labels.instance }} is experiencing {{ printf "%.2f" $value }}% RPC HTTP errors. "expr": "100 * sum(rate(jaeger_client_jaeger_rpc_http_requests{status_code=~\"4xx|5xx\"}[1m])) by (instance, job, namespace) / sum(rate(jaeger_client_jaeger_rpc_http_requests[1m])) by (instance, job, namespace)> 1" "for": "15m" "labels": "severity": "warning" - "alert": "JaegerClientSpansDropped" "annotations": "message": | service {{ $labels.job }} {{ $labels.instance }} is dropping {{ printf "%.2f" $value }}% spans. "expr": "100 * sum(rate(jaeger_reporter_spans{result=~\"dropped|err\"}[1m])) by (instance, job, namespace) / sum(rate(jaeger_reporter_spans[1m])) by (instance, job, namespace)> 1" "for": "15m" "labels": "severity": "warning" - "alert": "JaegerAgentSpansDropped" "annotations": "message": | agent {{ $labels.job }} {{ $labels.instance }} is dropping {{ printf "%.2f" $value }}% spans. "expr": "100 * sum(rate(jaeger_agent_reporter_batches_failures_total[1m])) by (instance, job, namespace) / sum(rate(jaeger_agent_reporter_batches_submitted_total[1m])) by (instance, job, namespace)> 1" "for": "15m" "labels": "severity": "warning" - "alert": "JaegerCollectorDroppingSpans" "annotations": "message": | collector {{ $labels.job }} {{ $labels.instance }} is dropping {{ printf "%.2f" $value }}% spans. "expr": "100 * sum(rate(jaeger_collector_spans_dropped_total[1m])) by (instance, job, namespace) / sum(rate(jaeger_collector_spans_received_total[1m])) by (instance, job, namespace)> 1" "for": "15m" "labels": "severity": "warning" - "alert": "JaegerSamplingUpdateFailing" "annotations": "message": | {{ $labels.job }} {{ $labels.instance }} is failing {{ printf "%.2f" $value }}% in updating sampling policies. "expr": "100 * sum(rate(jaeger_sampler_queries{result=\"err\"}[1m])) by (instance, job, namespace) / sum(rate(jaeger_sampler_queries[1m])) by (instance, job, namespace)> 1" "for": "15m" "labels": "severity": "warning" - "alert": "JaegerThrottlingUpdateFailing" "annotations": "message": | {{ $labels.job }} {{ $labels.instance }} is failing {{ printf "%.2f" $value }}% in updating throttling policies. "expr": "100 * sum(rate(jaeger_throttler_updates{result=\"err\"}[1m])) by (instance, job, namespace) / sum(rate(jaeger_throttler_updates[1m])) by (instance, job, namespace)> 1" "for": "15m" "labels": "severity": "warning" - "alert": "JaegerQueryReqsFailing" "annotations": "message": | {{ $labels.job }} {{ $labels.instance }} is seeing {{ printf "%.2f" $value }}% query errors on {{ $labels.operation }}. "expr": "100 * sum(rate(jaeger_query_requests_total{result=\"err\"}[1m])) by (instance, job, namespace, operation) / sum(rate(jaeger_query_requests_total[1m])) by (instance, job, namespace, operation)> 1" "for": "15m" "labels": "severity": "warning" ================================================ FILE: monitoring/jaeger-mixin/prometheus_alerts_v2.yml ================================================ groups: - name: jaeger_alerts rules: - alert: OtelHttpServerErrors annotations: message: | {{ $labels.job }} {{ $labels.instance }} is experiencing {{ printf "%.2f" $value }}% HTTP errors. expr: | 100 * sum(rate(otelcol_http_server_duration_count{http_status_code=~"5.."}[1m])) by (instance, job) / sum(rate(otelcol_http_server_duration_count[1m])) by (instance, job) > 1 for: 15m labels: severity: warning - alert: OtelExporterQueueFull annotations: message: | {{ $labels.job }} {{ $labels.instance }} exporter queue is at {{ printf "%.2f" $value }} items (over 80% capacity). expr: | 100 * otelcol_exporter_queue_size / otelcol_exporter_queue_capacity > 80 for: 15m labels: severity: warning - alert: OtelHighMemoryUsage annotations: message: | {{ $labels.job }} {{ $labels.instance }} memory usage is high at {{ humanize $value }} bytes. expr: | otelcol_process_memory_rss > 100000000 for: 15m labels: severity: warning - alert: OtelHighCpuUsage annotations: message: | {{ $labels.job }} {{ $labels.instance }} CPU usage is high ({{ printf "%.2f" $value }} seconds of CPU time in 5m). expr: | rate(otelcol_process_cpu_seconds[5m]) > 0.8 for: 15m labels: severity: warning - alert: OtelProcessorBatchHighCardinality annotations: message: | {{ $labels.job }} {{ $labels.instance }} has high metadata cardinality ({{ printf "%.0f" $value }} combinations). expr: | otelcol_processor_batch_metadata_cardinality > 1000 for: 15m labels: severity: warning ================================================ FILE: ports/ports.go ================================================ // Copyright (c) 2019 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package ports import ( "strconv" ) const ( // CollectorV2GRPC is the HTTP port for remote sampling extension CollectorV2SamplingHTTP = 5778 // CollectorV2GRPC is the gRPC port for remote sampling extension CollectorV2SamplingGRPC = 5779 // CollectorV2HealthChecks is the port for health checks extension CollectorV2HealthChecks = 13133 // QueryGRPC is the default port of GRPC requests for Query trace retrieval QueryGRPC = 16685 // QueryHTTP is the default port for UI and Query API (e.g. /api/* endpoints) QueryHTTP = 16686 // MCPHTTP is the default port for MCP (Model Context Protocol) server HTTP endpoint MCPHTTP = 16687 // RemoteStorageGRPC is the default port of GRPC requests for Remote Storage RemoteStorageGRPC = 17271 // RemoteStorageHTTP is the default admin HTTP port (health check, metrics, etc.) RemoteStorageAdminHTTP = 17270 ) // PortToHostPort converts the port into a host:port address string func PortToHostPort(port int) string { return ":" + strconv.Itoa(port) } ================================================ FILE: ports/ports_test.go ================================================ // Copyright (c) 2020 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 package ports import ( "testing" "github.com/stretchr/testify/assert" "github.com/jaegertracing/jaeger/internal/testutils" ) func TestPortToHostPort(t *testing.T) { assert.Equal(t, ":42", PortToHostPort(42)) } func TestMain(m *testing.M) { testutils.VerifyGoLeaks(m) } ================================================ FILE: renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:best-practices", ":gitSignOff" ], "dependencyDashboard": true, "dependencyDashboardHeader": "Project settings are at https://developer.mend.io/", "labels": [ "changelog:dependencies" ], "postUpdateOptions": [ "gomodTidy", "gomodUpdateImportPaths" ], "suppressNotifications": [ "prEditedNotification" ], "schedule": [ "on the first day of the month" ], "packageRules": [ { "matchFileNames": [ "docker-compose/**/docker-compose.y*ml" ], "matchUpdateTypes": [ "major", "patch", "digest" ], "enabled": false }, { "matchManagers": [ "github-actions" ], "groupName": "github-actions deps" }, { "matchManagers": [ "github-actions" ], "matchUpdateTypes": [ "patch", "digest" ], "enabled": false }, { "groupName": "All OTEL SDK + contrib packages", "groupSlug": "go-otel-sdk", "matchDatasources": [ "go" ], "matchPackageNames": [ "go.opentelemetry.io/otel/**", "go.opentelemetry.io/contrib/**", "github.com/open-telemetry/opentelemetry-go-contrib/**" ], "schedule": [ "on friday" ] }, { "groupName": "All OTEL Collector packages", "matchManagers": [ "gomod" ], "matchPackageNames": [ "go.opentelemetry.io/collector{/,}**" ], "schedule": [ "on friday" ] }, { "groupName": "All OTEL Collector contrib packages", "matchManagers": [ "gomod" ], "matchPackageNames": [ "github.com/open-telemetry/opentelemetry-collector-contrib{/,}**" ], "schedule": [ "on friday" ] }, { "groupName": "All google.golang.org packages", "matchManagers": [ "gomod" ], "matchSourceUrls": [ "google.golang.org{/,}**" ] }, { "groupName": "All golang.org/x packages", "matchManagers": [ "gomod" ], "matchPackageNames": [ "golang.org/x{/,}**" ] }, { "groupName": "All github.com/prometheus packages", "matchManagers": [ "gomod" ], "matchPackageNames": [ "github.com/prometheus{/,}**" ] }, { "groupName": "Exclude frequent tools upgrades", "matchDatasources": [ "go" ], "matchPackageNames": [ "github.com/vektra/mockery/**" ], "matchUpdateTypes": [ "patch" ], "enabled": false }, { "groupName": "All Jaeger Docker images", "matchDatasources": [ "docker" ], "matchPackageNames": [ "cr.jaegertracing.io/jaegertracing/jaeger", "cr.jaegertracing.io/jaegertracing/jaeger-tracegen" ] }, { "groupName": "All ClickHouse packages", "matchManagers": [ "gomod" ], "matchPackageNames": [ "github.com/ClickHouse/ch-go", "github.com/ClickHouse/clickhouse-go/v2" ] }, { "groupName": "OpenSearch 3.x docker images", "matchDatasources": [ "docker" ], "matchPackageNames": [ "opensearchproject/opensearch" ], "matchFileNames": [ "docker-compose/opensearch/v3/docker-compose.yml", "docker-compose/monitor/docker-compose-opensearch.yml" ], "allowedVersions": "3.x" }, { "groupName": "Elasticsearch 9.x docker images", "matchDatasources": [ "docker" ], "matchPackageNames": [ "docker.elastic.co/elasticsearch/elasticsearch" ], "matchFileNames": [ "docker-compose/elasticsearch/v9/docker-compose.yml", "docker-compose/monitor/docker-compose-elasticsearch.yml" ], "allowedVersions": "9.x" } ] } ================================================ FILE: scripts/build/build-all-in-one-image.sh ================================================ #!/bin/bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 set -euf -o pipefail print_help() { echo "Usage: $0 [-D] [-h] [-l] [-o] [-p platforms]" echo " -D: Disable building of images with debugger" echo " -h: Print help" echo " -l: Enable local-only mode that only pushes images to local registry" echo " -o: overwrite image in the target remote repository even if the semver tag already exists" echo " -p: Comma-separated list of platforms to build for (default: all supported)" exit 1 } add_debugger='Y' platforms="$(make echo-linux-platforms)" FLAGS=() BINARY="jaeger" # this script doesn't use BRANCH and GITHUB_SHA itself, but its dependency scripts do. export BRANCH=${BRANCH?'env var is required'} export GITHUB_SHA=${GITHUB_SHA:-$(git rev-parse HEAD)} while getopts "Dhlop:" opt; do case "${opt}" in D) add_debugger='N' ;; l) # in the local-only mode the images will only be pushed to local registry FLAGS=("${FLAGS[@]}" -l) ;; o) FLAGS=("${FLAGS[@]}" -o) ;; p) platforms=${OPTARG} ;; ?) print_help ;; esac done # remove flags, leave only positional args shift $((OPTIND - 1)) # Only build the jaeger binary export HEALTHCHECK_V2=true set -x # Set default GOARCH variable to the host GOARCH, the target architecture can # be overrided by passing architecture value to the script: # `GOARCH= ./scripts/build/build-all-in-one-image.sh`. GOARCH=${GOARCH:-$(go env GOARCH)} image="jaegertracing/${BINARY}" make build-ui run_integration_test() { local image_name="$1" CID=$(docker run -d -p 16686:16686 -p 13133:13133 -p 5778:5778 "${image_name}:${GITHUB_SHA}") if ! make all-in-one-integration-test ; then echo "---- integration test failed unexpectedly ----" echo "--- check the docker log below for details ---" echo "::group::docker logs" docker logs "$CID" echo "::endgroup::" docker kill "$CID" exit 1 fi docker kill "$CID" } build_test_upload() { # Loop through each platform (separated by commas) for platform in $(echo "$platforms" | tr ',' ' '); do arch=${platform##*/} # Remove everything before the last slash make "build-${BINARY}" GOOS=linux GOARCH="${arch}" done make create-baseimg LINUX_PLATFORMS="$platforms" # build all-in-one image locally for integration test (the explicit -l switch) bash scripts/build/build-upload-a-docker-image.sh -l -b -c "${BINARY}" -d "cmd/${BINARY}" -p "${platforms}" -t release run_integration_test "localhost:5000/$image" # build all-in-one image and upload to dockerhub/quay.io bash scripts/build/build-upload-a-docker-image.sh "${FLAGS[@]}" -b -c "${BINARY}" -d "cmd/${BINARY}" -p "${platforms}" -t release } build_test_upload_with_debugger() { make "build-${BINARY}" GOOS=linux GOARCH="$GOARCH" DEBUG_BINARY=1 make create-baseimg-debugimg LINUX_PLATFORMS="$platforms" # build locally for integration test (the -l switch) bash scripts/build/build-upload-a-docker-image.sh -l -b -c "${BINARY}-debug" -d "cmd/${BINARY}" -p "${platforms}" -t release -t debug run_integration_test "localhost:5000/${image}-debug" # build & upload official image bash scripts/build/build-upload-a-docker-image.sh "${FLAGS[@]}" -b -c "${BINARY}-debug" -d "cmd/${BINARY}" -p "${platforms}" -t debug } build_test_upload if [[ "${add_debugger}" == "Y" ]]; then build_test_upload_with_debugger fi ================================================ FILE: scripts/build/build-hotrod-image.sh ================================================ #!/bin/bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 set -exuf -o pipefail print_help() { echo "Usage: $0 [-h] [-l] [-o] [-p platforms] [-r runtime]" echo "-h: Print help" echo "-l: Enable local-only mode that only pushes images to local registry" echo "-o: overwrite image in the target remote repository even if the semver tag already exists" echo "-p: Comma-separated list of platforms to build for (default: all supported)" echo "-r: Runtime to test with (docker|k8s, default: docker)" exit 1 } docker_compose_file="./examples/hotrod/docker-compose.yml" platforms="$(make echo-linux-platforms)" current_platform="$(go env GOOS)/$(go env GOARCH)" binary="jaeger" FLAGS=() success="false" runtime="docker" while getopts "hlop:r:" opt; do case "${opt}" in l) # in the local-only mode the images will only be pushed to local registry FLAGS=("${FLAGS[@]}" -l) ;; o) FLAGS=("${FLAGS[@]}" -o) ;; p) platforms=${OPTARG} ;; r) case "${OPTARG}" in docker|k8s) runtime="${OPTARG}" ;; *) echo "Invalid runtime: ${OPTARG}. Use 'docker' or 'k8s'" >&2; exit 1 ;; esac ;; *) print_help ;; esac done set -x dump_logs() { local runtime=$1 local compose_file=$2 echo "::group:: Logs" if [ "$runtime" == "k8s" ]; then kubectl logs -l app.kubernetes.io/name=jaeger else docker compose -f "$compose_file" logs fi echo "::endgroup::" } teardown() { echo "::group::Tearing down..." if [[ "$success" == "false" ]]; then dump_logs "${runtime}" "${docker_compose_file}" fi if [[ "${runtime}" == "k8s" ]]; then if [[ -n "${HOTROD_PORT_FWD_PID:-}" ]]; then kill "$HOTROD_PORT_FWD_PID" || true fi if [[ -n "${JAEGER_PORT_FWD_PID:-}" ]]; then kill "$JAEGER_PORT_FWD_PID" || true fi helm uninstall jaeger --ignore-not-found || true helm uninstall prometheus --ignore-not-found || true else docker compose -f "$docker_compose_file" down fi echo "::endgroup::" } trap teardown EXIT make prepare-docker-buildx make create-baseimg LINUX_PLATFORMS="$platforms" # Build hotrod binary for each target platform (separated by commas) for platform in $(echo "$platforms" | tr ',' ' '); do # Extract the operating system from the platform string os=${platform%%/*} #remove everything after the last slash # Extract the architecture from the platform string arch=${platform##*/} # Remove everything before the last slash make build-examples GOOS="${os}" GOARCH="${arch}" done # Build hotrod image locally (-l) for integration test. # Note: hotrod's Dockerfile is different from main binaries, # so we do not pass flags like -b and -t. bash scripts/build/build-upload-a-docker-image.sh -l -c example-hotrod -d examples/hotrod -p "${current_platform}" # Build jaeger image locally (-l) for integration test make build-${binary} bash scripts/build/build-upload-a-docker-image.sh -l -b -c "${binary}" -d cmd/"${binary}" -p "${current_platform}" -t release if [[ "${runtime}" == "k8s" ]]; then if ! kubectl cluster-info >/dev/null 2>&1; then echo "Error: Cannot connect to Kubernetes cluster" exit 1 fi echo '::group:: run on Kubernetes' echo '::group:: Loading images into Kind cluster' docker pull localhost:5000/jaegertracing/jaeger:"${GITHUB_SHA}" docker pull localhost:5000/jaegertracing/example-hotrod:"${GITHUB_SHA}" # Get the actual cluster name CLUSTER_NAME=$(kind get clusters | head -n1) if [[ -n "$CLUSTER_NAME" ]]; then echo "Loading images into '$CLUSTER_NAME' cluster..." kind load docker-image localhost:5000/jaegertracing/jaeger:"${GITHUB_SHA}" --name "$CLUSTER_NAME" kind load docker-image localhost:5000/jaegertracing/example-hotrod:"${GITHUB_SHA}" --name "$CLUSTER_NAME" else echo "No Kind clusters found!" exit 1 fi bash ./examples/oci/deploy-all.sh local "${GITHUB_SHA}" kubectl wait --for=condition=available --timeout=180s deployment/jaeger-hotrod kubectl wait --for=condition=available --timeout=180s deployment/jaeger kubectl port-forward svc/jaeger-hotrod 8080:80 & HOTROD_PORT_FWD_PID=$! kubectl port-forward svc/jaeger-query 16686:16686 & JAEGER_PORT_FWD_PID=$! echo '::endgroup::' else echo '::group:: docker compose' JAEGER_VERSION=$GITHUB_SHA HOTROD_VERSION=$GITHUB_SHA REGISTRY="localhost:5000/" docker compose -f "$docker_compose_file" up -d echo '::endgroup::' fi if [[ "${runtime}" == "k8s" ]]; then HOTROD_URL="http://localhost:8080/hotrod" JAEGER_QUERY_URL="http://localhost:16686/jaeger" else HOTROD_URL="http://localhost:8080" JAEGER_QUERY_URL="http://localhost:16686" fi i=0 while [[ "$(curl -s -o /dev/null -w '%{http_code}' ${HOTROD_URL})" != "200" && $i -lt 30 ]]; do sleep 1 i=$((i+1)) done echo '::group:: check HTML' echo 'Check that home page contains text Rides On Demand' body=$(curl ${HOTROD_URL}) if [[ $body != *"Rides On Demand"* ]]; then echo "String \"Rides On Demand\" is not present on the index page" exit 1 fi echo '::endgroup::' response=$(curl -i -X POST "${HOTROD_URL}/dispatch?customer=123") TRACE_ID=$(echo "$response" | grep -Fi "Traceresponse:" | awk '{print $2}' | cut -d '-' -f 2) if [ -n "$TRACE_ID" ]; then echo "TRACE_ID is not empty: $TRACE_ID" else echo "TRACE_ID is empty" exit 1 fi EXPECTED_SPANS=35 MAX_RETRIES=30 SLEEP_INTERVAL=3 poll_jaeger() { local trace_id=$1 local url="${JAEGER_QUERY_URL}/api/traces/${trace_id}" curl -s "${url}" | jq '.data[0].spans | length' || echo "0" } # Poll Jaeger until trace with desired number of spans is loaded or we timeout. span_count=0 for ((i=1; i<=MAX_RETRIES; i++)); do span_count=$(poll_jaeger "${TRACE_ID}") if [[ "$span_count" -ge "$EXPECTED_SPANS" ]]; then echo "Trace found with $span_count spans." break fi echo "Retry $i/$MAX_RETRIES: Trace not found or insufficient spans ($span_count/$EXPECTED_SPANS). Retrying in $SLEEP_INTERVAL seconds..." sleep $SLEEP_INTERVAL done if [[ "$span_count" -lt "$EXPECTED_SPANS" ]]; then echo "Failed to find the trace with the expected number of spans within the timeout period." exit 1 fi success="true" # Ensure the image is published after successful test (maybe with -l flag if on a pull request). # This is where all those multi-platform binaries we built earlier are utilized. bash scripts/build/build-upload-a-docker-image.sh "${FLAGS[@]}" -c example-hotrod -d examples/hotrod -p "${platforms}" ================================================ FILE: scripts/build/build-upload-a-docker-image.sh ================================================ #!/bin/bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 set -euf -o pipefail print_help() { echo "Usage: $0 [-c] [-D] [-h] [-l] [-o] [-p platforms]" echo "-h: Print help" echo "-b: add base_image and debug_image arguments to the build command" echo "-c: name of the component to build" echo "-d: directory for the Dockerfile" echo "-f: override the name of the Dockerfile (-d still respected)" echo "-o: overwrite image in the target remote repository even if the semver tag already exists" echo "-p: Comma-separated list of platforms to build for (default: all supported)" echo "-t: Release target (release|debug) if required by the Dockerfile" exit 1 } echo "BRANCH=${BRANCH:?'expecting BRANCH env var'}" base_debug_img_arg="" docker_file_arg="Dockerfile" target_arg="" local_test_only='N' platforms="linux/$(go env GOARCH)" namespace="jaegertracing" overwrite='N' upload_readme='N' while getopts "bc:d:f:hlop:t:" opt; do # shellcheck disable=SC2220 # we don't need a *) case case "${opt}" in b) base_debug_img_arg="--build-arg base_image=localhost:5000/baseimg_alpine:latest --build-arg debug_image=localhost:5000/debugimg_alpine:latest " ;; c) component_name=${OPTARG} ;; d) dir_arg=${OPTARG} ;; f) docker_file_arg=${OPTARG} ;; l) local_test_only='Y' ;; o) overwrite='Y' ;; p) platforms=${OPTARG} ;; t) target_arg=${OPTARG} ;; ?) print_help ;; esac done set -x if [ -n "${target_arg}" ]; then target_arg="--target ${target_arg}" fi docker_file_arg="${dir_arg}/${docker_file_arg}" check_overwrite() { for image in "$@"; do if [[ "$image" == "--tag" ]]; then continue fi if [[ $image =~ -snapshot ]]; then continue fi tag=${image#*:} if [[ $tag =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)?$ ]]; then echo "Checking if image $image already exists" if docker manifest inspect "$image" >/dev/null 2>&1; then echo "❌ ERROR: Image $image already exists and overwrite=$overwrite" exit 1 fi fi done } upload_comment="" if [[ "${local_test_only}" = "Y" ]]; then IMAGE_TAGS=("--tag" "localhost:5000/${namespace}/${component_name}:${GITHUB_SHA}") PUSHTAG="type=image,push=true" else echo "::group:: compute tags ${component_name}" # shellcheck disable=SC2086 IFS=" " read -r -a IMAGE_TAGS <<< "$(bash scripts/utils/compute-tags.sh ${namespace}/${component_name})" echo "::endgroup::" # Only push multi-arch images to dockerhub/quay.io for main branch or for release tags vM.N.P{-rcX} if [[ "$BRANCH" == "main" || $BRANCH =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)?$ ]]; then echo "will build docker images and upload to dockerhub/quay.io, BRANCH=$BRANCH" bash scripts/utils/docker-login.sh PUSHTAG="type=image,push=true" upload_comment=" and uploading" if [[ "$overwrite" == 'N' ]]; then check_overwrite "${IMAGE_TAGS[@]}" fi upload_readme='Y' else echo 'skipping docker images upload, because not on tagged release or main branch' PUSHTAG="type=image,push=false" fi fi echo "::group:: docker build ${component_name}" # Some of the variables can be blank and should not produce extra arguments, # so we need to disable the linter checks for quoting. # TODO: collect arguments into an array and add optional once conditionally # shellcheck disable=SC2086 docker buildx build --output "${PUSHTAG}" ${target_arg} ${base_debug_img_arg} \ --progress=plain \ --platform="${platforms}" \ --file "${docker_file_arg}" \ "${IMAGE_TAGS[@]}" \ "${dir_arg}" echo "::endgroup::" echo "Finished building${upload_comment} ${component_name} ==============" if [[ "$upload_readme" == "Y" ]]; then echo "::group:: docker upload ${dir_arg}/README.md" bash scripts/build/upload-docker-readme.sh "${component_name}" "${dir_arg}"/README.md echo "::endgroup::" fi echo "::group:: docker prune" df -h / docker buildx prune --all --force docker system prune --force df -h / echo "::endgroup::" ================================================ FILE: scripts/build/build-upload-docker-images.sh ================================================ #!/bin/bash # # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 set -euf -o pipefail print_help() { echo "Usage: $0 [-B] [-D] [-h] [-l] [-o] [-p platforms]" echo "-h: Print help" echo "-B: Skip building of the binaries (e.g. when they were already built)" echo "-D: Disable building of images with debugger" echo "-l: Enable local-only mode that only pushes images to local registry" echo "-o: overwrite image in the target remote repository even if the semver tag already exists" echo "-p: Comma-separated list of platforms to build for (default: all supported)" exit 1 } add_debugger='Y' build_binaries='Y' platforms="$(make echo-linux-platforms)" FLAGS=() while getopts "BDhlop:" opt; do case "${opt}" in B) build_binaries='N' echo "Will not build binaries as requested" ;; D) add_debugger='N' echo "Will not build debug images as requested" ;; l) # in the local-only mode the images will only be pushed to local registry FLAGS=("${FLAGS[@]}" -l) ;; o) FLAGS=("${FLAGS[@]}" -o) ;; p) platforms=${OPTARG} ;; ?) print_help ;; esac done set -x if [[ "$build_binaries" == "Y" ]]; then for platform in $(echo "$platforms" | tr ',' ' '); do arch=${platform##*/} # Remove everything before the last slash make "build-binaries-linux-$arch" done fi baseimg_target='create-baseimg-debugimg' if [[ "${add_debugger}" == "N" ]]; then baseimg_target='create-baseimg' fi make "$baseimg_target" LINUX_PLATFORMS="$platforms" # Helper function to build and upload docker images # Args: component_name, source_dir, [use_base_image], [build_debug] build_image() { local component=$1 local dir=$2 local use_base_image=${3:-false} local build_debug=${4:-false} local base_flags=() if [[ "$use_base_image" == "true" ]]; then base_flags=(-b) fi local target_flags=() if [[ "$use_base_image" == "true" ]]; then target_flags=(-t release) fi bash scripts/build/build-upload-a-docker-image.sh "${FLAGS[@]}" "${base_flags[@]}" -c "$component" -d "$dir" -p "${platforms}" "${target_flags[@]}" if [[ "$build_debug" == "true" ]] && [[ "${add_debugger}" == "Y" ]]; then bash scripts/build/build-upload-a-docker-image.sh "${FLAGS[@]}" "${base_flags[@]}" -c "${component}-debug" -d "$dir" -p "${platforms}" -t debug fi } # Build images with special handling for debug images build_image jaeger-remote-storage cmd/remote-storage true true # Build utility images build_image jaeger-es-index-cleaner cmd/es-index-cleaner true false build_image jaeger-es-rollover cmd/es-rollover true false build_image jaeger-cassandra-schema internal/storage/v1/cassandra/ false false # Build tool images build_image jaeger-tracegen cmd/tracegen false false build_image jaeger-anonymizer cmd/anonymizer false false ================================================ FILE: scripts/build/clean-binaries.sh ================================================ #!/bin/bash # # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 platforms=$(make echo-platforms) for main in ./cmd/*/main.go; do dir=$(dirname "$main") bin=$(basename "$dir") rm -rf "${dir:?}/$bin" for platform in $(echo "$platforms" | tr ',' ' ' | tr '/' '-'); do b="${dir:?}/$bin-$platform" echo "$b" rm -f "$b" b="${dir:?}/$bin-debug-$platform" echo "$b" rm -f "$b" done done ================================================ FILE: scripts/build/docker/base/Dockerfile ================================================ # Copyright (c) 2025 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 FROM alpine:3.23.3@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659 AS cert RUN apk add --update --no-cache ca-certificates mailcap FROM alpine:3.23.3@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659 COPY --from=cert /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=cert /etc/mime.types /etc/mime.types ================================================ FILE: scripts/build/docker/debug/Dockerfile ================================================ # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # We use a pre-built base image that includes Delve debugger. # See https://github.com/jaegertracing/base-image-with-debugger FROM ghcr.io/jaegertracing/base-image-with-debugger:0.1.0@sha256:dad2b5e8e26ea6d9092b8ecf4b582563376a8421fc44ed72f374b18ae840d5eb ================================================ FILE: scripts/build/docker/debug/go.mod ================================================ module debug-delve go 1.26.0 require github.com/go-delve/delve v1.26.0 require ( github.com/cilium/ebpf v0.11.0 // indirect github.com/cosiner/argv v0.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/derekparker/trie/v3 v3.2.0 // indirect github.com/go-delve/liner v1.2.3-0.20231231155935-4726ab1d7f62 // indirect github.com/google/go-dap v0.12.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spf13/cobra v1.9.1 // indirect github.com/spf13/pflag v1.0.6 // indirect go.starlark.net v0.0.0-20231101134539-556fd59b42f6 // indirect golang.org/x/arch v0.11.0 // indirect golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/telemetry v0.0.0-20241106142447-58a1122356f5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: scripts/build/docker/debug/go.sum ================================================ github.com/cilium/ebpf v0.11.0 h1:V8gS/bTCCjX9uUnkUFUpPsksM8n1lXBAvHcpiFk1X2Y= github.com/cilium/ebpf v0.11.0/go.mod h1:WE7CZAnqOL2RouJ4f1uyNhqr2P4CCvXFIqdRDUgWsVs= github.com/cosiner/argv v0.1.0 h1:BVDiEL32lwHukgJKP87btEPenzrrHUjajs/8yzaqcXg= github.com/cosiner/argv v0.1.0/go.mod h1:EusR6TucWKX+zFgtdUsKT2Cvg45K5rtpCcWz4hK06d8= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.20 h1:VIPb/a2s17qNeQgDnkfZC35RScx+blkKF8GV68n80J4= github.com/creack/pty v1.1.20/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/derekparker/trie/v3 v3.2.0 h1:fET3Qbp9xSB7yc7tz6Y2GKMNl0SycYFo3cmiRI3Gpf0= github.com/derekparker/trie/v3 v3.2.0/go.mod h1:P94lW0LPgiaMgKAEQD59IDZD2jMK9paKok8Nli/nQbE= github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-delve/delve v1.26.0 h1:YZT1kXD76mxba4/wr+tyUa/tSmy7qzoDsmxutT42PIs= github.com/go-delve/delve v1.26.0/go.mod h1:8BgFFOXTi1y1M+d/4ax1LdFw0mlqezQiTZQpbpwgBxo= github.com/go-delve/liner v1.2.3-0.20231231155935-4726ab1d7f62 h1:IGtvsNyIuRjl04XAOFGACozgUD7A82UffYxZt4DWbvA= github.com/go-delve/liner v1.2.3-0.20231231155935-4726ab1d7f62/go.mod h1:biJCRbqp51wS+I92HMqn5H8/A0PAhxn2vyOT+JqhiGI= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-dap v0.12.0 h1:rVcjv3SyMIrpaOoTAdFDyHs99CwVOItIJGKLQFQhNeM= github.com/google/go-dap v0.12.0/go.mod h1:tNjCASCm5cqePi/RVXXWEVqtnNLV1KTWtYOqu6rZNzc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= go.starlark.net v0.0.0-20231101134539-556fd59b42f6 h1:+eC0F/k4aBLC4szgOcjd7bDTEnpxADJyWJE0yowgM3E= go.starlark.net v0.0.0-20231101134539-556fd59b42f6/go.mod h1:LcLNIzVOMp4oV+uusnpk+VU+SzXaJakUuBjoCSWH5dM= golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI= golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20241106142447-58a1122356f5 h1:TCDqnvbBsFapViksHcHySl/sW4+rTGNIAoJJesHRuMM= golang.org/x/telemetry v0.0.0-20241106142447-58a1122356f5/go.mod h1:8nZWdGp9pq73ZI//QJyckMQab3yq7hoWi7SI0UIusVI= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: scripts/build/docker/debug/tools.go ================================================ // Copyright (c) 2024 The Jaeger Authors. // SPDX-License-Identifier: Apache-2.0 //go:build tools package tools // This file follows the recommendation at // https://go.dev/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module // on how to pin tooling dependencies to a go.mod file. // This ensures that all systems use the same version of tools in addition to regular dependencies. import ( _ "github.com/go-delve/delve/cmd/dlv" ) ================================================ FILE: scripts/build/package-deploy.sh ================================================ #!/bin/bash # # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 set -euxf -o pipefail # This script uses --sort=name option that is not supported by MacOS tar. # On MacOS, install `brew install gnu-tar` and run this script with TARCMD=gtar. TARCMD=${TARCMD:-tar} print_help() { echo "Usage: $0 [-h] [-k gpg_key_id] [-p platforms]" echo "-h: Print help" echo "-k: Override default GPG signing key ID. Use 'skip' to skip signing." echo "-p: Comma-separated list of platforms to build for (default: all supported)" exit 1 } # Default signing key (accessible to maintainers-only), documented in https://www.jaegertracing.io/download/. gpg_key_id="B42D1DB0F079690F" platforms="$(make echo-platforms)" while getopts "hk:p:" opt; do case "${opt}" in k) gpg_key_id=${OPTARG} ;; p) platforms=${OPTARG} ;; ?) print_help ;; esac done # stage-platform-files stages files for the platform ($1) into the package # staging dir ($2). If you pass in a file extension ($3) it will be used when # copying the source files function stage-platform-files { local -r PLATFORM=$1 local -r PACKAGE_STAGING_DIR=$2 local -r FILE_EXTENSION=${3:-} cp "./cmd/jaeger/jaeger-${PLATFORM}" "${PACKAGE_STAGING_DIR}/jaeger${FILE_EXTENSION}" cp "./examples/hotrod/hotrod-${PLATFORM}" "${PACKAGE_STAGING_DIR}/example-hotrod${FILE_EXTENSION}" } # stage-tool-platform-files stages the different tool files in the platform ($1) into the package # staging dir ($2). If you pass in a file extension ($3) it will be used when # copying on the source function stage-tool-platform-files { local -r PLATFORM=$1 local -r TOOLS_PACKAGE_STAGING_DIR=$2 local -r FILE_EXTENSION=${3:-} cp "./cmd/es-index-cleaner/es-index-cleaner-${PLATFORM}" "${TOOLS_PACKAGE_STAGING_DIR}/jaeger-es-index-cleaner${FILE_EXTENSION}" cp "./cmd/es-rollover/es-rollover-${PLATFORM}" "${TOOLS_PACKAGE_STAGING_DIR}/jaeger-es-rollover${FILE_EXTENSION}" cp "./cmd/esmapping-generator/esmapping-generator-${PLATFORM}" "${TOOLS_PACKAGE_STAGING_DIR}/jaeger-esmapping-generator${FILE_EXTENSION}" } # package pulls built files for the platform ($2) and compresses it using the compression ($1). # If you pass in a file extension ($3) it will be look for binaries with that extension. function package { local -r COMPRESSION=$1 local -r PLATFORM=$2 local -r FILE_EXTENSION=${3:-} local -r PACKAGE_NAME=jaeger-${VERSION}-$PLATFORM local -r TOOLS_PACKAGE_NAME=jaeger-tools-${VERSION}-$PLATFORM echo "Packaging binaries for $PLATFORM" PACKAGES=("$PACKAGE_NAME" "$TOOLS_PACKAGE_NAME") for d in "${PACKAGES[@]}"; do if [ -d "$d" ]; then rm -vrf "$d" fi mkdir "$d" done stage-platform-files "$PLATFORM" "$PACKAGE_NAME" "$FILE_EXTENSION" stage-tool-platform-files "$PLATFORM" "$TOOLS_PACKAGE_NAME" "$FILE_EXTENSION" # Create a checksum file for all the files being packaged in the archive. Sorted by filename. for d in "${PACKAGES[@]}"; do find "$d" -type f -exec shasum -b -a 256 {} \; | sort -k2 | tee "./deploy/$d.sha256sum.txt" done if [ "$COMPRESSION" == "zip" ] then for d in "${PACKAGES[@]}"; do local ARCHIVE_NAME="$d.zip" echo "Packaging into $ARCHIVE_NAME:" zip -r "./deploy/$ARCHIVE_NAME" "$d" done else for d in "${PACKAGES[@]}"; do local ARCHIVE_NAME="$d.tar.gz" echo "Packaging into $ARCHIVE_NAME:" ${TARCMD} --sort=name -czvf "./deploy/$ARCHIVE_NAME" "$d" done fi for d in "${PACKAGES[@]}"; do rm -vrf "$d" done } VERSION="$(make echo-version | perl -lne 'print $1 if /^v(\d+.\d+.\d+(-rc\d+)?)$/' )" echo "Working on version: $VERSION" if [ -z "$VERSION" ]; then # We want to halt if for some reason the version string is empty as this is an obvious error case >&2 echo 'Failed to detect a version string' exit 1 fi # make needed directories rm -rf deploy mkdir deploy # Loop through each platform (separated by commas) for platform in $(echo "$platforms" | tr ',' ' '); do os=${platform%%/*} # Remove everything after the slash arch=${platform##*/} # Remove everything before the last slash if [[ "$os" == "windows" ]]; then package tar "${os}-${arch}" .exe package zip "${os}-${arch}" .exe else package tar "${os}-${arch}" fi done # Create a checksum file for all non-checksum files in the deploy directory. Strips the leading 'deploy/' directory from filepaths. Sort by filename. find deploy \( ! -name '*sha256sum.txt' \) -type f -exec shasum -b -a 256 {} \; \ | sed -r 's#(\w+\s+\*?)deploy/(.*)#\1\2#' \ | sort -k2 \ | tee "./deploy/jaeger-${VERSION}.sha256sum.txt" # Use gpg to sign the (g)zip files (excluding checksum files) into .asc files. if [[ "${gpg_key_id}" == "skip" ]]; then echo "Skipping GPG signing as requested" else echo "Signing archives with GPG key ${gpg_key_id}" gpg --list-keys "${gpg_key_id}" find deploy \( ! -name '*sha256sum.txt' \) -type f -exec gpg -v --local-user "${gpg_key_id}" --armor --detach-sign {} \; fi # show your work ls -lF deploy/ ================================================ FILE: scripts/build/rebuild-ui.sh ================================================ #!/bin/bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 set -euxf -o pipefail cd jaeger-ui if [[ "$(git rev-parse --is-shallow-repository)" == "true" ]]; then git fetch --unshallow fi git fetch --all --tags git log --oneline --decorate=full -n 10 | cat last_tag=$(git describe --tags --dirty 2>/dev/null) if [[ "$last_tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then branch_hash=$(git rev-parse HEAD) last_tag_hash=$(git rev-parse "$last_tag") if [[ "$branch_hash" == "$last_tag_hash" ]]; then temp_file=$(mktemp) # shellcheck disable=SC2064 trap "rm -f ${temp_file}" EXIT release_url="https://github.com/jaegertracing/jaeger-ui/releases/download/${last_tag}/assets.tar.gz" if curl --silent --fail --location --output "$temp_file" "$release_url"; then mkdir -p packages/jaeger-ui/build/ rm -r -f packages/jaeger-ui/build/ tar -zxvf "$temp_file" packages/jaeger-ui/build/ exit 0 fi fi fi # do a regular full build nvm use npm ci && cd packages/jaeger-ui && npm run build ================================================ FILE: scripts/build/upload-docker-readme.sh ================================================ #!/bin/bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 set -euf -o pipefail usage() { echo "Usage: $0 " exit 1 } if [ "$#" -ne 2 ]; then echo "🛑 Error: Missing arguments." usage fi repo="$1" readme_path="$2" # Check if README file exists before calling realpath if [ ! -f "$readme_path" ]; then echo "🟡 Warning: no README file found at path $readme_path" exit 0 fi abs_readme_path=$(realpath "$readme_path") repository="jaegertracing/$repo" DOCKERHUB_TOKEN=${DOCKERHUB_TOKEN:?'missing Docker Hub token'} DOCKERHUB_USERNAME=${DOCKERHUB_USERNAME:-"jaegertracingbot"} QUAY_TOKEN=${QUAY_TOKEN:?'missing Quay token'} dockerhub_url="https://hub.docker.com/v2/repositories/$repository/" quay_url="https://quay.io/api/v1/repository/${repository}" readme_content=$(<"$abs_readme_path") # 🛑 IMPORTANT: do not echo commands as they contain tokens set +x # Handle DockerHUB upload # Get Docker Hub JWT token from PAT dockerhub_credentials=$(jq -n \ --arg pwd "$DOCKERHUB_TOKEN" \ --arg user "$DOCKERHUB_USERNAME" \ '{username: $user, password: $pwd}') dockerhub_jwt=$(curl -s -H "Content-Type: application/json" \ -X POST -d "$dockerhub_credentials" \ https://hub.docker.com/v2/users/login/ | jq -r .token) if [ "$dockerhub_jwt" = "null" ] || [ -z "$dockerhub_jwt" ]; then echo "🛑 Failed to get Docker Hub JWT token" exit 1 fi # encode readme as properly escaped JSON body=$(jq -n \ --arg full_desc "$readme_content" \ '{full_description: $full_desc}') dockerhub_response=$(curl -s -w "%{http_code}" -X PATCH "$dockerhub_url" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $dockerhub_jwt" \ -d "$body") http_code="${dockerhub_response: -3}" response_body="${dockerhub_response:0:${#dockerhub_response}-3}" if [ "$http_code" -eq 200 ]; then echo "✅ Successfully updated Docker Hub README for $repository" else echo "🛑 Failed to update Docker Hub README for $repository with status code $http_code" echo "🛑 Full response: $response_body" fi # Handle Quay upload # encode readme as properly escaped JSON quay_body=$(jq -n \ --arg full_desc "$readme_content" \ '{description: $full_desc}') quay_response=$(curl -s -w "%{http_code}" -X PUT "$quay_url" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $QUAY_TOKEN" \ -d "$quay_body") quay_http_code="${quay_response: -3}" quay_response_body="${quay_response:0:${#quay_response}-3}" if [ "$quay_http_code" -eq 200 ]; then echo "✅ Successfully updated Quay.io README for $repository" else echo "🛑 Failed to update Quay.io README for $repository with status code $quay_http_code" echo "🛑 Full response: $quay_response_body" fi ================================================ FILE: scripts/e2e/adaptive-sampling-integration-test.sh ================================================ #!/bin/bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 set -euf -o pipefail # This script is currently a placeholder. # Commands to run integration test: # SAMPLING_STORAGE_TYPE=memory SAMPLING_CONFIG_TYPE=adaptive go run -tags=ui ./cmd/all-in-one --log-level=debug # go run ./cmd/tracegen -adaptive-sampling=http://localhost:14268/api/sampling -pause=10ms -duration=60m # Check how strategy is changing # curl 'http://localhost:14268/api/sampling?service=tracegen' | jq . # Issues # - SDK does not report sampling probability in the tags the way Jaeger SDKs did # - Server probably does not recognize spans as having adaptive sampling without sampler info # - There is no way to modify target traces-per-second dynamically, must restart collector. ================================================ FILE: scripts/e2e/cassandra.sh ================================================ #!/bin/bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 set -euxf -o pipefail export CASSANDRA_USERNAME="cassandra" export CASSANDRA_PASSWORD="cassandra" success="false" timeout=600 end_time=$((SECONDS + timeout)) SKIP_APPLY_SCHEMA=${SKIP_APPLY_SCHEMA:-"false"} export CASSANDRA_CREATE_SCHEMA=${SKIP_APPLY_SCHEMA} usage() { echo $"Usage: $0 " echo " storage_test: direct | e2e" exit 1 } check_arg() { if [ ! $# -eq 3 ]; then echo "ERROR: need exactly three arguments, " usage fi } setup_cassandra() { local compose_file=$1 docker compose -f "$compose_file" up -d } healthcheck_cassandra() { local cas_version=$1 local container_name="cassandra-${cas_version}" # Since the healthcheck in cassandra is done at the interval of 30s local wait_seconds=30 while [ $SECONDS -lt $end_time ]; do status=$(docker inspect -f '{{ .State.Health.Status }}' "${container_name}") if [[ ${status} == "healthy" ]]; then echo "✅ $container_name is healthy" return 0 fi echo "Waiting for $container_name to be healthy. Current status: $status" sleep $wait_seconds done echo "❌ ERROR: $container_name did not become healthy in time" exit 1 } dump_logs() { local compose_file=$1 echo "::group::🚧 🚧 🚧 Cassandra logs" docker compose -f "${compose_file}" logs echo "::endgroup::" } teardown_cassandra() { local compose_file=$1 if [[ "$success" == "false" ]]; then dump_logs "${compose_file}" fi docker compose -f "$compose_file" down } apply_schema() { local image=cassandra-schema local schema_dir=internal/storage/v1/cassandra/ local schema_version=$1 local keyspace=$2 local params=( --rm --env CQLSH_HOST=localhost --env CQLSH_PORT=9042 --env "TEMPLATE=/cassandra-schema/${schema_version}.cql.tmpl" --env "KEYSPACE=${keyspace}" --env "CASSANDRA_USERNAME=${CASSANDRA_USERNAME}" --env "CASSANDRA_PASSWORD=${CASSANDRA_PASSWORD}" --network host ) docker build -t ${image} ${schema_dir} docker run "${params[@]}" ${image} } run_integration_test() { local version=$1 local major_version=${version%%.*} local schema_version=$2 local storageTest=$3 local primaryKeyspace="jaeger_v1_dc1" local archiveKeyspace="jaeger_v1_dc1_archive" local compose_file="docker-compose/cassandra/v$major_version/docker-compose.yaml" setup_cassandra "${compose_file}" # shellcheck disable=SC2064 trap "teardown_cassandra ${compose_file}" EXIT healthcheck_cassandra "${major_version}" if [ "${SKIP_APPLY_SCHEMA}" = "false" ]; then apply_schema "$schema_version" "$primaryKeyspace" apply_schema "$schema_version" "$archiveKeyspace" fi if [ "${storageTest}" = "direct" ]; then STORAGE=cassandra make storage-integration-test elif [ "${storageTest}" == "e2e" ]; then STORAGE=cassandra make jaeger-v2-storage-integration-test else echo "Unknown storage_test value $storageTest. Valid options are direct or e2e" exit 1 fi success="true" } main() { check_arg "$@" echo "Executing integration test for $1 with schema $2.cql.tmpl" run_integration_test "$1" "$2" "$3" } main "$@" ================================================ FILE: scripts/e2e/clickhouse.sh ================================================ #!/bin/bash # Copyright (c) 2025 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 set -euxf -o pipefail success="false" timeout=600 end_time=$((SECONDS + timeout)) compose_file="docker-compose/clickhouse/docker-compose.yml" container_name="clickhouse" setup_clickhouse() { echo "Starting ClickHouse with $compose_file" docker compose -f "$compose_file" up -d } healthcheck_clickhouse() { local wait_seconds=10 while [ $SECONDS -lt $end_time ]; do status=$(docker inspect -f '{{ .State.Health.Status }}' "${container_name}") if [[ ${status} == "healthy" ]]; then echo "✅ $container_name is healthy" return 0 fi echo "Waiting for $container_name to be healthy. Current status: $status" sleep $wait_seconds done echo "❌ ERROR: $container_name did not become healthy in time" exit 1 } dump_logs() { echo "::group::🚧 🚧 🚧 Clickhouse logs" docker compose -f "${compose_file}" logs echo "::endgroup::" } teardown_clickhouse() { if [[ "$success" == "false" ]]; then dump_logs "${compose_file}" fi docker compose -f "$compose_file" down } run_integration_test() { setup_clickhouse trap teardown_clickhouse EXIT healthcheck_clickhouse STORAGE=clickhouse make jaeger-v2-storage-integration-test success="true" } main() { echo "Executing ClickHouse integration tests" run_integration_test } main ================================================ FILE: scripts/e2e/compare_metrics.py ================================================ # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 import argparse import sys from difflib import unified_diff from bisect import insort from prometheus_client.parser import text_string_to_metric_families import re EXCLUDED_LABELS = {'service_instance_id', 'otel_scope_version', 'otel_scope_schema_url'} # Configuration for transient labels that should be normalized during comparison TRANSIENT_LABEL_PATTERNS = { 'kafka': { 'topic': { 'pattern': r'jaeger-spans-\d+', 'replacement': 'jaeger-spans-' } }, # Add more patterns here as needed # Example: # 'elasticsearch': { # 'index': { # 'pattern': r'jaeger-\d{4}-\d{2}-\d{2}', # 'replacement': 'jaeger-YYYY-MM-DD' # } # } } METRIC_EXCLUSION_RULES = { # excluding HTTP 5xx responses as these can be flaky 'http_5xx_errors': { 'condition': 'label_match', 'label': 'http_response_status_code', 'pattern': r'^5\d{2}$', }, } def should_exclude_metric(metric_name, labels): """ Determines if a metric should be excluded from comparison based on configured rules. Args: metric_name: The name of the metric labels: Dictionary of labels for the metric Returns: tuple: (should_exclude: bool, reason: str or None) """ for rule_name, rule_config in METRIC_EXCLUSION_RULES.items(): condition = rule_config['condition'] if condition == 'label_match': label = rule_config['label'] pattern = rule_config['pattern'] if label in labels and re.match(pattern, labels[label]): return True return False def suppress_transient_labels(metric_name, labels): """ Suppresses transient labels in metrics based on configured patterns. Args: metric_name: The name of the metric labels: Dictionary of labels for the metric Returns: Dictionary of labels with transient values normalized """ labels_copy = labels.copy() for service_pattern, label_configs in TRANSIENT_LABEL_PATTERNS.items(): if service_pattern in metric_name: for label_name, pattern_config in label_configs.items(): if label_name in labels_copy: pattern = pattern_config['pattern'] replacement = pattern_config['replacement'] labels_copy[label_name] = re.sub(pattern, replacement, labels_copy[label_name]) return labels_copy def read_metric_file(file_path): with open(file_path, 'r') as f: return f.readlines() def parse_metrics(content): metrics = [] metrics_exclusion_count = 0 for family in text_string_to_metric_families(content): for sample in family.samples: labels = dict(sample.labels) if should_exclude_metric(sample.name, labels): metrics_exclusion_count += 1 continue # Remove undesirable metric labels to match the diff generation for label in EXCLUDED_LABELS: labels.pop(label, None) labels = suppress_transient_labels(sample.name, labels) label_pairs = sorted(labels.items(), key=lambda x: x[0]) label_str = ','.join(f'{k}="{v}"' for k,v in label_pairs) metric = f"{family.name}{{{label_str}}}" insort(metrics , metric) return metrics,metrics_exclusion_count def generate_diff(file1_content, file2_content): if isinstance(file1_content, list): file1_content = ''.join(file1_content) if isinstance(file2_content, list): file2_content = ''.join(file2_content) metrics1,excluded_metrics_count1 = parse_metrics(file1_content) metrics2,excluded_metrics_count2 = parse_metrics(file2_content) diff = unified_diff(metrics1, metrics2,lineterm='',n=0) total_excluded = excluded_metrics_count1 + excluded_metrics_count2 exclusion_lines = '' if total_excluded > 0: exclusion_lines = f'\nMetrics excluded from A: {excluded_metrics_count1}\nMetrics excluded from B: {excluded_metrics_count2}' return '\n'.join(diff) + exclusion_lines def write_diff_file(diff_lines, output_path): with open(output_path, 'w') as f: f.write(diff_lines) f.write('\n') # Add final newline print(f"Diff file successfully written to: {output_path}") def main(): parser = argparse.ArgumentParser(description='Generate diff between two Jaeger metric files') parser.add_argument('--file1', help='Path to first metric file') parser.add_argument('--file2', help='Path to second metric file') parser.add_argument('--output', '-o', default='metrics_diff.txt', help='Output diff file path (default: metrics_diff.txt)') args = parser.parse_args() # Read input files file1_lines = read_metric_file(args.file1) file2_lines = read_metric_file(args.file2) # Generate diff diff_lines = generate_diff(file1_lines, file2_lines) # Check if there are any differences if diff_lines: print("differences found between the metric files.") print("=== Metrics Comparison Results ===") print(diff_lines) write_diff_file(diff_lines, args.output) return 1 print("no difference found") return 0 if __name__ == '__main__': sys.exit(main()) ================================================ FILE: scripts/e2e/elasticsearch.sh ================================================ #!/bin/bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 PS4='T$(date "+%H:%M:%S") ' set -euf -o pipefail # use global variables to reflect status of db db_is_up= success="false" usage() { echo "Usage: $0 " echo " backend: elasticsearch | opensearch" echo " backend_version: major version, e.g. 7.x" echo " storage_test: direct | e2e" exit 1 } check_arg() { if [ ! $# -eq 3 ]; then echo "ERROR: need exactly three arguments" usage fi } # start the elasticsearch/opensearch container setup_db() { local compose_file=$1 docker compose -f "${compose_file}" up -d } # check if the storage is up and running wait_for_storage() { local distro=$1 local url=$2 local compose_file=$3 local params=( --silent --output /dev/null --write-out "%{http_code}" ) local max_attempts=60 local attempt=0 echo "Waiting for ${distro} to be available at ${url}..." until [[ "$(curl "${params[@]}" "${url}")" == "200" ]] || (( attempt >= max_attempts )); do attempt=$(( attempt + 1 )) echo "Attempt: ${attempt} ${distro} is not yet available at ${url}..." sleep 10 done # if after all the attempts the storage is not accessible, terminate it and exit if [[ "$(curl "${params[@]}" "${url}")" != "200" ]]; then echo "ERROR: ${distro} is not ready at ${url} after $(( attempt * 10 )) seconds" db_is_up=0 else echo "SUCCESS: ${distro} is available at ${url}" db_is_up=1 fi } bring_up_storage() { local distro=$1 local version=$2 local major_version=${version%%.*} local compose_file="docker-compose/${distro}/v${major_version}/docker-compose.yml" echo "starting ${distro} ${major_version}" for retry in 1 2 3 do echo "attempt $retry" if [ "${distro}" = "elasticsearch" ] || [ "${distro}" = "opensearch" ]; then setup_db "${compose_file}" else echo "Unknown distribution $distro. Valid options are opensearch or elasticsearch" usage fi wait_for_storage "${distro}" "http://localhost:9200" "${compose_file}" if [ ${db_is_up} = "1" ]; then break fi done # shellcheck disable=SC2064 trap "teardown_storage ${compose_file} ${distro}" EXIT if [ ${db_is_up} != "1" ]; then echo "ERROR: unable to start ${distro}" exit 1 fi } dump_logs() { local compose_file=$1 local distro=$2 echo "::group::${distro} logs" docker compose -f "${compose_file}" logs echo "::endgroup::" } # terminate the elasticsearch/opensearch container teardown_storage() { local compose_file=$1 local distro=$2 if [[ "$success" == "false" ]]; then dump_logs "${compose_file}" "${distro}" fi docker compose -f "${compose_file}" down } build_local_img(){ make build-es-index-cleaner GOOS=linux make build-es-rollover GOOS=linux make create-baseimg PLATFORMS="linux/$(go env GOARCH)" #build es-index-cleaner and es-rollover images GITHUB_SHA=local-test BRANCH=local-test bash scripts/build/build-upload-a-docker-image.sh -l -b -c jaeger-es-index-cleaner -d cmd/es-index-cleaner -t release -p "linux/$(go env GOARCH)" GITHUB_SHA=local-test BRANCH=local-test bash scripts/build/build-upload-a-docker-image.sh -l -b -c jaeger-es-rollover -d cmd/es-rollover -t release -p "linux/$(go env GOARCH)" } main() { check_arg "$@" local distro=$1 local es_version=$2 local storage_test=$3 set -x bring_up_storage "${distro}" "${es_version}" build_local_img if [[ "${storage_test}" == "e2e" ]]; then STORAGE=${distro} SPAN_STORAGE_TYPE=${distro} make jaeger-v2-storage-integration-test elif [[ "${storage_test}" == "direct" ]]; then STORAGE=${distro} make storage-integration-test make index-cleaner-integration-test make index-rollover-integration-test else echo "ERROR: Invalid argument value storage_test=${storage_test}, expecting direct or e2e" exit 1 fi success="true" } main "$@" ================================================ FILE: scripts/e2e/filter_coverage.py ================================================ #!/usr/bin/env python3 # Copyright (c) 2026 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # # Filters a Go coverage profile in-place by applying the same exclusions defined # in .codecov.yml so coverage metrics stay in sync between this gate and Codecov. # # Usage: # python3 scripts/e2e/filter_coverage.py [path/to/.codecov.yml] import fnmatch import os import sys def load_exclusions(codecov_path: str) -> list[str]: """Return raw glob patterns from the ignore: section of .codecov.yml.""" patterns = [] in_ignore = False with open(codecov_path) as f: for line in f: stripped = line.strip() if stripped == 'ignore:': in_ignore = True elif in_ignore: if stripped.startswith('#'): continue if stripped.startswith('- '): patterns.append(stripped[2:].strip('"').strip("'")) elif stripped and not line[0].isspace(): in_ignore = False return patterns def read_module_path(codecov_path: str) -> str: """ Read the Go module path so we can strip it from coverage import paths to produce repo-relative paths that match the .codecov.yml patterns. """ go_mod_path = os.path.join(os.path.dirname(codecov_path), 'go.mod') with open(go_mod_path) as f: for line in f: if line.startswith('module '): return line.split()[1].strip() raise ValueError(f'no module directive found in {go_mod_path}') def should_exclude(path: str, patterns: list[str]) -> bool: """Return True if path matches any exclusion pattern. Patterns with wildcards are matched via fnmatch. Patterns without wildcards are treated as plain path prefixes. """ for pattern in patterns: if '*' in pattern or '?' in pattern: if fnmatch.fnmatch(path, pattern): return True else: if path.startswith(pattern): return True return False def main() -> None: if len(sys.argv) < 2: print(f'usage: {sys.argv[0]} [.codecov.yml]', file=sys.stderr) sys.exit(1) coverage_path = sys.argv[1] codecov_path = sys.argv[2] if len(sys.argv) > 2 else '.codecov.yml' try: exclusions = load_exclusions(codecov_path) except FileNotFoundError: print(f'error: {codecov_path} not found', file=sys.stderr) sys.exit(1) module_prefix = read_module_path(codecov_path) + '/' kept = skipped = 0 kept_lines = [] with open(coverage_path) as f: for line in f: if line.startswith('mode:'): kept_lines.append(line) continue # Coverage lines: "github.com/.../file.go:line.col,line.col stmts count" # Extract the file path (everything before the first colon). import_path = line.split(':')[0] # Strip module prefix to get a repo-relative path for matching. if import_path.startswith(module_prefix): path = import_path[len(module_prefix):] else: path = import_path if should_exclude(path, exclusions): skipped += 1 else: kept_lines.append(line) kept += 1 with open(coverage_path, 'w') as f: f.writelines(kept_lines) print(f'filter_coverage: kept {kept}, excluded {skipped} lines', file=sys.stderr) if __name__ == '__main__': main() ================================================ FILE: scripts/e2e/kafka.sh ================================================ #!/bin/bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 set -euf -o pipefail compose_file="" kafka_version="v3" manage_kafka="true" success="false" usage() { echo "Usage: $0 [-S] [-v ]" echo " -S: 'no storage' - do not start or stop Kafka container (useful for local testing)" echo " -v: kafka major version (3.x); default: 3.x" exit 1 } parse_args() { while getopts "v:Sh" opt; do case "${opt}" in v) case ${OPTARG} in 3.x) kafka_version="v3" ;; 2.x) kafka_version="v2" ;; *) echo "Error: Invalid Kafka version. Valid options are 3.x or 2.x" usage ;; esac ;; S) manage_kafka="false" ;; *) usage ;; esac done compose_file="docker-compose/kafka/${kafka_version}/docker-compose.yml" } setup_kafka() { echo "Starting Kafka using Docker Compose..." docker compose -f "${compose_file}" up -d kafka } dump_logs() { echo "::group::🚧 🚧 🚧 Kafka logs" docker compose -f "${compose_file}" logs echo "::endgroup::" } teardown_kafka() { if [[ "$success" == "false" ]]; then dump_logs fi echo "Stopping Kafka..." docker compose -f "${compose_file}" down } is_kafka_ready() { docker compose -f "${compose_file}" \ exec kafka /opt/kafka/bin/kafka-topics.sh \ --list \ --bootstrap-server localhost:9092 \ >/dev/null 2>&1 } wait_for_kafka() { local timeout=180 local interval=5 local end_time=$((SECONDS + timeout)) while [ $SECONDS -lt $end_time ]; do if is_kafka_ready; then return fi echo "Kafka broker not ready, waiting ${interval} seconds" sleep $interval done echo "Timed out waiting for Kafka to start" exit 1 } run_integration_test() { export STORAGE=kafka make jaeger-v2-storage-integration-test } main() { parse_args "$@" echo "Executing Kafka integration test." echo "Kafka version ${kafka_version}." set -x if [[ "$manage_kafka" == "true" ]]; then setup_kafka trap 'teardown_kafka' EXIT fi wait_for_kafka run_integration_test success="true" } main "$@" ================================================ FILE: scripts/e2e/metrics_summary.py ================================================ # Copyright (c) 2025 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 import argparse import json from collections import defaultdict def parse_diff_file(diff_path): """ Parses a unified diff file and categorizes changes into added, removed, and modified metrics. Also captures the raw diff sections for each metric and the exclusion count. """ changes = { 'added': defaultdict(list), 'removed': defaultdict(list), 'modified': defaultdict(list) } # Store raw diff sections for each metric - just collect all lines related to each metric raw_diff_sections = defaultdict(list) exclusion_count = 0 with open(diff_path, 'r') as f: lines = f.readlines() current_metric = None for line in lines: original_line = line.rstrip('\n') stripped = original_line.strip() if stripped.startswith('Metrics excluded from A: ') or stripped.startswith('Metrics excluded from B: '): count_str = stripped.split(': ')[1] exclusion_count += int(count_str) continue # Skip diff headers if stripped.startswith('+++') or stripped.startswith('---'): continue # Check if this line contains a metric change if stripped.startswith('+') or stripped.startswith('-'): metric_name = extract_metric_name(stripped[1:].strip()) if metric_name: # Track the change type change_type = 'added' if stripped.startswith('+') else 'removed' changes[change_type][metric_name].append(stripped[1:].strip()) # Always add to raw diff sections regardless of change type raw_diff_sections[metric_name].append(original_line) current_metric = metric_name else: # If we're in a metric section, keep adding lines if current_metric: raw_diff_sections[current_metric].append(original_line) elif stripped.startswith(' ') and current_metric: # Context line - add to current metric's raw section raw_diff_sections[current_metric].append(original_line) else: # End of current metric section current_metric = None # Identify modified metrics (same metric name with both additions and removals) common_metrics = set(changes['added'].keys()) & set(changes['removed'].keys()) for metric in common_metrics: changes['modified'][metric] = { 'added': changes['added'].pop(metric), 'removed': changes['removed'].pop(metric) } return changes, raw_diff_sections, exclusion_count def extract_metric_name(line): """Extracts metric name from a metric line, matching the diff generation format""" if '{' in line: return line.split('{')[0].strip() return line.strip() def get_raw_diff_sample(raw_lines, max_lines=7): """ Get sample raw diff lines, preserving original diff formatting. """ if not raw_lines: return [] # Take up to max_lines sample_lines = raw_lines[:max_lines] if len(raw_lines) > max_lines: sample_lines.append("...") return sample_lines def generate_diff_summary(changes, raw_diff_sections, exclusion_count): """ Generates a markdown summary from the parsed diff changes with raw diff samples. """ summary = [] # Statistics header total_added = sum(len(v) for v in changes['added'].values()) total_removed = sum(len(v) for v in changes['removed'].values()) total_modified = len(changes['modified']) summary.append(f"**Total Changes:** {total_added + total_removed + total_modified}\n") summary.append(f"- 🆕 Added: {total_added} metrics") summary.append(f"- ❌ Removed: {total_removed} metrics") summary.append(f"- 🔄 Modified: {total_modified} metrics") summary.append(f"- 🚫 Excluded: {exclusion_count} metrics\n") # Added metrics if changes['added']: summary.append("\n#### 🆕 Added Metrics") for metric, samples in changes['added'].items(): summary.append(f"- `{metric}` ({len(samples)} variants)") raw_samples = get_raw_diff_sample(raw_diff_sections.get(metric, [])) if raw_samples: summary.append("
") summary.append("View diff sample") summary.append("") summary.append("```diff") summary.extend(raw_samples) summary.append("```") summary.append("
") # Removed metrics if changes['removed']: summary.append("\n#### ❌ Removed Metrics") for metric, samples in changes['removed'].items(): summary.append(f"- `{metric}` ({len(samples)} variants)") raw_samples = get_raw_diff_sample(raw_diff_sections.get(metric, [])) if raw_samples: summary.append("
") summary.append("View diff sample") summary.append("") summary.append("```diff") summary.extend(raw_samples) summary.append("```") summary.append("
") # Modified metrics if changes['modified']: summary.append("\n#### 🔄 Modified Metrics") for metric, versions in changes['modified'].items(): summary.append(f"- `{metric}`") summary.append(f" - Added variants: {len(versions['added'])}") summary.append(f" - Removed variants: {len(versions['removed'])}") raw_samples = get_raw_diff_sample(raw_diff_sections.get(metric, [])) if raw_samples: summary.append("
") summary.append(" View diff sample") summary.append("") summary.append(" ```diff") summary.extend([f" {line}" for line in raw_samples]) summary.append(" ```") summary.append("
") return "\n".join(summary) MAX_METRIC_NAMES = 200 def generate_structured_json(changes): """ Generates a structured JSON-serializable dict of metric change data. Contains only metric names (strings) and counts (ints) — no raw diff lines or free-form text — so it is safe to pass through ci-summary.json to the trusted publish workflow. Counts use metric-name semantics (number of unique metric names per category) so that they match the displayed metric_names list. Note: the TOTAL_CHANGES headline uses variant-level counts from the markdown summary; the per-snapshot detail intentionally shows the simpler metric-name-level view. """ added_names = sorted(changes['added'].keys()) removed_names = sorted(changes['removed'].keys()) modified_names = sorted(changes['modified'].keys()) # Union of all changed metric names, deduplicated, sorted, and capped # to avoid unbounded artifact growth. The publish workflow enforces a # matching cap (MAX_METRIC_NAMES_PER_SNAPSHOT). all_names = sorted(set(added_names) | set(removed_names) | set(modified_names)) capped = all_names[:MAX_METRIC_NAMES] # Compute counts from the capped list so they match the displayed names. added_set = set(added_names) removed_set = set(removed_names) modified_set = set(modified_names) return { 'added': sum(1 for n in capped if n in added_set), 'removed': sum(1 for n in capped if n in removed_set), 'modified': sum(1 for n in capped if n in modified_set), 'metric_names': capped, } def main(): parser = argparse.ArgumentParser(description='Generate metrics diff summary') parser.add_argument('--diff', required=True, help='Path to unified diff file') parser.add_argument('--output', required=True, help='Output summary file path') parser.add_argument('--json-output', default=None, help='Optional path to write structured JSON change data') args = parser.parse_args() changes, raw_diff_sections, exclusion_count = parse_diff_file(args.diff) summary = generate_diff_summary(changes, raw_diff_sections, exclusion_count) with open(args.output, 'w') as f: f.write(summary) if args.json_output: structured = generate_structured_json(changes) with open(args.json_output, 'w') as f: json.dump(structured, f, indent=2) print(f"Structured JSON saved to {args.json_output}") print(f"Generated diff summary with {len(changes['added'])} additions, " f"{len(changes['removed'])} removals, " f"{len(changes['modified'])} modifications and " f"{exclusion_count} exclusions") print(f"Summary saved to {args.output}") if __name__ == '__main__': main() ================================================ FILE: scripts/e2e/metrics_summary.sh ================================================ #!/bin/bash # Copyright (c) 2025 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # Enable debug tracing and exit on error set -exo pipefail METRICS_DIR="${METRICS_DIR:-./.artifacts}" declare -a summary_files=() declare -a json_files=() total_changes=0 echo "Starting metrics diff processing in directory: $METRICS_DIR" echo "Directory structure:" ls -la "$METRICS_DIR" || echo "Metrics directory listing failed" # Verify 1-to-1: every metrics_snapshot_* artifact must have a diff_metrics_snapshot_* artifact. # verify-metrics-snapshot always uploads a diff artifact on PRs (empty stub if no baseline), # so a missing diff dir means that action never ran for that snapshot — an infra failure. echo "=== Checking for missing diff artifacts ===" declare -a missing_diffs=() found_any_snapshot=false for snapshot_dir in "$METRICS_DIR"/metrics_snapshot_*/; do [ -d "$snapshot_dir" ] || continue found_any_snapshot=true name=$(basename "$snapshot_dir") if [ ! -d "$METRICS_DIR/diff_$name" ]; then echo "::error::Missing diff artifact for snapshot: $name" missing_diffs+=("$name") else echo "OK: diff_$name present" fi done if [ "$found_any_snapshot" = false ]; then echo "::error::No metrics_snapshot_* artifacts found; E2E jobs may not have run" missing_diffs+=("(no snapshot artifacts found)") fi if [ ${#missing_diffs[@]} -gt 0 ]; then echo "INFRA_ERRORS=true" >> "$GITHUB_OUTPUT" else echo "INFRA_ERRORS=false" >> "$GITHUB_OUTPUT" fi # Debug: List all diff files found echo "=== Searching for diff files ===" find "$METRICS_DIR" -type f -name "diff_*.txt" | while read -r file; do echo "Found diff file: $file" done # Process all non-empty diff files. # Empty diff files are stubs uploaded by verify-metrics-snapshot when there is no # baseline or when compare_metrics.py found no differences (it only writes to the # output file when differences exist). The 1-to-1 directory check above already # verified the action ran; here we only want to summarise actual changes. while IFS= read -r -d '' diff_file; do if [ ! -s "$diff_file" ]; then echo "Skipping empty diff file (no changes or no baseline): $diff_file" continue fi echo "Processing diff file: $diff_file" # Derive the unique snapshot name from the artifact directory (e.g., # diff_metrics_snapshot_cassandras_4.x_v004_v2_manual -> metrics_snapshot_cassandras_4.x_v004_v2_manual). # Using the directory rather than the file name is necessary because all matrix # variants of the same backend share an identical file name inside their artifact # (e.g., diff_metrics_snapshot_cassandra.txt), while the artifact directory name # is always unique (it includes major version, schema, jaeger-version, etc.). dir=$(dirname "$diff_file") snapshot_name=$(basename "$dir") snapshot_name=${snapshot_name#diff_} # Generate summary for this diff summary_file="$dir/summary_$snapshot_name.md" json_file="$dir/changes_$snapshot_name.json" echo "Generating summary for $snapshot_name" python3 ./scripts/e2e/metrics_summary.py \ --diff "$diff_file" \ --output "$summary_file" \ --json-output "$json_file" summary_files+=("$summary_file") json_files+=("$json_file") echo "Generated summary at: $summary_file" done < <(find "$METRICS_DIR" -type f -name "diff_*.txt" -print0) # Output results # Calculate total changes across all files total_changes=0 if [ ${#summary_files[@]} -eq 0 ]; then echo "No diff files found; all metrics are within baseline." else for summary_file in "${summary_files[@]}"; do changes=$(grep -F "**Total Changes:**" "$summary_file" | awk '{print $3}') total_changes=$((total_changes + changes)) done fi echo "Total changes across all snapshots: $total_changes" echo "TOTAL_CHANGES=$total_changes" >> "$GITHUB_OUTPUT" if [ ${#missing_diffs[@]} -gt 0 ]; then echo "CONCLUSION=failure" >> "$GITHUB_OUTPUT" elif [ "$total_changes" -gt 0 ]; then echo "CONCLUSION=failure" >> "$GITHUB_OUTPUT" else echo "CONCLUSION=success" >> "$GITHUB_OUTPUT" fi # Merge per-snapshot JSON files into a single metrics_snapshots.json. # Each entry gets a "snapshot" field with the snapshot name. # Capped at 50 entries to match the publish workflow's MAX_SNAPSHOTS limit. # The trusted publish workflow validates this data before rendering. python3 - "$METRICS_DIR" "${json_files[@]}" <<'PYEOF' import json, os, sys metrics_dir = sys.argv[1] MAX_SNAPSHOTS = 50 snapshots = [] for path in sys.argv[2:]: if len(snapshots) >= MAX_SNAPSHOTS: print(f"Warning: capped at {MAX_SNAPSHOTS} snapshots", file=sys.stderr) break try: with open(path) as f: data = json.load(f) basename = os.path.basename(path) name = basename.removeprefix('changes_').removesuffix('.json') data['snapshot'] = name snapshots.append(data) except Exception as e: print(f"Warning: could not read {path}: {e}", file=sys.stderr) output_path = os.path.join(metrics_dir, 'metrics_snapshots.json') snapshots.sort(key=lambda s: s.get('snapshot', '')) with open(output_path, 'w') as f: json.dump(snapshots, f, indent=2) print(f"Merged {len(snapshots)} snapshot(s) into {output_path}") PYEOF # Log the combined summary to the console (visible in CI run logs). # Structured conclusions are already emitted to $GITHUB_OUTPUT above. echo "=== Metrics Comparison Summary ===" if [ ${#missing_diffs[@]} -gt 0 ]; then echo "::error::Infrastructure error: diff artifacts missing for: ${missing_diffs[*]}" echo "(These snapshots did not produce a diff artifact — the verify-metrics-snapshot action may not have run.)" fi if [ "$total_changes" -gt 0 ]; then echo "::error::${total_changes} metric change(s) detected across all snapshots" echo "" for summary_file in "${summary_files[@]}"; do file_name=$(basename "$summary_file" .md) echo "--- ${file_name} ---" echo "" cat "$summary_file" echo "" done elif [ ${#missing_diffs[@]} -gt 0 ]; then echo "No metric changes in available diffs, but some diff artifacts were missing (see above)." else echo "No metric changes detected." fi echo "Metrics diff processing completed" ================================================ FILE: scripts/e2e/spm.sh ================================================ #!/bin/bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 set -euf -o pipefail # Log function that adds timestamp to all messages log() { echo "[$(date -u '+%Y-%m-%d %H:%M:%S')] $*" } print_help() { log "Usage: $0 [-m metricstore]" log "-m: Which database to use as metrics store: 'prometheus' (default) or 'elasticsearch' or 'opensearch'" log "-h: Print help" exit 1 } METRICSTORE='prometheus' compose_file=docker-compose/monitor/docker-compose.yml make_target="dev" while getopts "m:h" opt; do case "${opt}" in m) METRICSTORE=${OPTARG} ;; *) print_help ;; esac done set -x # Enable verbose logging for debugging # Validate metricstore option case "$METRICSTORE" in "prometheus"|"elasticsearch"|"opensearch") # Valid options ;; *) log "❌ ERROR: Invalid metricstore option: $METRICSTORE" print_help ;; esac # Set compose file based on metricstore if [ "$METRICSTORE" == "elasticsearch" ]; then compose_file=docker-compose/monitor/docker-compose-elasticsearch.yml make_target="elasticsearch" fi if [ "$METRICSTORE" == "opensearch" ]; then compose_file=docker-compose/monitor/docker-compose-opensearch.yml make_target="opensearch" fi timeout=600 end_time=$((SECONDS + timeout)) success="false" export SPANMETRICS_FLUSH_INTERVAL=1s # flush quickly to make IT run faster check_service_health() { local service_name=$1 local url=$2 log "Checking health of service: $service_name at $url" local wait_seconds=3 local curl_params=( --silent --output /dev/null --write-out "%{http_code}" ) while [ $SECONDS -lt $end_time ]; do if [[ "$(curl "${curl_params[@]}" "${url}")" == "200" ]]; then log "✅ $service_name is healthy" return 0 fi log "Waiting for $service_name to be healthy..." sleep $wait_seconds done log "❌ ERROR: $service_name did not become healthy in time" return 1 } # Function to check if all services are healthy wait_for_services_to_be_healthy() { log "Waiting for services to be up and running..." case "$METRICSTORE" in "elasticsearch") check_service_health "Elasticsearch" "http://localhost:9200" ;; "opensearch") check_service_health "Opensearch" "http://localhost:9200" ;; "prometheus") check_service_health "Prometheus" "http://localhost:9090/query" ;; esac check_service_health "Jaeger" "http://localhost:16686" } get_expected_operations_of_service() { # Which span names do we expect from which service? # See https://github.com/yurishkuro/microsim/blob/main/config/hotrod.go local service=$1 case "$service" in "driver") echo "/FindNearest" ;; "customer") echo "/customer" ;; "mysql") echo "/sql_select" ;; "redis") echo "/FindDriverIDs /GetDriver" ;; "frontend") echo "/dispatch" ;; "route") echo "/GetShortestRoute" ;; "ui") echo "/" ;; *) echo "" ;; esac } # Validate that found operations match expected operations for a service validate_operations_for_service() { local service=$1 local found_operations=$2 local expected_operations expected_operations=$(get_expected_operations_of_service "$service") # If no expected operations defined for this service, skip validation if [[ -z "$expected_operations" ]]; then return 0 fi # Log expected and found operations if [[ -n "$found_operations" ]]; then echo "Expected operations for service '$service': [$expected_operations] | Found operations: [$found_operations]" else echo "Expected operations for service '$service': [$expected_operations] | Found operations: []" fi # If no operations found, that's an error if [[ -z "$found_operations" ]]; then echo "❌ ERROR: No operations found for service '$service', but expected: [$expected_operations]" return 1 fi # Parse comma-separated operations (format: "op1, op2, op3") # Convert to space-separated and normalize whitespace local found_ops_list found_ops_list=$(echo "$found_operations" | sed 's/,/ /g' | tr -s ' ' | sed 's/^ *//;s/ *$//') # Check each found operation against expected ones local found_op for found_op in $found_ops_list; do # Remove any leading/trailing spaces found_op=$(echo "$found_op" | sed 's/^ *//;s/ *$//') # Skip empty operations if [[ -z "$found_op" ]]; then continue fi # Check if this operation is in the expected list local is_expected=false local expected_op for expected_op in $expected_operations; do if [[ "$found_op" == "$expected_op" ]]; then is_expected=true break fi done if [[ "$is_expected" == "false" ]]; then echo "❌ ERROR: Unexpected operation '$found_op' found for service '$service'. Expected operations: [$expected_operations]" return 1 fi done echo "✅ Operation validation passed for service '$service'" return 0 } curl_metrics() { local endpoint=$1 local service=$2 local extra_query=${3:-} # Time constants in milliseconds local fiveMinutes=300000 local oneMinute=60000 local tenSeconds=10000 # When endTs=(blank) the server will default it to now(). local url="http://localhost:16686/api/metrics/${endpoint}?service=${service}&endTs=&lookback=${fiveMinutes}&step=${tenSeconds}&ratePer=${oneMinute}" if [[ -n "$extra_query" ]]; then url="${url}&${extra_query}" fi curl -s "$url" } # Function to validate the service metrics validate_service_metrics() { local service=$1 response=$(curl_metrics "calls" "$service") if ! assert_service_name_equals "$response" "$service" ; then return 1 fi # Check that we receive some non-zero metric values from this service local non_zero_count non_zero_count=$(count_non_zero_metrics_point "$response") local desired_non_zero_count desired_non_zero_count=4 log "Metrics data points found (non-zero): ${non_zero_count}" if [[ $non_zero_count -lt $desired_non_zero_count ]]; then echo "⏳ Want to see at least $desired_non_zero_count non-zero data points" return 1 fi # Validate if labels are correct response=$(curl_metrics "calls" "$service" "groupByOperation=true") if ! assert_labels_set_equals "$response" "operation service_name" ; then return 1 fi # Validate operations from this service are what we expect. echo "Checking operations for service: $service" local operations operations=$(extract_operations "$response") if [[ -n "$operations" ]]; then # Validate that found operations match expected ones if ! validate_operations_for_service "$service" "$operations" "calls"; then return 1 fi else echo "❌ ERROR No operations found yet for service '${service}'. We expected to find some." fi ### Validate Errors Rate metrics response=$(curl_metrics "errors" "$service") if ! assert_service_name_equals "$response" "$service" ; then return 1 fi response=$(curl_metrics "errors" "$service" "groupByOperation=true") if ! assert_labels_set_equals "$response" "operation service_name" ; then return 1 fi non_zero_count=$(count_non_zero_metrics_point "$response") local services_with_error="driver frontend ui redis" if [[ "$services_with_error" =~ $service ]]; then # the service is in the list if [[ $non_zero_count == "0" ]]; then log "⏳ ERROR: expect service $service to have positive errors rate. You may have to wait for an error span to be created because microsim generates errors probabilistically: https://github.com/yurishkuro/microsim/blob/d532cf986675389494c11254ea3ae12c4297e94f/config/hotrod.go#L116" return 1 fi else if [[ $non_zero_count != "0" ]]; then log "❌ ERROR: expect service $service to have 0 errors, but have $non_zero_count data points with positive errors" return 1 fi fi return 0 } assert_service_name_equals() { local response=$1 local expected=$2 # First check if metrics structure exists at all if ! echo "$response" | jq -e '.metrics and .metrics[0]' >/dev/null; then log "⏳ Metrics not available yet (no metrics array)" return 1 fi service_name=$(echo "$response" | jq -r 'if .metrics and .metrics[0] then .metrics[0].labels[] | select(.name=="service_name") | .value else empty end') if [[ "$service_name" != "$expected" ]]; then log "❌ ERROR: Obtained service_name: '$service_name' are not same as expected: '$expected'" return 1 fi return 0 } assert_labels_set_equals() { local response=$1 local expected="$2 " # need one extra space due to how labels is computed labels=$(echo "$response" | jq -r '.metrics[0].labels[].name' | sort | tr '\n' ' ') if [[ "$labels" != "$expected" ]]; then log "❌ ERROR: Obtained labels: '$labels' are not same as expected labels: '$expected'" return 1 fi return 0 } extract_operations() { local response=$1 # Extract all unique operation names from all metrics in the response # Each metric has labels array, and when groupByOperation=true, each metric has a label with name=="operation" local operations operations=$(echo "$response" | jq -r ' if .metrics and (.metrics | length > 0) then [.metrics[] | .labels[] | select(.name=="operation") | .value] | unique | sort | .[] else empty end' 2>/dev/null) if [[ -z "$operations" ]]; then echo "" return 0 fi # Return operations as a comma-separated list echo "$operations" | tr '\n' ',' | sed 's/,$//' | sed 's/,/, /g' } count_non_zero_metrics_point() { echo "$1" | jq -r '[.metrics[0].metricPoints[].gaugeValue.doubleValue | select(. != 0 and (. | tostring != "NaN"))] | length' } check_spm() { local wait_seconds=10 local successful_service=0 services_list=("driver" "customer" "mysql" "redis" "frontend" "route" "ui") for service in "${services_list[@]}"; do log "Processing service: $service" while [ $SECONDS -lt $end_time ]; do if validate_service_metrics "$service"; then log "✅ Found all expected metrics for service '$service'" successful_service=$((successful_service + 1)) break fi sleep $wait_seconds done done if [ $successful_service -lt ${#services_list[@]} ]; then log "❌ ERROR: Expected metrics from ${#services_list[@]} services, found only ${successful_service}" exit 1 else log "✅ All service have valid metrics" fi } dump_logs() { log "::group:: docker logs" docker compose -f $compose_file logs log "::endgroup::" } teardown_services() { if [[ "$success" == "false" ]]; then dump_logs fi docker compose -f $compose_file down } main() { (cd docker-compose/monitor && make build BINARY="jaeger" && make $make_target DOCKER_COMPOSE_ARGS="-d") wait_for_services_to_be_healthy check_spm success="true" } trap teardown_services EXIT INT main ================================================ FILE: scripts/lint/check-go-version.sh ================================================ #!/bin/bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 version_regex='[0-9]\.[0-9][0-9]' update=false verbose=false while getopts "uvdx" opt; do case $opt in u) update=true ;; v) verbose=true ;; x) set -x ;; *) echo "Usage: $0 [-u] [-v] [-d]" >&2 exit 1 ;; esac done # Fetch latest go release version # go_latest_version=$(curl -s https://go.dev/dl/?mode=json | jq -r '.[0].version' | awk -F'.' '{gsub("go", ""); print $1"."$2}') # # UPDATE: we don't use the logic above because it causes CI to fail when new version of Go is released, # which may create circular dependencies when other utilities need to be upgraded. Instead use the go # version declared in the main go.mod. Updates to that version will be handled by the bots. go_latest_version=$(grep "^go " go.mod | sed 's/^go \([0-9]\.[0-9]*\).*/\1/') files_to_update=0 function update() { local file=$1 local pattern=$2 local current=$3 local target=$4 newfile=$(mktemp) old_IFS=$IFS IFS='' while read -r line; do match=$(echo "$line" | grep -e "$pattern") if [[ "$match" != "" ]]; then line=${line//${current}/${target}} fi echo "$line" >> "$newfile" done < "$file" IFS=$old_IFS if [ $verbose = true ]; then diff "$file" "$newfile" fi mv "$newfile" "$file" } function check() { local file=$1 local pattern=$2 local target=$3 go_version=$(grep -e "$pattern" "$file" | head -1 | sed "s/^.*\($version_regex\).*$/\1/") if [ "$go_version" = "$target" ]; then mismatch='' else mismatch="*** needs update to $target ***" files_to_update=$((files_to_update+1)) fi if [[ $update = true && "$mismatch" != "" ]]; then # Detect if the line includes a patch version if [[ "$go_version" =~ $version_regex\.[0-9]+ ]]; then echo "Patch version detected in $file. Manual update required." exit 1 fi update "$file" "$pattern" "$go_version" "$target" mismatch="*** => $target ***" fi printf "%-50s Go version: %s %s\n" "$file" "$go_version" "$mismatch" } # In the main go.mod file (and linter config) we want the same Go version N. # All importable code has been moved to internal packages, so there's no need # to maintain backward compatibility with older compilers. check go.mod "^go\s\+$version_regex" "$go_latest_version" check .golangci.yml "go:\s\+\"$version_regex\"" "$go_latest_version" # find all other go.mod files in the repository and check for latest Go version for file in $(find . -type f -name go.mod | grep -v '^./go.mod'); do if [[ $file == "./idl/go.mod" ]]; then continue fi if [[ $file == "./idl/internal/tools/go.mod" ]]; then continue fi check "$file" "^go\s\+$version_regex" "$go_latest_version" done IFS='|' read -r -a gha_workflows <<< "$(grep -rl go-version .github/workflows | tr '\n' '|')" for gha_workflow in "${gha_workflows[@]}"; do check "$gha_workflow" "^\s*go-version:\s\+$version_regex" "$go_latest_version" done if [ $files_to_update -eq 0 ]; then echo "All files are up to date." else if [[ $update = true ]]; then echo "$files_to_update file(s) updated." else echo "$files_to_update file(s) must be updated. Rerun this script with -u argument." exit 1 fi fi ================================================ FILE: scripts/lint/check-goleak-files.sh ================================================ #!/bin/bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 set -euo pipefail bad_pkgs=0 total_pkgs=0 failed_pkgs=0 invalid_use_pkgs=0 # shellcheck disable=SC2048 for dir in $*; do ((total_pkgs+=1)) if [[ -f "${dir}/.nocover" ]]; then continue fi testFiles=$(find "${dir}" -maxdepth 1 -name '*_test.go') if [[ -z "$testFiles" ]]; then continue fi good=0 invalid=0 for test in ${testFiles}; do if grep -q "TestMain" "${test}" && grep -q "testutils.VerifyGoLeaks" "${test}"; then if [ "${dir}" != "./internal/storage/integration/" ] && grep -q "testutils.VerifyGoLeaksForES" "${test}"; then invalid=1 break fi good=1 break fi done if ((good == 0)); then if ((invalid == 1)); then echo "Error(check-goleak): VerifyGoLeaksForES should only be used in integration package but it is used in ${dir} also" ((invalid_use_pkgs+=1)) else echo "Error(check-goleak): no goleak check in package ${dir}" ((bad_pkgs+=1)) ((failed_pkgs+=1)) fi fi done function help() { echo " See pkg/version/package_test.go as example of adding the checks." } if ((failed_pkgs > 0)); then echo "⛔ Fatal(check-goleak): no goleak check in ${bad_pkgs} package(s), ${failed_pkgs} of which not allowed." help exit 1 elif ((invalid_use_pkgs > 0)); then echo "⛔ Fatal(check-goleak): use of VerifyGoLeaksForES in package(s) ${invalid_use_pkgs} which is not allowed" help exit 1 elif ((bad_pkgs > 0)); then echo "🐞 Warning(check-goleak): no goleak check in ${bad_pkgs} package(s)." help else echo "✅ Info(check-goleak): no issues after scanning ${total_pkgs} package(s)." fi ================================================ FILE: scripts/lint/check-jaeger-idl-version.sh ================================================ #!/bin/bash # Copyright (c) 2025 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 set -euo pipefail dependency="github.com/jaegertracing/jaeger-idl" get_gomod_version() { gomod_dep=$(grep $dependency &2 exit 1 fi gomod_version=$(echo "$gomod_dep" | awk '{print $2}') echo "$gomod_version" } get_submodule_version() { cd idl git fetch --tags commit_version=$(git rev-parse HEAD) semver=$(git describe --tags --exact-match "$commit_version") if [ ! "$semver" ]; then printf "Error: failed getting version from submodule\n" >&2 exit 1 fi echo "$semver" } gomod_semver=$(get_gomod_version) || exit 1 submod_semver=$(get_submodule_version) || exit 1 if [[ "$gomod_semver" != "$submod_semver" ]]; then printf "Error: jaeger-idl version mismatch: go.mod %s != submodule %s\n" "$gomod_semver" "$submod_semver" >&2 exit 1 fi echo "jaeger-idl version match: OK" ================================================ FILE: scripts/lint/check-semconv-version.sh ================================================ #!/bin/bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 set -euf -o pipefail package_name="go.opentelemetry.io/otel/semconv" version_regex="v[0-9]\.[0-9]\+\.[0-9]\+" function find_files() { find . -type f -name "*.go" -exec grep -o -H "$package_name/$version_regex" {} + \ | tr ':' ' ' \ | sed "s|$package_name/||g" } count=$(find_files | awk '{print $2}' | sort -u | wc -l) if [ "$count" -gt 1 ]; then printf "%-70s | %s\n" "Source File" "Semconv Version" printf "%-70s | %s\n" "================" "================" while IFS=' ' read -r file_name version; do printf "%-70s | %s\n" "$file_name" "$version" done < <(find_files) printf "Error: %d different semconv versions detected.\n" "$count" echo "Run ./scripts/lint/update-semconv-version.sh to update semconv to latest version." exit 1 fi ================================================ FILE: scripts/lint/check-test-files.sh ================================================ #!/bin/bash # Copyright (c) 2023 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # This script checks that all directories with go files # have at least one *_test.go file or a .nocover file. set -euo pipefail NO_TEST_FILE_DIRS="" total_pkgs=0 # shellcheck disable=SC2048 for dir in $*; do ((total_pkgs+=1)) mainFile=$(find "${dir}" -maxdepth 1 -name 'main.go') testFiles=$(find "${dir}" -maxdepth 1 -name '*_test.go') if [ -z "${testFiles}" ]; then if [ -n "${mainFile}" ]; then continue # single main does not require tests fi if [ -e "${dir}/.nocover" ]; then reason=$(cat "${dir}/.nocover") if [ "${reason}" == "" ]; then echo "error: ${dir}/.nocover must specify reason" >&2 exit 1 fi echo "Package excluded from coverage: ${dir}" echo " reason: ${reason}" | sed "s/FIXME/🔴 FIXME/" continue fi if [ -z "${NO_TEST_FILE_DIRS}" ]; then NO_TEST_FILE_DIRS="${dir}" else NO_TEST_FILE_DIRS="${NO_TEST_FILE_DIRS} ${dir}" fi fi done if [ -n "${NO_TEST_FILE_DIRS}" ]; then echo "*** directories without *_test.go files:" >&2 echo "${NO_TEST_FILE_DIRS}" | tr ' ' '\n' >&2 echo "error: at least one *_test.go file must be in all directories with go files so that they are counted for code coverage" >&2 echo " if no tests are possible for a package (e.g. it only defines types), create empty_test.go" >&2 exit 1 else echo "✅ Info(check-test-files): no issues after scanning ${total_pkgs} package(s)." fi ================================================ FILE: scripts/lint/dco_check.py ================================================ # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # Script copied from https://github.com/christophebedard/dco-check/blob/master/dco_check/dco_check.py # # Copyright 2020 Christophe Bedard # # 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. """Check that all commits for a proposed change are signed off.""" import argparse from collections import defaultdict import json import os import re import subprocess import sys from typing import Any from typing import Dict from typing import List from typing import Optional from typing import Tuple from urllib import request __version__ = '0.4.0' DEFAULT_BRANCH = 'master' DEFAULT_REMOTE = 'origin' ENV_VAR_CHECK_MERGE_COMMITS = 'DCO_CHECK_CHECK_MERGE_COMMITS' ENV_VAR_DEFAULT_BRANCH = 'DCO_CHECK_DEFAULT_BRANCH' ENV_VAR_DEFAULT_BRANCH_FROM_REMOTE = 'DCO_CHECK_DEFAULT_BRANCH_FROM_REMOTE' ENV_VAR_DEFAULT_REMOTE = 'DCO_CHECK_DEFAULT_REMOTE' ENV_VAR_EXCLUDE_EMAILS = 'DCO_CHECK_EXCLUDE_EMAILS' ENV_VAR_EXCLUDE_PATTERN = 'DCO_CHECK_EXCLUDE_PATTERN' ENV_VAR_QUIET = 'DCO_CHECK_QUIET' ENV_VAR_VERBOSE = 'DCO_CHECK_VERBOSE' TRAILER_KEY_SIGNED_OFF_BY = 'Signed-off-by:' class EnvDefaultOption(argparse.Action): """ Action that uses an env var value as the default if it exists. Inspired by: https://stackoverflow.com/a/10551190/6476709 """ def __init__( self, env_var: str, default: Any, help: Optional[str] = None, # noqa: A002 **kwargs: Any, ) -> None: """Create an EnvDefaultOption.""" # Set default to env var value if it exists if env_var in os.environ: default = os.environ[env_var] if help: # pragma: no cover help += f' [env: {env_var}]' super(EnvDefaultOption, self).__init__( default=default, help=help, **kwargs, ) def __call__( # noqa: D102 self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, values: Any, option_string: Optional[str] = None, ) -> None: setattr(namespace, self.dest, values) class EnvDefaultStoreTrue(argparse.Action): """ Action similar to 'store_true' that uses an env var value as the default if it exists. Partly copied from arparse.{_StoreConstAction,_StoreTrueAction}. """ def __init__( self, option_strings: str, dest: str, env_var: str, default: bool = False, help: Optional[str] = None, # noqa: A002 ) -> None: """Create an EnvDefaultStoreTrue.""" # Set default value to true if the env var exists default = env_var in os.environ if help: # pragma: no cover help += f' [env: {env_var} (any value to enable)]' super(EnvDefaultStoreTrue, self).__init__( option_strings=option_strings, dest=dest, nargs=0, const=True, default=default, required=False, help=help, ) def __call__( # noqa: D102 self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, values: Any, option_string: Optional[str] = None, ) -> None: setattr(namespace, self.dest, self.const) def get_parser() -> argparse.ArgumentParser: """Get argument parser.""" parser = argparse.ArgumentParser( description='Check that all commits of a proposed change have a DCO, i.e. are signed-off.', ) default_branch_group = parser.add_mutually_exclusive_group() default_branch_group.add_argument( '-b', '--default-branch', metavar='BRANCH', action=EnvDefaultOption, env_var=ENV_VAR_DEFAULT_BRANCH, default=DEFAULT_BRANCH, help=( 'default branch to use, if necessary (default: %(default)s)' ), ) default_branch_group.add_argument( '--default-branch-from-remote', action=EnvDefaultStoreTrue, env_var=ENV_VAR_DEFAULT_BRANCH_FROM_REMOTE, default=False, help=( 'get the default branch value from the remote (default: %(default)s)' ), ) parser.add_argument( '-m', '--check-merge-commits', action=EnvDefaultStoreTrue, env_var=ENV_VAR_CHECK_MERGE_COMMITS, default=False, help=( 'check sign-offs on merge commits as well (default: %(default)s)' ), ) parser.add_argument( '-r', '--default-remote', metavar='REMOTE', action=EnvDefaultOption, env_var=ENV_VAR_DEFAULT_REMOTE, default=DEFAULT_REMOTE, help=( 'default remote to use, if necessary (default: %(default)s)' ), ) parser.add_argument( '-e', '--exclude-emails', metavar='EMAIL[,EMAIL]', action=EnvDefaultOption, env_var=ENV_VAR_EXCLUDE_EMAILS, default=None, help=( 'exclude a comma-separated list of author emails from checks ' '(commits with an author email matching one of these emails will be ignored)' ), ) parser.add_argument( '-p', '--exclude-pattern', metavar='REGEX', action=EnvDefaultOption, env_var=ENV_VAR_EXCLUDE_PATTERN, default=None, help=( 'exclude regular expresssion matched author emails from checks ' '(commits with an author email matching regular expression pattern will be ignored)' ), ) output_options_group = parser.add_mutually_exclusive_group() output_options_group.add_argument( '-q', '--quiet', action=EnvDefaultStoreTrue, env_var=ENV_VAR_QUIET, default=False, help=( 'quiet mode (do not print anything; simply exit with 0 or non-zero) ' '(default: %(default)s)' ), ) output_options_group.add_argument( '-v', '--verbose', action=EnvDefaultStoreTrue, env_var=ENV_VAR_VERBOSE, default=False, help=( 'verbose mode (print out more information) (default: %(default)s)' ), ) parser.add_argument( '--version', action='version', help='show version number and exit', version=f'dco-check version {__version__}', ) return parser def parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace: """ Parse arguments. :param argv: the arguments to use, or `None` for sys.argv :return: the parsed arguments """ return get_parser().parse_args(argv) class Options: """Simple container and utilities for options.""" def __init__(self, parser: argparse.ArgumentParser) -> None: """Create using default argument values.""" self.check_merge_commits = parser.get_default('m') self.default_branch = parser.get_default('b') self.default_branch_from_remote = parser.get_default('default-branch-from-remote') self.default_remote = parser.get_default('r') self.exclude_emails = parser.get_default('e') self.exclude_pattern = parser.get_default('p') self.quiet = parser.get_default('q') self.verbose = parser.get_default('v') def set_options(self, args: argparse.Namespace) -> None: """Set options using parsed arguments.""" self.check_merge_commits = args.check_merge_commits self.default_branch = args.default_branch self.default_branch_from_remote = args.default_branch_from_remote self.default_remote = args.default_remote # Split into list and filter out empty elements self.exclude_emails = list(filter(None, (args.exclude_emails or '').split(','))) self.exclude_pattern = ( None if not args.exclude_pattern else re.compile(args.exclude_pattern) ) self.quiet = args.quiet self.verbose = args.verbose # Shouldn't happen with a mutually exclusive group, # but can happen if one is set with an env var # and the other is set with an arg if self.quiet and self.verbose: # Similar message to what is printed when using args for both get_parser().print_usage() print("options '--quiet' and '--verbose' cannot both be true") sys.exit(1) if self.default_branch != DEFAULT_BRANCH and self.default_branch_from_remote: # Similar message to what is printed when using args for both get_parser().print_usage() print( "options '--default-branch' and '--default-branch-from-remote' cannot both be set" ) sys.exit(1) def get_options(self) -> Dict[str, Any]: """Get all options as a dict.""" return self.__dict__ options = Options(get_parser()) class Logger: """Simple logger to stdout which can be quiet or verbose.""" def __init__(self, parser: argparse.ArgumentParser) -> None: """Create using default argument values.""" self.__quiet = parser.get_default('q') self.__verbose = parser.get_default('v') def set_options(self, options: Options) -> None: """Set options using options object.""" self.__quiet = options.quiet self.__verbose = options.verbose def print(self, msg: str = '', *args: Any, **kwargs: Any) -> None: # noqa: A003 """Print if not quiet.""" if not self.__quiet: print(msg, *args, **kwargs) def verbose_print(self, msg: str = '', *args: Any, **kwargs: Any) -> None: """Print if verbose.""" if self.__verbose: print(msg, *args, **kwargs) logger = Logger(get_parser()) def run( command: List[str], ) -> Optional[str]: """ Run command. :param command: the command list :return: the stdout output if the return code is 0, otherwise `None` """ output = None try: env = os.environ.copy() if 'LANG' in env: del env['LANG'] for key in list(env.keys()): if key.startswith('LC_'): del env[key] process = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env, ) output_stdout, _ = process.communicate() if process.returncode != 0: logger.print(f'error: {output_stdout.decode("utf8")}') else: output = output_stdout.rstrip().decode('utf8').strip('\n') except subprocess.CalledProcessError as e: logger.print(f'error: {e.output.decode("utf8")}') return output def is_valid_email( email: str, ) -> bool: """ Check if email is valid. Simple regex checking for: @. :param email: the email address to check :return: true if email is valid, false otherwise """ return bool(re.match(r'^\S+@\S+\.\S+', email)) def get_head_commit_hash() -> Optional[str]: """ Get the hash of the HEAD commit. :return: the hash of the HEAD commit, or `None` if it failed """ command = [ 'git', 'rev-parse', '--verify', 'HEAD', ] return run(command) def get_common_ancestor_commit_hash( base_ref: str, ) -> Optional[str]: """ Get the common ancestor commit of the current commit and a given reference. See: git merge-base --fork-point :param base_ref: the other reference :return: the common ancestor commit, or `None` if it failed """ command = [ 'git', 'merge-base', '--fork-point', base_ref, ] return run(command) def fetch_branch( branch: str, remote: str = 'origin', ) -> int: """ Fetch branch from remote. See: git fetch :param branch: the name of the branch :param remote: the name of the remote :return: zero for success, nonzero otherwise """ command = [ 'git', 'fetch', remote, branch, ] # We don't want the output return 0 if run(command) is not None else 1 def get_default_branch_from_remote( remote: str, ) -> Optional[str]: """ Get default branch from remote. :param remote: the remote name :return: the default branch, or None if it failed """ # https://stackoverflow.com/questions/28666357/git-how-to-get-default-branch#comment92366240_50056710 # noqa: E501 # $ git remote show origin cmd = ['git', 'remote', 'show', remote] result = run(cmd) if not result: return None result_lines = result.split('\n') branch = None for result_line in result_lines: # There is a two-space indentation match = re.match(' HEAD branch: (.*)', result_line) if match: branch = match[1] break return branch def get_commits_data( base: str, head: str, ignore_merge_commits: bool = True, ) -> Optional[str]: """ Get data (full sha & commit body) for commits in a range. The range excludes the 'before' commit, e.g. ]base, head] The output data contains data for individual commits, separated by special characters: * 1st line: full commit sha * 2nd line: author name and email * 3rd line: commit title (subject) * subsequent lines: commit body (which excludes the commit title line) * record separator (0x1e) :param base: the sha of the commit just before the start of the range :param head: the sha of the last commit of the range :param ignore_merge_commits: whether to ignore merge commits :return: the data, or `None` if it failed """ command = [ 'git', 'log', f'{base}..{head}', '--pretty=%H%n%an <%ae>%n%s%n%-b%x1e', ] if ignore_merge_commits: command += ['--no-merges'] return run(command) def split_commits_data( commits_data: str, commits_sep: str = '\x1e', ) -> List[str]: """ Split data into individual commits using a separator. :param commits_data: the full data to be split :param commits_sep: the string which separates individual commits :return: the list of data for each individual commit """ # Remove leading/trailing newlines commits_data = commits_data.strip('\n') # Split in individual commits and remove leading/trailing newlines individual_commits = [ single_output.strip('\n') for single_output in commits_data.split(commits_sep) ] # Filter out empty elements individual_commits = list(filter(None, individual_commits)) return individual_commits def extract_name_and_email( name_and_email: str, ) -> Optional[Tuple[str, str]]: """ Extract a name and an email from a 'name ' string. :param name_and_email: the name and email string :return: the extracted (name, email) tuple, or `None` if it failed """ match = re.search('(.*) <(.*)>', name_and_email) if not match: return None return match.group(1), match.group(2) def format_name_and_email( name: Optional[str], email: Optional[str], ) -> str: """ Format a name and a email into a 'name ' string. :param name: the name, or `None` if N/A :param email: the email, or `None` if N/A :return: the formatted string """ return f"{name or 'N/A'} <{email or 'N/A'}>" def get_env_var( env_var: str, print_if_not_found: bool = True, default: Optional[str] = None, ) -> Optional[str]: """ Get the value of an environment variable. :param env_var: the environment variable name/key :param print_if_not_found: whether to print if the environment variable could not be found :param default: the value to use if the environment variable could not be found :return: the environment variable value, or `None` if not found and no default value was given """ value = os.environ.get(env_var, None) if value is None: if default is not None: if print_if_not_found: logger.print( f"could not get environment variable: '{env_var}'; " f"using value default value: '{default}'" ) value = default elif print_if_not_found: logger.print(f"could not get environment variable: '{env_var}'") return value class CommitInfo: """Container for all necessary commit information.""" def __init__( self, commit_hash: str, title: str, body: List[str], author_name: Optional[str], author_email: Optional[str], is_merge_commit: bool = False, ) -> None: """Create a CommitInfo object.""" self.hash = commit_hash self.title = title self.body = body self.author_name = author_name self.author_email = author_email self.is_merge_commit = is_merge_commit class CommitDataRetriever: """ Abstract commit data retriever. It first provides a method to check whether it applies to the current setup or not. It also provides other methods to get commits to be checked. These should not be called if it doesn't apply. """ def name(self) -> str: """Get a name that represents this retriever.""" raise NotImplementedError # pragma: no cover def applies(self) -> bool: """Check if this retriever applies, i.e. can provide commit data.""" raise NotImplementedError # pragma: no cover def get_commit_range(self) -> Optional[Tuple[str, str]]: """ Get the range of commits to be checked: (last commit that was checked, latest commit). The range excludes the first commit, e.g. ]first commit, second commit] :return the (last commit that was checked, latest commit) tuple, or `None` if it failed """ raise NotImplementedError # pragma: no cover def get_commits(self, base: str, head: str, **kwargs: Any) -> Optional[List[CommitInfo]]: """Get commit data.""" raise NotImplementedError # pragma: no cover class GitRetriever(CommitDataRetriever): """Implementation for any git repository.""" def name(self) -> str: # noqa: D102 return 'git (default)' def applies(self) -> bool: # noqa: D102 # Unless we only have access to a partial commit history return True def get_commit_range(self) -> Optional[Tuple[str, str]]: # noqa: D102 default_branch = options.default_branch logger.verbose_print(f"\tusing default branch '{default_branch}'") commit_hash_base = get_common_ancestor_commit_hash(default_branch) if not commit_hash_base: return None commit_hash_head = get_head_commit_hash() if not commit_hash_head: return None return commit_hash_base, commit_hash_head def get_commits( # noqa: D102 self, base: str, head: str, check_merge_commits: bool = False, **kwargs: Any, ) -> Optional[List[CommitInfo]]: ignore_merge_commits = not check_merge_commits commits_data = get_commits_data(base, head, ignore_merge_commits=ignore_merge_commits) commits: List[CommitInfo] = [] if commits_data is None: return commits individual_commits = split_commits_data(commits_data) for commit_data in individual_commits: commit_lines = commit_data.split('\n') commit_hash = commit_lines[0] commit_author_data = commit_lines[1] commit_title = commit_lines[2] commit_body = commit_lines[3:] author_result = extract_name_and_email(commit_author_data) author_name, author_email = None, None if author_result: author_name, author_email = author_result # There won't be any merge commits at this point is_merge_commit = False commits.append( CommitInfo( commit_hash, commit_title, commit_body, author_name, author_email, is_merge_commit, ) ) return commits class GitLabRetriever(GitRetriever): """Implementation for GitLab CI.""" def name(self) -> str: # noqa: D102 return 'GitLab' def applies(self) -> bool: # noqa: D102 return get_env_var('GITLAB_CI', print_if_not_found=False) is not None def get_commit_range(self) -> Optional[Tuple[str, str]]: # noqa: D102 # See: https://docs.gitlab.com/ee/ci/variables/predefined_variables.html default_branch = get_env_var('CI_DEFAULT_BRANCH', default=options.default_branch) commit_hash_head = get_env_var('CI_COMMIT_SHA') if not commit_hash_head: return None current_branch = get_env_var('CI_COMMIT_BRANCH') if get_env_var('CI_PIPELINE_SOURCE') == 'schedule': # Do not check scheduled pipelines logger.verbose_print("\ton scheduled pipeline: won't check commits") return commit_hash_head, commit_hash_head elif current_branch == default_branch: # If we're on the default branch, just test new commits logger.verbose_print( f"\ton default branch '{current_branch}': " 'will check new commits' ) commit_hash_base = get_env_var('CI_COMMIT_BEFORE_SHA') if commit_hash_base == '0000000000000000000000000000000000000000': logger.verbose_print('\tfound no new commits') return commit_hash_head, commit_hash_head if not commit_hash_base: return None return commit_hash_base, commit_hash_head elif get_env_var('CI_MERGE_REQUEST_ID', print_if_not_found=False): # Get merge request target branch target_branch = get_env_var('CI_MERGE_REQUEST_TARGET_BRANCH_NAME') if not target_branch: return None logger.verbose_print( f"\ton merge request branch '{current_branch}': " f"will check new commits off of target branch '{target_branch}'" ) target_branch_sha = get_env_var('CI_MERGE_REQUEST_TARGET_BRANCH_SHA') if not target_branch_sha: return None return target_branch_sha, commit_hash_head elif get_env_var('CI_EXTERNAL_PULL_REQUEST_IID', print_if_not_found=False): # Get external merge request target branch target_branch = get_env_var('CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME') if not target_branch: return None logger.verbose_print( f"\ton merge request branch '{current_branch}': " f"will check new commits off of target branch '{target_branch}'" ) target_branch_sha = get_env_var('CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA') if not target_branch_sha: return None return target_branch_sha, commit_hash_head else: if not default_branch: return None # Otherwise test all commits off of the default branch logger.verbose_print( f"\ton branch '{current_branch}': " f"will check forked commits off of default branch '{default_branch}'" ) # Fetch default branch remote = options.default_remote if 0 != fetch_branch(default_branch, remote): logger.print(f"failed to fetch '{default_branch}' from remote '{remote}'") return None # Use remote default branch ref remote_branch_ref = remote + '/' + default_branch commit_hash_base = get_common_ancestor_commit_hash(remote_branch_ref) if not commit_hash_base: return None return commit_hash_base, commit_hash_head class CircleCiRetriever(GitRetriever): """Implementation for CircleCI.""" def name(self) -> str: # noqa: D102 return 'CircleCI' def applies(self) -> bool: # noqa: D102 return get_env_var('CIRCLECI', print_if_not_found=False) is not None def get_commit_range(self) -> Optional[Tuple[str, str]]: # noqa: D102 # See: https://circleci.com/docs/2.0/env-vars/#built-in-environment-variables default_branch = options.default_branch commit_hash_head = get_env_var('CIRCLE_SHA1') if not commit_hash_head: return None # Check if base revision is provided to the environment, e.g. # environment: # CIRCLE_BASE_REVISION: << pipeline.git.base_revision >> # See: # https://circleci.com/docs/2.0/pipeline-variables/ # https://circleci.com/docs/2.0/env-vars/#built-in-environment-variables base_revision = get_env_var('CIRCLE_BASE_REVISION', print_if_not_found=False) if base_revision: # For PRs, this is the commit of the base branch, # and, for pushes to a branch, this is the commit before the new commits logger.verbose_print( f"\tchecking commits off of base revision '{base_revision}'" ) return base_revision, commit_hash_head else: current_branch = get_env_var('CIRCLE_BRANCH') if not current_branch: return None # Test all commits off of the default branch logger.verbose_print( f"\ton branch '{current_branch}': " f"will check forked commits off of default branch '{default_branch}'" ) # Fetch default branch remote = options.default_remote if 0 != fetch_branch(default_branch, remote): logger.print(f"failed to fetch '{default_branch}' from remote '{remote}'") return None # Use remote default branch ref remote_branch_ref = remote + '/' + default_branch commit_hash_base = get_common_ancestor_commit_hash(remote_branch_ref) if not commit_hash_base: return None return commit_hash_base, commit_hash_head class AzurePipelinesRetriever(GitRetriever): """Implementation for Azure Pipelines.""" def name(self) -> str: # noqa: D102 return 'Azure Pipelines' def applies(self) -> bool: # noqa: D102 return get_env_var('TF_BUILD', print_if_not_found=False) is not None def get_commit_range(self) -> Optional[Tuple[str, str]]: # noqa: D102 # See: https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#build-variables # noqa: E501 commit_hash_head = get_env_var('BUILD_SOURCEVERSION') if not commit_hash_head: return None current_branch = get_env_var('BUILD_SOURCEBRANCHNAME') if not current_branch: return None base_branch = None # Check if pull request is_pull_request = get_env_var( 'SYSTEM_PULLREQUEST_PULLREQUESTID', print_if_not_found=False, ) if is_pull_request: # Test all commits off of the target branch target_branch = get_env_var('SYSTEM_PULLREQUEST_TARGETBRANCH') if not target_branch: return None logger.verbose_print( f"\ton pull request branch '{current_branch}': " f"will check forked commits off of target branch '{target_branch}'" ) base_branch = target_branch else: # Test all commits off of the default branch default_branch = options.default_branch logger.verbose_print( f"\ton branch '{current_branch}': " f"will check forked commits off of default branch '{default_branch}'" ) base_branch = default_branch # Fetch base branch assert base_branch remote = options.default_remote if 0 != fetch_branch(base_branch, remote): logger.print(f"failed to fetch '{base_branch}' from remote '{remote}'") return None # Use remote default branch ref remote_branch_ref = remote + '/' + base_branch commit_hash_base = get_common_ancestor_commit_hash(remote_branch_ref) if not commit_hash_base: return None return commit_hash_base, commit_hash_head class AppVeyorRetriever(GitRetriever): """Implementation for AppVeyor.""" def name(self) -> str: # noqa: D102 return 'AppVeyor' def applies(self) -> bool: # noqa: D102 return get_env_var('APPVEYOR', print_if_not_found=False) is not None def get_commit_range(self) -> Optional[Tuple[str, str]]: # noqa: D102 # See: https://www.appveyor.com/docs/environment-variables/ default_branch = options.default_branch commit_hash_head = get_env_var('APPVEYOR_REPO_COMMIT') if not commit_hash_head: commit_hash_head = get_head_commit_hash() if not commit_hash_head: return None branch = get_env_var('APPVEYOR_REPO_BRANCH') if not branch: return None # Check if pull request if get_env_var('APPVEYOR_PULL_REQUEST_NUMBER', print_if_not_found=False): current_branch = get_env_var('APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH') if not current_branch: return None target_branch = branch logger.verbose_print( f"\ton pull request branch '{current_branch}': " f"will check commits off of target branch '{target_branch}'" ) commit_hash_head = get_env_var('APPVEYOR_PULL_REQUEST_HEAD_COMMIT') or commit_hash_head if not commit_hash_head: return None commit_hash_base = get_common_ancestor_commit_hash(target_branch) if not commit_hash_base: return None return commit_hash_base, commit_hash_head else: # Otherwise test all commits off of the default branch current_branch = branch logger.verbose_print( f"\ton branch '{current_branch}': " f"will check forked commits off of default branch '{default_branch}'" ) commit_hash_base = get_common_ancestor_commit_hash(default_branch) if not commit_hash_base: return None return commit_hash_base, commit_hash_head class GitHubRetriever(CommitDataRetriever): """Implementation for GitHub CI.""" def name(self) -> str: # noqa: D102 return 'GitHub CI' def applies(self) -> bool: # noqa: D102 return get_env_var('GITHUB_ACTIONS', print_if_not_found=False) == 'true' def get_commit_range(self) -> Optional[Tuple[str, str]]: # noqa: D102 # See: https://docs.gitlab.com/ee/ci/variables/predefined_variables.html self.github_token = get_env_var('GITHUB_TOKEN') if not self.github_token: logger.print('Did you forget to include this in your workflow config?') logger.print('\n\tenv:\n\t\tGITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}') return None # See: https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables # noqa: E501 event_payload_path = get_env_var('GITHUB_EVENT_PATH') if not event_payload_path: return None f = open(event_payload_path) self.event_payload = json.load(f) f.close() # Get base & head commits depending on the workflow event type event_name = get_env_var('GITHUB_EVENT_NAME') if not event_name: return None commit_hash_base = None commit_hash_head = None if event_name in ('pull_request', 'pull_request_target'): # See: https://developer.github.com/v3/activity/events/types/#pullrequestevent commit_hash_base = self.event_payload['pull_request']['base']['sha'] commit_hash_head = self.event_payload['pull_request']['head']['sha'] commit_branch_base = self.event_payload['pull_request']['base']['ref'] commit_branch_head = self.event_payload['pull_request']['head']['ref'] logger.verbose_print( f"\ton pull request branch '{commit_branch_head}': " f"will check commits off of base branch '{commit_branch_base}'" ) elif event_name == 'push': # See: https://developer.github.com/v3/activity/events/types/#pushevent created = self.event_payload['created'] if created: # If the branch was just created, there won't be a 'before' commit, # therefore just get the first commit in the new branch and append '^' # to get the commit before that one commits = self.event_payload['commits'] # TODO check len(commits), # it's probably 0 when pushing a new branch that is based on an existing one commit_hash_base = commits[0]['id'] + '^' else: commit_hash_base = self.event_payload['before'] commit_hash_head = self.event_payload['head_commit']['id'] else: # pragma: no cover logger.print('Unknown workflow event:', event_name) return None return commit_hash_base, commit_hash_head def get_commits( # noqa: D102 self, base: str, head: str, **kwargs: Any, ) -> Optional[List[CommitInfo]]: # Request commit data compare_url_template = self.event_payload['repository']['compare_url'] compare_url = compare_url_template.format(base=base, head=head) req = request.Request(compare_url, headers={ 'User-Agent': 'dco_check', 'Authorization': 'token ' + (self.github_token or ''), }) response = request.urlopen(req) if 200 != response.getcode(): # pragma: no cover from pprint import pformat logger.print('Request failed: compare_url') logger.print('reponse:', pformat(response.read().decode())) return None # Extract data response_json = json.load(response) commits = [] for commit in response_json['commits']: commit_hash = commit['sha'] message = commit['commit']['message'].split('\n') message = list(filter(None, message)) commit_title = message[0] commit_body = message[1:] author_name = commit['commit']['author']['name'] author_email = commit['commit']['author']['email'] is_merge_commit = len(commit['parents']) > 1 commits.append( CommitInfo( commit_hash, commit_title, commit_body, author_name, author_email, is_merge_commit, ) ) return commits def process_commits( commits: List[CommitInfo], check_merge_commits: bool, ) -> Dict[str, List[str]]: """ Process commit information to detect DCO infractions. :param commits: the list of commit info :param check_merge_commits: true to check merge commits, false otherwise :return: the infractions as a dict {commit sha, infraction explanation} """ infractions: Dict[str, List[str]] = defaultdict(list) for commit in commits: # Skip this commit if it is a merge commit and the # option for checking merge commits is not enabled if commit.is_merge_commit and not check_merge_commits: logger.verbose_print('\t' + 'ignoring merge commit:', commit.hash) logger.verbose_print() continue logger.verbose_print( '\t' + commit.hash + (' (merge commit)' if commit.is_merge_commit else '') ) logger.verbose_print('\t' + format_name_and_email(commit.author_name, commit.author_email)) logger.verbose_print('\t' + commit.title) logger.verbose_print('\t' + '\n\t'.join(commit.body)) # Check author name and email if any(not d for d in [commit.author_name, commit.author_email]): infractions[commit.hash].append( f'could not extract author data for commit: {commit.hash}' ) continue # Check if the commit should be ignored because of the commit author email if options.exclude_emails and commit.author_email in options.exclude_emails: logger.verbose_print('\t\texcluding commit since author email is in exclude list') logger.verbose_print() continue # Check if the commit should be ignored because of the commit author email pattern if commit.author_email and options.exclude_pattern: if options.exclude_pattern.search(commit.author_email): logger.verbose_print('\t\texcluding commit since author email is matched by') logger.verbose_print('\t\tpattern') logger.verbose_print() continue # Extract sign-off data sign_offs = [ body_line.replace(TRAILER_KEY_SIGNED_OFF_BY, '').strip(' ') for body_line in commit.body if body_line.startswith(TRAILER_KEY_SIGNED_OFF_BY) ] # Check that there is at least one sign-off right away if len(sign_offs) == 0: infractions[commit.hash].append('no sign-off found') continue # Extract sign off information sign_offs_name_email: List[Tuple[str, str]] = [] for sign_off in sign_offs: sign_off_result = extract_name_and_email(sign_off) if not sign_off_result: continue name, email = sign_off_result logger.verbose_print(f'\t\tfound sign-off: {format_name_and_email(name, email)}') if not is_valid_email(email): infractions[commit.hash].append(f'invalid email: {email}') else: sign_offs_name_email.append((name, email.lower())) # Check that author is in the sign-offs if not (commit.author_name, commit.author_email.lower()) in sign_offs_name_email: infractions[commit.hash].append( 'sign-off not found for commit author: ' f'{commit.author_name} {commit.author_email}; found: {sign_offs}' ) # Separator between commits logger.verbose_print() return infractions def check_infractions( infractions: Dict[str, List[str]], ) -> int: """ Check infractions. :param infractions: the infractions dict {commit sha, infraction explanation} :return: 0 if no infractions, non-zero otherwise """ if len(infractions) > 0: logger.print('Missing sign-off(s):') logger.print() for commit_sha, commit_infractions in infractions.items(): logger.print('\t' + commit_sha) for commit_infraction in commit_infractions: logger.print('\t\t' + commit_infraction) return 1 logger.print('All good!') return 0 def main(argv: Optional[List[str]] = None) -> int: """ Entrypoint. :param argv: the arguments to use, or `None` for sys.argv :return: 0 if successful, non-zero otherwise """ args = parse_args(argv) options.set_options(args) logger.set_options(options) # Print options if options.verbose: logger.verbose_print('Options:') for name, value in options.get_options().items(): logger.verbose_print(f'\t{name}: {str(value)}') logger.verbose_print() # Detect CI # Use first one that applies retrievers = [ GitLabRetriever, GitHubRetriever, AzurePipelinesRetriever, AppVeyorRetriever, CircleCiRetriever, GitRetriever, ] commit_retriever = None for retriever_cls in retrievers: retriever = retriever_cls() if retriever.applies(): commit_retriever = retriever break if not commit_retriever: logger.print('Could not find an applicable GitRetriever') return 1 logger.print('Detected:', commit_retriever.name()) # Get default branch from remote if enabled if options.default_branch_from_remote: remote_default_branch = get_default_branch_from_remote(options.default_remote) if not remote_default_branch: logger.print('Could not get default branch from remote') return 1 options.default_branch = remote_default_branch logger.print(f"\tgot default branch '{remote_default_branch}' from remote") # Get range of commits commit_range = commit_retriever.get_commit_range() if not commit_range: return 1 commit_hash_base, commit_hash_head = commit_range logger.print() # Return success now if base == head if commit_hash_base == commit_hash_head: logger.print('No commits to check') return 0 logger.print(f'Checking commits: {commit_hash_base}..{commit_hash_head}') logger.print() # Get commits commits = commit_retriever.get_commits( commit_hash_base, commit_hash_head, check_merge_commits=options.check_merge_commits, ) if commits is None: return 1 # Process them infractions = process_commits(commits, options.check_merge_commits) # Check if there are any infractions result = check_infractions(infractions) if len(commits) == 0: logger.print('Warning: no commits were actually checked') return result if __name__ == '__main__': # pragma: no cover sys.exit(main()) ================================================ FILE: scripts/lint/import-order-cleanup.py ================================================ #!/usr/bin/env python3 # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 import argparse def cleanup_imports_and_return(imports): os_packages = [] jaeger_packages = [] thirdparty_packages = [] for i in imports: if i.strip() == "": continue if i.find("github.com/jaegertracing/jaeger/") != -1 or i.find("github.com/jaegertracing/jaeger-idl/") != -1: jaeger_packages.append(i) elif i.find(".com") != -1 or i.find(".net") != -1 or i.find(".org") != -1 or i.find(".in") != -1 or i.find(".io") != -1: thirdparty_packages.append(i) else: os_packages.append(i) l = [] needs_new_line = False if os_packages: l.extend(os_packages) needs_new_line = True if thirdparty_packages: if needs_new_line: l.append("") l.extend(thirdparty_packages) needs_new_line = True if jaeger_packages: if needs_new_line: l.append("") l.extend(jaeger_packages) imports_reordered = imports != l l.insert(0, "import (") l.append(")") return l, imports_reordered def parse_go_file(f): with open(f, 'r') as go_file: lines = [i.rstrip() for i in go_file.readlines()] in_import_block = False imports_reordered = False imports = [] output_lines = [] for line in lines: if in_import_block: endIdx = line.find(")") if endIdx != -1: in_import_block = False ordered_imports, imports_reordered = cleanup_imports_and_return(imports) output_lines.extend(ordered_imports) imports = [] continue imports.append(line) else: importIdx = line.find("import (") if importIdx != -1: in_import_block = True continue output_lines.append(line) output_lines.append("") return "\n".join(output_lines), imports_reordered def main(): parser = argparse.ArgumentParser( description='Tool to make cleaning up import orders easily') parser.add_argument('-o', '--output', default='stdout', choices=['inplace', 'stdout'], help='output target [default: stdout]') parser.add_argument('-t', '--target', help='list of filenames to operate upon', nargs='+', required=True) args = parser.parse_args() output = args.output go_files = args.target for f in go_files: parsed, imports_reordered = parse_go_file(f) if output == "stdout": if imports_reordered: print(f + " imports out of order") else: with open(f, 'w') as ofile: ofile.write(parsed) if __name__ == '__main__': main() ================================================ FILE: scripts/lint/replace_license_headers.py ================================================ #!/usr/bin/env python3 # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # Replace Apache 2.0 license headers with SPDX license identifiers. import re import sys def replace_license_header(file_path, dry_run=False): with open(file_path, 'r') as file: content = file.read() # Pattern to match the entire old header, including multiple copyright lines header_pattern = re.compile(r'(?s)^(// Copyright.*?(?:\n// Copyright.*?)*\n//\n// Licensed under the Apache License.*?limitations under the License\.)\s*\n') match = header_pattern.match(content) if match: old_header = match.group(1) if "SPDX-License-Identifier: Apache-2.0" in old_header: print(f"Skipping {file_path}: SPDX identifier already present") return False if dry_run: print(f"Would update {file_path}") return True # Preserve all copyright lines and add SPDX identifier copyright_lines = re.findall(r'// Copyright.*', old_header) new_header = "\n".join(copyright_lines) + "\n// SPDX-License-Identifier: Apache-2.0\n\n" new_content = header_pattern.sub(new_header, content, count=1) with open(file_path, 'w') as file: file.write(new_content) print(f"Updated {file_path}") return True else: print(f"Warning: {file_path} - Could not find expected license header") return False def main(): dry_run = '--dry-run' in sys.argv files = [f for f in sys.argv[1:] if f != '--dry-run'] if not files: print("Usage: python replace_license_headers.py [--dry-run] [ ...]") sys.exit(1) if dry_run: print("Performing dry run - no files will be modified") total_updated = sum(replace_license_header(file, dry_run) for file in files) print(f"Total files {'that would be' if dry_run else ''} updated: {total_updated}") if __name__ == "__main__": main() ================================================ FILE: scripts/lint/update-semconv-version.sh ================================================ #!/bin/bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 package_name="go.opentelemetry.io/otel/semconv" version_regex="v[0-9]\.[0-9]\+\.[0-9]\+" latest_semconv_version=$( curl -s https://pkg.go.dev/$package_name \ | grep -oP 'data-id="v\d+\.\d+\.\d+"' \ | sed -E "s/\"($version_regex)\"/v\1/" \ | sort -Vr \ | head -n 1 \ | awk -F'"' '{print $2}' ) latest_package_string="$package_name/$latest_semconv_version" while IFS=: read -r file_name package_string; do version_number=${package_string##*/} if [ "$version_number" != "$latest_semconv_version" ]; then sed -i "s#$package_name/$version_regex#$latest_package_string#g" "$file_name" { printf "Source File: %-60s | Previous Semconv Version: %s | Updated Semconv Version: %s\n" "$file_name" "$version_number" "$latest_semconv_version" } | column -t -s '|' fi done < <(find . -type f -name "*.go" -exec grep -o -H "$package_name/$version_regex" {} +) ================================================ FILE: scripts/lint/updateLicense.py ================================================ #!/usr/bin/env python3 # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 import logging import re import sys from datetime import datetime logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) CURRENT_YEAR = datetime.today().year LICENSE_BLOB = """Copyright (c) %d The Jaeger Authors. SPDX-License-Identifier: Apache-2.0""" % CURRENT_YEAR def get_license_blob_lines(comment_prefix): return [ (comment_prefix + ' ' + l).strip() + '\n' for l in LICENSE_BLOB.split('\n') ] COPYRIGHT_RE = re.compile(r'Copyright \(c\) (\d+)', re.I) SHEBANG_RE = re.compile(r'^#!\s*/[^\s]+') def update_license(name, license_lines): with open(name) as f: orig_lines = list(f) lines = list(orig_lines) found = False changed = False jaeger = False for i, line in enumerate(lines[:5]): m = COPYRIGHT_RE.search(line) if not m: continue found = True jaeger = 'Jaeger' in line year = int(m.group(1)) if year == CURRENT_YEAR: break # Avoid updating the copyright year. # # new_line = COPYRIGHT_RE.sub('Copyright (c) %d' % CURRENT_YEAR, line) # assert line != new_line, ('Could not change year in: %s' % line) # lines[i] = new_line # changed = True break # print('found=%s, changed=%s, jaeger=%s' % (found, changed, jaeger)) first_line = lines[0] shebang_match = SHEBANG_RE.match(first_line) def replace(header_lines): if 'Code generated by' in first_line: lines[1:1] = ['\n'] + header_lines elif shebang_match: lines[1:1] = header_lines else: lines[0:0] = header_lines if not found: # depend on file type if(shebang_match): replace(['\n'] + license_lines) else: replace(license_lines + ['\n']) changed = True else: if not jaeger: replace(license_lines[0]) changed = True if changed: with open(name, 'w') as f: for line in lines: f.write(line) print(name) def get_license_type(file): license_blob_lines_go = get_license_blob_lines('//') license_blob_lines_script = get_license_blob_lines('#') ext_map = { '.go' : license_blob_lines_go, '.mk' : license_blob_lines_script, 'Makefile' : license_blob_lines_script, 'Dockerfile' : license_blob_lines_script, '.py' : license_blob_lines_script, '.sh' : license_blob_lines_script, } license_type = None for ext, license in ext_map.items(): if file.endswith(ext): license_type = license break return license_type def main(): if len(sys.argv) == 1: print('USAGE: %s FILE ...' % sys.argv[0]) sys.exit(1) for name in sys.argv[1:]: license_type = get_license_type(name) if license_type: try: update_license(name, license_type) except Exception as error: logger.error('Failed to process file %s', name) logger.exception(error) raise error else: raise NotImplementedError('Unsupported file type: %s' % name) if __name__ == "__main__": main() ================================================ FILE: scripts/makefiles/BuildBinaries.mk ================================================ # Copyright (c) 2023 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 GOBUILD_EXEC := CGO_ENABLED=0 installsuffix=cgo $(GO) build -trimpath STYLE_BOLD_BLUE := \e[1m\e[34m STYLE_BOLD_ORANGE := \033[1m\033[38;5;208m STYLE_RESET := \e[39m\e[0m # This macro expects $GOOS/$GOARCH env variables set to reflect the desired target platform. # It also expects one argument: the name of the binary being built. define GOBUILD @printf "🚧 building binary '$(STYLE_BOLD_ORANGE)%s$(STYLE_RESET)' for $$(go env GOOS)-$$(go env GOARCH)\n" "$1" $(GOBUILD_EXEC) endef ifeq ($(DEBUG_BINARY),) DISABLE_OPTIMIZATIONS = SUFFIX = TARGET = release else DISABLE_OPTIMIZATIONS = -gcflags="all=-N -l" SUFFIX = -debug TARGET = debug endif .PHONY: build-ui build-ui: cmd/jaeger/internal/extension/jaegerquery/internal/ui/actual/index.html.gz cmd/jaeger/internal/extension/jaegerquery/internal/ui/actual/index.html.gz: jaeger-ui/packages/jaeger-ui/build/index.html # do not delete dot-files rm -rf cmd/jaeger/internal/extension/jaegerquery/internal/ui/actual/* cp -r jaeger-ui/packages/jaeger-ui/build/* cmd/jaeger/internal/extension/jaegerquery/internal/ui/actual/ find cmd/jaeger/internal/extension/jaegerquery/internal/ui/actual -type f | grep -v .gitignore | xargs gzip --no-name # copy the timestamp for index.html.gz from the original file touch -t $$(date -r jaeger-ui/packages/jaeger-ui/build/index.html '+%Y%m%d%H%M.%S') cmd/jaeger/internal/extension/jaegerquery/internal/ui/actual/index.html.gz ls -lF cmd/jaeger/internal/extension/jaegerquery/internal/ui/actual/ jaeger-ui/packages/jaeger-ui/build/index.html: $(MAKE) rebuild-ui .PHONY: rebuild-ui rebuild-ui: @echo "::group::rebuild-ui logs" bash ./scripts/build/rebuild-ui.sh @echo "NOTE: This target only rebuilds the UI assets inside jaeger-ui/packages/jaeger-ui/build/." @echo "NOTE: To make them usable from query-service run 'make build-ui'." @echo "::endgroup::" .PHONY: build-examples build-examples: $(GOBUILD) -o ./examples/hotrod/hotrod-$(GOOS)-$(GOARCH) ./examples/hotrod/main.go .PHONY: build-tracegen build-tracegen: $(call GOBUILD,tracegen) -o ./cmd/tracegen/tracegen-$(GOOS)-$(GOARCH) ./cmd/tracegen/ .PHONY: build-anonymizer build-anonymizer: $(call GOBUILD,anonymizer) -o ./cmd/anonymizer/anonymizer-$(GOOS)-$(GOARCH) ./cmd/anonymizer/ .PHONY: build-esmapping-generator build-esmapping-generator: $(call GOBUILD,esmapping-generator) -o ./cmd/esmapping-generator/esmapping-generator-$(GOOS)-$(GOARCH) ./cmd/esmapping-generator/ .PHONY: build-es-index-cleaner build-es-index-cleaner: $(call GOBUILD,es-index-cleaner) -o ./cmd/es-index-cleaner/es-index-cleaner-$(GOOS)-$(GOARCH) ./cmd/es-index-cleaner/ .PHONY: build-es-rollover build-es-rollover: $(call GOBUILD,es-rollover) -o ./cmd/es-rollover/es-rollover-$(GOOS)-$(GOARCH) ./cmd/es-rollover/ # Requires variables: $(BIN_NAME) $(BIN_PATH) $(GO_TAGS) $(DISABLE_OPTIMIZATIONS) $(SUFFIX) $(GOOS) $(GOARCH) $(BUILD_INFO) # Other targets can depend on this one but with a unique suffix to ensure it is always executed. BIN_PATH = ./cmd/$(BIN_NAME) .PHONY: _build-a-binary _build-a-binary-%: $(call GOBUILD,$(BIN_PATH)) $(DISABLE_OPTIMIZATIONS) $(GO_TAGS) -o $(BIN_PATH)/$(BIN_NAME)$(SUFFIX)-$(GOOS)-$(GOARCH) $(BUILD_INFO) $(BIN_PATH) .PHONY: build-jaeger build-jaeger: BIN_NAME = jaeger build-jaeger: build-ui _build-a-binary-jaeger$(SUFFIX)-$(GOOS)-$(GOARCH) @ set -euf -o pipefail ; \ echo "Checking version of built binary" ; \ REAL_GOOS=$(shell GOOS= $(GO) env GOOS) ; \ REAL_GOARCH=$(shell GOARCH= $(GO) env GOARCH) ; \ if [ "$(GOOS)" == "$$REAL_GOOS" ] && [ "$(GOARCH)" == "$$REAL_GOARCH" ]; then \ ./cmd/jaeger/jaeger$(SUFFIX)-$(GOOS)-$(GOARCH) version 2>/dev/null ; \ echo "" ; \ want=$(GIT_CLOSEST_TAG) ; \ have=$$(./cmd/jaeger/jaeger$(SUFFIX)-$(GOOS)-$(GOARCH) version 2>/dev/null | jq -r .gitVersion) ; \ if [ "$$want" == "$$have" ]; then \ echo "☑️ versions match: want=$$want, have=$$have" ; \ else \ echo "❌ ERROR: version mismatch: want=$$want, have=$$have" ; \ false; \ fi ; \ else \ echo ".. skipping version check for cross-compilation" ; \ echo ".. see build-binaries-$(GOOS)-$(GOARCH)" ; \ fi .PHONY: build-remote-storage build-remote-storage: BIN_NAME = remote-storage build-remote-storage: _build-a-binary-remote-storage$(SUFFIX)-$(GOOS)-$(GOARCH) # build all binaries for the current platform .PHONY: build-binaries build-binaries: _build-platform-binaries .PHONY: build-binaries-linux-amd64 build-binaries-linux-amd64: GOOS=linux GOARCH=amd64 $(MAKE) _build-platform-binaries # helper sysp targets are defined in Makefile.Windows.mk .PHONY: build-binaries-windows-amd64 build-binaries-windows-amd64: $(MAKE) _build-syso GOOS=windows GOARCH=amd64 $(MAKE) _build-platform-binaries $(MAKE) _clean-syso .PHONY: build-binaries-darwin-amd64 build-binaries-darwin-amd64: GOOS=darwin GOARCH=amd64 $(MAKE) _build-platform-binaries .PHONY: build-binaries-darwin-arm64 build-binaries-darwin-arm64: GOOS=darwin GOARCH=arm64 $(MAKE) _build-platform-binaries .PHONY: build-binaries-linux-s390x build-binaries-linux-s390x: GOOS=linux GOARCH=s390x $(MAKE) _build-platform-binaries .PHONY: build-binaries-linux-arm64 build-binaries-linux-arm64: GOOS=linux GOARCH=arm64 $(MAKE) _build-platform-binaries .PHONY: build-binaries-linux-ppc64le build-binaries-linux-ppc64le: GOOS=linux GOARCH=ppc64le $(MAKE) _build-platform-binaries # build all binaries for one specific platform GOOS/GOARCH .PHONY: _build-platform-binaries _build-platform-binaries: \ build-jaeger \ build-remote-storage \ build-examples \ build-tracegen \ build-anonymizer \ build-esmapping-generator \ build-es-index-cleaner \ build-es-rollover # invoke make recursively such that DEBUG_BINARY=1 can take effect # skip debug builds if SKIP_DEBUG_BINARIES is set to 1 (e.g., during PRs to save CI time) ifneq ($(SKIP_DEBUG_BINARIES),1) $(MAKE) _build-platform-binaries-debug GOOS=$(GOOS) GOARCH=$(GOARCH) endif # build binaries that support DEBUG release, for one specific platform GOOS/GOARCH # Uses recursive make calls so that DEBUG_BINARY=1 is set at parse time, # ensuring SUFFIX=-debug is correctly evaluated in the ifeq conditional at the top. .PHONY: _build-platform-binaries-debug _build-platform-binaries-debug: $(MAKE) build-jaeger build-remote-storage GOOS=$(GOOS) GOARCH=$(GOARCH) DEBUG_BINARY=1 .PHONY: build-all-platforms build-all-platforms: for platform in $$(echo "$(PLATFORMS)" | tr ',' ' ' | tr '/' '-'); do \ echo "Building binaries for $$platform"; \ $(MAKE) build-binaries-$$platform; \ done ================================================ FILE: scripts/makefiles/BuildInfo.mk ================================================ # Copyright (c) 2023 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 GIT_SHA=$(shell git rev-parse HEAD) DATE=$(shell TZ=UTC0 git show --quiet --date='format-local:%Y-%m-%dT%H:%M:%SZ' --format="%cd") # Defer evaluation of semver tags until actually needed, using trick from StackOverflow: # https://stackoverflow.com/questions/44114466/how-to-declare-a-deferred-variable-that-is-computed-only-once-for-all GIT_CLOSEST_TAG = $(eval GIT_CLOSEST_TAG := $(shell scripts/utils/compute-version.sh))$(GIT_CLOSEST_TAG) # args: (1) - name, (2) - value define buildinfo $(JAEGER_IMPORT_PATH)/internal/version.$(1)=$(2) endef define buildinfoflags -ldflags "-X $(call buildinfo,commitSHA,$(GIT_SHA)) -X $(call buildinfo,latestVersion,$(GIT_CLOSEST_TAG)) -X $(call buildinfo,date,$(DATE))" endef BUILD_INFO=$(call buildinfoflags) ================================================ FILE: scripts/makefiles/Docker.mk ================================================ # Copyright (c) 2023 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 DOCKER_NAMESPACE ?= jaegertracing DOCKER_TAG ?= latest DOCKER_REGISTRY ?= localhost:5000 BASE_IMAGE ?= $(DOCKER_REGISTRY)/baseimg_alpine:latest DEBUG_IMAGE ?= $(DOCKER_REGISTRY)/debugimg_alpine:latest create-baseimg-debugimg: create-baseimg create-debugimg create-baseimg: prepare-docker-buildx @echo "::group:: create-baseimg" docker buildx build -t $(BASE_IMAGE) --push \ --platform=$(LINUX_PLATFORMS) \ scripts/build/docker/base @echo "::endgroup::" create-debugimg: prepare-docker-buildx @echo "::group:: create-debugimg" docker buildx build -t $(DEBUG_IMAGE) --push \ --platform=$(LINUX_PLATFORMS) \ scripts/build/docker/debug @echo "::endgroup::" create-fake-debugimg: prepare-docker-buildx @echo "::group:: create-fake-debugimg" docker buildx build -t $(DEBUG_IMAGE) --push \ --platform=$(LINUX_PLATFORMS) \ scripts/build/docker/base @echo "::endgroup::" .PHONY: prepare-docker-buildx prepare-docker-buildx: @echo "::group:: prepare-docker-buildx" docker buildx inspect jaeger-build > /dev/null || docker buildx create --use --name=jaeger-build --buildkitd-flags="--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host" --driver-opt="network=host" docker inspect registry > /dev/null || docker run --rm -d -p 5000:5000 --name registry registry:2 @echo "::endgroup::" .PHONY: clean-docker-buildx clean-docker-buildx: docker buildx rm jaeger-build docker rm -f registry .PHONY: docker-hotrod docker-hotrod: GOOS=linux $(MAKE) build-examples docker build -t $(DOCKER_NAMESPACE)/example-hotrod:${DOCKER_TAG} ./examples/hotrod --build-arg TARGETARCH=$(GOARCH) @echo "Finished building hotrod ==============" .PHONY: docker-images-tracegen docker-images-tracegen: docker build -t $(DOCKER_NAMESPACE)/jaeger-tracegen:${DOCKER_TAG} cmd/tracegen/ --build-arg TARGETARCH=$(GOARCH) @echo "Finished building jaeger-tracegen ==============" .PHONY: docker-images-anonymizer docker-images-anonymizer: docker build -t $(DOCKER_NAMESPACE)/jaeger-anonymizer:${DOCKER_TAG} cmd/anonymizer/ --build-arg TARGETARCH=$(GOARCH) @echo "Finished building jaeger-anonymizer ==============" .PHONY: docker-images-cassandra docker-images-cassandra: docker build -t $(DOCKER_NAMESPACE)/jaeger-cassandra-schema:${DOCKER_TAG} internal/storage/v1/cassandra/ @echo "Finished building jaeger-cassandra-schema ==============" .PHONY: docker-images-elastic docker-images-elastic: create-baseimg GOOS=linux GOARCH=$(GOARCH) $(MAKE) build-esmapping-generator GOOS=linux GOARCH=$(GOARCH) $(MAKE) build-es-index-cleaner GOOS=linux GOARCH=$(GOARCH) $(MAKE) build-es-rollover docker build -t $(DOCKER_NAMESPACE)/jaeger-es-index-cleaner:${DOCKER_TAG} --build-arg base_image=$(BASE_IMAGE) --build-arg TARGETARCH=$(GOARCH) cmd/es-index-cleaner docker build -t $(DOCKER_NAMESPACE)/jaeger-es-rollover:${DOCKER_TAG} --build-arg base_image=$(BASE_IMAGE) --build-arg TARGETARCH=$(GOARCH) cmd/es-rollover @echo "Finished building jaeger-elasticsearch tools ==============" ================================================ FILE: scripts/makefiles/IntegrationTests.mk ================================================ # Copyright (c) 2023 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 STORAGE_PKGS = ./internal/storage/integration/... JAEGER_V2_STORAGE_PKGS = ./cmd/jaeger/internal/integration .PHONY: all-in-one-integration-test all-in-one-integration-test: TEST_MODE=integration $(GOTEST) ./cmd/jaeger/internal/all_in_one_test.go # A general integration tests for jaeger-v2 storage backends, # these tests placed at `./cmd/jaeger/internal/integration/*_test.go`. # The integration tests are filtered by STORAGE env. .PHONY: jaeger-v2-storage-integration-test jaeger-v2-storage-integration-test: (cd cmd/jaeger/ && go build .) # Expire tests results for jaeger storage integration tests since the environment # might have changed even though the code remains the same. go clean -testcache bash -c "set -e; set -o pipefail; $(GOTEST) -coverpkg=./... -coverprofile $(COVEROUT) $(JAEGER_V2_STORAGE_PKGS) $(COLORIZE)" .PHONY: storage-integration-test storage-integration-test: # Expire tests results for storage integration tests since the environment might change # even though the code remains the same. go clean -testcache bash -c "set -e; set -o pipefail; $(GOTEST) -coverpkg=./... -coverprofile $(COVEROUT) $(STORAGE_PKGS) $(COLORIZE)" .PHONY: badger-storage-integration-test badger-storage-integration-test: STORAGE=badger $(MAKE) storage-integration-test .PHONY: grpc-storage-integration-test grpc-storage-integration-test: STORAGE=grpc $(MAKE) storage-integration-test # this test assumes STORAGE environment variable is set to elasticsearch|opensearch .PHONY: index-cleaner-integration-test index-cleaner-integration-test: docker-images-elastic $(MAKE) storage-integration-test COVEROUT=cover-index-cleaner.out # this test assumes STORAGE environment variable is set to elasticsearch|opensearch .PHONY: index-rollover-integration-test index-rollover-integration-test: docker-images-elastic $(MAKE) storage-integration-test COVEROUT=cover-index-rollover.out .PHONY: tail-sampling-integration-test tail-sampling-integration-test: SAMPLING=tail $(MAKE) jaeger-v2-storage-integration-test ================================================ FILE: scripts/makefiles/Protobuf.mk ================================================ # Copyright (c) 2023 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # Generate gogo, swagger, go-validators, gRPC-storage-plugin output. # # -I declares import folders, in order of importance. This is how proto resolves the protofile imports. # It will check for the protofile relative to each of thesefolders and use the first one it finds. # # --gogo_out generates GoGo Protobuf output with gRPC plugin enabled. # --govalidators_out generates Go validation files for our messages types, if specified. # # The lines starting with Mgoogle/... are proto import replacements, # which cause the generated file to import the specified packages # instead of the go_package's declared by the imported protof files. # DOCKER=docker DOCKER_PROTOBUF_VERSION=0.5.0 DOCKER_PROTOBUF=jaegertracing/protobuf:$(DOCKER_PROTOBUF_VERSION) PROTOC := ${DOCKER} run --rm -u ${shell id -u} -v${PWD}:${PWD} -w${PWD} ${DOCKER_PROTOBUF} --proto_path=${PWD} PROTO_GEN=internal/proto-gen PATCHED_OTEL_PROTO_DIR = $(PROTO_GEN)/.patched-otel-proto PROTO_INCLUDES := \ -Iidl/proto/api_v2 \ -Iinternal/proto/metrics \ -I/usr/include/github.com/gogo/protobuf \ -Iidl/opentelemetry-proto # Remapping of std types to gogo types (must not contain spaces) PROTO_GOGO_MAPPINGS := $(shell echo \ Mgoogle/protobuf/descriptor.proto=github.com/gogo/protobuf/types \ Mgoogle/protobuf/timestamp.proto=github.com/gogo/protobuf/types \ Mgoogle/protobuf/duration.proto=github.com/gogo/protobuf/types \ Mgoogle/protobuf/empty.proto=github.com/gogo/protobuf/types \ Mgoogle/api/annotations.proto=github.com/gogo/googleapis/google/api \ Mmodel.proto=github.com/jaegertracing/jaeger-idl/model/v1 \ | $(SED) 's/ */,/g') OPENMETRICS_PROTO_FILES=$(wildcard internal/proto/metrics/*.proto) # The source directory for OTLP Protobufs from the sub-sub-module. OTEL_PROTO_SRC_DIR=idl/opentelemetry-proto/opentelemetry/proto # Find all OTEL .proto files, remove leading path (only keep relevant namespace dirs). OTEL_PROTO_FILES=$(subst $(OTEL_PROTO_SRC_DIR)/,,\ $(shell ls $(OTEL_PROTO_SRC_DIR)/{common,resource,trace}/v1/*.proto)) # Macro to execute a command passed as argument. # DO NOT DELETE EMPTY LINE at the end of the macro, it's required to separate commands. define exec-command $(1) endef # DO NOT DELETE EMPTY LINE at the end of the macro, it's required to separate commands. define print_caption @echo "🏗️ " @echo "🏗️ " $1 @echo "🏗️ " endef # Macro to compile Protobuf $(2) into directory $(1). $(3) can provide additional flags. # DO NOT DELETE EMPTY LINE at the end of the macro, it's required to separate commands. # Arguments: # $(1) - output directory # $(2) - path to the .proto file # $(3) - additional flags to pass to protoc, e.g. extra -Ixxx # $(4) - additional options to pass to gogo plugin define proto_compile $(call print_caption, "Processing $(2) --> $(1)") $(PROTOC) \ $(PROTO_INCLUDES) \ --gogo_out=plugins=grpc,$(strip $(4)),$(PROTO_GOGO_MAPPINGS):$(PWD)/$(strip $(1)) \ $(3) $(2) endef .PHONY: proto proto: \ proto-storage-v2 \ proto-hotrod \ proto-zipkin \ proto-openmetrics \ proto-api-v3 API_V2_PATCHED_DIR=$(PROTO_GEN)/.patched/api_v2 .PHONY: patch-api-v2 patch-api-v2: mkdir -p $(API_V2_PATCHED_DIR) cp idl/proto/api_v2/collector.proto $(API_V2_PATCHED_DIR)/ cp idl/proto/api_v2/sampling.proto $(API_V2_PATCHED_DIR)/ cat idl/proto/api_v2/query.proto | $(SED) 's|jaegertracing/jaeger-idl/model/v1.|jaegertracing/jaeger/model.|g' > $(API_V2_PATCHED_DIR)/query.proto .PHONY: proto-openmetrics proto-openmetrics: $(call print_caption, Processing OpenMetrics Protos) $(foreach file,$(OPENMETRICS_PROTO_FILES),$(call proto_compile, $(PROTO_GEN)/api_v2/metrics, $(file))) STORAGE_V2_PATH=$(PROTO_GEN)/storage/v2 STORAGE_V2_PATCHED_DIR=$(PROTO_GEN)/.patched/storage_v2 STORAGE_V2_PATCHED_TRACE=$(STORAGE_V2_PATCHED_DIR)/trace_storage.proto STORAGE_V2_PATCHED_DEPENDENCY=$(STORAGE_V2_PATCHED_DIR)/dependency_storage.proto .PHONY: patch-storage-v2 patch-storage-v2: mkdir -p $(STORAGE_V2_PATCHED_DIR) cat idl/proto/storage/v2/trace_storage.proto | \ $(SED) -f ./$(PROTO_GEN)/patch.sed \ > $(STORAGE_V2_PATCHED_TRACE) cat idl/proto/storage/v2/dependency_storage.proto | \ $(SED) -f ./$(PROTO_GEN)/patch.sed \ > $(STORAGE_V2_PATCHED_DEPENDENCY) .PHONY: proto-storage-v2 proto-storage-v2: patch-storage-v2 $(call proto_compile, $(STORAGE_V2_PATH), $(STORAGE_V2_PATCHED_TRACE), -I$(STORAGE_V2_PATCHED_DIR) -Iinternal/storage/v2/grpc/) $(call proto_compile, $(STORAGE_V2_PATH), $(STORAGE_V2_PATCHED_DEPENDENCY), -I$(STORAGE_V2_PATCHED_DIR) -Iinternal/storage/v2/grpc/) @echo "🏗️ replace first instance of OTEL import with internal type" $(SED) -i '0,/go.opentelemetry.io\/proto\/otlp\/trace\/v1/s|go.opentelemetry.io/proto/otlp/trace/v1|github.com/jaegertracing/jaeger/internal/jptrace|' $(STORAGE_V2_PATH)/*.pb.go @echo "🏗️ remove all remaining OTEL imports because we're not using any other OTLP types" $(SED) -i 's+^.*v1 "go.opentelemetry.io/proto/otlp/trace/v1".*$$++' $(STORAGE_V2_PATH)/*.pb.go .PHONY: proto-hotrod proto-hotrod: $(call proto_compile, , examples/hotrod/services/driver/driver.proto) .PHONY: proto-zipkin proto-zipkin: $(call proto_compile, $(PROTO_GEN)/zipkin, idl/proto/zipkin.proto, -Iidl/proto) # The API v3 service uses official OTEL type opentelemetry.proto.trace.v1.TracesData, # which at runtime is mapped to a custom type in cmd/jaeger/internal/extension/jaegerquery/internal/internal/api_v3/traces.go # Unfortunately, gogoproto.customtype annotation cannot be applied to a method's return type, # only to fields in a struct, so we use regex search/replace to swap it. # Note that the .pb.go types must be generated into the same internal package $(API_V3_PATH) # where a manually defined traces.go file is located. API_V3_PATH=internal/proto/api_v3 API_V3_PATCHED_DIR=$(PROTO_GEN)/.patched/api_v3 API_V3_PATCHED=$(API_V3_PATCHED_DIR)/query_service.proto .PHONY: patch-api-v3 patch-api-v3: mkdir -p $(API_V3_PATCHED_DIR) cat idl/proto/api_v3/query_service.proto | \ $(SED) -f ./$(PROTO_GEN)/patch.sed \ > $(API_V3_PATCHED) .PHONY: proto-api-v3 proto-api-v3: patch-api-v3 $(call proto_compile, $(API_V3_PATH), $(API_V3_PATCHED), -I$(API_V3_PATCHED_DIR) -Iidl/opentelemetry-proto) @echo "🏗️ replace first instance of OTEL import with internal type" $(SED) -i '0,/go.opentelemetry.io\/proto\/otlp\/trace\/v1/s|go.opentelemetry.io/proto/otlp/trace/v1|github.com/jaegertracing/jaeger/internal/jptrace|' $(API_V3_PATH)/query_service.pb.go @echo "🏗️ remove all remaining OTEL imports because we're not using any other OTLP types" $(SED) -i 's+^.*v1 "go.opentelemetry.io/proto/otlp/trace/v1".*$$++' $(API_V3_PATH)/query_service.pb.go ================================================ FILE: scripts/makefiles/Tools.mk ================================================ # Copyright (c) 2024 The Jaeger Authors. # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 TOOLS_MOD_DIR := $(SRC_ROOT)/internal/tools TOOLS_BIN_DIR := $(SRC_ROOT)/.tools TOOLS_MOD_REGEX := "\s+_\s+\".*\"" TOOLS_PKG_NAMES := $(shell grep -E $(TOOLS_MOD_REGEX) < $(TOOLS_MOD_DIR)/tools.go | tr -d " _\"") TOOLS_BIN_NAMES := $(addprefix $(TOOLS_BIN_DIR)/, $(notdir $(shell echo $(TOOLS_PKG_NAMES) | sed 's|/v[0-9]||g'))) GOFUMPT := $(TOOLS_BIN_DIR)/gofumpt GOVERSIONINFO := $(TOOLS_BIN_DIR)/goversioninfo GOVULNCHECK := $(TOOLS_BIN_DIR)/govulncheck LINT := $(TOOLS_BIN_DIR)/golangci-lint MOCKERY := $(TOOLS_BIN_DIR)/mockery SCHEMAGEN := $(TOOLS_BIN_DIR)/schemagen GOCOVMERGE := $(TOOLS_BIN_DIR)/gocovmerge # this target is useful for setting up local workspace, but from CI we want to call more specific ones .PHONY: install-tools install-tools: $(TOOLS_BIN_NAMES) .PHONY: install-test-tools install-test-tools: $(LINT) $(GOFUMPT) .PHONY: install-coverage-tools install-coverage-tools: $(GOCOVMERGE) .PHONY: install-ci install-ci: install-test-tools list-internal-tools: @echo Third party tool modules: @echo $(TOOLS_PKG_NAMES) | tr ' ' '\n' | sed 's/^/- /g' @echo Third party tool binaries: @echo $(TOOLS_BIN_NAMES) | tr ' ' '\n' | sed 's/^/- /g' $(TOOLS_BIN_DIR): mkdir -p $@ $(TOOLS_BIN_NAMES): $(TOOLS_BIN_DIR) $(TOOLS_MOD_DIR)/go.mod $(TOOLS_MOD_DIR)/go.sum cd $(TOOLS_MOD_DIR) && $(GO) build -o $@ -trimpath $(shell echo $(TOOLS_PKG_NAMES) | tr ' ' '\n' | grep $(notdir $@)) ================================================ FILE: scripts/makefiles/Windows.mk ================================================ # Copyright (c) 2024 The Jaeger Authors. # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 SYSOFILE=resource.syso # Magic values: # - LangID "0409" is "US-English". # - CharsetID "04B0" translates to decimal 1200 for "Unicode". # - FileOS "040004" defines the Windows kernel "Windows NT". # - FileType "01" is "Application". # https://learn.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource define VERSIONINFO { "FixedFileInfo": { "FileVersion": { "Major": $(SEMVER_MAJOR), "Minor": $(SEMVER_MINOR), "Patch": $(SEMVER_PATCH), "Build": 0 }, "ProductVersion": { "Major": $(SEMVER_MAJOR), "Minor": $(SEMVER_MINOR), "Patch": $(SEMVER_PATCH), "Build": 0 }, "FileFlagsMask": "3f", "FileFlags ": "00", "FileOS": "040004", "FileType": "01", "FileSubType": "00" }, "StringFileInfo": { "FileDescription": "$(NAME)", "FileVersion": "$(SEMVER_MAJOR).$(SEMVER_MINOR).$(SEMVER_PATCH).0", "LegalCopyright": "2015-2024 The Jaeger Project Authors", "ProductName": "$(NAME)", "ProductVersion": "$(SEMVER_MAJOR).$(SEMVER_MINOR).$(SEMVER_PATCH).0" }, "VarFileInfo": { "Translation": { "LangID": "0409", "CharsetID": "04B0" } } } endef export VERSIONINFO .PHONY: _build_syso_once _build_syso_once: echo $$VERSIONINFO echo $$VERSIONINFO | $(GOVERSIONINFO) -64 -o="$(PKGPATH)/$(SYSOFILE)" - define _build_syso_macro $(MAKE) _build_syso_once NAME="$(1)" PKGPATH="$(2)" SEMVER_MAJOR=$(SEMVER_MAJOR) SEMVER_MINOR=$(SEMVER_MINOR) SEMVER_PATCH=$(SEMVER_PATCH) endef .PHONY: _build-syso _build-syso: $(GOVERSIONINFO) $(eval SEMVER_ALL := $(shell scripts/utils/compute-version.sh -s)) $(eval SEMVER_MAJOR := $(word 2, $(SEMVER_ALL))) $(eval SEMVER_MINOR := $(word 3, $(SEMVER_ALL))) $(eval SEMVER_PATCH := $(word 4, $(SEMVER_ALL))) $(call _build_syso_macro,Jaeger,cmd/jaeger) $(call _build_syso_macro,Jaeger Remote Storage,cmd/remote-storage) $(call _build_syso_macro,Jaeger Tracegen,cmd/tracegen) $(call _build_syso_macro,Jaeger Anonymizer,cmd/anonymizer) $(call _build_syso_macro,Jaeger ES-Index-Cleaner,cmd/es-index-cleaner) $(call _build_syso_macro,Jaeger ES-Rollover,cmd/es-rollover) .PHONY: _clean-syso _clean-syso: rm ./cmd/*/$(SYSOFILE) ================================================ FILE: scripts/release/draft.py ================================================ #!/usr/bin/env python3 # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 import argparse import re import subprocess version_pattern = re.compile(r"^[# ]*v?(\d+\.\d+\.\d+)") underline_pattern = re.compile(r"^[-]+$", flags=0) def main(title, repo, dry_run=False): changelog_text, version = get_changelog(repo) header = f"{title} v{version}" tag = f"v{version}" full_repo = f"jaegertracing/{repo}" if dry_run: print("Dry run: skipping release creation.") print(f"Repository: {full_repo}") print(f"Tag: {tag}") print(f"Title: {header}") print("Changelog:") print("-" * 20) print(changelog_text) print("-" * 20) return print(changelog_text) output_string = subprocess.check_output( [ "gh", "release", "create", tag, "--draft", "--title", header, "--repo", full_repo, "-F", "-", ], input=changelog_text, text=True, ) print(f"Draft created at: {output_string}") print("Please review, then edit it and click 'Publish release'.") def get_changelog(repo): # ... (rest of get_changelog remains the same) changelog_text = "" in_changelog_text = False version = "" with open("CHANGELOG.md") as f: for line in f: versions = version_pattern.findall(line) if versions: # Found the first release headers. if in_changelog_text: # Found the next release. break else: # If both v1 and v2 are present, pick v2 (usually the last one). # If only one version is present, pick it. version = versions[-1] in_changelog_text = True else: underline_match = underline_pattern.match(line) if underline_match is not None: continue elif in_changelog_text: changelog_text += line return changelog_text, version if __name__ == "__main__": parser = argparse.ArgumentParser( description="List changes based on git log for release notes." ) parser.add_argument( "--title", type=str, default="Release", help="The title of the release. (default: Release)", ) parser.add_argument( "--repo", type=str, default="jaeger", help="The repository name where the draft release will be created. (default: jaeger)", ) parser.add_argument( "-d", "--dry-run", action="store_true", help="Print the release details without creating it.", ) args = parser.parse_args() main(args.title, args.repo, args.dry_run) ================================================ FILE: scripts/release/formatter.py ================================================ #!/usr/bin/env python3 # Copyright (c) 2025 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 import re import sys def extract_section_from_file(file_path, start_marker, end_marker): with open(file_path, 'r') as f: text = f.read() start_index = text.find(start_marker) if start_index == -1: raise Exception(f"start marker {start_marker!r} not found") start_index += len(start_marker) end_index = text.find(end_marker) if end_index == -1: raise Exception(f"end marker {end_marker!r} not found") return text[start_index:end_index] def replace_star(text): re_star = re.compile(r'(\n\s*)(\*)(\s)') text = re_star.sub(r'\1\2 [ ]\3', text) return text def replace_dash(text): re_dash = re.compile(r'(\n\s*)(\-)') text = re_dash.sub(r'\1* [ ]', text) return text def replace_num(text): re_num = re.compile(r'(\n\s*)([0-9]*\.)(\s)') text = re_num.sub(r'\1* [ ]\3', text) return text def replace_version(ui_text, backend_text, doc_text, pattern, ver): ui_text = re.sub(pattern, ver, ui_text) backend_text = re.sub(pattern, ver, backend_text) doc_text = re.sub(pattern, ver, doc_text) return ui_text, backend_text, doc_text def fetch_content(file_name): start_marker = "" end_marker = "" text = extract_section_from_file(file_name, start_marker, end_marker) return text def main(): version = sys.argv[1] ui_filename = sys.argv[2] loc = sys.argv[3] try: backend_file_name = "RELEASE.md" backend_section = fetch_content(backend_file_name) except Exception as e: sys.exit(f"Failed to extract backendSection: {e}") backend_section = replace_star(backend_section) backend_section = replace_num(backend_section) try: doc_filename = loc doc_section = fetch_content(doc_filename) except Exception as e: sys.exit(f"Failed to extract documentation section: {e}") doc_section=replace_dash(doc_section) try: ui_section = fetch_content(ui_filename) except Exception as e: sys.exit(f"Failed to extract UI section: {e}") ui_section=replace_dash(ui_section) ui_section=replace_num(ui_section) # Concrete version - replace version patterns with the single version version_pattern = r'(?:X\.Y\.Z|[0-9]+\.[0-9]+\.[0-9]+|[0-9]+\.x\.x)' ui_section, backend_section, doc_section = replace_version(ui_section, backend_section, doc_section, version_pattern, version) print("# UI Release") print(ui_section) print("# Backend Release") print(backend_section) print("# Doc Release") print(doc_section) if __name__ == "__main__": main() ================================================ FILE: scripts/release/notes.py ================================================ #!/usr/bin/env python3 # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # This script can read N latest commits from one of Jaeger repos # and output them in the release notes format: # * {title} ({author} in {pull_request}) # # Requires personal GitHub token with default permissions: # https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token # # Usage: ./release-notes.py --help # import argparse import json import os.path import urllib.parse from os.path import expanduser import sys from urllib.request import ( urlopen, Request ) from urllib.error import HTTPError def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) def print_token_error(): """Print error message about GitHub token requirements.""" generate_token_url = "https://github.com/settings/tokens/new?description=GitHub%20Changelog%20Generator%20token" eprint("\nError: Missing, invalid, or unauthorized GitHub token.") eprint("\nPlease ensure your GitHub token:") eprint(" 1. Is valid and has not expired") eprint(" 2. Has 'repo' permissions (required to access repository data)") eprint(f"\nTo generate a new token, visit: {generate_token_url}") eprint("Make sure to select the 'repo' scope when creating the token.") eprint("\nPlace the token in your --token-file and protect it: chmod 0600 ") sys.exit(1) def github_api_request(url, token, additional_headers=None): """Make a GitHub API request with error handling. Args: url: The API URL to request token: GitHub personal access token additional_headers: Optional dict of additional headers to add Returns: Parsed JSON response """ try: req = Request(url) req.add_header('Authorization', f'token {token}') if additional_headers: for header, value in additional_headers.items(): req.add_header(header, value) return json.loads(urlopen(req).read()) except HTTPError as e: if e.code == 401: print_token_error() raise def num_commits_since_prev_tag(token, base_url, branch, verbose): tags_url = f"{base_url}/tags" tags = github_api_request(tags_url, token) prev_release_tag = tags[0]['name'] compare_url = f"{base_url}/compare/{branch}...{prev_release_tag}" compare_results = github_api_request(compare_url, token) num_commits = compare_results['behind_by'] if verbose: eprint(f"There are {num_commits} new commits since {prev_release_tag}") return num_commits UNCATTEGORIZED = 'Uncategorized' categories = [ {'title': '#### ⛔ Breaking Changes', 'label': 'changelog:breaking-change'}, {'title': '#### ✨ New Features', 'label': 'changelog:new-feature'}, {'title': '#### 🐞 Bug fixes, Minor Improvements', 'label': 'changelog:bugfix-or-minor-feature'}, {'title': '#### 🚧 Experimental Features', 'label': 'changelog:experimental'}, {'title': '#### 👷 CI Improvements', 'label': 'changelog:ci'}, {'title': '#### ⚙️ Refactoring', 'label': 'changelog:refactoring'}, {'title': '#### 📖 Documentation', 'label': 'changelog:documentation'}, {'title': None, 'label': 'changelog:test'}, {'title': None, 'label': 'changelog:skip'}, {'title': None, 'label': 'changelog:dependencies'}, ] def updateProgress(iteration, total_iterations): progress = (iteration + 1) / total_iterations percentage = progress * 100 sys.stderr.write('\r[' + '='*int(progress*50) + ' '*(50-int(progress*50)) + f'] {percentage:.2f}%') sys.stderr.flush() if iteration >= total_iterations - 1: eprint() return iteration + 1 def main(token, repo, branch, num_commits, exclude_dependabot, verbose): accept_header = "application/vnd.github.groot-preview+json" base_url = f"https://api.github.com/repos/jaegertracing/{repo}" commits_url = f"{base_url}/commits" skipped_dependabot = 0 # If num_commits isn't set, get the number of commits made since the previous release tag. if not num_commits: num_commits = num_commits_since_prev_tag(token, base_url, branch, verbose) if not num_commits: return # Load commits data = urllib.parse.urlencode({'per_page': num_commits}) commits = github_api_request(commits_url + '?' + data, token) if verbose: eprint(req.full_url) eprint('Retrieved', len(commits), 'commits') # Load PR for each commit and print summary category_results = {category['title']: [] for category in categories} other_results = [] commits_with_multiple_labels = [] progress_iterator = 0 for commit in commits: if verbose: # Update the progress bar progress_iterator = updateProgress(progress_iterator, num_commits) sha = commit['sha'] author = commit['author'] author_login = author['login'] if author else 'unknown' author_url = commit['author']['html_url'] if author else '' if exclude_dependabot and author == "dependabot[bot]": skipped_dependabot += 1 continue msg_lines = commit['commit']['message'].split('\n') msg = msg_lines[0].capitalize() pulls = github_api_request(f"{commits_url}/{sha}/pulls", token, {'accept': accept_header}) if len(pulls) > 1: print(f"WARNING: More than one pull request for commit {sha}") # Handle commits without pull requests. if not pulls: short_sha = sha[:7] commit_url = commit['html_url'] result = f'* {msg} ([@{author_login}]({author_url}) in [{short_sha}]({commit_url}))' other_results.append(result) continue pull = pulls[0] pull_id = pull['number'] pull_url = pull['html_url'] msg = msg.replace(f'(#{pull_id})', '').strip() # Check if the pull request has changelog label pull_labels = get_pull_request_labels(token, repo, pull_id) changelog_labels = [label for label in pull_labels if label.startswith('changelog:')] # Handle multiple changelog labels if len(changelog_labels) > 1: commits_with_multiple_labels.append((sha, pull_id, changelog_labels)) continue category = UNCATTEGORIZED if changelog_labels: for cat in categories: if changelog_labels[0].startswith(cat['label']): category = cat['title'] break result = f'* {msg} ([@{author_login}]({author_url}) in [#{pull_id}]({pull_url}))' if category == UNCATTEGORIZED: other_results.append(result) else: category_results[category].append(result) # Print categorized pull requests if repo == 'jaeger': print() print('### Backend Changes') print() for category, results in category_results.items(): if results and category: print(f'{category}\n') for result in results: print(result) print() # Print pull requests in the 'UNCATTEGORIZED' category if other_results: print(f'### 💩💩💩 The following commits cannot be categorized (missing "changelog:*" labels):') for result in other_results: print(result) print(f'### 💩💩💩 Please attach labels to these ^^^ PRs and rerun the script.') print(f'### 💩💩💩 Do not include this section in the changelog.') # Print warnings for commits with more than one changelog label if commits_with_multiple_labels: eprint("Warnings: Commits with more than one changelog label found. Please fix them:\n") for sha, pull_id, labels in commits_with_multiple_labels: pr_url = f"https://github.com/jaegertracing/{repo}/pull/{pull_id}" eprint(f"Commit {sha} associated with multiple changelog labels: {', '.join(labels)}") eprint(f"Pull Request URL: {pr_url}\n") print() if skipped_dependabot: if verbose: eprint(f"(Skipped dependabot commits: {skipped_dependabot})") def get_pull_request_labels(token, repo, pull_number): labels_url = f"https://api.github.com/repos/jaegertracing/{repo}/issues/{pull_number}/labels" labels = github_api_request(labels_url, token) return [label['name'] for label in labels] if __name__ == "__main__": parser = argparse.ArgumentParser(description='List changes based on git log for release notes.') parser.add_argument('--token-file', type=str, default="~/.github_token", help='The file containing your personal github token to access the github API. ' + '(default: ~/.github_token)') parser.add_argument('--repo', type=str, default='jaeger', help='The repository name to fetch commit logs from. (default: jaeger)') parser.add_argument('--branch', type=str, default='main', help='The branch name to fetch commit logs from. (default: main)') parser.add_argument('--exclude-dependabot', action='store_true', help='Excludes dependabot commits. (default: false)') parser.add_argument('--num-commits', type=int, help='Print this number of commits from git log. ' + '(default: number of commits before the previous tag)') parser.add_argument('--verbose', action='store_true', help='Whether output debug logs. (default: false)') args = parser.parse_args() token_file = expanduser(args.token_file) if not os.path.exists(token_file): eprint(f"No such token-file: {token_file}.") print_token_error() with open(token_file, 'r') as file: token = file.read().replace('\n', '') if not token: eprint(f"{token_file} is missing your personal github token.") print_token_error() main(token, args.repo, args.branch, args.num_commits, args.exclude_dependabot, args.verbose) ================================================ FILE: scripts/release/prepare.sh ================================================ #!/usr/bin/env bash # Copyright (c) 2025 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # This script automates the Jaeger release preparation process. # It creates a pull request with all the changes necessary for release: # 1. Update CHANGELOG.md with the new version and auto-generated release notes. # 2. Update the jaeger-ui submodule to the corresponding version # 3. Rotate the release managers table in RELEASE.md # 4. Create a PR with the 'changelog:skip' label # 5. Include exact tag commands in the PR description for post-merge execution. # # Use: # bash scripts/release/prepare.sh # OR # make prepare-release VERSION= # # Example: # bash scripts/release/prepare.sh 2.14.0 # make prepare-release VERSION=2.14.0 # # After the PR is merged, follow the tag commands in the PR description. set -euo pipefail check_prerequisites() { for tool in gh git python3; do if ! command -v "$tool" &> /dev/null; then echo "Error: $tool is not installed or not in PATH" exit 1 fi done } # Verify we're on main branch verify_on_main_branch() { local current_branch current_branch=$(git rev-parse --abbrev-ref HEAD) if [ "$current_branch" != "main" ]; then echo "Warning: Not on main branch (current: ${current_branch})" read -p "Continue anyway? (y/n) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1 fi fi } # find by checking URL patterns fetch_from_official_remote() { local official_remote if ! official_remote=$(bash scripts/utils/find-official-remote.sh); then exit 1 fi echo "Fetching from official repo: $official_remote" git fetch "$official_remote" } # Create a new branch create_release_branch() { local version=$1 local branch_name branch_name="prepare-release-v${version}-$(date +%s)" git checkout -b "${branch_name}" echo "$branch_name" } # Update UI submodule update_ui_submodule() { local version=$1 local ui_version="v${version}" echo "Updating UI submodule..." make init-submodules # Verify if the directory exists and is not empty if [ ! -d "jaeger-ui" ] || [ ! "$(ls -A jaeger-ui)" ]; then echo "Error: jaeger-ui directory does not exist or is empty" exit 1 fi pushd jaeger-ui > /dev/null git fetch origin if git rev-parse "${ui_version}" >/dev/null 2>&1; then git checkout "${ui_version}" echo "Checked out jaeger-ui ${ui_version}" else # UI version not found echo "Warning: UI version ${ui_version} not found" read -r -p "Enter UI version to use, e.g. ${ui_version} (or Enter to skip): " ui_input if [ -n "$ui_input" ]; then git checkout "$ui_input" else echo "Skipping UI version update" git checkout main && git pull fi fi popd > /dev/null git add jaeger-ui } # Generate changelog entries and update CHANGELOG.md update_changelog() { local version=$1 local release_date local changelog_content echo "Updating CHANGELOG.md..." release_date=$(date +%Y-%m-%d) changelog_content=$(make -s changelog) python3 scripts/release/update-changelog.py "$version" --date "$release_date" --content "$changelog_content" --ui-changelog jaeger-ui/CHANGELOG.md git add CHANGELOG.md } rotate_release_managers() { echo "Rotating release managers table..." python3 scripts/release/rotate-managers.py # Stage RELEASE.md if it was modified git diff --quiet RELEASE.md || git add RELEASE.md } commit_changes() { local version=$1 git commit -s -m "Prepare release v${version} - Updated CHANGELOG.md with release notes - Updated jaeger-ui submodule - Rotated release managers table" } push_branch() { local branch_name=$1 git push origin "${branch_name}" } create_pull_request() { local version=$1 local pr_body="This PR prepares the release for v${version}. ## Changes - [x] Updated CHANGELOG.md with release notes - [x] Updated jaeger-ui submodule to v${version} - [x] Rotated release managers table in RELEASE.md After this PR is merged, continue with the release process as outlined in the release issue." # Create the PR gh pr create \ --title "Prepare release v${version}" \ --body "$pr_body" \ --label "changelog:skip" \ --base main } main() { if [ "$#" -ne 1 ]; then echo "Usage: $0 " echo "Example: $0 2.14.0" exit 1 fi local version="${1#v}" # Remove 'v' prefix if present echo "Preparing release for v${version}" check_prerequisites verify_on_main_branch fetch_from_official_remote local branch_name branch_name=$(create_release_branch "$version") update_ui_submodule "$version" update_changelog "$version" rotate_release_managers commit_changes "$version" push_branch "$branch_name" create_pull_request "$version" echo "Done. Review and merge the PR, then follow the instructions in the PR description." } main "$@" ================================================ FILE: scripts/release/rotate-managers.py ================================================ #!/usr/bin/env python3 # Copyright (c) 2025 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 """ This script finds the release managers table in RELEASE.md and move the first data row to the end, rotating the release manager schedule. """ import re import sys from datetime import datetime, timedelta def get_next_first_wednesday(last_date_str: str) -> str: # last_date_str e.g. "7 July 2026" last_date = datetime.strptime(last_date_str.strip(), "%d %B %Y") # Move to the next month if last_date.month == 12: next_month = last_date.replace(year=last_date.year + 1, month=1, day=1) else: next_month = last_date.replace(month=last_date.month + 1, day=1) # Find the first Wednesday # weekday() is 0 for Monday, 2 for Wednesday days_to_wednesday = (2 - next_month.weekday() + 7) % 7 first_wednesday = next_month + timedelta(days=days_to_wednesday) return first_wednesday.strftime("%-d %B %Y") def rotate_release_managers() -> None: with open('RELEASE.md', 'r') as f: content = f.read() # Find the release managers table # Matches the header, separator, and all data rows. # The last row might not have a trailing newline. table_pattern = r'(\| Version \| Release Manager \| Tentative release date *\|\n\|-+\|.*\|\n(?:\|.*\|.*\|(?:\n|$))*)' match = re.search(table_pattern, content) if not match: print("Error: Could not find release managers table", file=sys.stderr) sys.exit(1) table = match.group(0) # Ensure we preserve the exact line endings by not stripping the match if we don't need to. # But for rotation, we need the lines. lines = table.splitlines() # skip header and separator header_plus_sep = lines[:2] data_lines = lines[2:] if not data_lines: print("Error: No data lines found in release managers table", file=sys.stderr) sys.exit(1) # Find the maximum version currently in the table to determine the next one max_major, max_minor, max_patch = -1, -1, -1 last_date_str = "" for line in data_lines: # Split and filter to get clean parts parts = [p.strip() for p in line.split('|') if p.strip()] if len(parts) >= 3: v_str = parts[0] d_str = parts[2] v_parts = v_str.split('.') if len(v_parts) == 3: try: major, minor, patch = map(int, v_parts) if (major, minor, patch) > (max_major, max_minor, max_patch): max_major, max_minor, max_patch = major, minor, patch last_date_str = d_str except ValueError: continue if max_major == -1: print("Error: Could not find any valid versions in the table", file=sys.stderr) sys.exit(1) next_version = f"{max_major}.{max_minor + 1}.0" next_date = get_next_first_wednesday(last_date_str) # Get the first row (the one to be rotated) first_row = data_lines[0] first_row_parts = [p.strip() for p in first_row.split('|') if p.strip()] if len(first_row_parts) < 2: print(f"Error: First data row is malformed (expected at least 2 columns): {first_row}", file=sys.stderr) sys.exit(1) manager = first_row_parts[1] # Create the new row for the bottom # Version (7) + Manager (15) + Date (25) # Total width with spaces: (1+7+1) + (1+15+1) + (1+25+1) = 9 + 17 + 27 = 53 dashes/chars. new_bottom_row = f"| {next_version:<7} | {manager:<15} | {next_date:<25} |" # Move first line to the end (rotation) rotated = data_lines[1:] + [new_bottom_row] # Reconstruct the table with a trailing newline to avoid corruption when joining back new_table = '\n'.join(header_plus_sep + rotated) + '\n' content = content[:match.start()] + new_table + content[match.end():] with open('RELEASE.md', 'w') as f: f.write(content) print(f"Rotated release managers table. Added {next_version} for {manager} on {next_date}") def main(): rotate_release_managers() if __name__ == "__main__": main() ================================================ FILE: scripts/release/start.sh ================================================ #!/usr/bin/env bash # Copyright (c) 2025 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 #Requires bash version to be >=4. Will add alternative for lower versions set -euo pipefail dry_run=false while getopts "dh" opt; do case "${opt}" in d) dry_run=true ;; h) echo "Usage: $0 [-d]" exit 0 ;; *) echo "Usage: $0 [-d]" exit 1 ;; esac done if ! current_version=$(make "echo-version"); then echo "Error: Failed to fetch current version from make echo-version." exit 1 fi # removing the v so that in the line "New version: v2.13.0", v cannot be removed with backspace clean_version="${current_version#v}" IFS='.' read -r major minor patch <<< "$clean_version" minor=$((minor + 1)) patch=0 suggested_version="${major}.${minor}.${patch}" echo "Current version: ${current_version}" read -r -e -p "New version: v" -i "${suggested_version}" user_version new_version="v${user_version}" echo "Using new version: ${new_version}" DOCS_TEMPLATE=$(mktemp "/tmp/DOC_RELEASE.XXXXXX") wget -O "$DOCS_TEMPLATE" https://raw.githubusercontent.com/jaegertracing/documentation/main/RELEASE.md UI_TMPFILE=$(mktemp "/tmp/UI_RELEASE.XXXXXX") wget -O "$UI_TMPFILE" https://raw.githubusercontent.com/jaegertracing/jaeger-ui/main/RELEASE.md issue_body=$(python3 scripts/release/formatter.py "${user_version}" "${UI_TMPFILE}" "${DOCS_TEMPLATE}") if $dry_run; then echo "${issue_body}" else gh issue create -R jaegertracing/jaeger --title "Prepare Jaeger Release ${new_version}" --body "$issue_body" fi rm "${DOCS_TEMPLATE}" "${UI_TMPFILE}" ================================================ FILE: scripts/release/update-changelog.py ================================================ #!/usr/bin/env python3 # Copyright (c) 2025 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 """ This script inserts a new release section into CHANGELOG.md with the provided version, date, and changelog content. If no content is provided, it will insert the placeholder text. """ import argparse import os import sys def extract_version_content(changelog_path: str, version: str) -> str: """ Extracts the content of a specific version section from a changelog file. """ if not os.path.exists(changelog_path): return "" with open(changelog_path, 'r') as f: lines = f.readlines() start_line = -1 v_header = f"## v{version}" for i, line in enumerate(lines): if line.startswith(v_header): start_line = i + 1 break if start_line == -1: return "" content = [] for i in range(start_line, len(lines)): if lines[i].startswith('## v'): break content.append(lines[i]) return ''.join(content).strip() def update_changelog(version: str, release_date: str, changelog_content: str, ui_changelog: str = None) -> None: with open('CHANGELOG.md', 'r') as f: lines = f.readlines() # Find the template section end template_end = -1 for i, line in enumerate(lines): if '' in line: template_end = i + 1 break if template_end == -1: print("Error: Could not find template end marker", file=sys.stderr) sys.exit(1) # Create the new changelog section new_section = [] new_section.append(f"\nv{version} ({release_date})\n") new_section.append("-" * 31 + "\n") if not changelog_content.startswith('\n'): new_section.append("\n") new_section.append(changelog_content) if not changelog_content.endswith('\n'): new_section.append("\n") if ui_changelog: ui_content = extract_version_content(ui_changelog, version) if ui_content: new_section.append("\n### 📊 UI Changes\n\n") new_section.append(ui_content) new_section.append("\n") with open('CHANGELOG.md', 'w') as f: # Write the updated CHANGELOG.md f.writelines(lines[:template_end]) f.writelines(new_section) f.writelines(lines[template_end:]) print(f"Updated CHANGELOG.md with v{version}") def main(): parser = argparse.ArgumentParser( description="Update CHANGELOG.md with a new version section." ) parser.add_argument( "version", type=str, help="Version number (e.g., 2.14.0)" ) parser.add_argument( "--date", type=str, help="Release date in YYYY-MM-DD format (default: today)", default=None ) parser.add_argument( "--content", type=str, help="Changelog content (default: placeholder text)", default=None ) parser.add_argument( "--ui-changelog", type=str, help="Path to the UI changelog file to extract notes from", default=None ) args = parser.parse_args() # Use provided date or default to today from datetime import date release_date = args.date if args.date else date.today().strftime("%Y-%m-%d") update_changelog(args.version, release_date, args.content, args.ui_changelog) if __name__ == "__main__": main() ================================================ FILE: scripts/utils/compare_metrics.py ================================================ # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 import json import argparse import subprocess #Instructions of use: # To generate V1_Metrics.json and V2_Metrics.json, run the following commands: # i.e for elastic search first run the following command: # docker compose -f docker-compose/elasticsearch/v7/docker-compose.yml up # 1. Generate V1_Metrics.json and V2_Metrics.json by the following commands: # V1 binary cmd: SPAN_STORAGE_TYPE=elasticsearch go run -tags=ui ./cmd/all-in-one # extract the metrics by running the following command: # prom2json http://localhost:14269/metrics > V1_Metrics.json # Stop the v1 binary and for v2 binary run the following command: # go run -tags ui ./cmd/jaeger/main.go --config ./cmd/jaeger/config-elasticsearch.yaml # extract the metrics by running the following command: # prom2json http://localhost:8888/metrics > V2_Metrics.json # it is first recomended to generate the differences for all-in-one.json by running the following command: # python3 compare_metrics.py --out md --is_storage F # rename that file to all_in_one.json and use it to filter out the overlapping metrics by using the is_storage falg to T # 2. Run the script with the following command: # python3 compare_metrics.py --out {json or md} --is_storage {T or F} # 3. The script will compare the metrics in V1_Metrics.json and V2_Metrics.json and output the differences to differences.json # Extract names and labels of the metrics def extract_metrics_with_labels(metrics, strip_prefix=None): result = {} for metric in metrics: name = metric['name'] print(name) if strip_prefix and name.startswith(strip_prefix): name = name[len(strip_prefix):] labels = {} if 'metrics' in metric and 'labels' in metric['metrics'][0]: labels = metric['metrics'][0]['labels'] result[name] = labels return result def remove_overlapping_metrics(all_in_one_data, other_json_data): """Remove overlapping metrics found in all_in-one.json from another JSON.""" # Loop through v1 and v2 metrics to remove overlaps for metric_category in ['common_metrics', 'v1_only_metrics', 'v2_only_metrics']: if metric_category in all_in_one_data and metric_category in other_json_data: for metric in all_in_one_data[metric_category]: if metric in other_json_data[metric_category]: del other_json_data[metric_category][metric] return other_json_data # Your current compare_metrics.py logic goes here def main(): parser = argparse.ArgumentParser(description='Compare metrics and output format.') parser.add_argument('--out', choices=['json', 'md'], default='json', help='Output format: json (default) or md') parser.add_argument('--is_storage', choices=['T','F'],default='F', help='Remove overlapping storage metrics') # Parse the arguments args = parser.parse_args() # Call your existing compare logic here print("Running metric comparison...") v1_metrics_path = "" #Add the path to the V1_Metrics.json file v2_metrics_path = "" #Add the path to the V2_Metrics.json file with open(v1_metrics_path, 'r') as file: v1_metrics = json.load(file) with open(v2_metrics_path, 'r') as file: v2_metrics = json.load(file) v1_metrics_with_labels = extract_metrics_with_labels(v1_metrics) v2_metrics_with_labels = extract_metrics_with_labels( v2_metrics, strip_prefix="otelcol_") # Compare the metrics names and labels common_metrics = {} v1_only_metrics = {} v2_only_metrics = {} for name, labels in v1_metrics_with_labels.items(): if name in v2_metrics_with_labels: common_metrics[name] = labels elif not name.startswith("jaeger_agent"): v1_only_metrics[name] = labels for name, labels in v2_metrics_with_labels.items(): if name not in v1_metrics_with_labels: v2_only_metrics[name] = labels differences = { "common_metrics": common_metrics, "v1_only_metrics": v1_only_metrics, "v2_only_metrics": v2_only_metrics, } #Write the differences to a new JSON file differences_path = "./differences.json" with open(differences_path, 'w') as file: json.dump(differences, file, indent=4) print(f"Differences written to {differences_path}") if args.is_storage == 'T': all_in_one_path = "" #Add the path to the all_in_one.json file with open(all_in_one_path, 'r') as file: all_in_one_data = json.load(file) with open(differences_path, 'r') as file: other_json_data = json.load(file) other_json_data = remove_overlapping_metrics(all_in_one_data, other_json_data) with open(differences_path, 'w') as file: json.dump(other_json_data, file, indent=4) print(f"Overlapping storage metrics removed from {differences_path}") # If the user requested markdown output, run metrics_md.py if args.out == 'md': try: print("Running metrics_md.py to generate markdown output...") subprocess.run(['python3', 'metrics-md.py'], check=True) except subprocess.CalledProcessError as e: print(f"Error running metrics_md.py: {e}") # If json output is requested or no output type is provided (default is json) else: print("Output in JSON format.") if __name__ == "__main__": main() ================================================ FILE: scripts/utils/compute-tags.sh ================================================ #!/bin/bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # Compute major/minor/etc image tags based on the current branch set -ef -o pipefail if [[ -z $QUIET ]]; then set -x fi set -u BASE_BUILD_IMAGE=${1:?'expecting Docker image name as argument, such as jaegertracing/jaeger'} BRANCH=${BRANCH:?'expecting BRANCH env var'} GITHUB_SHA=${GITHUB_SHA:-$(git rev-parse HEAD)} # accumulate output in this variable IMAGE_TAGS="" # append given tag for docker.io and quay.io tags() { if [[ -n "$IMAGE_TAGS" ]]; then # append space IMAGE_TAGS="${IMAGE_TAGS} " fi IMAGE_TAGS="${IMAGE_TAGS}--tag docker.io/${1} --tag quay.io/${1}" } ## If we are on a release tag, let's extract the version number. ## The other possible values are 'main' or another branch name. if [[ $BRANCH =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)?$ ]]; then MAJOR_MINOR_PATCH=${BRANCH#v} tags "${BASE_BUILD_IMAGE}:${MAJOR_MINOR_PATCH}" tags "${BASE_BUILD_IMAGE}:latest" elif [[ $BRANCH != "main" ]]; then # not on release tag nor on main - no tags are needed since we won't publish echo "" exit fi tags "${BASE_BUILD_IMAGE}-snapshot:${GITHUB_SHA}" tags "${BASE_BUILD_IMAGE}-snapshot:latest" echo "${IMAGE_TAGS}" ================================================ FILE: scripts/utils/compute-tags.test.sh ================================================ #!/bin/bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # This script uses https://github.com/kward/shunit2 to run unit tests. # The path to this repo must be provided via SHUNIT2 env var. SHUNIT2="${SHUNIT2:?'expecting SHUNIT2 env var pointing to a dir with https://github.com/kward/shunit2 clone'}" # allow substituting for ggrep, since default grep on MacOS doesn't grok -P flag. # if running on MacOS, `brew install grep` and run with GREP=ggrep GREP=${GREP:-grep} # shellcheck disable=SC2086 computeTags="$(dirname $0)/compute-tags.sh" # suppress command echoing by compute-tags.sh export QUIET=1 # unset env vars that were possibly set by the caller, since we test against them unset BRANCH unset GITHUB_SHA testRequireImageName() { err=$(bash "$computeTags" 2>&1) assertContains "$err" 'expecting Docker image name' } testRequireBranch() { err=$(GITHUB_SHA=sha bash "$computeTags" foo/bar 2>&1) assertContains "$err" "$err" 'expecting BRANCH env var' } testGithubShaIsDefaulted() { out=$(BRANCH=main bash "$computeTags" foo/bar) expected=( "foo/bar-snapshot:$(git rev-parse HEAD)" "foo/bar-snapshot:latest" ) expect "${expected[@]}" } # out is global var which is populated for every output under test out="" scan_list() { local target="$1" echo "$out" | tr ' ' '\n' | $GREP -v '^--tag$' | $GREP -Po "^($target)"'$' } expect_contains() { local target="$1" # shellcheck disable=SC2155 local found=$(scan_list "$target") assertContains "$found" "$target" } expect_not_contains() { local target="$1" # shellcheck disable=SC2155 local found=$(scan_list "$target") assertNotContains "$found" "$target" } expect() { echo ' Actual:' "$out" while [ "$#" -gt 0 ]; do echo ' checking includes' "$1" expect_contains "docker.io/$1" expect_contains "quay.io/$1" shift done } expect_not() { echo ' Actual:' "$out" while [ "$#" -gt 0 ]; do echo ' checking excludes' "$1" expect_not_contains "docker.io/$1" expect_not_contains "quay.io/$1" shift done } testRandomBranch() { out=$(BRANCH=random GITHUB_SHA=sha bash "$computeTags" foo/bar) expect } testMainBranch() { out=$(BRANCH=main GITHUB_SHA=sha bash "$computeTags" foo/bar) expected=( "foo/bar-snapshot:sha" "foo/bar-snapshot:latest" ) expect "${expected[@]}" expect_not "foo/bar" "foo/bar:latest" } testSemVerBranch() { out=$(BRANCH=v1.2.3 GITHUB_SHA=sha bash "$computeTags" foo/bar) expected=( "foo/bar:latest" "foo/bar:1.2.3" "foo/bar-snapshot:sha" "foo/bar-snapshot:latest" ) expect "${expected[@]}" expect_not "foo/bar" } testSemVerRCBranch() { out=$(BRANCH=v1.22.33-rc12 GITHUB_SHA=sha bash "$computeTags" foo/bar) expected=( "foo/bar:latest" "foo/bar:1.22.33-rc12" "foo/bar-snapshot:sha" "foo/bar-snapshot:latest" ) expect "${expected[@]}" expect_not "foo/bar" } # shellcheck disable=SC1091 source "${SHUNIT2}/shunit2" ================================================ FILE: scripts/utils/compute-version.sh ================================================ #!/bin/bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # Extract and parse Jaeger release version from the closest Git tag. set -euf -o pipefail SED=${SED:-sed} usage() { echo "Usage: $0 [-s] [-v]" echo " -s split semver into 4 parts: semver major minor patch" echo " -v verbose" exit 1 } verbose="false" split="false" while getopts "sv" opt; do # shellcheck disable=SC2220 # we don't need a *) case case "${opt}" in s) split="true" ;; v) verbose="true" ;; esac done shift $((OPTIND - 1)) # Always use v2 JAEGER_MAJOR=v2 print_result() { if [[ "$split" == "true" ]]; then echo "$1" "$2" "$3" "$4" else echo "$1" fi } if [[ "$verbose" == "true" ]]; then set -x fi # Some of GitHub Actions workflows do a shallow checkout without tags. This avoids logging warnings from git. if [[ $(git rev-parse --is-shallow-repository) == "false" ]]; then GIT_CLOSEST_TAG=$(git describe --abbrev=0 --tags) else if [[ "$verbose" == "true" ]]; then echo "The repository is a shallow clone, cannot determine most recent tag" >&2 fi print_result 0.0.0 0 0 0 exit fi MATCHING_TAG='' for tag in $(git tag --list --contains "$(git rev-parse "$GIT_CLOSEST_TAG")"); do if [[ "${tag:0:2}" == "$JAEGER_MAJOR" ]]; then MATCHING_TAG="$tag" break fi done if [[ "$MATCHING_TAG" == "" ]]; then if [[ "$verbose" == "true" ]]; then echo "Did not find a tag matching major version $JAEGER_MAJOR" >&2 fi print_result 0.0.0 0 0 0 exit fi if [[ $MATCHING_TAG =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then MAJOR="${BASH_REMATCH[1]}" MINOR="${BASH_REMATCH[2]}" PATCH="${BASH_REMATCH[3]}" else echo "Invalid semver format: $MATCHING_TAG" exit 1 fi print_result "$MATCHING_TAG" "$MAJOR" "$MINOR" "$PATCH" ================================================ FILE: scripts/utils/docker-login.sh ================================================ #!/bin/bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 set -exu DOCKERHUB_USERNAME=${DOCKERHUB_USERNAME:-"jaegertracingbot"} DOCKERHUB_TOKEN=${DOCKERHUB_TOKEN:-} QUAY_USERNAME=${QUAY_USERNAME:-"jaegertracing+github_workflows"} QUAY_TOKEN=${QUAY_TOKEN:-} echo "Performing a 'docker login' for DockerHub" echo "${DOCKERHUB_TOKEN}" | docker login -u "${DOCKERHUB_USERNAME}" docker.io --password-stdin echo "Performing a 'docker login' for Quay" echo "${QUAY_TOKEN}" | docker login -u "${QUAY_USERNAME}" quay.io --password-stdin ================================================ FILE: scripts/utils/find-official-remote.sh ================================================ #!/usr/bin/env bash # Copyright (c) 2025 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # finds the git remote pointing to the official jaeger repo. set -euo pipefail OFFICIAL_REPO_PATTERN='(github\.com[:/])jaegertracing/jaeger(\.git)?$' for remote in $(git remote); do remote_url=$(git remote get-url "$remote" 2>/dev/null || true) if echo "$remote_url" | grep -Eq "$OFFICIAL_REPO_PATTERN"; then echo "$remote" exit 0 fi done echo "Error: could not find a remote pointing to jaegertracing/jaeger" >&2 echo "Available remotes:" >&2 git remote -v >&2 exit 1 ================================================ FILE: scripts/utils/generate-help-output.sh ================================================ #!/bin/bash # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 # This script runs the `help` command on all Jaeger binaries (using go run) with variations of SPAN_STORAGE_TYPE. # It can be used to compare the CLI API changes between releases. dir=$1 if [[ "$dir" = "" ]]; then echo specify output dir exit 1 fi function gen { bin=$1 shift for s in "$@" do SPAN_STORAGE_TYPE=$s go run "./cmd/$bin" help > "$dir/$bin-$s.txt" done } set -ex gen collector cassandra elasticsearch memory kafka badger grpc gen query cassandra elasticsearch memory badger grpc gen ingester cassandra elasticsearch memory badger grpc gen all-in-one cassandra elasticsearch memory badger grpc ================================================ FILE: scripts/utils/ids-to-base64.py ================================================ #!/usr/bin/env python3 # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 import base64 import os import re import sys def trace_id_base64(match): id = int(match.group(1), 16) hex = '%032x' % id b64 = base64.b64encode(hex.decode('hex')) return '"traceId": "%s"' % b64 def span_id_base64(match): id = int(match.group(1), 16) hex = '%016x' % id b64 = base64.b64encode(hex.decode('hex')) return f'"spanId": "{b64}"' for file in sys.argv[1:]: print(file) backup = f'{file}.bak' with open(file, 'r') as fin: with open(backup, 'w') as fout: for line in fin: # line = line[:-1] # remove \n line = re.sub(r'"traceId": "(.+)"', trace_id_base64, line) line = re.sub(r'"spanId": "(.+)"', span_id_base64, line) fout.write(line) os.remove(file) os.rename(backup, file) ================================================ FILE: scripts/utils/metrics-md.py ================================================ # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 import json def generate_spans_markdown_table(v1_spans, v2_spans): """ Generates a markdown table specifically for spans metrics with two main columns V1 and V2. Args: v1_spans (dict): The dictionary of V1 spans metrics. v2_spans (dict): The dictionary of V2 spans metrics. Returns: str: The generated markdown table as a string. """ table = "### Equivalent Metrics\n\n" table += "| V1 Metric | V1 Parameters | V2 Metric | V2 Parameters |\n" table += "|-----------|---------------|-----------|---------------|\n" # Iterate through the metrics using zip_longest to handle mismatched lengths from itertools import zip_longest for (v1_metric, v1_params), (v2_metric, v2_params) in zip_longest(v1_spans.items(), v2_spans.items(), fillvalue=('', {})): v1_inner_keys = ', '.join(v1_params.keys()) if v1_params else '' v2_inner_keys = ', '.join(v2_params.keys()) if v2_params else '' table += f"| {v1_metric} | {v1_inner_keys} | {v2_metric} | {v2_inner_keys} |\n" return table def generate_combined_markdown_table(common_metrics, v1_metrics, v2_metrics): """ Generates a markdown table for combined metrics from common, V1, and V2. Args: common_metrics (dict): The dictionary of common metrics. v1_metrics (dict): The dictionary of V1 only metrics. v2_metrics (dict): The dictionary of V2 only metrics. Returns: str: The generated markdown table as a string. """ table = "### Combined Metrics\n\n" table += "| V1 Metric | V1 Parameters | V2 Metric | V2 Parameters |\n" table += "|-----------|---------------|-----------|---------------|\n" for metric_name, params in common_metrics.items(): v1_params = ', '.join(common_metrics[metric_name].keys()) if params else 'N/A' v2_params = ', '.join(common_metrics[metric_name].keys()) if params else 'N/A' table += f"| {metric_name} | {v1_params} | {metric_name} | {v2_params} |\n" # Then, handle V1-only metrics (V2 shows as N/A) for metric_name, v1_params in v1_metrics.items(): v1_params_str = ', '.join(v1_params.keys()) if v1_params else 'N/A' table += f"| {metric_name} | {v1_params_str} | N/A | N/A |\n" # Then, handle V2-only metrics (V1 shows as N/A) for metric_name, v2_params in v2_metrics.items(): v2_params_str = ', '.join(v2_params.keys()) if v2_params else 'N/A' table += f"| N/A | N/A | {metric_name} | {v2_params_str} |\n" return table class ConvertJson: def __init__(self, json_fp, h1): self.fp = json_fp self.h1 = h1 self.jdata = self.get_json() self.mddata = self.format_json_to_md() def get_json(self): with open(self.fp) as f: res = json.load(f) return res def format_json_to_md(self): text = f'# {self.h1}\n' dct = self.jdata # Extracting individual metric dictionaries common_metrics = dct.get("common_metrics", {}) v1_only_metrics = dct.get("v1_only_metrics", {}) v2_only_metrics = dct.get("v2_only_metrics", {}) # Generate combined table combined_metrics_table = generate_combined_markdown_table( common_metrics, v1_only_metrics, v2_only_metrics ) filtered_v1_metrics = { "jaeger_collector_spans_rejected_total": {"debug": "false", "format": "","svc": "","transport":""}, "jaeger_build_info": {"build_date": "","revision": ""," version": ""} # Add more metrics as needed } # Hardcoding filtered v2 metrics filtered_v2_metrics = { "receiver_refused_spans": {"receiver": "","service_instance_id": "","service_name": "","service_version": "","transport": ""}, "target_info": {"service_instance_id": "","service_name": "","service_version": ""} # Add more metrics as needed } spans_metrics_table = generate_spans_markdown_table(filtered_v1_metrics, filtered_v2_metrics) text += combined_metrics_table+spans_metrics_table return text def convert_dict_to_md(self, output_fn): with open(output_fn, 'w') as writer: writer.writelines(self.mddata) print('Dict successfully converted to md') # Usage fn = '' # Enter the path of the JSON file generated by compare_metrics.py title = "TITLE" converter = ConvertJson(fn, title) converter.convert_dict_to_md(output_fn='metrics.md') ================================================ FILE: scripts/utils/platforms-to-gh-matrix.sh ================================================ #!/bin/bash # # Copyright (c) 2024 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 set -euf -o pipefail echo -n '{ "include": [ ' first="true" for pair in $(make echo-platforms | tr ',' ' '); do os=$(echo "$pair" | cut -d '/' -f 1) arch=$(echo "$pair" | cut -d '/' -f 2) if [[ "$first" == "true" ]]; then first="false" else echo -n ' ,' fi echo -n "{ \"os\": \"$os\", \"arch\": \"$arch\" }" done echo "]}" ================================================ FILE: scripts/utils/run-tests.sh ================================================ #!/bin/bash # Copyright (c) 2025 The Jaeger Authors. # SPDX-License-Identifier: Apache-2.0 UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$UTILS_DIR/../.." # Define list of test files explicitly here , to be dynamic to the location of the test file TEST_FILES=( "$UTILS_DIR/compute-tags.test.sh" "$REPO_ROOT/internal/storage/v1/cassandra/schema/create.test.sh" ) run_test_file() { local test_file="$1" if [ ! -f "$test_file" ]; then echo "Error: Test file not found: $test_file" return 1 fi echo "Running tests from: $test_file" export SHUNIT2="${SHUNIT2:?'SHUNIT2 environment variable must be set'}" bash "$test_file" local result=$? echo "Test file $test_file completed with status: $result" return $result } main() { if [ ! -f "${SHUNIT2}/shunit2" ]; then echo "Error: shunit2 not found at ${SHUNIT2}/shunit2" exit 1 fi local failed=0 local total=0 local passed=0 local failed_tests=() # Run all test files for test_file in "${TEST_FILES[@]}"; do ((total++)) if ! run_test_file "$test_file"; then failed=1 failed_tests+=("$test_file") else ((passed++)) fi done echo "-------------------" echo "Test Summary:" echo "Total: $total" echo "Passed: $passed" echo "Failed: $((total - passed))" if [ ${#failed_tests[@]} -gt 0 ]; then echo "Failed tests:" for test in "${failed_tests[@]}"; do echo " - $(basename "$test")" done fi exit $failed } main