diff --git a/Makefile b/Makefile index da8d8910..fe668145 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ NOTARY_UI_FILES := $(shell find ui/src/ -type f) ui/package.json ui/package-lock NOTARY_ARTIFACT_NAME := notary NOTARY_CONFIG_FILE := config.yaml +NOTARY_DOCKER_CONFIG_FILE := config-docker.yaml NOTARY_TLS_CERT := cert.pem NOTARY_TLS_KEY := key.pem ROCK_ARTIFACT_NAME := notary.rock @@ -30,7 +31,7 @@ hotswap: lxc exec notary -- docker cp ./notary notary:/bin/notary lxc exec notary -- docker exec notary pebble restart notary -deploy: $(ARTIFACT_FOLDER)/$(ROCK_ARTIFACT_NAME) +deploy: $(ARTIFACT_FOLDER)/$(ROCK_ARTIFACT_NAME) $(ARTIFACT_FOLDER)/$(NOTARY_DOCKER_CONFIG_FILE) $(ARTIFACT_FOLDER)/$(NOTARY_TLS_CERT) $(ARTIFACT_FOLDER)/$(NOTARY_TLS_KEY) @# Start notary container if it's not available @if [ "$$(lxc list 2> /dev/null | grep notary > /dev/null; echo $$?)" = 1 ]; then \ echo "creating new notary VM instance in LXD"; \ @@ -45,10 +46,30 @@ deploy: $(ARTIFACT_FOLDER)/$(ROCK_ARTIFACT_NAME) lxc exec notary -- snap install rockcraft --classic ;\ \ echo "pushing config files"; \ - lxc file push $(ARTIFACT_FOLDER)/$(ROCK_ARTIFACT_NAME) notary/root/$(ROCK_ARTIFACT_NAME); \ - lxc file push $(ARTIFACT_FOLDER)/$(NOTARY_CONFIG_FILE) notary/root/$(NOTARY_CONFIG_FILE); \ - lxc file push $(ARTIFACT_FOLDER)/$(NOTARY_TLS_CERT) notary/root/$(NOTARY_TLS_CERT); \ - lxc file push $(ARTIFACT_FOLDER)/$(NOTARY_TLS_KEY) notary/root/$(NOTARY_TLS_KEY); \ + lxc file push -p $(ARTIFACT_FOLDER)/$(ROCK_ARTIFACT_NAME) notary/root/$(ROCK_ARTIFACT_NAME); \ + lxc file push -p $(ARTIFACT_FOLDER)/$(NOTARY_DOCKER_CONFIG_FILE) notary/root/$(NOTARY_DOCKER_CONFIG_FILE); \ + lxc file push -p $(ARTIFACT_FOLDER)/$(NOTARY_TLS_CERT) notary/root/$(NOTARY_TLS_CERT); \ + lxc file push -p $(ARTIFACT_FOLDER)/$(NOTARY_TLS_KEY) notary/root/$(NOTARY_TLS_KEY); \ + fi + + @# Deploy Jaeger if it hasn't been deployed yet + @if [ "$$(lxc exec notary -- docker ps 2> /dev/null | grep jaeger > /dev/null; echo $$?)" = 1 ]; then \ + echo "creating and running jaeger in Docker"; \ + # The forwarded ports are, in order: \ + # HTTP, /api/v3/*, OTLP-based JSON over HTTP (GUI) \ + # gRPC, ExportTraceServiceRequest, OTLP Protobuf \ + # HTTP, /v1/traces, OTLP Protobuf or OTLP JSON \ + # HTTP, /sampling, sampling.proto_via Protobuf-to-JSON mapping_ \ + # HTTP, /api/v2/spans, Zipkin v2 JSON or Protobuf \ + lxc exec notary -- docker run -d --name jaeger \ + --network host \ + -p 16686:16686 \ + -p 4317:4317 \ + -p 4318:4318 \ + -p 5778:5778 \ + -p 9411:9411 \ + jaegertracing/jaeger:2.6.0; \ + sleep 10; \ fi @# Remove the old notary if it was still there @@ -64,7 +85,7 @@ deploy: $(ARTIFACT_FOLDER)/$(ROCK_ARTIFACT_NAME) -v /root:/config \ --network host \ -p 2111:2111 \ - notary:latest --args notary start --config /config/config.yaml; + notary:latest --args notary start -m --config /config/$(NOTARY_DOCKER_CONFIG_FILE); @echo "You can access notary at $$(lxc info notary | grep enp5s0 -A 15 | grep inet: | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}'):2111" logs: @@ -83,14 +104,27 @@ $(ARTIFACT_FOLDER)/$(NOTARY_CONFIG_FILE): @echo 'key_path: "artifacts/key.pem"' >> $@;\ echo 'cert_path: "artifacts/cert.pem"' >> $@;\ echo 'db_path: "artifacts/notary.db"' >> $@;\ + echo 'port: 2111' >> $@;\ + echo 'pebble_notifications: false' >> $@;\ + echo 'encryption_backend:' >> $@;\ + echo ' type: "none"' >> $@;\ + +$(ARTIFACT_FOLDER)/$(NOTARY_DOCKER_CONFIG_FILE): + @echo 'key_path: "/config/key.pem"' >> $@;\ + echo 'cert_path: "/config/cert.pem"' >> $@;\ + echo 'db_path: "/config/notary.db"' >> $@;\ echo 'port: 2111' >> $@;\ echo 'pebble_notifications: false' >> $@;\ echo 'logging:' >> $@;\ echo ' system:' >> $@;\ echo ' level: "debug"' >> $@;\ - echo ' output: "artifacts/notary.log"' >> $@;\ - echo 'encryption_backend:' >> $@;\ - echo ' type: "none"' >> $@;\ + echo ' output: "/config/notary.log"' >> $@;\ + echo 'encryption_backend:' >> $@;\ + echo ' type: "none"' >> $@;\ + echo 'tracing:' >> $@;\ + echo ' service_name: "notary"' >> $@;\ + echo ' endpoint: "127.0.0.1:4317"' >> $@;\ + echo ' sampling_rate: "100%"' >> $@ $(ARTIFACT_FOLDER)/$(NOTARY_TLS_CERT) $(ARTIFACT_FOLDER)/$(NOTARY_TLS_KEY): openssl req -newkey rsa:2048 -nodes -keyout $(ARTIFACT_FOLDER)/$(NOTARY_TLS_KEY) -x509 -days 1 -out $(ARTIFACT_FOLDER)/$(NOTARY_TLS_CERT) -subj "/CN=example.com" diff --git a/cmd/start.go b/cmd/start.go index 51da7fda..af206cdd 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -50,6 +50,7 @@ https://canonical-notary.readthedocs-hosted.com/en/latest/reference/config_file/ ExternalHostname: appContext.ExternalHostname, EnablePebbleNotifications: appContext.PebbleNotificationsEnabled, Logger: appContext.Logger, + Tracer: appContext.Tracer, PublicConfig: appContext.PublicConfig, }) if err != nil { diff --git a/docs/.wordlist.txt b/docs/.wordlist.txt index e2c3be15..d0411b13 100644 --- a/docs/.wordlist.txt +++ b/docs/.wordlist.txt @@ -36,3 +36,6 @@ RBAC GCM HashiCorp Requestor +Grafana +OpenTelemetry +url diff --git a/docs/reference/config_file.md b/docs/reference/config_file.md index 0916bb13..3b414341 100644 --- a/docs/reference/config_file.md +++ b/docs/reference/config_file.md @@ -31,6 +31,12 @@ Or If you are using the snap you can modify the config under `/var/snap/notary/c - `approle_secret_id` (string): Secret ID for AppRole authentication. - `tls_ca_cert` (string): Path to the CA certificate for TLS verification (optional). - `tls_skip_verify` (boolean): Whether to skip TLS certificate verification (optional, defaults to `false`). It is strongly discouraged to set this to `true` outside of development environments +- `tracing` (object): Configuration for tracing. + - `service_name` (string): The name that will identify your service in the tracing system + - `endpoint` (string): The URL of your OpenTelemetry collector endpoint + - `sampling_rate` (string): The percentage of traces to sample. Can be specified as a percentage (50%) + or a decimal value between 0.0 and 1.0 (0.0, 0.5, 1.0). + ## Examples @@ -47,6 +53,10 @@ logging: level: "info" output: "stdout" encryption_backend: {} +tracing: + service_name: "notary" + endpoint: "127.0.0.1:4317" + sampling_rate: "100%" ``` ### With HSM as an Encryption Backend @@ -67,4 +77,8 @@ encryption_backend: lib_path: "/path/to/yubihsm_pkcs11.so" pin: "0001password" aes_encryption_key_id: 0x1234 +tracing: + service_name: "notary" + endpoint: "127.0.0.1:4317" + sampling_rate: "100%" ``` diff --git a/docs/reference/index.md b/docs/reference/index.md index 4d00e7ba..b9ae2fb8 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -9,4 +9,5 @@ api/index.md config_file.md metrics.md roles.md +tracing.md ``` diff --git a/docs/reference/tracing.md b/docs/reference/tracing.md new file mode 100644 index 00000000..9b9f50df --- /dev/null +++ b/docs/reference/tracing.md @@ -0,0 +1,96 @@ +# Distributed Tracing in Notary + +Notary supports distributed tracing using OpenTelemetry, which allows you to monitor and troubleshoot request flows across the service. Traces can be viewed in any compatible visualization tool, such as Grafana Tempo. + +## Configuration + +To enable tracing in Notary, add the following configuration to your YAML configuration file: + +```yaml +tracing: + service_name: "notary" # Optional, defaults to "notary" + endpoint: "tempo:4317" # Required if enabled, the OpenTelemetry gRPC endpoint + sampling_rate: "100%" # Optional, defaults to 100% (1.0) +``` + +### Configuration Options + +- **service_name**: The name that will identify your service in the tracing system +- **endpoint**: The URL of your Tempo (or other OpenTelemetry collector) endpoint +- **sampling_rate**: The percentage of traces to sample. Can be specified as: + - A percentage (e.g., "10%", "50%", "100%") + - A decimal value between 0.0 and 1.0 (e.g., "0.1", "0.5", "1.0") + +## Viewing Traces + +Traces are sent to the configured Tempo URL, where they can be visualized using Grafana or any other compatible tool. + +### Example Tempo Configuration with Docker Compose + +Here's a minimal example of how to set up Tempo with Docker Compose: + +```yaml +version: '3' +services: + tempo: + image: grafana/tempo:latest + command: [ "-config.file=/etc/tempo.yaml" ] + volumes: + - ./tempo.yaml:/etc/tempo.yaml + ports: + - "3200:3200" # Tempo server + - "4317:4317" # OTLP gRPC + + grafana: + image: grafana/grafana:latest + volumes: + - ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml + ports: + - "3000:3000" + depends_on: + - tempo +``` + +### trace flow + +Notary's tracing implementation tracks HTTP requests through the system, including: + +- HTTP method and path +- Response status codes +- Error information +- Duration of requests + +## Troubleshooting + +If traces are not appearing in your visualization tool: + +1. Verify that tracing is enabled in your configuration +2. Check that the `endpoint` is correct and accessible from your Notary instance +3. Examine the Notary logs for any errors related to tracing +4. Ensure that your sampling rate is high enough to capture traces (set to "100%" for testing) + +## Performance Considerations + +Tracing adds a small overhead to request processing. In production environments, consider: + +- Using a lower sampling rate (e.g., "10%") to reduce overhead +- Monitoring the impact on service response times +- Adjusting batch settings if handling very high volumes + +## Extending Tracing + +For developers extending Notary, you can add custom spans to functions by accessing the tracer: + +```go +// Example of adding a custom span +func myFunction(ctx context.Context) { + tracer := otel.Tracer("github.com/canonical/notary") + ctx, span := tracer.Start(ctx, "myFunction") + defer span.End() + + // Add attributes to the span + span.SetAttributes(attribute.String("key", "value")) + + // Your function code here +} +``` diff --git a/go.mod b/go.mod index f91518b2..b4c5de0a 100644 --- a/go.mod +++ b/go.mod @@ -6,42 +6,63 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/go-cmp v0.7.0 github.com/hashicorp/vault-client-go v0.4.3 - github.com/mattn/go-sqlite3 v1.14.28 - github.com/pressly/goose/v3 v3.25.0 - github.com/prometheus/client_golang v1.22.0 - github.com/spf13/cobra v1.9.1 - github.com/spf13/pflag v1.0.7 - github.com/spf13/viper v1.20.1 + github.com/mattn/go-sqlite3 v1.14.24 + github.com/pressly/goose/v3 v3.26.0 + github.com/prometheus/client_golang v1.21.1 + github.com/spf13/cobra v1.10.1 + github.com/spf13/pflag v1.0.10 + github.com/spf13/viper v1.21.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 + go.opentelemetry.io/otel v1.37.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 + go.opentelemetry.io/otel/sdk v1.29.0 + go.opentelemetry.io/otel/trace v1.37.0 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.40.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // 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/ryanuber/go-glob v1.0.0 // indirect - github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.12.0 // indirect - github.com/spf13/cast v1.7.1 // 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/subosito/gotenv v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/proto/otlp v1.1.0 // indirect go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.42.0 // indirect golang.org/x/sync v0.16.0 // indirect - golang.org/x/text v0.27.0 // indirect + golang.org/x/text v0.28.0 // indirect golang.org/x/time v0.8.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect + google.golang.org/grpc v1.67.3 // indirect ) require ( diff --git a/go.sum b/go.sum index 3dc7a2bf..cb789eac 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ 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/canonical/sqlair v0.0.0-20241004123011-77313b5382fd h1:lVn7391CX7QQ5WBQriNUtCB4fvfurZg6XJwH9aVsRII= github.com/canonical/sqlair v0.0.0-20241004123011-77313b5382fd/go.mod h1:T+40I2sXshY3KRxx0QQpqqn6hCibSKJ2KHzjBvJj8T4= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -11,10 +13,17 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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/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/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= @@ -23,6 +32,8 @@ 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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= github.com/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= @@ -49,8 +60,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 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-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= -github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= @@ -61,14 +72,14 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +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/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/pressly/goose/v3 v3.25.0 h1:6WeYhMWGRCzpyd89SpODFnCBCKz41KrVbRT58nVjGng= -github.com/pressly/goose/v3 v3.25.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM= +github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= +github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= +github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= 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.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= @@ -82,45 +93,73 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= -github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +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/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= -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/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= -github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= -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.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= -github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= -github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= -github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= -github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +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.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +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/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= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= +go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= go.uber.org/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.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= 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/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= +google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8= +google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/config/config.go b/internal/config/config.go index 8cbac6e6..773b57fe 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,6 +1,7 @@ package config import ( + "context" "errors" "fmt" "os" @@ -8,9 +9,13 @@ import ( "slices" "strings" + "strconv" + eb "github.com/canonical/notary/internal/encryption_backend" + "github.com/canonical/notary/internal/tracing" "github.com/spf13/pflag" "github.com/spf13/viper" + "go.opentelemetry.io/otel" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) @@ -41,6 +46,11 @@ func CreateAppContext(cmdFlags *pflag.FlagSet, configFilePath string) (*NotaryAp if err != nil { return nil, fmt.Errorf("couldn't initialize logger: %w", err) } + // initialize tracer + tracer, err := initializeTracing(cfg.Sub("tracing"), logger) + if err != nil { + return nil, fmt.Errorf("couldn't initialize tracer: %w", err) + } // initialize encryption backend backendType, backend, err := initializeEncryptionBackend(cfg.Sub("encryption_backend"), logger) if err != nil { @@ -56,6 +66,7 @@ func CreateAppContext(cmdFlags *pflag.FlagSet, configFilePath string) (*NotaryAp appContext.TLSCertificate = cert appContext.TLSPrivateKey = key appContext.Logger = logger + appContext.Tracer = tracer appContext.EncryptionBackend = backend appContext.EncryptionBackendType = backendType appContext.PublicConfig = &PublicConfigData{ @@ -86,6 +97,7 @@ func initializeServerConfig(cmdFlags *pflag.FlagSet, configFilePath string) (*vi v.SetDefault("logging.system.level", "debug") v.SetDefault("logging.system.output", "stdout") + if configFilePath == "" { return nil, errors.New("config file path not provided") } @@ -229,3 +241,65 @@ func initializeLogger(cfg *viper.Viper) (*zap.Logger, error) { return logger, nil } + +// initializeTracing creates and configures a tracer based on the configuration. +func initializeTracing(cfg *viper.Viper, logger *zap.Logger) (*Tracer, error) { + if cfg == nil { + return nil, nil + } + cfg.SetDefault("tracing.service_name", "notary") + cfg.SetDefault("tracing.sampling_rate", "100%") + + if !cfg.IsSet("endpoint") { + return nil, errors.New("`tracing.endpoint` is required when tracing is enabled") + } + serviceName := cfg.GetString("service_name") + endpoint := cfg.GetString("endpoint") + samplingRate, err := parseSamplingRate(cfg.GetString("sampling_rate")) + if err != nil { + return nil, fmt.Errorf("invalid sampling rate: %w", err) + } + tracer := otel.Tracer("notary") + shutdownFunc, err := tracing.SetupTracing(context.Background(), endpoint, serviceName, samplingRate, logger) + if err != nil { + return nil, fmt.Errorf("failed to set up tracing: %w", err) + } + return &Tracer{ + Tracer: tracer, + ShutdownFunc: shutdownFunc, + }, nil +} + +// parseSamplingRate converts a string sampling rate (percentage or decimal) to a float64 +func parseSamplingRate(rate string) (float64, error) { + // Try to parse as a float first + samplingRate, err := strconv.ParseFloat(rate, 64) + if err == nil { + // Check if the value is between 0 and 1 inclusive + if samplingRate < 0 || samplingRate > 1 { + return 0, fmt.Errorf("sampling rate must be between 0 and 1, got %f", samplingRate) + } + return samplingRate, nil + } + + // If parsing as float failed, check if it's a percentage string + if len(rate) > 1 && rate[len(rate)-1] == '%' { + // Remove % and parse as float + percentage, err := strconv.ParseFloat(rate[:len(rate)-1], 64) + if err != nil { + return 0, fmt.Errorf("invalid sampling rate format: %s", rate) + } + + // Convert percentage to decimal + samplingRate = percentage / 100.0 + + // Check if the value is between 0 and 1 inclusive + if samplingRate < 0 || samplingRate > 1 { + return 0, fmt.Errorf("sampling rate percentage must be between 0%% and 100%%, got %s", rate) + } + + return samplingRate, nil + } + + return 0, fmt.Errorf("invalid sampling rate format: %s", rate) +} diff --git a/internal/config/types.go b/internal/config/types.go index e2718ae3..a2cb0bff 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -2,6 +2,8 @@ package config import ( "github.com/canonical/notary/internal/encryption_backend" + "github.com/canonical/notary/internal/tracing" + "go.opentelemetry.io/otel/trace" "go.uber.org/zap" ) @@ -13,53 +15,6 @@ const ( EncryptionBackendTypeNone = "none" ) -// VaultBackendConfigYaml BackendConfig for Vault-specific fields. -type VaultBackendConfigYaml struct { - Endpoint string `yaml:"endpoint"` - Mount string `yaml:"mount"` - KeyName string `yaml:"key_name"` - Token string `yaml:"token"` - AppRoleID string `yaml:"approle_role_id"` - AppRoleSecretID string `yaml:"approle_secret_id"` - TlsCaCertificate string `yaml:"tls_ca_cert,omitempty"` // Optional path to a CA file for Vault TLS verification - TlsSkipVerify bool `yaml:"tls_skip_verify,omitempty"` // Optional flag to skip TLS verification -} - -// PKCS11BackendConfigYaml BackendConfig for PKCS11-specific fields. -type PKCS11BackendConfigYaml struct { - LibPath string `yaml:"lib_path"` - KeyID uint16 `yaml:"aes_encryption_key_id"` - Pin string `yaml:"pin"` -} - -// NamedBackendConfigYaml represents a single named backend configuration -type NamedBackendConfigYaml struct { - PKCS11 *PKCS11BackendConfigYaml `yaml:"pkcs11,omitempty"` - Vault *VaultBackendConfigYaml `yaml:"vault,omitempty"` -} - -type EncryptionBackendConfigYaml map[string]NamedBackendConfigYaml - -type SystemLoggingConfigYaml struct { - Level string `yaml:"level"` - Output string `yaml:"output"` -} - -type LoggingConfigYaml struct { - System SystemLoggingConfigYaml `yaml:"system"` -} - -type ConfigYAML struct { - KeyPath string `yaml:"key_path"` - CertPath string `yaml:"cert_path"` - ExternalHostname string `yaml:"external_hostname"` - DBPath string `yaml:"db_path"` - Port int `yaml:"port"` - PebbleNotifications bool `yaml:"pebble_notifications"` - Logging LoggingConfigYaml `yaml:"logging"` - EncryptionBackend EncryptionBackendConfigYaml `yaml:"encryption_backend"` -} - type LoggingLevel string const ( @@ -71,13 +26,9 @@ const ( Panic LoggingLevel = "panic" ) -type SystemLoggingOptions struct { - Level LoggingLevel - Output string -} - -type LoggerOptions struct { - System SystemLoggingOptions +type Tracer struct { + Tracer trace.Tracer + ShutdownFunc tracing.TracerShutdownFunc } // PublicConfigData contains non-sensitive configuration fields that are safe to expose @@ -91,7 +42,6 @@ type PublicConfigData struct { type NotaryAppContext struct { // The YAML configuration file content - Config *ConfigYAML PublicConfig *PublicConfigData // TLSPrivateKey and Certificate for the webserver and the listener port @@ -111,8 +61,9 @@ type NotaryAppContext struct { // Send pebble notifications if enabled. Read more at github.com/canonical/pebble PebbleNotificationsEnabled bool - // Options for the logger + // Options for the logger and tracer Logger *zap.Logger + Tracer *Tracer // Encryption backend to be used for encrypting and decrypting sensitive data EncryptionBackendType diff --git a/internal/server/middleware.go b/internal/server/middleware.go index 8648406a..709be079 100644 --- a/internal/server/middleware.go +++ b/internal/server/middleware.go @@ -10,10 +10,14 @@ import ( "strconv" "strings" + "github.com/canonical/notary/internal/config" "github.com/canonical/notary/internal/db" "github.com/canonical/notary/internal/metrics" "github.com/golang-jwt/jwt/v5" "github.com/prometheus/client_golang/prometheus/promhttp" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" "go.uber.org/zap" ) @@ -27,6 +31,7 @@ type middlewareContext struct { responseStatusCode int jwtSecret []byte logger *zap.Logger + tracer *config.Tracer } // createMiddlewareStack chains the given middleware functions to wrap the api. @@ -44,6 +49,7 @@ func createMiddlewareStack(middleware ...middleware) middleware { } } +// limitRequestSize is a middleware that limits the size of the request body to maxKilobytes. func limitRequestSize(maxKilobytes int64, logger *zap.Logger) middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -97,6 +103,45 @@ func loggingMiddleware(ctx *middlewareContext) middleware { } } +// tracingMiddleware adds OpenTelemetry span creation and propagation to each request +func tracingMiddleware(ctx *middlewareContext) middleware { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/_next") || r.URL.Path == "/metrics" { + next.ServeHTTP(w, r) + return + } + if ctx.tracer == nil { + next.ServeHTTP(w, r) + return + } + + spanName := fmt.Sprintf("%s %s", r.Method, r.URL.Path) + spanCtx, span := ctx.tracer.Tracer.Start( + r.Context(), + spanName, + trace.WithAttributes( + attribute.String("http.method", r.Method), + attribute.String("http.url", r.URL.String()), + attribute.String("http.host", r.Host), + attribute.String("http.user_agent", r.UserAgent()), + ), + ) + defer span.End() + + clonedWriter := newResponseWriter(w) + r = r.WithContext(spanCtx) + next.ServeHTTP(clonedWriter, r) + span.SetAttributes( + attribute.Int("http.status_code", clonedWriter.statusCode), + ) + if clonedWriter.statusCode >= 400 { + span.SetStatus(codes.Error, http.StatusText(clonedWriter.statusCode)) + } + }) + } +} + func requirePermission(permission string, jwtSecret []byte, handler http.HandlerFunc, logger *zap.Logger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { claims, err := getClaimsFromAuthorizationHeader(r.Header.Get("Authorization"), jwtSecret) diff --git a/internal/server/router.go b/internal/server/router.go index f101fc14..b02dbb72 100644 --- a/internal/server/router.go +++ b/internal/server/router.go @@ -49,14 +49,17 @@ func NewRouter(config *HandlerConfig) http.Handler { ctx := middlewareContext{ jwtSecret: config.JWTSecret, logger: config.Logger, + tracer: config.Tracer, } apiMiddlewareStack := createMiddlewareStack( limitRequestSize(MAX_KILOBYTES, config.Logger), metricsMiddleware(m), loggingMiddleware(&ctx), + tracingMiddleware(&ctx), ) metricsMiddlewareStack := createMiddlewareStack( metricsMiddleware(m), + tracingMiddleware(&ctx), ) router := http.NewServeMux() diff --git a/internal/server/server.go b/internal/server/server.go index 17fb7b03..106f9e3e 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -7,21 +7,11 @@ import ( "net/http" "time" - "github.com/canonical/notary/internal/config" - "github.com/canonical/notary/internal/db" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) -type HandlerConfig struct { - DB *db.Database - Logger *zap.Logger - ExternalHostname string - JWTSecret []byte - SendPebbleNotifications bool - PublicConfig config.PublicConfigData -} - // New creates an environment and an http server with handlers that Go can start listening to func New(opts *ServerOpts) (*Server, error) { serverCerts, err := tls.X509KeyPair(opts.TLSCertificate, opts.TLSPrivateKey) @@ -38,10 +28,21 @@ func New(opts *ServerOpts) (*Server, error) { cfg.JWTSecret = opts.Database.JWTSecret cfg.ExternalHostname = opts.ExternalHostname cfg.Logger = opts.Logger + cfg.Tracer = opts.Tracer cfg.PublicConfig = *opts.PublicConfig cfg.DB = opts.Database router := NewRouter(cfg) + if cfg.Tracer != nil { + router = otelhttp.NewHandler( + router, + "http_server", + otelhttp.WithMessageEvents(otelhttp.ReadEvents, otelhttp.WriteEvents), + otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string { + return fmt.Sprintf("%s %s", r.Method, r.URL.Path) + }), + ) + } s := &http.Server{ Addr: fmt.Sprintf(":%d", opts.Port), ErrorLog: stdErrLog, diff --git a/internal/server/types.go b/internal/server/types.go index 694a2bcf..5c540628 100644 --- a/internal/server/types.go +++ b/internal/server/types.go @@ -25,6 +25,7 @@ type ServerOpts struct { Database *db.Database Logger *zap.Logger + Tracer *config.Tracer } type Server struct { @@ -33,4 +34,16 @@ type Server struct { type middleware func(http.Handler) http.Handler -type NotificationKey int \ No newline at end of file +type NotificationKey int + +// HandlerConfig holds the dependencies to be injected into the HTTP handlers for use during +// request processing. +type HandlerConfig struct { + DB *db.Database + Logger *zap.Logger + Tracer *config.Tracer + ExternalHostname string + JWTSecret []byte + SendPebbleNotifications bool + PublicConfig config.PublicConfigData +} \ No newline at end of file diff --git a/internal/tracing/trace.go b/internal/tracing/trace.go new file mode 100644 index 00000000..9e45d0ac --- /dev/null +++ b/internal/tracing/trace.go @@ -0,0 +1,73 @@ +package tracing + +import ( + "context" + "fmt" + "time" + + "github.com/canonical/notary/version" + "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" + semconv "go.opentelemetry.io/otel/semconv/v1.24.0" + "go.uber.org/zap" +) + +// TracerShutdownFunc is a function that can be called to clean up tracing resources. +type TracerShutdownFunc func(context.Context) error + +// SetupTracing initializes OpenTelemetry tracing with configuration from the app config +func SetupTracing(ctx context.Context, endpoint string, serviceName string, samplingRate float64, logger *zap.Logger) (TracerShutdownFunc, error) { + if endpoint == "" { + return nil, fmt.Errorf("tracing is enabled but endpoint is not configured") + } + + logger.Info("Setting up tracing", + zap.String("service_name", serviceName), + zap.String("endpoint", endpoint), + zap.Float64("sampling_rate", samplingRate)) + + client := otlptracegrpc.NewClient( + otlptracegrpc.WithEndpoint(endpoint), + otlptracegrpc.WithInsecure(), // TODO: support TLS + ) + exporter, err := otlptrace.New(ctx, client) + if err != nil { + return nil, fmt.Errorf("failed to create trace exporter: %w", err) + } + res, err := resource.New(ctx, + resource.WithAttributes( + semconv.ServiceName(serviceName), + semconv.ServiceVersion(version.GetVersion()), + ), + ) + if err != nil { + return nil, fmt.Errorf("failed to create resource: %w", err) + } + + tp := sdktrace.NewTracerProvider( + sdktrace.WithSampler(sdktrace.TraceIDRatioBased(samplingRate)), + sdktrace.WithBatcher(exporter, + sdktrace.WithBatchTimeout(5*time.Second), + sdktrace.WithMaxExportBatchSize(512), + ), + sdktrace.WithResource(res), + ) + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + )) + logger.Info("Tracing has been successfully configured") + + return func(ctx context.Context) error { + logger.Info("Shutting down tracer provider") + if err := tp.Shutdown(ctx); err != nil { + return fmt.Errorf("failed to shutdown tracer provider: %w", err) + } + return nil + }, nil +}