diff --git a/.web/docs/.vitepress/config.ts b/.web/docs/.vitepress/config.ts index 717a9759..78991d1c 100644 --- a/.web/docs/.vitepress/config.ts +++ b/.web/docs/.vitepress/config.ts @@ -174,6 +174,20 @@ export default defineConfig({ text: 'Rate Limiting', link: '/guide/rate-limiting', }, + { + text: 'OpenTelemetry', + link: '/guide/otel/', + items: [ + { + text: 'Grafana Cloud', + link: '/guide/otel/grafana-cloud/', + }, + { + text: 'Honeycomb', + link: '/guide/otel/honeycomb/', + }, + ], + }, ], }, { diff --git a/.web/docs/developers/telemetry/telemetry.md b/.web/docs/developers/telemetry/telemetry.md new file mode 100644 index 00000000..17762f02 --- /dev/null +++ b/.web/docs/developers/telemetry/telemetry.md @@ -0,0 +1,251 @@ +# Gate Telemetry + +Gate supports OpenTelemetry for metrics and distributed tracing, allowing operators to monitor their deployment's health, performance, and behavior. + +## Configuration + +Enable telemetry in your `config.yml`: + +```yaml +telemetry: + # Metrics configuration using OpenTelemetry + metrics: + enabled: true + endpoint: "0.0.0.0:8888" # Endpoint for /metrics + anonymousMetrics: true # Send anonymous usage metrics + exporter: prometheus # Supported: prometheus, otlp + prometheus: + path: "/metrics" # Path for Prometheus scraping + + # Distributed tracing configuration + tracing: + enabled: false # Disabled by default + endpoint: "localhost:4317" # OTLP collector endpoint + sampler: "parentbased_always_on" + exporter: otlp # Supported: otlp, jaeger, stdout +``` + +## Setup Options + +### Self-Hosted Stack + +1. **Prometheus + Grafana + Loki + Tempo** + +```yaml +version: '3' +services: + prometheus: + image: prom/prometheus + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" + + grafana: + image: grafana/grafana + ports: + - "3000:3000" + environment: + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + + loki: + image: grafana/loki + ports: + - "3100:3100" + + tempo: + image: grafana/tempo + ports: + - "14250:14250" +``` + +prometheus.yml: +```yaml +scrape_configs: + - job_name: 'gate' + static_configs: + - targets: ['localhost:8888'] +``` + +2. **Jaeger All-in-One** + +```bash +docker run -d --name jaeger \ + -e COLLECTOR_OTLP_ENABLED=true \ + -p 16686:16686 \ + -p 4317:4317 \ + jaegertracing/all-in-one:latest +``` + +### Managed Services + +1. **Grafana Cloud** + - Create account at https://grafana.com/ + - Get endpoints and credentials + - Configure Gate: + ```yaml + telemetry: + metrics: + enabled: true + endpoint: "prometheus-us-central1.grafana.net:9090" + exporter: otlp + tracing: + enabled: true + endpoint: "tempo-us-central1.grafana.net:4317" + exporter: otlp + ``` + +2. **Honeycomb** + - Sign up at https://www.honeycomb.io/ + - Get API key + - Configure Gate: + ```yaml + telemetry: + tracing: + enabled: true + endpoint: "api.honeycomb.io:443" + exporter: otlp + ``` + +3. **New Relic** + - Create account at https://newrelic.com/ + - Get license key + - Configure Gate: + ```yaml + telemetry: + metrics: + enabled: true + endpoint: "otlp.nr-data.net:4317" + exporter: otlp + tracing: + enabled: true + endpoint: "otlp.nr-data.net:4317" + exporter: otlp + ``` + +## Sample Grafana Dashboard + +```json +{ + "annotations": { + "list": [] + }, + "editable": true, + "panels": [ + { + "title": "Connected Players", + "type": "graph", + "datasource": "Prometheus", + "targets": [ + { + "expr": "gate_players_current", + "legendFormat": "Players" + } + ] + }, + { + "title": "Server Performance", + "type": "gauge", + "datasource": "Prometheus", + "targets": [ + { + "expr": "gate_performance_tps", + "legendFormat": "TPS" + } + ], + "options": { + "maxValue": 20, + "minValue": 0, + "thresholds": [ + { "value": 15, "color": "red" }, + { "value": 18, "color": "yellow" }, + { "value": 19.5, "color": "green" } + ] + } + }, + { + "title": "Player Sessions", + "type": "heatmap", + "datasource": "Prometheus", + "targets": [ + { + "expr": "rate(gate_connection_duration_bucket[5m])", + "legendFormat": "{{le}}" + } + ] + }, + { + "title": "Command Executions", + "type": "timeseries", + "datasource": "Prometheus", + "targets": [ + { + "expr": "sum(rate(gate_command_executions_total[5m])) by (command)", + "legendFormat": "{{command}}" + } + ] + }, + { + "title": "Server Load", + "type": "timeseries", + "datasource": "Prometheus", + "targets": [ + { + "expr": "sum(rate(gate_server_connections_total[5m])) by (server)", + "legendFormat": "{{server}}" + } + ] + } + ], + "rows": [ + { + "panels": [ + { + "title": "Player Logs", + "type": "logs", + "datasource": "Loki", + "targets": [ + { + "expr": "{job=\"gate\"} |= \"player\"" + } + ] + } + ] + }, + { + "panels": [ + { + "title": "Traces", + "type": "traces", + "datasource": "Tempo", + "targets": [ + { + "query": "service.name=\"gate\"" + } + ] + } + ] + } + ] +} +``` + +## Anonymous Metrics + +When enabled, Gate sends the following anonymous data: +- Random installation ID +- Gate version +- Operating system and architecture +- Number of connected players (aggregate only) +- Performance metrics (TPS, latency histograms) +- Error rates and types +- Feature usage statistics + +This data helps: +- Identify performance bottlenecks +- Prioritize features and fixes +- Understand deployment patterns +- Improve stability + +No personal data or player information is collected. \ No newline at end of file diff --git a/.web/docs/guide/otel/grafana-cloud/index.md b/.web/docs/guide/otel/grafana-cloud/index.md new file mode 100644 index 00000000..c4362923 --- /dev/null +++ b/.web/docs/guide/otel/grafana-cloud/index.md @@ -0,0 +1,78 @@ +# Grafana Cloud + +[Grafana Cloud](https://grafana.com/products/cloud/) is a fully managed observability platform that supports OpenTelemetry. Follow these steps to set up Gate with Grafana Cloud: + +1. **Create a Grafana Cloud Account** + + - Sign up at [Grafana.com](https://grafana.com/auth/sign-up/create-user) + - Navigate to your organization + - Create an Access Policy with write permissions at [Access Policies](https://grafana.com/orgs/your-org/access-policies) + - Generate and save your API token + +2. **Configure Stack** + + Navigate to your Grafana Cloud Stack (e.g., grafana.com/orgs/your-org/stacks/xxxxx) and: + + - Click "Send Traces" in the Tempo section to get your traces endpoint + - Click "Send Metrics" in the Prometheus section to get your metrics endpoint + + ![Stack](./stack.png) + +3. **Prepare Your Authentication** + + You'll need to encode your credentials in base64 format. Use one of these methods: + + - Using the command line: + + ```bash + echo "YOUR_INSTANCE_ID:YOUR_API_TOKEN" | base64 + ``` + + - Or visit an online base64 encoder like [base64encode.org](https://www.base64encode.org/) + +4. **Configure Gate** + + Export the following environment variables before starting Gate: + + ```bash + # For traces (Tempo) + export OTEL_EXPORTER_OTLP_ENDPOINT="https://tempo-prod-XX-prod-XX-XXXXX.grafana.net/tempo" + export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic YOUR_BASE64_ENCODED_CREDENTIALS" + + # For metrics (Prometheus) + export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" + export OTEL_METRICS_EXPORTER="otlp" + export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT="https://prometheus-prod-XX-prod-XX-XXXXX.grafana.net/api/prom/push" + export OTEL_EXPORTER_OTLP_METRICS_HEADERS="Authorization=Basic YOUR_BASE64_ENCODED_CREDENTIALS" + ``` + + ::: tip + For production deployments, consider setting these environment variables in your system configuration or container orchestration platform rather than exporting them manually. + ::: + +5. **Start Gate** + + Once the environment variables are set, start Gate normally. It will automatically begin sending telemetry data to Grafana Cloud. + + ```bash + gate + ``` + + See [Install](/guide/install/) for more information on how to start Gate. + +6. **View Your Data** + + Log into your Grafana Cloud account and click on the "Launch" button for Grafana: + + - Navigate to the Tempo service to view your traces + - Navigate to the Prometheus service to view your metrics + + ![Launch](./launch.png) + + - Go to the "Explore" section and select "Tempo" to in the sources + + ![tempo-source](./tempo-source.png) + + ![Trace](./trace.png) + + - Or select "Prometheus" to view your metrics diff --git a/.web/docs/guide/otel/grafana-cloud/launch.png b/.web/docs/guide/otel/grafana-cloud/launch.png new file mode 100644 index 00000000..8da870a1 Binary files /dev/null and b/.web/docs/guide/otel/grafana-cloud/launch.png differ diff --git a/.web/docs/guide/otel/grafana-cloud/stack.png b/.web/docs/guide/otel/grafana-cloud/stack.png new file mode 100644 index 00000000..682b5565 Binary files /dev/null and b/.web/docs/guide/otel/grafana-cloud/stack.png differ diff --git a/.web/docs/guide/otel/grafana-cloud/tempo-source.png b/.web/docs/guide/otel/grafana-cloud/tempo-source.png new file mode 100644 index 00000000..d3dbb757 Binary files /dev/null and b/.web/docs/guide/otel/grafana-cloud/tempo-source.png differ diff --git a/.web/docs/guide/otel/grafana-cloud/trace.png b/.web/docs/guide/otel/grafana-cloud/trace.png new file mode 100644 index 00000000..52dc896d Binary files /dev/null and b/.web/docs/guide/otel/grafana-cloud/trace.png differ diff --git a/.web/docs/guide/otel/honeycomb/index.md b/.web/docs/guide/otel/honeycomb/index.md new file mode 100644 index 00000000..e2e847bb --- /dev/null +++ b/.web/docs/guide/otel/honeycomb/index.md @@ -0,0 +1,49 @@ +# Honeycomb + +[Honeycomb](https://www.honeycomb.io/) is an OpenTelemetry-compatible observability platform that requires minimal setup - just sign up, create an environment, and get your API key to start collecting telemetry data. Here's how to get started: + +1. **Create a Honeycomb Account** + + - Sign up at [Honeycomb.io](https://ui.honeycomb.io/signup) + - Create a new environment (or use an existing one) + - Get your API key from Environment Settings + +2. **Configure Gate** + + Export the following environment variables before starting Gate: + + ::: code-group + + ```bash [US Region] + export OTEL_EXPORTER_OTLP_ENDPOINT="https://api.honeycomb.io:443" + export OTEL_EXPORTER_OTLP_HEADERS="x-honeycomb-team=your-api-key" + ``` + + ```bash [EU Region] + export OTEL_EXPORTER_OTLP_ENDPOINT="https://api.eu1.honeycomb.io:443" + export OTEL_EXPORTER_OTLP_HEADERS="x-honeycomb-team=your-api-key" + ``` + + ::: + + ::: tip + For production deployments, consider setting these environment variables in your system configuration or container orchestration platform rather than exporting them manually. + ::: + +3. **Start Gate** + + Once the environment variables are set, start Gate normally. It will automatically begin sending telemetry data to Honeycomb. + + ```bash + gate + ``` + + See [Install](/guide/install/) for more information on how to start Gate. + +4. **View Your Data** + + Log into your Honeycomb account and navigate to your environment. You should see your Gate service appearing in the list of services, and you can start creating queries and visualizations to analyze your data. + + ![Trace](trace.png) + + ![Metric](metric.png) diff --git a/.web/docs/guide/otel/honeycomb/metric.png b/.web/docs/guide/otel/honeycomb/metric.png new file mode 100644 index 00000000..20eb964e Binary files /dev/null and b/.web/docs/guide/otel/honeycomb/metric.png differ diff --git a/.web/docs/guide/otel/honeycomb/trace.png b/.web/docs/guide/otel/honeycomb/trace.png new file mode 100644 index 00000000..a2fc59b5 Binary files /dev/null and b/.web/docs/guide/otel/honeycomb/trace.png differ diff --git a/.web/docs/guide/otel/index.md b/.web/docs/guide/otel/index.md new file mode 100644 index 00000000..07e977b9 --- /dev/null +++ b/.web/docs/guide/otel/index.md @@ -0,0 +1,134 @@ +# OpenTelemetry + +Gate uses OpenTelemetry for observability, leveraging the [otel-config-go](https://github.com/honeycombio/otel-config-go) library for configuration. This provides a simple way to configure tracing and metrics collection through environment variables. + +## Configuration + +Gate's OpenTelemetry implementation can be configured using the following environment variables: + +| Environment Variable | Required | Default | Description | +| --------------------------- | -------- | ---------------------- | ------------------------- | +| OTEL_SERVICE_NAME | No | `gate` | Name of your service | +| OTEL_SERVICE_VERSION | No | - | Version of your service | +| OTEL_EXPORTER_OTLP_ENDPOINT | No | `localhost:4317` | Endpoint for OTLP export | +| OTEL_LOG_LEVEL | No | `info` | Logging level | +| OTEL_PROPAGATORS | No | `tracecontext,baggage` | Configured propagators | +| OTEL_METRICS_ENABLED | No | `true` | Enable metrics collection | +| OTEL_TRACES_ENABLED | No | `true` | Enable trace collection | + +Additional environment variables for exporters: + +| Environment Variable | Required | Default | Description | +| ----------------------------------- | -------- | ---------------- | ------------------------------------ | +| OTEL_EXPORTER_OTLP_HEADERS | No | `{}` | Global headers for OTLP exporter | +| OTEL_EXPORTER_OTLP_TRACES_HEADERS | No | `{}` | Headers specific to trace exporter | +| OTEL_EXPORTER_OTLP_METRICS_HEADERS | No | `{}` | Headers specific to metrics exporter | +| OTEL_EXPORTER_OTLP_PROTOCOL | No | `grpc` | Protocol for OTLP export (grpc/http) | +| OTEL_EXPORTER_OTLP_TRACES_ENDPOINT | No | `localhost:4317` | Endpoint for trace export | +| OTEL_EXPORTER_OTLP_TRACES_INSECURE | No | `false` | Allow insecure trace connections | +| OTEL_EXPORTER_OTLP_METRICS_ENDPOINT | No | `localhost:4317` | Endpoint for metrics export | +| OTEL_EXPORTER_OTLP_METRICS_INSECURE | No | `false` | Allow insecure metrics connections | +| OTEL_EXPORTER_OTLP_METRICS_PERIOD | No | `30s` | Metrics reporting interval | +| OTEL_RESOURCE_ATTRIBUTES | No | - | Additional resource attributes | + +## Example Configuration + +Here's an example configuration for sending telemetry to a local OpenTelemetry collector: + +```env +OTEL_SERVICE_NAME="my-gate-service" +OTEL_EXPORTER_OTLP_ENDPOINT="localhost:4317" +OTEL_EXPORTER_OTLP_PROTOCOL="grpc" +OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production" +``` + +## Observability Solutions + +You can use various solutions to collect and visualize OpenTelemetry data. Here are some popular options: + +### Cloud Solutions + +::: info Our recommendations + +- [Grafana Cloud](/guide/otel/grafana-cloud/) - Fully managed observability platform with support for metrics, logs, and traces +- [Honeycomb](/guide/otel/honeycomb/) - Observability platform designed for debugging complex systems + +::: + +- [New Relic](https://newrelic.com/) - Full-stack observability platform with APM capabilities +- [Datadog](https://www.datadog.com/) - Cloud monitoring and analytics platform +- [Azure Monitor](https://azure.microsoft.com/services/monitor/) - Microsoft's cloud-native monitoring solution +- [AWS X-Ray](https://aws.amazon.com/xray/) - Distributed tracing system for AWS applications +- [Google Cloud Operations Suite](https://cloud.google.com/operations) - Formerly Stackdriver, for monitoring, logging, and diagnostics + +### Self-Hosted Solutions + +#### Tracing + +- [Tempo](https://grafana.com/oss/tempo/) - Grafana Tempo is a high-scale distributed tracing backend +- [Jaeger](https://www.jaegertracing.io/) - Open source, end-to-end distributed tracing + +#### Metrics + +- [Mimir](https://grafana.com/oss/mimir/) - Grafana Mimir is a highly scalable Prometheus solution + +#### Visualization + +- [Grafana](https://grafana.com/oss/grafana/) - The open and composable observability and data visualization platform + +## Best Practices + +1. **Service Name**: Always set a meaningful `OTEL_SERVICE_NAME` that clearly identifies your service. + + ```env + # Good examples: + OTEL_SERVICE_NAME="gate-proxy-eu" + OTEL_SERVICE_NAME="gate-proxy-lobby" + + # Bad examples: + OTEL_SERVICE_NAME="proxy" # too generic + OTEL_SERVICE_NAME="gate" # not specific enough + ``` + +2. **Service Version**: Set `OTEL_SERVICE_VERSION` to track your application version: + + ```env + # Semantic versioning + OTEL_SERVICE_VERSION="v1.2.3" + + # Git commit hash + OTEL_SERVICE_VERSION="git-8f45d91" + + # Build number + OTEL_SERVICE_VERSION="build-1234" + ``` + +3. **Resource Attributes**: Use `OTEL_RESOURCE_ATTRIBUTES` to add important context like environment, region, or deployment info. + + ```env + # Single attribute + OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production" + + # Multiple attributes + OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,cloud.region=eu-west-1,kubernetes.namespace=game-servers" + + # With detailed context + OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,service.instance.id=gate-1,cloud.provider=aws,cloud.region=us-east-1" + ``` + +4. **Security**: In production environments: + + ```env + # Secure endpoint configuration + OTEL_EXPORTER_OTLP_ENDPOINT="https://otel-collector.example.com:4317" + OTEL_EXPORTER_OTLP_HEADERS="api-key=secret123,tenant=team-a" + + # Ensure TLS is enabled + OTEL_EXPORTER_OTLP_INSECURE=false + ``` + +## Further Reading + +- [OpenTelemetry Documentation](https://opentelemetry.io/docs/) +- [otel-config-go Repository](https://github.com/honeycombio/otel-config-go) +- [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) diff --git a/README.md b/README.md index 2f7caf4a..a558124f 100644 --- a/README.md +++ b/README.md @@ -57,4 +57,4 @@ graph LR ## Developers Starter Template The starter template is designed to help you get started with your own Gate powered project. -Fork it! 🚀 - [minekube/gate-plugin-template](https://github.com/minekube/gate-plugin-template) +Fork it! 🚀 - [minekube/gate-plugin-template](https://github.com/minekube/gate-plugin-template) \ No newline at end of file diff --git a/cmd/gate/root.go b/cmd/gate/root.go index a0592372..8217c2ff 100644 --- a/cmd/gate/root.go +++ b/cmd/gate/root.go @@ -12,6 +12,7 @@ import ( "github.com/spf13/viper" "github.com/urfave/cli/v2" "go.minekube.com/gate/pkg/gate" + "go.minekube.com/gate/pkg/telemetry" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) @@ -97,6 +98,13 @@ Visit the website https://gate.minekube.com/ for more information.` log.Info("logging verbosity", "verbosity", verbosity) log.Info("using config file", "config", v.ConfigFileUsed()) + // Initialize telemetry + otelShutdown, err := telemetry.Init(c.Context, cfg) + if err != nil { + return cli.Exit(fmt.Errorf("failed to initialize telemetry: %w", err), 1) + } + defer otelShutdown() + // Start Gate if err = gate.Start(c.Context, gate.WithConfig(*cfg), diff --git a/go.mod b/go.mod index 92cad438..4adce70c 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,7 @@ module go.minekube.com/gate +replace go.minekube.com/gate => ./ + go 1.23.2 toolchain go1.23.5 @@ -9,6 +11,7 @@ require ( github.com/Tnze/go-mc v1.20.2 github.com/agext/levenshtein v1.2.3 github.com/coder/websocket v1.8.12 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/dboslee/lru v0.0.1 github.com/edwingeng/deque/v2 v2.1.1 github.com/gammazero/deque v1.0.0 @@ -18,11 +21,13 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da github.com/google/uuid v1.6.0 github.com/gookit/color v1.5.4 + github.com/honeycombio/otel-config-go v1.17.0 github.com/jellydator/ttlcache/v3 v3.3.0 github.com/knadh/koanf/providers/file v1.1.2 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/pires/go-proxyproto v0.8.0 - github.com/robinbraemer/event v0.1.1 + github.com/prometheus/client_golang v1.20.5 + github.com/robinbraemer/event v0.0.1 github.com/rs/xid v1.6.0 github.com/sandertv/go-raknet v1.13.0 github.com/sandertv/gophertunnel v1.37.0 @@ -33,48 +38,87 @@ require ( go.minekube.com/brigodier v0.0.1 go.minekube.com/common v0.0.6 go.minekube.com/connect v0.6.2 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 + go.opentelemetry.io/otel v1.34.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 + go.opentelemetry.io/otel/exporters/prometheus v0.56.0 + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.34.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0 + go.opentelemetry.io/otel/metric v1.34.0 + go.opentelemetry.io/otel/sdk v1.34.0 + go.opentelemetry.io/otel/sdk/metric v1.34.0 + go.opentelemetry.io/otel/trace v1.34.0 go.uber.org/atomic v1.11.0 go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f - golang.org/x/net v0.34.0 + golang.org/x/net v0.32.0 golang.org/x/sync v0.10.0 golang.org/x/text v0.21.0 - golang.org/x/time v0.9.0 - google.golang.org/grpc v1.70.0 - google.golang.org/protobuf v1.36.4 + golang.org/x/time v0.8.0 + google.golang.org/grpc v1.68.0 + google.golang.org/protobuf v1.36.3 gopkg.in/yaml.v3 v3.0.1 ) require ( buf.build/gen/go/minekube/connect/protocolbuffers/go v1.35.2-20240220124425-904ce30425c9.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/francoispqt/gojay v1.2.13 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/go-gl/mathgl v1.1.0 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/klauspost/compress v1.17.2 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.61.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.6.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/segmentio/fasthash v1.0.3 // indirect + github.com/sethvargo/go-envconfig v1.1.0 // indirect + github.com/shirou/gopsutil/v4 v4.24.6 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.8.0 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/host v0.53.0 // indirect + go.opentelemetry.io/contrib/instrumentation/runtime v0.53.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.28.0 // indirect + go.opentelemetry.io/contrib/propagators/ot v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/image v0.18.0 // indirect golang.org/x/sys v0.29.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) diff --git a/go.sum b/go.sum index 3b2f7a8e..9d08b3c1 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.31.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.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= -connectrpc.com/connect v1.17.0 h1:W0ZqMhtVzn9Zhn2yATuUokDLO5N+gIuBWMOnsQrfmZk= -connectrpc.com/connect v1.17.0/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= @@ -20,8 +18,14 @@ github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7l github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +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/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= @@ -40,6 +44,8 @@ github.com/edwingeng/deque/v2 v2.1.1/go.mod h1:HukI8CQe9KDmZCcURPZRYVYjH79Zy2tIj github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= @@ -57,10 +63,16 @@ github.com/go-faker/faker/v4 v4.5.0 h1:ARzAY2XoOL9tOUK+KSecUQzyXQsUaZHefjyF8x6YF github.com/go-faker/faker/v4 v4.5.0/go.mod h1:p3oq1GRjG2PZ7yqeFFfQI20Xm61DoBDlCA8RiSyZ48M= github.com/go-gl/mathgl v1.1.0 h1:0lzZ+rntPX3/oGrDzYGdowSLC2ky8Osirvf5uAwfIEA= github.com/go-gl/mathgl v1.1.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/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/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 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-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -82,7 +94,6 @@ github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+u github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= @@ -92,16 +103,20 @@ github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/Q github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/honeycombio/otel-config-go v1.17.0 h1:3/zig0L3IGnfgiCrEfAwBsM0rF57+TKTyJ/a8yqW2eM= +github.com/honeycombio/otel-config-go v1.17.0/go.mod h1:g2mMdfih4sYKfXBtz2mNGvo3HiQYqX4Up4pdA8JOF2s= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc= github.com/jellydator/ttlcache/v3 v3.3.0/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= -github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 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/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -112,9 +127,12 @@ github.com/kr/pty v1.1.3/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/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +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/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/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI= +github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= @@ -125,6 +143,8 @@ github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 h1:BpfhmL github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +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/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= @@ -138,14 +158,24 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE 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/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= +github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/robinbraemer/event v0.1.1 h1:1T7GturBzxsa8UUe/r3EmW9aHLErKBggfn43up5hOUA= -github.com/robinbraemer/event v0.1.1/go.mod h1:fKkjL2UbPajNcxc4oWYyRCcUalss0YtPxwMtZTuNo8o= -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/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/robinbraemer/event v0.0.1 h1:2499Bm1c13+//IZyAQpjoTg4vQ+dndE8trxo1aUxWdI= +github.com/robinbraemer/event v0.0.1/go.mod h1:fKkjL2UbPajNcxc4oWYyRCcUalss0YtPxwMtZTuNo8o= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= @@ -162,6 +192,14 @@ github.com/sandertv/gophertunnel v1.37.0/go.mod h1:4El8ZfEpUmOMIJhPt5SCc1PyLNiuQ github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sethvargo/go-envconfig v1.1.0 h1:cWZiJxeTm7AlCvzGXrEXaSTCNgip5oJepekh/BOQuog= +github.com/sethvargo/go-envconfig v1.1.0/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw= +github.com/shirou/gopsutil/v4 v4.24.6 h1:9qqCSYF2pgOU+t+NgJtp7Co5+5mHF/HyKBUckySQL64= +github.com/shirou/gopsutil/v4 v4.24.6/go.mod h1:aoebb2vxetJ/yIDZISmduFvVNPHqXQ9SEJwRXxkf0RA= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= @@ -205,6 +243,10 @@ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= +github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= @@ -213,17 +255,59 @@ github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHg github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +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/zyedidia/generic v1.2.1 h1:Zv5KS/N2m0XZZiuLS82qheRG4X1o5gsWreGb0hR7XDc= github.com/zyedidia/generic v1.2.1/go.mod h1:ly2RBz4mnz1yeuVbQA/VFwGjK3mnHGRj1JuoG336Bis= go.minekube.com/brigodier v0.0.1 h1:v5x+fZNefM24JIi+fYQjQcjZ8rwJbfRSpnnpw4b/x6k= go.minekube.com/brigodier v0.0.1/go.mod h1:WJf/lyJVTId/phiY6phPW6++qkTjCQ72rbOWqo4XIqc= -go.minekube.com/common v0.0.5 h1:h9EqMI3drSewTroBssy/eQniIP+Itirtj+av2PxyoP4= -go.minekube.com/common v0.0.5/go.mod h1:PCdSdTInlQv6ggDIbVjLFs7ehSRP4i9KqYsLAeeNUYU= go.minekube.com/common v0.0.6 h1:XA4mcgDG13hQEBcY2JdmK0Ca2mx2jOZ9M8pflZ85dkE= go.minekube.com/common v0.0.6/go.mod h1:RTT2cwrMS+hwGAjJOt06bWtbKx04MuiF0tScyvGeAZo= go.minekube.com/connect v0.6.2 h1:RajPc7YgqygcOviV2g4xetL3J0Wzi8b/lsYXUzIltxE= go.minekube.com/connect v0.6.2/go.mod h1:28k11I4RyzUfAL3AkOXt3atzjeOFAC8eqbCMwsYKAb0= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +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/detectors/aws/lambda v0.53.0 h1:KG6fOUk3EwSH1dEpsAbsLKFbn3cFwN9xDu8plGu55zI= +go.opentelemetry.io/contrib/detectors/aws/lambda v0.53.0/go.mod h1:bSd579exEkh/P5msRcom8YzVB6NsUxYKyV+D/FYOY7Y= +go.opentelemetry.io/contrib/instrumentation/host v0.53.0 h1:X4r+5n6bSqaQUbPlSO5baoM7tBvipkT0mJFyuPFnPAU= +go.opentelemetry.io/contrib/instrumentation/host v0.53.0/go.mod h1:NTaDj8VCnJxWleEcRQRQaN36+aCZjO9foNIdJunEjUQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= +go.opentelemetry.io/contrib/instrumentation/runtime v0.53.0 h1:nOlJEAJyrcy8hexK65M+dsCHIx7CVVbybcFDNkcTcAc= +go.opentelemetry.io/contrib/instrumentation/runtime v0.53.0/go.mod h1:u79lGGIlkg3Ryw425RbMjEkGYNxSnXRyR286O840+u4= +go.opentelemetry.io/contrib/propagators/b3 v1.28.0 h1:XR6CFQrQ/ttAYmTBX2loUEFGdk1h17pxYI8828dk/1Y= +go.opentelemetry.io/contrib/propagators/b3 v1.28.0/go.mod h1:DWRkzJONLquRz7OJPh2rRbZ7MugQj62rk7g6HRnEqh0= +go.opentelemetry.io/contrib/propagators/ot v1.28.0 h1:rmlG+2pc5k5M7Y7izDrxAHZUIwDERdGMTD9oMV7llMk= +go.opentelemetry.io/contrib/propagators/ot v1.28.0/go.mod h1:MNgXIn+UrMbNGpd7xyckyo2LCHIgCdmdjEE7YNZGG+w= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0 h1:aLmmtjRke7LPDQ3lvpFz+kNEH43faFhzW7v8BFIEydg= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0/go.mod h1:TC1pyCt6G9Sjb4bQpShH+P5R53pO6ZuGnHuuln9xMeE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk= +go.opentelemetry.io/otel/exporters/prometheus v0.56.0 h1:GnCIi0QyG0yy2MrJLzVrIM7laaJstj//flf1zEJCG+E= +go.opentelemetry.io/otel/exporters/prometheus v0.56.0/go.mod h1:JQcVZtbIIPM+7SWBB+T6FK+xunlyidwLp++fN0sUaOk= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.34.0 h1:czJDQwFrMbOr9Kk+BPo1y8WZIIFIK58SA1kykuVeiOU= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.34.0/go.mod h1:lT7bmsxOe58Tq+JIOkTQMCGXdu47oA+VJKLZHbaBKbs= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0 h1:jBpDk4HAUsrnVO1FsfCfCOTEc/MkInJmvfCHYLFiT80= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0/go.mod h1:H9LUIM1daaeZaz91vZcfeM0fejXPmgCYE8ZhzqfJuiU= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= 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/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -256,10 +340,6 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -276,8 +356,9 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/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-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -288,8 +369,6 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -306,36 +385,18 @@ google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4= google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f h1:C1QccEa9kUwvMgEUORqQD9S17QesQijxjZ84sO82mfo= google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= -google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= -google.golang.org/grpc v1.69.0 h1:quSiOM1GJPmPH5XtU+BCoVXcDVJJAzNcoyfC2cCjGkI= -google.golang.org/grpc v1.69.0/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= -google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= -google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= -google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= -google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= -google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= -google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/pkg/edition/java/auth/authenticator.go b/pkg/edition/java/auth/authenticator.go index a474f6eb..0fc15ab4 100644 --- a/pkg/edition/java/auth/authenticator.go +++ b/pkg/edition/java/auth/authenticator.go @@ -20,6 +20,10 @@ import ( "time" "github.com/go-logr/logr" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "go.minekube.com/gate/pkg/edition/java/profile" "go.minekube.com/gate/pkg/version" @@ -134,6 +138,7 @@ func New(options Options) (Authenticator, error) { Timeout: time.Second * 10, } } + cli.Transport = otelhttp.NewTransport(cli.Transport) cli.Transport = withHeader(cli.Transport, version.UserAgentHeader()) hasJoinedURLFn := options.HasJoinedURLFn @@ -181,7 +186,16 @@ func (a *authenticator) DecryptSharedSecret(encrypted []byte) (decrypted []byte, return rsa.DecryptPKCS1v15(rand.Reader, a.private, encrypted) } +var tracer = otel.Tracer("java/auth") + func (a *authenticator) AuthenticateJoin(ctx context.Context, serverID, username, ip string) (Response, error) { + ctx, span := tracer.Start(ctx, "AuthenticateJoin", trace.WithAttributes( + attribute.String("server.id", serverID), + attribute.String("user.name", username), + attribute.String("user.ip", ip), + )) + defer span.End() + hasJoinedURL := a.hasJoinedURLFn(serverID, username, ip) req, err := http.NewRequestWithContext(ctx, http.MethodGet, hasJoinedURL, nil) if err != nil { diff --git a/pkg/edition/java/netmc/connection.go b/pkg/edition/java/netmc/connection.go index a894b5c4..e90c5849 100644 --- a/pkg/edition/java/netmc/connection.go +++ b/pkg/edition/java/netmc/connection.go @@ -6,16 +6,22 @@ import ( "encoding/binary" "errors" "fmt" - "go.minekube.com/gate/pkg/edition/java/proto/state/states" - "go.minekube.com/gate/pkg/edition/java/proto/util/queue" "net" "sync" "time" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "go.minekube.com/gate/pkg/edition/java/proto/state/states" + "go.minekube.com/gate/pkg/edition/java/proto/util/queue" + "github.com/go-logr/logr" - "go.minekube.com/gate/pkg/edition/java/proxy/phase" "go.uber.org/atomic" + "go.minekube.com/gate/pkg/edition/java/proxy/phase" + "go.minekube.com/gate/pkg/edition/java/proto/packet" "go.minekube.com/gate/pkg/edition/java/proto/state" "go.minekube.com/gate/pkg/edition/java/proto/version" @@ -128,39 +134,46 @@ type SessionHandler interface { // NewMinecraftConn returns a new MinecraftConn and the func to start the blocking read-loop. func NewMinecraftConn( - ctx context.Context, - base net.Conn, - direction proto.Direction, - readTimeout time.Duration, - writeTimeout time.Duration, - compressionLevel int, +ctx context.Context, +base net.Conn, +direction proto.Direction, +readTimeout time.Duration, +writeTimeout time.Duration, +compressionLevel int, ) (conn MinecraftConn, startReadLoop func()) { - in := proto.ServerBound // reads from client are server bound (proxy <- client) - out := proto.ClientBound // writes to client are client bound (proxy -> client) - logName := "client" - if direction == proto.ClientBound { // if is a backend server connection - in = proto.ClientBound // reads from backend are client bound (proxy <- backend) - out = proto.ServerBound // writes to backend are server bound (proxy -> backend) - logName = "server" - } - - log := logr.FromContextOrDiscard(ctx).WithName(logName) - ctx = logr.NewContext(ctx, log) - - ctx, cancel := context.WithCancel(ctx) - c := &minecraftConn{ - log: log, - c: base, - ctx: ctx, - cancelCtx: cancel, - rd: NewReader(base, in, readTimeout, log), - wr: NewWriter(base, out, writeTimeout, compressionLevel, log), - state: state.Handshake, - protocol: version.Minecraft_1_7_2.Protocol, - connType: phase.Undetermined, - direction: direction, - autoReading: newStateControl(true), - } +in := proto.ServerBound // reads from client are server bound (proxy <- client) +out := proto.ClientBound // writes to client are client bound (proxy -> client) +logName := "client" +if direction == proto.ClientBound { // if is a backend server connection +in = proto.ClientBound // reads from backend are client bound (proxy <- backend) +out = proto.ServerBound // writes to backend are server bound (proxy -> backend) +logName = "server" +} + +log := logr.FromContextOrDiscard(ctx).WithName(logName) +ctx = logr.NewContext(ctx, log) + +ctx, cancel := context.WithCancel(ctx) + +// Create connection with atomic counter for bytes written +var bytesWritten atomic.Int64 +c := &minecraftConn{ +log: log, +c: base, +ctx: ctx, +cancelCtx: cancel, +rd: NewReader(base, in, readTimeout, log), +wr: NewWriter(base, out, writeTimeout, compressionLevel, log, func(n int) { +bytesWritten.Add(int64(n)) +}), +state: state.Handshake, +protocol: version.Minecraft_1_7_2.Protocol, +connType: phase.Undetermined, +direction: direction, +autoReading: newStateControl(true), +bytesWritten: &bytesWritten, +interceptor: NewTelemetryInterceptor(log), +} c.sessionHandlerMu.sessionHandlers = make(map[*state.Registry]SessionHandler) return c, c.startReadLoop } @@ -168,64 +181,95 @@ func NewMinecraftConn( // minecraftConn is a Minecraft connection. // It may be the connection of client -> proxy or proxy -> backend server. type minecraftConn struct { - c net.Conn // underlying connection - log logr.Logger // connections own logger - direction proto.Direction +c net.Conn // underlying connection +log logr.Logger // connections own logger +direction proto.Direction - rd Reader - wr Writer +rd Reader +wr Writer - autoReading *stateControl // Whether the connection should automatically read packets from the underlying connection. +autoReading *stateControl // Whether the connection should automatically read packets from the underlying connection. - ctx context.Context // is canceled when connection closed - cancelCtx context.CancelFunc - closeOnce sync.Once // Makes sure the connection is closed once, while blocking proceeding calls. - knownDisconnect atomic.Bool // Silences disconnect (any error is known) +ctx context.Context // is canceled when connection closed +cancelCtx context.CancelFunc +closeOnce sync.Once // Makes sure the connection is closed once, while blocking proceeding calls. +knownDisconnect atomic.Bool // Silences disconnect (any error is known) - protocol proto.Protocol // Client's protocol version. +protocol proto.Protocol // Client's protocol version. - mu sync.RWMutex // Protects following fields - state *state.Registry // Client state. - connType phase.ConnectionType // Connection type - playPacketQueue *queue.PlayPacketQueue +mu sync.RWMutex // Protects following fields +state *state.Registry // Client state. +connType phase.ConnectionType // Connection type +playPacketQueue *queue.PlayPacketQueue - sessionHandlerMu struct { - sync.RWMutex - activeSessionHandler SessionHandler // The current session handler. - sessionHandlers map[*state.Registry]SessionHandler // Session handlers by state. - } +sessionHandlerMu struct { +sync.RWMutex +activeSessionHandler SessionHandler // The current session handler. +sessionHandlers map[*state.Registry]SessionHandler // Session handlers by state. +} + +bytesWritten *atomic.Int64 // Total bytes written by this connection +interceptor PacketInterceptor // Packet interceptor for telemetry } // StartReadLoop is the main goroutine of this connection and // reads packets to pass them further to the current SessionHandler. // Close will be called on method return. func (c *minecraftConn) startReadLoop() { - // Make sure to close connection on return, if not already closed - defer func() { _ = c.closeKnown(false) }() +// Make sure to close connection on return, if not already closed +defer func() { _ = c.closeKnown(false) }() + +// Start connection lifetime span +ctx, span := otel.Tracer("netmc").Start(c.ctx, "MinecraftConnection", +trace.WithAttributes( +attribute.String("net.transport", "tcp"), +attribute.String("net.peer.ip", c.RemoteAddr().String()), +)) +defer span.End() +c.ctx = ctx + +bytesRead := 0 + +defer func() { +// Record total I/O metrics at connection end +span.SetAttributes( +attribute.Int("net.bytes.read", bytesRead), +attribute.Int64("net.bytes.written", c.bytesWritten.Load()), +) +}() - next := func() bool { - // Wait until auto reading is enabled, if not already - c.autoReading.Wait() +next := func() bool { +// Wait until auto reading is enabled, if not already +c.autoReading.Wait() - // Read next packet from underlying connection. - packetCtx, err := c.rd.ReadPacket() - if err != nil { - if errors.Is(err, ErrReadPacketRetry) { - // Sleep briefly and try again - time.Sleep(time.Millisecond * 5) - return true - } - return false - } +// Read next packet from underlying connection. +packetCtx, err := c.rd.ReadPacket() +if err != nil { +if errors.Is(err, ErrReadPacketRetry) { +// Sleep briefly and try again +time.Sleep(time.Millisecond * 5) +return true +} +return false +} + +sessionHandler := c.ActiveSessionHandler() - // TODO wrap packetCtx into struct with source info - // (minecraftConn) and chain into packet interceptor to... - // - packet interception - // - statistics / count bytes - // - in turn call session handler +// Wrap packet context with tracing +tracedCtx := &TracedPacketContext{ +PacketContext: packetCtx, +Conn: c, +Interceptor: c.interceptor, +} + +// Run through interceptor +if err := c.interceptor.InterceptPacket(c.ctx, packetCtx); err != nil { +c.log.Error(err, "failed to intercept packet") +return false +} - // Handle packet by connection's session handler. - c.ActiveSessionHandler().HandlePacket(packetCtx) +// Handle packet by connection's session handler using traced context +sessionHandler.HandlePacket(tracedCtx.PacketContext) return true } diff --git a/pkg/edition/java/netmc/interceptor.go b/pkg/edition/java/netmc/interceptor.go new file mode 100644 index 00000000..edaaebb2 --- /dev/null +++ b/pkg/edition/java/netmc/interceptor.go @@ -0,0 +1,74 @@ +package netmc + +import ( +"context" +"fmt" + +"github.com/davecgh/go-spew/spew" +"github.com/go-logr/logr" +"go.opentelemetry.io/otel" +"go.opentelemetry.io/otel/attribute" +"go.opentelemetry.io/otel/trace" +"go.minekube.com/gate/pkg/gate/proto" +) + +// PacketInterceptor intercepts packets for telemetry and statistics +type PacketInterceptor interface { + // InterceptPacket intercepts a packet before it's handled + InterceptPacket(ctx context.Context, pc *proto.PacketContext) error +} + +// TracedPacketContext wraps a packet context with source information +type TracedPacketContext struct { + *proto.PacketContext + Conn *minecraftConn // Source connection + Interceptor PacketInterceptor +} + +// telemetryInterceptor implements PacketInterceptor for OpenTelemetry +type telemetryInterceptor struct { + log logr.Logger + tracer trace.Tracer +} + +// NewTelemetryInterceptor creates a new telemetry interceptor +func NewTelemetryInterceptor(log logr.Logger) PacketInterceptor { + return &telemetryInterceptor{ + log: log, + tracer: otel.Tracer("netmc"), + } +} + +// InterceptPacket implements PacketInterceptor +func (t *telemetryInterceptor) InterceptPacket(ctx context.Context, pc *proto.PacketContext) error { + if pc == nil { + return nil + } + + // Create span for packet handling + ctx, span := t.tracer.Start(ctx, "HandlePacket", + trace.WithAttributes( + attribute.String("packet.id", pc.PacketID.String()), + attribute.Int("packet.size", pc.Size), + attribute.String("packet.direction", pc.Direction.String()), + )) + defer span.End() + + // Add packet type info and dump if known packet and debug enabled + if pc.KnownPacket() { + attrs := []attribute.KeyValue{ + attribute.String("packet.type", fmt.Sprintf("%T", pc.Packet)), + } + + // Add detailed packet dump in debug mode + if t.log.V(1).Enabled() { + attrs = append(attrs, + attribute.String("packet.dump", spew.Sdump(pc.Packet)), + ) + } + + span.SetAttributes(attrs...) + } + + return nil +} \ No newline at end of file diff --git a/pkg/edition/java/netmc/writer.go b/pkg/edition/java/netmc/writer.go index 09b78898..d5ea2247 100644 --- a/pkg/edition/java/netmc/writer.go +++ b/pkg/edition/java/netmc/writer.go @@ -12,39 +12,63 @@ import ( // Writer is a packet writer. type Writer interface { - // WritePacket writes a packet to the connection's write buffer. - WritePacket(packet proto.Packet) (n int, err error) - // Write encodes payload and writes it to the underlying writer. - // The payload must not already be compressed nor encrypted and must - // start with the packet's id VarInt and then the packet's data. - Write(payload []byte) (n int, err error) - // Flush flushes the connection's write buffer. - Flush() (err error) +// WritePacket writes a packet to the connection's write buffer. +WritePacket(packet proto.Packet) (n int, err error) +// Write encodes payload and writes it to the underlying writer. +// The payload must not already be compressed nor encrypted and must +// start with the packet's id VarInt and then the packet's data. +Write(payload []byte) (n int, err error) +// Flush flushes the connection's write buffer. +Flush() (err error) - StateChanger - Direction() proto.Direction +StateChanger +Direction() proto.Direction + +// UpdateBytesWritten updates the total bytes written counter. +// This is used internally for connection metrics. +UpdateBytesWritten(n int) } // NewWriter returns a new packet writer. -func NewWriter(conn net.Conn, direction proto.Direction, writeTimeout time.Duration, compressionLevel int, log logr.Logger) Writer { - writeBuf := bufio.NewWriter(conn) - return &writer{ - log: log.WithName("writer"), - writeTimeout: writeTimeout, - compressionLevel: compressionLevel, - c: conn, - writeBuf: writeBuf, - Encoder: codec.NewEncoder(writeBuf, direction, log.V(2)), - } +func NewWriter(conn net.Conn, direction proto.Direction, writeTimeout time.Duration, compressionLevel int, log logr.Logger, updateBytes func(int)) Writer { +writeBuf := bufio.NewWriter(conn) +return &writer{ +log: log.WithName("writer"), +writeTimeout: writeTimeout, +compressionLevel: compressionLevel, +c: conn, +writeBuf: writeBuf, +Encoder: codec.NewEncoder(writeBuf, direction, log.V(2)), +updateBytes: updateBytes, +} } type writer struct { - log logr.Logger - writeTimeout time.Duration - compressionLevel int - c net.Conn // underlying connection - writeBuf *bufio.Writer - *codec.Encoder +log logr.Logger +writeTimeout time.Duration +compressionLevel int +c net.Conn // underlying connection +writeBuf *bufio.Writer +*codec.Encoder +updateBytes func(int) // Callback to update connection's bytes written counter +} + +func (w *writer) UpdateBytesWritten(n int) { +if w.updateBytes != nil { +w.updateBytes(n) +} +} + +func (w *writer) Write(payload []byte) (n int, err error) { +n, err = w.Encoder.Write(payload) +w.UpdateBytesWritten(n) +return n, err +} + +func (w *writer) WritePacket(packet proto.Packet) (n int, err error) { +n, err = w.Encoder.WritePacket(packet) +w.UpdateBytesWritten(n) +return n, err } func (w *writer) Flush() (err error) { diff --git a/pkg/edition/java/proxy/otel.go b/pkg/edition/java/proxy/otel.go new file mode 100644 index 00000000..d60038c3 --- /dev/null +++ b/pkg/edition/java/proxy/otel.go @@ -0,0 +1,30 @@ +package proxy + +import ( + "context" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/metric" +) + +var ( + meter = otel.Meter("java/proxy") + tracer = otel.Tracer("java/proxy") +) + +func (p *Proxy) initMeter() error { + var err error + _, err = meter.Int64ObservableGauge( + "proxy.player_count", + metric.WithInt64Callback(func(ctx context.Context, o metric.Int64Observer) error { + o.Observe(int64(p.PlayerCount())) + return nil + }), + metric.WithDescription("The current total player count on the proxy"), + metric.WithUnit("1"), + ) + if err != nil { + return err + } + return nil +} diff --git a/pkg/edition/java/proxy/proxy.go b/pkg/edition/java/proxy/proxy.go index f0663966..23281af3 100644 --- a/pkg/edition/java/proxy/proxy.go +++ b/pkg/edition/java/proxy/proxy.go @@ -4,8 +4,6 @@ import ( "context" "errors" "fmt" - "go.minekube.com/gate/pkg/edition/java/lite" - "go.minekube.com/gate/pkg/edition/java/proto/state" "net" "reflect" "strings" @@ -13,11 +11,19 @@ import ( "sync/atomic" "time" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "go.minekube.com/gate/pkg/edition/java/lite" + "go.minekube.com/gate/pkg/edition/java/proto/state" + "github.com/go-logr/logr" "github.com/pires/go-proxyproto" "github.com/robinbraemer/event" "go.minekube.com/common/minecraft/component" "go.minekube.com/common/minecraft/component/codec/legacy" + "golang.org/x/sync/errgroup" + "go.minekube.com/gate/pkg/command" "go.minekube.com/gate/pkg/edition/java/auth" "go.minekube.com/gate/pkg/edition/java/config" @@ -31,7 +37,6 @@ import ( "go.minekube.com/gate/pkg/util/netutil" "go.minekube.com/gate/pkg/util/uuid" "go.minekube.com/gate/pkg/util/validation" - "golang.org/x/sync/errgroup" ) // Proxy is Gate's Java edition Minecraft proxy. @@ -109,6 +114,10 @@ func New(options Options) (p *Proxy, err error) { // Connection & login rate limiters p.initQuota(&options.Config.Quota) + if err = p.initMeter(); err != nil { + return nil, fmt.Errorf("error initializing meter: %w", err) + } + return p, nil } @@ -142,6 +151,9 @@ func (p *Proxy) Start(ctx context.Context) error { p.closeMu.Unlock() return ErrProxyAlreadyRun } + ctx, span := tracer.Start(ctx, "Proxy.Start") + defer span.End() + p.started = true now := time.Now() p.startTime.Store(&now) @@ -516,6 +528,21 @@ func (p *Proxy) HandleConn(raw net.Conn) { return } + // Create context for connection + ctx, ok := raw.(context.Context) + if !ok { + ctx = context.Background() + } + ctx = logr.NewContext(ctx, p.log) + ctx = trace.ContextWithSpan(ctx, trace.SpanFromContext(p.startCtx)) + + // OpenTelemetry span for connection + ctx, span := tracer.Start(ctx, "HandleConn", trace.WithAttributes( + attribute.String("remote.host", netutil.Host(raw.RemoteAddr())), + attribute.Stringer("local.addr", raw.LocalAddr()), + )) + defer span.End() + // Fire connection event if p.event.HasSubscriber((*ConnectionEvent)(nil)) { conn := &connwrap.Conn{Conn: raw} @@ -532,13 +559,6 @@ func (p *Proxy) HandleConn(raw net.Conn) { raw = e.Connection() } - // Create context for connection - ctx, ok := raw.(context.Context) - if !ok { - ctx = context.Background() - } - ctx = logr.NewContext(ctx, p.log) - // Create client connection conn, readLoop := netmc.NewMinecraftConn( ctx, raw, proto.ServerBound, diff --git a/pkg/edition/java/proxy/server.go b/pkg/edition/java/proxy/server.go index f1faa43c..73e9eb5c 100644 --- a/pkg/edition/java/proxy/server.go +++ b/pkg/edition/java/proxy/server.go @@ -5,22 +5,27 @@ import ( "encoding/json" "errors" "fmt" - "go.minekube.com/gate/pkg/edition/java/forge/modernforge" - "go.minekube.com/gate/pkg/edition/java/profile" - "go.minekube.com/gate/pkg/edition/java/proto/state/states" "net" "strings" "sync" "time" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "go.minekube.com/gate/pkg/edition/java/forge/modernforge" + "go.minekube.com/gate/pkg/edition/java/profile" + "go.minekube.com/gate/pkg/edition/java/proto/state/states" + "github.com/dboslee/lru" "github.com/go-logr/logr" + "go.uber.org/atomic" + "go.minekube.com/gate/pkg/edition/java/internal/protoutil" "go.minekube.com/gate/pkg/edition/java/netmc" "go.minekube.com/gate/pkg/edition/java/proto/version" "go.minekube.com/gate/pkg/edition/java/proxy/phase" "go.minekube.com/gate/pkg/gate/proto" - "go.uber.org/atomic" "go.minekube.com/gate/pkg/edition/java/config" "go.minekube.com/gate/pkg/edition/java/forge" @@ -311,6 +316,12 @@ type ServerDialer interface { } func (s *serverConnection) dial(ctx context.Context) (net.Conn, error) { + ctx, span := tracer.Start(ctx, "serverConnection.dial", trace.WithAttributes( + attribute.String("server.name", s.server.info.Name()), + attribute.Stringer("server.addr", s.server.info.Addr()), + )) + defer span.End() + var ( sd ServerDialer ok bool @@ -318,6 +329,7 @@ func (s *serverConnection) dial(ctx context.Context) (net.Conn, error) { if sd, ok = s.Server().ServerInfo().(ServerDialer); !ok { if sd, ok = s.Server().(ServerDialer); !ok { dstAddr := s.Server().ServerInfo().Addr() + span.SetAttributes(attribute.Stringer("server.addr", dstAddr)) var d net.Dialer conn, err := d.DialContext(ctx, "tcp", dstAddr.String()) if err != nil { diff --git a/pkg/gate/config/config.go b/pkg/gate/config/config.go index a985ace4..23b2aba2 100644 --- a/pkg/gate/config/config.go +++ b/pkg/gate/config/config.go @@ -32,6 +32,23 @@ var DefaultConfig = Config{ Enabled: false, Config: api.DefaultConfig, }, + Telemetry: Telemetry{ + Metrics: TelemetryMetrics{ + Enabled: true, + Endpoint: "http://localhost:4317", + AnonymousMetrics: true, + Exporter: "otlp", + Prometheus: struct { + Path string `yaml:"path" json:"path"` + }{Path: "/metrics"}, + }, + Tracing: TelemetryTracing{ + Enabled: true, + Endpoint: "http://localhost:4317", + Sampler: "always", + Exporter: "otlp", + }, + }, } // Config is the root configuration of Gate. @@ -47,6 +64,8 @@ type Config struct { Connect connect.Config `json:"connect,omitempty" yaml:"connect,omitempty"` // See API struct. API API `json:"api,omitempty" yaml:"api,omitempty"` + // Telemetry configuration for metrics and tracing + Telemetry Telemetry `json:"telemetry,omitempty" yaml:"telemetry,omitempty"` } // Editions provides Minecraft edition specific configs. @@ -82,6 +101,31 @@ type API struct { Config api.Config `json:"config,omitempty" yaml:"config,omitempty"` } +// Telemetry configuration for metrics and tracing +type Telemetry struct { + Metrics TelemetryMetrics `yaml:"metrics,omitempty" json:"metrics,omitempty"` + Tracing TelemetryTracing `yaml:"tracing,omitempty" json:"tracing,omitempty"` +} + +// TelemetryMetrics configures OpenTelemetry metrics collection +type TelemetryMetrics struct { + Enabled bool `yaml:"enabled" json:"enabled"` + Endpoint string `yaml:"endpoint" json:"endpoint"` + AnonymousMetrics bool `yaml:"anonymousMetrics" json:"anonymousMetrics"` + Exporter string `yaml:"exporter" json:"exporter"` // prometheus or otlp + Prometheus struct { + Path string `yaml:"path" json:"path"` + } `yaml:"prometheus,omitempty" json:"prometheus,omitempty"` +} + +// TelemetryTracing configures OpenTelemetry tracing collection +type TelemetryTracing struct { + Enabled bool `yaml:"enabled" json:"enabled"` + Endpoint string `yaml:"endpoint" json:"endpoint"` + Sampler string `yaml:"sampler" json:"sampler"` + Exporter string `yaml:"exporter" json:"exporter"` // otlp, jaeger, or stdout +} + // Validate validates a Config and all enabled edition configs (Java / Bedrock). func (c *Config) Validate() (warns []error, errs []error) { e := func(m string, args ...any) { errs = append(errs, fmt.Errorf(m, args...)) } @@ -96,6 +140,28 @@ func (c *Config) Validate() (warns []error, errs []error) { } } + // Validate telemetry settings + if c.Telemetry.Metrics.Enabled { + if c.Telemetry.Metrics.Endpoint == "" { + e("Telemetry metrics endpoint cannot be empty when metrics are enabled") + } + if c.Telemetry.Metrics.Exporter != "prometheus" && c.Telemetry.Metrics.Exporter != "otlp" { + e("Invalid telemetry metrics exporter %q: must be one of prometheus,otlp", c.Telemetry.Metrics.Exporter) + } + if c.Telemetry.Metrics.Exporter == "prometheus" && c.Telemetry.Metrics.Prometheus.Path == "" { + e("Prometheus metrics path cannot be empty when prometheus exporter is enabled") + } + } + + if c.Telemetry.Tracing.Enabled { + if c.Telemetry.Tracing.Endpoint == "" { + e("Telemetry tracing endpoint cannot be empty when tracing is enabled") + } + if c.Telemetry.Tracing.Exporter != "otlp" && c.Telemetry.Tracing.Exporter != "jaeger" && c.Telemetry.Tracing.Exporter != "stdout" { + e("Invalid telemetry tracing exporter %q: must be one of otlp,jaeger,stdout", c.Telemetry.Tracing.Exporter) + } + } + prefix := func(p string, errs []error) (pErrs []error) { for _, err := range errs { pErrs = append(pErrs, fmt.Errorf("%s: %w", p, err)) diff --git a/pkg/gate/gate.go b/pkg/gate/gate.go index 661128ea..43b9540e 100644 --- a/pkg/gate/gate.go +++ b/pkg/gate/gate.go @@ -14,6 +14,7 @@ import ( "github.com/go-logr/logr" "github.com/robinbraemer/event" "github.com/spf13/viper" + "go.opentelemetry.io/otel" "gopkg.in/yaml.v3" "go.minekube.com/gate/pkg/bridge" @@ -24,6 +25,7 @@ import ( "go.minekube.com/gate/pkg/gate/config" "go.minekube.com/gate/pkg/internal/reload" "go.minekube.com/gate/pkg/runtime/process" + "go.minekube.com/gate/pkg/telemetry" connectcfg "go.minekube.com/gate/pkg/util/connectutil/config" errorsutil "go.minekube.com/gate/pkg/util/errs" "go.minekube.com/gate/pkg/util/interrupt" @@ -71,6 +73,20 @@ func New(options Options) (gate *Gate, err error) { bridge: &bridge.Bridge{}, } + // Initialize telemetry if enabled + if options.Config.Telemetry.Metrics.Enabled || options.Config.Telemetry.Tracing.Enabled { + tel, cleanup, err := telemetry.New(context.Background(), options.Config) + if err != nil { + return nil, fmt.Errorf("failed to initialize telemetry: %w", err) + } + gate.tel = tel + gate.proc.Add(process.RunnableFunc(func(ctx context.Context) error { + <-ctx.Done() + cleanup() + return nil + })) + } + c := options.Config if c.Editions.Java.Enabled { gate.bridge.JavaProxy, err = jproxy.New(jproxy.Options{ @@ -118,6 +134,11 @@ func New(options Options) (gate *Gate, err error) { return nil, err } + // Instrument Java proxy with OpenTelemetry if enabled + if gate.Java() != nil && gate.tel != nil { + gate.tel.InstrumentProxy(gate.Java()) + } + return gate, nil } @@ -125,6 +146,7 @@ func New(options Options) (gate *Gate, err error) { type Gate struct { bridge *bridge.Bridge // The proxies. proc process.Collection // Parallel running proc. + tel *telemetry.Telemetry // Telemetry instance } // Java returns the Java edition proxy, or nil if none. @@ -138,7 +160,11 @@ func (g *Gate) Bedrock() *bproxy.Proxy { } // Start starts the Gate instance and all underlying proc. -func (g *Gate) Start(ctx context.Context) error { return g.proc.Start(ctx) } +func (g *Gate) Start(ctx context.Context) error { + ctx, span := otel.Tracer("gate").Start(ctx, "gate.Start") + defer span.End() + return g.proc.Start(ctx) +} // Viper is the default viper instance used by Start to load in a config.Config. var Viper = viper.New() @@ -240,6 +266,13 @@ func Start(ctx context.Context, opts ...StartOption) error { }() } + // Initialize OpenTelemetry with config + otelShutdown, err := telemetry.Init(ctx, c.conf) + if err != nil { + return fmt.Errorf("error initializing OpenTelemetry: %w", err) + } + defer otelShutdown() + // Setup auto config reload if enabled. err = setupAutoConfigReload( ctx, configLog, eventMgr, @@ -363,3 +396,4 @@ func fixedReadInConfig(v *viper.Viper, defaultConfig *config.Config) error { return v.ReadConfig(bytes.NewReader(b)) } + diff --git a/pkg/gate/proto/proto.go b/pkg/gate/proto/proto.go index 2d7030b2..99891f75 100644 --- a/pkg/gate/proto/proto.go +++ b/pkg/gate/proto/proto.go @@ -65,6 +65,9 @@ type PacketContext struct { // It contains the actual received payload (maybe longer than what the Packet's Decode read). // This can be used to skip encoding Packet. Payload []byte // Empty when encoding. + + // Size represents the total number of bytes before decompression + Size int // Total bytes before decompression } // KnownPacket indicated whether the PacketID is known in the connection's current state.ProtocolRegistry. diff --git a/pkg/internal/otelutil/otel.go b/pkg/internal/otelutil/otel.go new file mode 100644 index 00000000..42937bec --- /dev/null +++ b/pkg/internal/otelutil/otel.go @@ -0,0 +1,34 @@ +// Package otelutil provides OpenTelemetry utilities for Gate +package otelutil + +import ( + "context" + + "github.com/honeycombio/otel-config-go/otelconfig" + "go.minekube.com/gate/pkg/gate/config" + "go.minekube.com/gate/pkg/telemetry" +) + +// Init initializes OpenTelemetry with configuration from environment variables and config +func Init(ctx context.Context, cfg *config.Config) (func(), error) { + // Initialize using honeycomb's otelconfig (config validation will handle defaults) + shutdown, err := otelconfig.ConfigureOpenTelemetry( + otelconfig.WithServiceName("gate"), + otelconfig.WithServiceVersion(telemetry.Version), + ) + if err != nil { + return nil, err + } + + // Create telemetry instance + _, cleanup, err := telemetry.New(ctx, cfg) + if err != nil { + shutdown() + return nil, err + } + + return func() { + cleanup() + shutdown() + }, nil +} diff --git a/pkg/telemetry/init.go b/pkg/telemetry/init.go new file mode 100644 index 00000000..9682f597 --- /dev/null +++ b/pkg/telemetry/init.go @@ -0,0 +1,18 @@ +package telemetry + +import ( + "context" + + "go.minekube.com/gate/pkg/gate/config" +) + +// Init initializes OpenTelemetry with configuration from environment variables and config. +// It returns a cleanup function and any error encountered. +func Init(ctx context.Context, cfg *config.Config) (func(), error) { + // Create new telemetry instance with validated config + _, cleanup, err := New(ctx, cfg) + if err != nil { + return nil, err + } + return cleanup, nil +} \ No newline at end of file diff --git a/pkg/telemetry/instrument.go b/pkg/telemetry/instrument.go new file mode 100644 index 00000000..2e67c8d4 --- /dev/null +++ b/pkg/telemetry/instrument.go @@ -0,0 +1,100 @@ +package telemetry + +import ( + "context" + "fmt" + + "github.com/robinbraemer/event" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "go.minekube.com/gate/pkg/edition/java/proxy" +) + +// ProxyServer represents a common interface for proxy servers +type ProxyServer interface { + Event() event.Manager +} + +// InstrumentProxy adds OpenTelemetry instrumentation to key proxy functions +func (t *Telemetry) InstrumentProxy(p ProxyServer) { + if p == nil { + return + } + + // Subscribe to events + t.subscribeEvents(p) +} + +func (t *Telemetry) subscribeEvents(p ProxyServer) { + eventMgr := p.Event() + if eventMgr == nil { + return + } + + // Track player login + eventMgr.Subscribe(&proxy.LoginEvent{}, 0, event.HandlerFunc(func(e event.Event) { + loginEvent := e.(*proxy.LoginEvent) + _, span := t.tracer.Start(context.Background(), "player.Login", + trace.WithAttributes( + attribute.String("username", loginEvent.Player().Username()), + attribute.String("uuid", loginEvent.Player().ID().String()), + attribute.Bool("online_mode", loginEvent.Player().OnlineMode()), + )) + defer span.End() + + // Record metric + t.RecordPlayerConnection(context.Background(), loginEvent.Player().Username()) + t.UpdatePlayerCount(context.Background(), 1) + })) + + // Track player disconnect + eventMgr.Subscribe(&proxy.DisconnectEvent{}, 0, event.HandlerFunc(func(e event.Event) { + disconnectEvent := e.(*proxy.DisconnectEvent) + _, span := t.tracer.Start(context.Background(), "player.Disconnect", + trace.WithAttributes( + attribute.String("username", disconnectEvent.Player().Username()), + attribute.String("uuid", disconnectEvent.Player().ID().String()), + )) + defer span.End() + + // Record metrics + t.UpdatePlayerCount(context.Background(), -1) + })) + + // Track server connections + eventMgr.Subscribe(&proxy.ServerPreConnectEvent{}, 0, event.HandlerFunc(func(e event.Event) { + serverEvent := e.(*proxy.ServerPreConnectEvent) + if serverEvent.Server() == nil { + return + } + _, span := t.tracer.Start(context.Background(), "player.ServerConnect", + trace.WithAttributes( + attribute.String("username", serverEvent.Player().Username()), + attribute.String("server", serverEvent.Server().ServerInfo().Name()), + )) + defer span.End() + })) + + // Track command executions + eventMgr.Subscribe(&proxy.CommandExecuteEvent{}, 0, event.HandlerFunc(func(e event.Event) { + cmdEvent := e.(*proxy.CommandExecuteEvent) + _, span := t.tracer.Start(context.Background(), "command.Execute", + trace.WithAttributes( + attribute.String("command", cmdEvent.Command()), + attribute.String("source", fmt.Sprintf("%T", cmdEvent.Source())), + )) + defer span.End() + })) + + // Track plugin messages + eventMgr.Subscribe(&proxy.PluginMessageEvent{}, 0, event.HandlerFunc(func(e event.Event) { + pluginEvent := e.(*proxy.PluginMessageEvent) + _, span := t.tracer.Start(context.Background(), "plugin.Message", + trace.WithAttributes( + attribute.String("identifier", fmt.Sprintf("%v", pluginEvent.Identifier())), + attribute.Int("data_length", len(pluginEvent.Data())), + attribute.String("source", fmt.Sprintf("%T", pluginEvent.Source())), + )) + defer span.End() + })) +} \ No newline at end of file diff --git a/pkg/telemetry/instrument_test.go b/pkg/telemetry/instrument_test.go new file mode 100644 index 00000000..8a2c7dce --- /dev/null +++ b/pkg/telemetry/instrument_test.go @@ -0,0 +1,600 @@ +package telemetry + +import ( + "context" + "net" + "testing" + "time" + + "github.com/robinbraemer/event" + "github.com/stretchr/testify/assert" + "go.minekube.com/common/minecraft/component" + "go.minekube.com/gate/pkg/command" + "go.minekube.com/gate/pkg/gate/config" + "go.minekube.com/gate/pkg/edition/java/profile" + "go.minekube.com/gate/pkg/edition/java/proxy" + "go.minekube.com/gate/pkg/edition/java/proxy/crypto" + "go.minekube.com/gate/pkg/edition/java/proxy/message" + "go.minekube.com/gate/pkg/edition/java/proxy/player" + "go.minekube.com/gate/pkg/edition/java/proxy/tablist" + "go.minekube.com/gate/pkg/gate/proto" + "go.minekube.com/gate/pkg/util/permission" + "go.minekube.com/gate/pkg/util/uuid" +) + +// Mock implementations +type simpleEventMgr struct { + handlers map[string][]event.HandlerFunc +} + +func newSimpleEventMgr() *simpleEventMgr { + return &simpleEventMgr{ + handlers: make(map[string][]event.HandlerFunc), + } +} + +func getEventType(e event.Event) string { + switch e.(type) { + case *proxy.LoginEvent: + return "login" + case *proxy.DisconnectEvent: + return "disconnect" + case *proxy.ServerPreConnectEvent: + return "serverConnect" + case *proxy.CommandExecuteEvent: + return "command" + case *proxy.PluginMessageEvent: + return "pluginMessage" + default: + return "unknown" + } +} + +func (m *simpleEventMgr) Subscribe(e event.Event, _ int, handler event.HandlerFunc) func() { + eventType := getEventType(e) + m.handlers[eventType] = append(m.handlers[eventType], handler) + return func() {} +} + +func (m *simpleEventMgr) Fire(e event.Event) { + eventType := getEventType(e) + for _, handler := range m.handlers[eventType] { + handler(e) + } +} + +// Additional methods to satisfy event.Manager interface +func (m *simpleEventMgr) SubscribeFn(eventType event.Type, fn func(e event.Event) error) {} +func (m *simpleEventMgr) Unsubscribe(listener interface{}) {} +func (m *simpleEventMgr) UnsubscribeAll(events ...event.Event) int { return 0 } +func (m *simpleEventMgr) HasSubscriber(events ...event.Event) bool { return false } +func (m *simpleEventMgr) FireParallel(e event.Event, handlers ...event.HandlerFunc) { m.Fire(e) } +func (m *simpleEventMgr) FireAsync(e event.Event) { m.Fire(e) } +func (m *simpleEventMgr) FireAsyncParallel(e event.Event, handlers ...event.HandlerFunc) { m.Fire(e) } +func (m *simpleEventMgr) Wait(events ...event.Event) {} + +type simpleProxy struct { + eventMgr event.Manager +} + +func (p *simpleProxy) Event() event.Manager { + return p.eventMgr +} + +// Mock implementation of tablist.TabList +type mockTabList struct{} + +func (m *mockTabList) HeaderFooter() (header, footer component.Component) { return nil, nil } +func (m *mockTabList) SetHeaderFooter(header, footer component.Component) error { return nil } +func (m *mockTabList) ClearHeaderFooter() {} +func (m *mockTabList) Add(entries ...tablist.Entry) error { return nil } +func (m *mockTabList) Entries() map[uuid.UUID]tablist.Entry { return nil } +func (m *mockTabList) RemoveAll(ids ...uuid.UUID) error { return nil } + +// Test event implementations +type testPlayer struct { + username string + id uuid.UUID + onlineMode bool + gameProfile profile.GameProfile + tabList tablist.TabList +} + +func (p *testPlayer) Username() string { return p.username } +func (p *testPlayer) ID() uuid.UUID { return p.id } +func (p *testPlayer) OnlineMode() bool { return p.onlineMode } +func (p *testPlayer) Active() bool { return true } +func (p *testPlayer) Protocol() proto.Protocol { return 0 } +func (p *testPlayer) WritePacket(packet proto.Packet) error { return nil } +func (p *testPlayer) BufferPacket(packet proto.Packet) error { return nil } +func (p *testPlayer) BufferPayload(payload []byte) error { return nil } +func (p *testPlayer) Flush() error { return nil } +func (p *testPlayer) SendMessage(msg component.Component, opts ...command.MessageOption) error { return nil } +func (p *testPlayer) SendActionBar(msg component.Component) error { return nil } +func (p *testPlayer) SendPluginMessage(identifier message.ChannelIdentifier, data []byte) error { return nil } +func (p *testPlayer) HasPermission(permission string) bool { return true } +func (p *testPlayer) PermissionValue(perm string) permission.TriState { return permission.Undefined } +func (p *testPlayer) CurrentServer() proxy.ServerConnection { return nil } +func (p *testPlayer) AppliedResourcePack() *proxy.ResourcePackInfo { return nil } +func (p *testPlayer) PendingResourcePack() *proxy.ResourcePackInfo { return nil } +func (p *testPlayer) AppliedResourcePacks() []*proxy.ResourcePackInfo { return nil } +func (p *testPlayer) PendingResourcePacks() []*proxy.ResourcePackInfo { return nil } +func (p *testPlayer) SendResourcePack(info proxy.ResourcePackInfo) error { return nil } +func (p *testPlayer) TransferToHost(addr string) error { return nil } +func (p *testPlayer) ClientBrand() string { return "" } +func (p *testPlayer) Context() context.Context { return context.Background() } +func (p *testPlayer) CreateConnectionRequest(target proxy.RegisteredServer) proxy.ConnectionRequest { return nil } +func (p *testPlayer) Disconnect(reason component.Component) {} +func (p *testPlayer) GameProfile() profile.GameProfile { return p.gameProfile } +func (p *testPlayer) IdentifiedKey() crypto.IdentifiedKey { return nil } +func (p *testPlayer) Ping() time.Duration { return 100 * time.Millisecond } +func (p *testPlayer) RemoteAddr() net.Addr { return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 12345} } +func (p *testPlayer) Settings() player.Settings { return player.DefaultSettings } +func (p *testPlayer) SpoofChatInput(input string) error { return nil } +func (p *testPlayer) TabList() tablist.TabList { return &mockTabList{} } +func (p *testPlayer) VirtualHost() net.Addr { return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 25565} } +func (p *testPlayer) Write(b []byte) error { return nil } + +type testLoginEvent struct { + player testPlayer +} + +func (e *testLoginEvent) Player() proxy.Player { + return &e.player +} + +type testDisconnectEvent struct { + player testPlayer +} + +func (e *testDisconnectEvent) Player() proxy.Player { + return &e.player +} + +type testServerInfo struct { + name string +} + +func (s *testServerInfo) Name() string { return s.name } +func (s *testServerInfo) Addr() net.Addr { return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 25565} } + +type testServer struct { + info testServerInfo +} + +func (s *testServer) ServerInfo() proxy.ServerInfo { + return &s.info +} + +func (s *testServer) Players() proxy.Players { + return nil +} + +type testServerPreConnectEvent struct { + player testPlayer + server testServer +} + +func (e *testServerPreConnectEvent) Player() proxy.Player { + return &e.player +} + +func (e *testServerPreConnectEvent) Server() proxy.RegisteredServer { + return &e.server +} + +type testCommandSource struct{} + +type testCommandExecuteEvent struct { + source testCommandSource + commandLine string +} + +func (e *testCommandExecuteEvent) Source() any { + return e.source +} + +func (e *testCommandExecuteEvent) Command() string { + return e.commandLine +} + +// Mock implementation of message.ChannelIdentifier +type testChannelIdentifier string + +func (t testChannelIdentifier) ID() string { + return string(t) +} + +type testPluginMessageEvent struct { + source testCommandSource + identifier message.ChannelIdentifier + data []byte +} + +func (e *testPluginMessageEvent) Source() any { + return e.source +} + +func (e *testPluginMessageEvent) Identifier() message.ChannelIdentifier { + return e.identifier +} + +func (e *testPluginMessageEvent) Data() []byte { + return e.data +} + +func TestInstrumentProxyTelemetry(t *testing.T) { + // Initialize telemetry with explicit configuration and stdout tracer + cfg := &config.Config{ + Telemetry: config.Telemetry{ + Metrics: config.TelemetryMetrics{ + Endpoint: "localhost:9464", + Prometheus: struct { + Path string `yaml:"path" json:"path"` + }{ + Path: "/metrics", + }, + }, + Tracing: config.TelemetryTracing{ + Enabled: true, + Exporter: "stdout", + Endpoint: "localhost:4317", + }, + }, + } + + // Create new telemetry instance + tel, cleanup, err := New(context.Background(), cfg) + assert.NoError(t, err) + defer cleanup() + + // Setup a simple proxy with event manager + eventMgr := newSimpleEventMgr() + p := &simpleProxy{eventMgr: eventMgr} + + // Instrument the proxy + tel.InstrumentProxy(p) + + // Test login event + loginEvent := &testLoginEvent{ + player: testPlayer{ + username: "testUser", + id: uuid.New(), + onlineMode: true, + }, + } + eventMgr.Fire(loginEvent) + + // Test disconnect event + disconnectEvent := &testDisconnectEvent{ + player: testPlayer{ + username: "testUser", + id: uuid.New(), + }, + } + eventMgr.Fire(disconnectEvent) + + // Test server connect event + serverEvent := &testServerPreConnectEvent{ + player: testPlayer{ + username: "testUser", + id: uuid.New(), + }, + server: testServer{ + info: testServerInfo{ + name: "testServer", + }, + }, + } + eventMgr.Fire(serverEvent) + + // Test command execute event + cmdEvent := &testCommandExecuteEvent{ + source: testCommandSource{}, + commandLine: "/test command", + } + eventMgr.Fire(cmdEvent) + + // Test plugin message event + pluginEvent := &testPluginMessageEvent{ + source: testCommandSource{}, + identifier: testChannelIdentifier("test:channel"), + data: []byte("test data"), + } + eventMgr.Fire(pluginEvent) +} + +func TestInstrumentProxyNil(t *testing.T) { + tel, cleanup, err := New(context.Background(), &config.Config{ + Telemetry: config.Telemetry{ + Metrics: config.TelemetryMetrics{ + Endpoint: "localhost:9464", + Prometheus: struct { + Path string `yaml:"path" json:"path"` + }{ + Path: "/metrics", + }, + }, + Tracing: config.TelemetryTracing{ + Endpoint: "localhost:4317", + }, + }, + }) + assert.NoError(t, err) + defer cleanup() + tel.InstrumentProxy(nil) // Should not panic +} + +func TestInstrumentProxyNilEventManager(t *testing.T) { + tel, cleanup, err := New(context.Background(), &config.Config{ + Telemetry: config.Telemetry{ + Metrics: config.TelemetryMetrics{ + Endpoint: "localhost:9464", + Prometheus: struct { + Path string `yaml:"path" json:"path"` + }{ + Path: "/metrics", + }, + }, + Tracing: config.TelemetryTracing{ + Endpoint: "localhost:4317", + }, + }, + }) + assert.NoError(t, err) + defer cleanup() + tel.InstrumentProxy(&simpleProxy{eventMgr: nil}) // Should not panic +} + +// TestTelemetryConfigRespected verifies that user-provided telemetry configurations +// are respected and not overwritten by defaults +func TestTelemetryConfigRespected(t *testing.T) { + // Create a config with custom telemetry settings + customConfig := &config.Config{ + Telemetry: config.Telemetry{ + Metrics: config.TelemetryMetrics{ + Enabled: true, + Endpoint: "http://localhost:9999", // Custom endpoint + Exporter: "prometheus", // Custom exporter + AnonymousMetrics: false, // Custom setting + Prometheus: struct { + Path string `yaml:"path" json:"path"` + }{ + Path: "/custom-metrics", // Custom metrics path + }, + }, + Tracing: config.TelemetryTracing{ + Enabled: true, + Endpoint: "localhost:4318", // Custom endpoint + Exporter: "otlp", // Custom exporter + Sampler: "parentbased_traceidratio", // Custom sampler + }, + }, + } + + // Create new telemetry instance with custom config + tel, cleanup, err := New(context.Background(), customConfig) + assert.NoError(t, err) + defer cleanup() + + // Verify the telemetry instance uses our custom config + assert.Equal(t, "http://localhost:9999", customConfig.Telemetry.Metrics.Endpoint, "Metrics endpoint should remain unchanged") + assert.Equal(t, "prometheus", customConfig.Telemetry.Metrics.Exporter, "Metrics exporter should remain unchanged") + assert.Equal(t, false, customConfig.Telemetry.Metrics.AnonymousMetrics, "Anonymous metrics setting should remain unchanged") + assert.Equal(t, "/custom-metrics", customConfig.Telemetry.Metrics.Prometheus.Path, "Prometheus metrics path should remain unchanged") + assert.Equal(t, "otlp", customConfig.Telemetry.Tracing.Exporter, "Tracing exporter should remain unchanged") + assert.Equal(t, "localhost:4318", customConfig.Telemetry.Tracing.Endpoint, "Tracing endpoint should remain unchanged") + assert.Equal(t, "parentbased_traceidratio", customConfig.Telemetry.Tracing.Sampler, "Tracing sampler should remain unchanged") + + // Setup a simple proxy and verify instrumentation works with custom config + eventMgr := newSimpleEventMgr() + p := &simpleProxy{eventMgr: eventMgr} + tel.InstrumentProxy(p) + + // Test an event to verify instrumentation works + loginEvent := &testLoginEvent{ + player: testPlayer{ + username: "testUser", + id: uuid.New(), + onlineMode: true, + }, + } + eventMgr.Fire(loginEvent) // Should not panic or modify config +} + +func TestTelemetryValidation(t *testing.T) { + tests := []struct { + name string + config *config.Config + wantErr bool + errMsg string + }{ + { + name: "valid configuration", + config: &config.Config{ + Telemetry: config.Telemetry{ + Metrics: config.TelemetryMetrics{ + Enabled: true, + Endpoint: "localhost:9464", + Exporter: "prometheus", + Prometheus: struct { + Path string `yaml:"path" json:"path"` + }{ + Path: "/metrics", + }, + }, + Tracing: config.TelemetryTracing{ + Enabled: true, + Endpoint: "localhost:4317", + Exporter: "otlp", + Sampler: "always", + }, + }, + }, + wantErr: false, + }, + { + name: "missing metrics endpoint when enabled", + config: &config.Config{ + Telemetry: config.Telemetry{ + Metrics: config.TelemetryMetrics{ + Enabled: true, + Endpoint: "", // Missing endpoint + Exporter: "prometheus", + }, + }, + }, + wantErr: true, + errMsg: "Telemetry metrics endpoint cannot be empty", + }, + { + name: "invalid metrics exporter", + config: &config.Config{ + Telemetry: config.Telemetry{ + Metrics: config.TelemetryMetrics{ + Enabled: true, + Endpoint: "localhost:9464", + Exporter: "invalid", + }, + }, + }, + wantErr: true, + errMsg: "Invalid telemetry metrics exporter", + }, + { + name: "missing prometheus path", + config: &config.Config{ + Telemetry: config.Telemetry{ + Metrics: config.TelemetryMetrics{ + Enabled: true, + Endpoint: "localhost:9464", + Exporter: "prometheus", + Prometheus: struct { + Path string `yaml:"path" json:"path"` + }{}, + }, + }, + }, + wantErr: true, + errMsg: "Prometheus metrics path cannot be empty", + }, + { + name: "missing tracing endpoint when enabled", + config: &config.Config{ + Telemetry: config.Telemetry{ + Tracing: config.TelemetryTracing{ + Enabled: true, + Endpoint: "", + Exporter: "otlp", + }, + }, + }, + wantErr: true, + errMsg: "Telemetry tracing endpoint cannot be empty", + }, + { + name: "invalid tracing exporter", + config: &config.Config{ + Telemetry: config.Telemetry{ + Tracing: config.TelemetryTracing{ + Enabled: true, + Endpoint: "localhost:4317", + Exporter: "invalid", + }, + }, + }, + wantErr: true, + errMsg: "Invalid telemetry tracing exporter", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, errs := tt.config.Validate() + if tt.wantErr { + assert.NotEmpty(t, errs, "Expected validation error") + assert.Contains(t, errs[0].Error(), tt.errMsg) + } else { + assert.Empty(t, errs, "No errors expected") + } + }) + } +} + +// TestTelemetryInitialization verifies telemetry is properly initialized and can be retrieved +func TestTelemetryInitialization(t *testing.T) { + // Create config with both metrics and tracing enabled + cfg := &config.Config{ + Telemetry: config.Telemetry{ + Metrics: config.TelemetryMetrics{ + Enabled: true, + Endpoint: "localhost:9464", + Exporter: "prometheus", + Prometheus: struct { + Path string `yaml:"path" json:"path"` + }{ + Path: "/metrics", + }, + }, + Tracing: config.TelemetryTracing{ + Enabled: true, + Endpoint: "localhost:4317", + Exporter: "otlp", + Sampler: "always", + }, + }, + } + + // Initialize telemetry + tel, cleanup, err := New(context.Background(), cfg) + assert.NoError(t, err) + defer cleanup() + + // Verify telemetry instance is created and properly configured + assert.NotNil(t, tel, "Telemetry instance should be created") + + // Test with metrics only + metricsCfg := &config.Config{ + Telemetry: config.Telemetry{ + Metrics: config.TelemetryMetrics{ + Enabled: true, + Endpoint: "localhost:9464", + Exporter: "prometheus", + Prometheus: struct { + Path string `yaml:"path" json:"path"` + }{ + Path: "/metrics", + }, + }, + }, + } + metricsTel, metricsCleanup, err := New(context.Background(), metricsCfg) + assert.NoError(t, err) + defer metricsCleanup() + assert.NotNil(t, metricsTel, "Telemetry instance should be created with metrics only") + + // Test with tracing only + tracingCfg := &config.Config{ + Telemetry: config.Telemetry{ + Tracing: config.TelemetryTracing{ + Enabled: true, + Endpoint: "localhost:4317", + Exporter: "otlp", + Sampler: "always", + }, + }, + } + tracingTel, tracingCleanup, err := New(context.Background(), tracingCfg) + assert.NoError(t, err) + defer tracingCleanup() + assert.NotNil(t, tracingTel, "Telemetry instance should be created with tracing only") + + // Test with neither metrics nor tracing + emptyCfg := &config.Config{ + Telemetry: config.Telemetry{}, + } + emptyTel, emptyCleanup, err := New(context.Background(), emptyCfg) + assert.NoError(t, err) + defer emptyCleanup() + assert.NotNil(t, emptyTel, "Telemetry instance should be created even with empty config") +} \ No newline at end of file diff --git a/pkg/telemetry/options.go b/pkg/telemetry/options.go new file mode 100644 index 00000000..64312e08 --- /dev/null +++ b/pkg/telemetry/options.go @@ -0,0 +1,9 @@ +package telemetry + +import "github.com/robinbraemer/event" + +// Options contains configuration options for telemetry initialization. +type Options struct { + // EventMgr is the optional event manager to use. + EventMgr event.Manager +} \ No newline at end of file diff --git a/pkg/telemetry/server.go b/pkg/telemetry/server.go new file mode 100644 index 00000000..5817a91a --- /dev/null +++ b/pkg/telemetry/server.go @@ -0,0 +1,88 @@ +package telemetry + +import ( + "context" + "fmt" + "net" + + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.20.0" + "go.opentelemetry.io/otel/trace" + "go.minekube.com/gate/pkg/edition/java/proxy" +) + +// TracedServerConnection wraps a proxy server connection with tracing +type TracedServerConnection struct { + proxy.ServerConnection + tracer trace.Tracer +} + +// WithServerConnectionTracing wraps server connection functions with tracing +func (t *Telemetry) WithServerConnectionTracing(s proxy.ServerConnection) proxy.ServerConnection { + if s == nil { + return nil + } + + ts := &TracedServerConnection{ + ServerConnection: s, + tracer: t.tracer, + } + + // Start connection span + ctx := context.Background() + _, span := ts.tracer.Start(ctx, "server.connection", + trace.WithAttributes( + semconv.PeerServiceKey.String("minecraft-server"), + attribute.String("connection_type", "server"), + )) + defer span.End() + + return ts +} + +// WithConnectionTracing wraps a net.Conn with OpenTelemetry tracing +func (t *Telemetry) WithConnectionTracing(conn net.Conn, name string) net.Conn { + if conn == nil { + return nil + } + return &tracedConn{ + Conn: conn, + name: name, + tracer: t.tracer, + } +} + +type tracedConn struct { + net.Conn + name string + tracer trace.Tracer +} + +func (t *tracedConn) Read(b []byte) (n int, err error) { + _, span := t.tracer.Start(context.Background(), fmt.Sprintf("%s.read", t.name)) + defer span.End() + + n, err = t.Conn.Read(b) + span.SetAttributes( + attribute.Int("bytes_read", n), + ) + return n, err +} + +func (t *tracedConn) Write(b []byte) (n int, err error) { + _, span := t.tracer.Start(context.Background(), fmt.Sprintf("%s.write", t.name)) + defer span.End() + + n, err = t.Conn.Write(b) + span.SetAttributes( + attribute.Int("bytes_written", n), + ) + return n, err +} + +func (t *tracedConn) Close() error { + _, span := t.tracer.Start(context.Background(), fmt.Sprintf("%s.close", t.name)) + defer span.End() + + return t.Conn.Close() +} \ No newline at end of file diff --git a/pkg/telemetry/server_test.go b/pkg/telemetry/server_test.go new file mode 100644 index 00000000..80c668ed --- /dev/null +++ b/pkg/telemetry/server_test.go @@ -0,0 +1,157 @@ +package telemetry + +import ( + "context" + "net" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.minekube.com/gate/pkg/gate/config" +) + +func TestTracedConnection(t *testing.T) { + // Create a real TCP connection for testing + listener, err := net.Listen("tcp", "localhost:0") + if err != nil { + t.Fatal(err) + } + defer listener.Close() + + // Accept connections in a goroutine + go func() { + conn, err := listener.Accept() + if err != nil { + return + } + defer conn.Close() + + // Echo server + buf := make([]byte, 1024) + for { + n, err := conn.Read(buf) + if err != nil { + return + } + conn.Write(buf[:n]) + } + }() + + // Connect to the server + conn, err := net.Dial("tcp", listener.Addr().String()) + if err != nil { + t.Fatal(err) + } + + // Initialize telemetry with explicit configuration + cfg := &config.Config{ + Telemetry: config.Telemetry{ + Metrics: config.TelemetryMetrics{ + Endpoint: "localhost:9464", + Prometheus: struct { + Path string `yaml:"path" json:"path"` + }{ + Path: "/metrics", + }, + }, + Tracing: config.TelemetryTracing{ + Enabled: true, + Exporter: "stdout", + Endpoint: "localhost:4317", + }, + }, + } + + // Create new telemetry instance + tel, cleanup, err := New(context.Background(), cfg) + assert.NoError(t, err) + defer cleanup() + + // Wrap with tracing + tracedConn := tel.WithConnectionTracing(conn, "test-connection") + + t.Run("read operation", func(t *testing.T) { + // Write some data + testData := []byte("hello") + _, err := tracedConn.Write(testData) + assert.NoError(t, err) + + // Read it back + buf := make([]byte, 1024) + n, err := tracedConn.Read(buf) + assert.NoError(t, err) + assert.Equal(t, testData, buf[:n]) + }) + + t.Run("write operation", func(t *testing.T) { + testData := []byte("world") + n, err := tracedConn.Write(testData) + assert.NoError(t, err) + assert.Equal(t, len(testData), n) + }) + + t.Run("close operation", func(t *testing.T) { + err := tracedConn.Close() + assert.NoError(t, err) + }) +} + +func TestTracedConnectionErrors(t *testing.T) { + tel, cleanup, err := New(context.Background(), &config.Config{ + Telemetry: config.Telemetry{ + Metrics: config.TelemetryMetrics{ + Endpoint: "localhost:9464", + Prometheus: struct { + Path string `yaml:"path" json:"path"` + }{ + Path: "/metrics", + }, + }, + Tracing: config.TelemetryTracing{ + Endpoint: "localhost:4317", + }, + }, + }) + assert.NoError(t, err) + defer cleanup() + + // Test with a closed connection + _, err = net.Dial("tcp", "localhost:0") // This should fail + assert.Error(t, err) + + // Still create a traced connection with nil + tracedConn := tel.WithConnectionTracing(nil, "test-connection") + assert.Nil(t, tracedConn) +} + +func TestConnectionTimeout(t *testing.T) { + if testing.Short() { + t.Skip("Skipping timeout test in short mode") + } + + tel, cleanup, err := New(context.Background(), &config.Config{ + Telemetry: config.Telemetry{ + Metrics: config.TelemetryMetrics{ + Endpoint: "localhost:9464", + Prometheus: struct { + Path string `yaml:"path" json:"path"` + }{ + Path: "/metrics", + }, + }, + Tracing: config.TelemetryTracing{ + Endpoint: "localhost:4317", + }, + }, + }) + assert.NoError(t, err) + defer cleanup() + + // Create a connection that will timeout + _, err = net.DialTimeout("tcp", "192.0.2.1:12345", 1*time.Second) // Use an unroutable IP + assert.Error(t, err) + + // Create traced connection with nil + tracedConn := tel.WithConnectionTracing(nil, "test-connection") + assert.Nil(t, tracedConn) +} \ No newline at end of file diff --git a/pkg/telemetry/telemetry.go b/pkg/telemetry/telemetry.go new file mode 100644 index 00000000..1406a22f --- /dev/null +++ b/pkg/telemetry/telemetry.go @@ -0,0 +1,260 @@ +package telemetry + +import ( + "context" + "fmt" + "net/http" + "os" + + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/exporters/prometheus" + "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/propagation" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.20.0" + "go.opentelemetry.io/otel/trace" + + gcfg "go.minekube.com/gate/pkg/gate/config" +) + +// Version information set by build flags +var ( + // Version is the current version of Gate. + // Set using -ldflags "-X go.minekube.com/gate/pkg/telemetry.Version=v1.2.3" + Version = "dev" +) + +// Telemetry holds telemetry instruments for a Gate instance +type Telemetry struct { + tracer trace.Tracer + meter metric.Meter + playerGauge metric.Int64UpDownCounter + connDuration metric.Float64Histogram + playerConnections metric.Int64Counter +} + +// New creates a new Telemetry instance for a Gate instance +func New(ctx context.Context, cfg *gcfg.Config) (*Telemetry, func(), error) { + // Create shared resource attributes + res, err := resource.New(ctx, + resource.WithAttributes( + semconv.ServiceName("gate"), + semconv.ServiceVersion(Version), + ), + ) + if err != nil { + return nil, nil, fmt.Errorf("failed to create resource: %w", err) + } + + var cleanupFuncs []func() + t := &Telemetry{} + + // Initialize metrics if enabled + if cfg.Telemetry.Metrics.Enabled { + metricCleanup, err := t.initMetrics(ctx, res, cfg) + if err != nil { + return nil, nil, fmt.Errorf("failed to initialize metrics: %w", err) + } + cleanupFuncs = append(cleanupFuncs, metricCleanup) + } + + // Initialize tracing if enabled + if cfg.Telemetry.Tracing.Enabled { + tracingCleanup, err := t.initTracing(ctx, res, cfg) + if err != nil { + return nil, nil, fmt.Errorf("failed to initialize tracing: %w", err) + } + cleanupFuncs = append(cleanupFuncs, tracingCleanup) + } + + cleanup := func() { + for _, cleanup := range cleanupFuncs { + cleanup() + } + } + + return t, cleanup, nil +} + +func (t *Telemetry) initMetrics(ctx context.Context, res *resource.Resource, cfg *gcfg.Config) (func(), error) { + switch cfg.Telemetry.Metrics.Exporter { + case "prometheus": + // Create Prometheus exporter + promExporter, err := prometheus.New() + if err != nil { + return nil, fmt.Errorf("failed to create prometheus exporter: %w", err) + } + + provider := sdkmetric.NewMeterProvider( + sdkmetric.WithResource(res), + sdkmetric.WithReader(promExporter), + ) + otel.SetMeterProvider(provider) + + // Start Prometheus HTTP server + go func() { + mux := http.NewServeMux() + mux.Handle(cfg.Telemetry.Metrics.Prometheus.Path, promhttp.Handler()) + if err := http.ListenAndServe(cfg.Telemetry.Metrics.Endpoint, mux); err != nil { + fmt.Printf("prometheus server error: %v\n", err) + } + }() + + return t.setupMetrics(ctx, provider) + + case "otlp": + otlpExporter, err := otlpmetricgrpc.New(ctx, + otlpmetricgrpc.WithEndpoint(cfg.Telemetry.Metrics.Endpoint), + otlpmetricgrpc.WithInsecure(), + ) + if err != nil { + return nil, fmt.Errorf("failed to create OTLP metrics exporter: %w", err) + } + + reader := sdkmetric.NewPeriodicReader(otlpExporter) + provider := sdkmetric.NewMeterProvider( + sdkmetric.WithResource(res), + sdkmetric.WithReader(reader), + ) + otel.SetMeterProvider(provider) + + return t.setupMetrics(ctx, provider) + + case "stdout": + stdoutExporter, err := stdoutmetric.New() + if err != nil { + return nil, fmt.Errorf("failed to create stdout metrics exporter: %w", err) + } + + reader := sdkmetric.NewPeriodicReader(stdoutExporter) + provider := sdkmetric.NewMeterProvider( + sdkmetric.WithResource(res), + sdkmetric.WithReader(reader), + ) + otel.SetMeterProvider(provider) + + return t.setupMetrics(ctx, provider) + + default: + return nil, fmt.Errorf("unknown metrics exporter: %s", cfg.Telemetry.Metrics.Exporter) + } +} + +func (t *Telemetry) setupMetrics(ctx context.Context, provider *sdkmetric.MeterProvider) (func(), error) { + t.meter = provider.Meter("gate") + + // Create metrics + var err1, err2, err3 error + + t.playerGauge, err1 = t.meter.Int64UpDownCounter( + "gate.players.current", + metric.WithDescription("Current number of connected players"), + ) + + t.playerConnections, err2 = t.meter.Int64Counter( + "gate.players.total", + metric.WithDescription("Total number of player connections"), + ) + + t.connDuration, err3 = t.meter.Float64Histogram( + "gate.connection.duration", + metric.WithDescription("Connection duration in seconds"), + ) + + for _, err := range []error{err1, err2, err3} { + if err != nil { + return nil, fmt.Errorf("failed to create metrics: %w", err) + } + } + + return func() { + if err := provider.Shutdown(ctx); err != nil { + fmt.Printf("failed to shutdown meter provider: %v\n", err) + } + }, nil +} + +func (t *Telemetry) initTracing(ctx context.Context, res *resource.Resource, cfg *gcfg.Config) (func(), error) { + var exporter sdktrace.SpanExporter + var err error + + switch cfg.Telemetry.Tracing.Exporter { + case "otlp": + exporter, err = otlptracegrpc.New(ctx, + otlptracegrpc.WithEndpoint(cfg.Telemetry.Tracing.Endpoint), + otlptracegrpc.WithInsecure(), + ) + + case "stdout": + exporter, err = stdouttrace.New( + stdouttrace.WithPrettyPrint(), + stdouttrace.WithWriter(os.Stdout), + ) + + case "jaeger": + return nil, fmt.Errorf("jaeger exporter is deprecated, use OTLP exporter with jaeger collector instead") + + default: + return nil, fmt.Errorf("unknown tracer type: %s", cfg.Telemetry.Tracing.Exporter) + } + + if err != nil { + return nil, fmt.Errorf("failed to create trace exporter: %w", err) + } + + tracerProvider := sdktrace.NewTracerProvider( + sdktrace.WithBatcher(exporter), + sdktrace.WithResource(res), + ) + + otel.SetTracerProvider(tracerProvider) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + )) + + t.tracer = tracerProvider.Tracer("gate") + + return func() { + if err := tracerProvider.Shutdown(ctx); err != nil { + fmt.Printf("failed to shutdown tracer provider: %v\n", err) + } + }, nil +} + +// RecordPlayerConnection records a new player connection metric +func (t *Telemetry) RecordPlayerConnection(ctx context.Context, username string) { + if t.playerConnections != nil { + t.playerConnections.Add(ctx, 1, + metric.WithAttributes( + attribute.String("username", username), + ), + ) + } +} + +// RecordPlayerDisconnection updates metrics when a player disconnects +func (t *Telemetry) RecordPlayerDisconnection(ctx context.Context, username string, duration float64) { + if t.connDuration != nil { + t.connDuration.Record(ctx, duration, + metric.WithAttributes( + attribute.String("username", username), + ), + ) + } +} + +// UpdatePlayerCount updates the current player count +func (t *Telemetry) UpdatePlayerCount(ctx context.Context, delta int64) { + if t.playerGauge != nil { + t.playerGauge.Add(ctx, delta) + } +} \ No newline at end of file