From 0b92ee3182e35f8c25095b9e3df7f7d13fc58be1 Mon Sep 17 00:00:00 2001 From: Benjamin Bengfort Date: Sun, 29 Mar 2026 12:33:14 -0500 Subject: [PATCH 1/2] [FEAT] OpenTelemetry and Configuration --- .env.template | 11 ++ README.md | 12 ++ cmd/uptime/main.go | 4 +- go.mod | 83 +++++++++--- go.sum | 187 ++++++++++++++++++++------- pkg/config/config.go | 74 +++++++++-- pkg/config/config_test.go | 69 ++++++++++ pkg/config/cors.go | 23 ++++ pkg/server/routes.go | 9 ++ pkg/server/server.go | 26 ++-- pkg/telemetry/applog.go | 41 ++++++ pkg/telemetry/metrics.go | 40 ++++++ pkg/telemetry/noop.go | 35 +++++ pkg/telemetry/otel.go | 117 +++++++++++++++++ pkg/telemetry/telemetry.go | 256 +++++++++++++++++++++++++++++++++++++ 15 files changed, 890 insertions(+), 97 deletions(-) create mode 100644 .env.template create mode 100644 pkg/config/config_test.go create mode 100644 pkg/config/cors.go create mode 100644 pkg/telemetry/applog.go create mode 100644 pkg/telemetry/metrics.go create mode 100644 pkg/telemetry/noop.go create mode 100644 pkg/telemetry/otel.go create mode 100644 pkg/telemetry/telemetry.go diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..cc19560 --- /dev/null +++ b/.env.template @@ -0,0 +1,11 @@ +# For development, copy this file to .env and modify as needed. +# NOTE: the settings below are recommended for development. +UPTIME_MAINTENANCE=false +UPTIME_MODE=debug +UPTIME_LOG_LEVEL=debug +UPTIME_CONSOLE_LOG=true +UPTIME_BIND_ADDR=127.0.0.1:8000 +UPTIME_ALLOWED_ORIGINS=http://localhost:8000 +UPTIME_TELEMETRY_ENABLED=false +OTEL_SERVICE_NAME=uptime +GIMLET_OTEL_SERVICE_ADDR=localhost:8000 diff --git a/README.md b/README.md index d0ee817..7c06365 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,15 @@ # Uptime +[![Tests](https://github.com/rotationalio/uptime/actions/workflows/tests.yaml/badge.svg)](https://github.com/rotationalio/uptime/actions/workflows/tests.yaml) + **Service status monitor for Rotational applications and systems.** + +Uptime performs status checks on Rotational services on a periodic basis in order to detect service availability, currently deployed version, and to quickly report if any Rotational services are down or unavailable. + +## License + +This project is licensed under the BSD 3-Clause License. See [`LICENSE`](./LICENSE) for details. Please feel free to use Uptime in your own projects and applications. + +## About Rotational Labs + +Uptime is developed by [Rotational Labs](https://rotational.io), a team of engineers and scientists building AI infrastructure for serious work. \ No newline at end of file diff --git a/cmd/uptime/main.go b/cmd/uptime/main.go index 4bae24a..090315b 100644 --- a/cmd/uptime/main.go +++ b/cmd/uptime/main.go @@ -7,8 +7,8 @@ import ( "text/tabwriter" "github.com/joho/godotenv" - confire "github.com/rotationalio/confire/usage" "github.com/urfave/cli/v3" + confire "go.rtnl.ai/confire/usage" "go.rtnl.ai/uptime/pkg" "go.rtnl.ai/uptime/pkg/config" "go.rtnl.ai/uptime/pkg/server" @@ -51,7 +51,7 @@ func main() { func serve(ctx context.Context, c *cli.Command) (err error) { var srv *server.Server - if srv, err = server.New(nil); err != nil { + if srv, err = server.New(); err != nil { return cli.Exit(err, 1) } diff --git a/go.mod b/go.mod index acb7d4a..ce370c2 100644 --- a/go.mod +++ b/go.mod @@ -3,51 +3,94 @@ module go.rtnl.ai/uptime go 1.26.1 require ( - github.com/gin-gonic/gin v1.11.0 + github.com/gin-contrib/cors v1.7.7 + github.com/gin-gonic/gin v1.12.0 github.com/joho/godotenv v1.5.1 - github.com/rotationalio/confire v1.1.0 - github.com/urfave/cli/v3 v3.7.0 - go.opentelemetry.io/otel v1.40.0 - go.opentelemetry.io/otel/trace v1.40.0 - go.rtnl.ai/gimlet v1.5.0 - go.rtnl.ai/x v1.11.0 + github.com/stretchr/testify v1.11.1 + github.com/urfave/cli/v3 v3.8.0 + go.opentelemetry.io/contrib/bridges/otelslog v0.17.0 + go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 + go.opentelemetry.io/contrib/propagators/autoprop v0.67.0 + go.opentelemetry.io/otel v1.42.0 + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 + go.opentelemetry.io/otel/log v0.18.0 + go.opentelemetry.io/otel/metric v1.42.0 + go.opentelemetry.io/otel/sdk v1.42.0 + go.opentelemetry.io/otel/sdk/log v0.18.0 + go.opentelemetry.io/otel/sdk/metric v1.42.0 + go.opentelemetry.io/otel/trace v1.42.0 + go.rtnl.ai/confire v1.2.0 + go.rtnl.ai/gimlet v1.6.2 + go.rtnl.ai/x v1.14.0 ) require ( - github.com/bytedance/gopkg v0.1.3 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bytedance/gopkg v0.1.4 // indirect github.com/bytedance/sonic v1.15.0 // indirect - github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/bytedance/sonic/loader v0.5.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect - github.com/gin-contrib/sse v1.1.0 // indirect + github.com/gin-contrib/sse v1.1.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect - github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-json v0.10.6 // indirect github.com/goccy/go-yaml v1.19.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pelletier/go-toml/v2 v2.3.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/otlptranslator v1.0.0 // indirect + github.com/prometheus/procfs v0.20.1 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect - github.com/rs/zerolog v1.34.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 // indirect + go.opentelemetry.io/contrib/propagators/aws v1.42.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.42.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.42.0 // indirect + go.opentelemetry.io/contrib/propagators/ot v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.64.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.rtnl.ai/ulid v1.2.0 // indirect - golang.org/x/arch v0.23.0 // indirect - golang.org/x/crypto v0.47.0 // indirect - golang.org/x/net v0.49.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.33.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + golang.org/x/arch v0.25.0 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 // indirect + google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 47e6f4b..3452f7b 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,28 @@ -github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= -github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +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/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= +github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= -github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= -github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI= +github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= -github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= -github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= -github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/gin-contrib/cors v1.7.7 h1:Oh9joP463x7Mw72vhvJ61YQm8ODh9b04YR7vsOErD0Q= +github.com/gin-contrib/cors v1.7.7/go.mod h1:K5tW0RkzJtWSiOdikXloy8VEZlgdVNpHNw8FpjUPNrE= +github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko= +github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -31,27 +36,35 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -59,20 +72,28 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= +github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= +github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= -github.com/rotationalio/confire v1.1.0 h1:h10RDxiO/XH6UStfxY+oMJOVxt3Elqociilb7fIfANs= -github.com/rotationalio/confire v1.1.0/go.mod h1:ug7pBDiZZl/4JjXJ2Effmj+L+0T2DBbG+Us1qQcRex0= -github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= -github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -88,40 +109,108 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -github.com/urfave/cli/v3 v3.7.0 h1:AGSnbUyjtLiM+WJUb4dzXKldl/gL+F8OwmRDtVr6g2U= -github.com/urfave/cli/v3 v3.7.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= +github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI= +github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= -go.rtnl.ai/gimlet v1.5.0 h1:7xXYHl9TXhUw4ZdAllRgPy08H+mqjW03DKn9ca1NnEE= -go.rtnl.ai/gimlet v1.5.0/go.mod h1:Izgj2yOV3tm3WwcOUQrLpAuktMQb8W8dH8qrLCFBT1Y= +go.opentelemetry.io/contrib/bridges/otelslog v0.17.0 h1:NFIS6x7wyObQ7cR84x7bt1sr8nYBx89s3x3GwRjw40k= +go.opentelemetry.io/contrib/bridges/otelslog v0.17.0/go.mod h1:39SaByOyDMRMe872AE7uelMuQZidIw7LLFAnQi0FWTE= +go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 h1:dkBzNEAIKADEaFnuESzcXvpd09vxvDZsOjx11gjUqLk= +go.opentelemetry.io/contrib/bridges/prometheus v0.67.0/go.mod h1:Z5RIwRkZgauOIfnG5IpidvLpERjhTninpP1dTG2jTl4= +go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 h1:4fnRcNpc6YFtG3zsFw9achKn3XgmxPxuMuqIL5rE8e8= +go.opentelemetry.io/contrib/exporters/autoexport v0.67.0/go.mod h1:qTvIHMFKoxW7HXg02gm6/Wofhq5p3Ib/A/NNt1EoBSQ= +go.opentelemetry.io/contrib/propagators/autoprop v0.67.0 h1:XhcQRf4MeqwQw96FcnatDAj6gwE19SUrWZ1VwNg77iE= +go.opentelemetry.io/contrib/propagators/autoprop v0.67.0/go.mod h1:7OK06SuNIBIlc5Uq3JGQEsKHuXw29t9OJemvDYyP1dk= +go.opentelemetry.io/contrib/propagators/aws v1.42.0 h1:Kbr3xDxs6kcxp5ThXTKWK2OtwLhNoXBVtqguNYcsZL0= +go.opentelemetry.io/contrib/propagators/aws v1.42.0/go.mod h1:Jzw9hZHtxdpCN7x8S17UH59X/EiFivp6VXLs9bdM1OQ= +go.opentelemetry.io/contrib/propagators/b3 v1.42.0 h1:B2Pew5ufEtgkjLF+tSkXjgYZXQr9m7aCm1wLKB0URbU= +go.opentelemetry.io/contrib/propagators/b3 v1.42.0/go.mod h1:iPgUcSEF5DORW6+yNbdw/YevUy+QqJ508ncjhrRSCjc= +go.opentelemetry.io/contrib/propagators/jaeger v1.42.0 h1:jP8unWI6q5kcb3gpGLjKDGaUa+JW+nHKWvpS/q+YuWA= +go.opentelemetry.io/contrib/propagators/jaeger v1.42.0/go.mod h1:xd89e/pUyPatUP1C4z1UknD9jHptESO99tWyvd4mWD4= +go.opentelemetry.io/contrib/propagators/ot v1.42.0 h1:uQjD1NNqX1+DfcAoWParPt1egNg9vC9gH4xarJ9Khxo= +go.opentelemetry.io/contrib/propagators/ot v1.42.0/go.mod h1:yw/c2TCmQLIv109HBOCn6NlJ8Dp7MNfjMcqQZRnAMmg= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 h1:deI9UQMoGFgrg5iLPgzueqFPHevDl+28YKfSpPTI6rY= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0/go.mod h1:PFx9NgpNUKXdf7J4Q3agRxMs3Y07QhTCVipKmLsMKnU= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 h1:icqq3Z34UrEFk2u+HMhTtRsvo7Ues+eiJVjaJt62njs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0/go.mod h1:W2m8P+d5Wn5kipj4/xmbt9uMqezEKfBjzVJadfABSBE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 h1:H7O6RlGOMTizyl3R08Kn5pdM06bnH8oscSj7o11tmLA= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0/go.mod h1:mBFWu/WOVDkWWsR7Tx7h6EpQB8wsv7P0Yrh0Pb7othc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc= +go.opentelemetry.io/otel/exporters/prometheus v0.64.0 h1:g0LRDXMX/G1SEZtK8zl8Chm4K6GBwRkjPKE36LxiTYs= +go.opentelemetry.io/otel/exporters/prometheus v0.64.0/go.mod h1:UrgcjnarfdlBDP3GjDIJWe6HTprwSazNjwsI+Ru6hro= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 h1:KJVjPD3rcPb98rIs3HznyJlrfx9ge5oJvxxlGR+P/7s= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0/go.mod h1:K3kRa2ckmHWQaTWQdPRHc7qGXASuVuoEQXzrvlA98Ws= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 h1:lSZHgNHfbmQTPfuTmWVkEu8J8qXaQwuV30pjCcAUvP8= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0/go.mod h1:so9ounLcuoRDu033MW/E0AD4hhUjVqswrMF5FoZlBcw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs= +go.opentelemetry.io/otel/log v0.18.0 h1:XgeQIIBjZZrliksMEbcwMZefoOSMI1hdjiLEiiB0bAg= +go.opentelemetry.io/otel/log v0.18.0/go.mod h1:KEV1kad0NofR3ycsiDH4Yjcoj0+8206I6Ox2QYFSNgI= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/sdk/log v0.18.0 h1:n8OyZr7t7otkeTnPTbDNom6rW16TBYGtvyy2Gk6buQw= +go.opentelemetry.io/otel/sdk/log v0.18.0/go.mod h1:C0+wxkTwKpOCZLrlJ3pewPiiQwpzycPI/u6W0Z9fuYk= +go.opentelemetry.io/otel/sdk/log/logtest v0.18.0 h1:l3mYuPsuBx6UKE47BVcPrZoZ0q/KER57vbj2qkgDLXA= +go.opentelemetry.io/otel/sdk/log/logtest v0.18.0/go.mod h1:7cHtiVJpZebB3wybTa4NG+FUo5NPe3PROz1FqB0+qdw= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.rtnl.ai/confire v1.2.0 h1:Q/UexW6tdaNwulwNOrzSFhgHEYMugg1jFPxWA6u1K7w= +go.rtnl.ai/confire v1.2.0/go.mod h1:g1rNOgKnnC0eHPU4kNKzPvxZmkeexU0cbvgO/SHDoSU= +go.rtnl.ai/gimlet v1.6.2 h1:BC1fA3nHSjCcREhZVhy1mGUwaO0kqwmX+ElmWqe5ckI= +go.rtnl.ai/gimlet v1.6.2/go.mod h1:xrRsL1kopxcGJ17uDu2qj0yLhxlkkIZOic19zkg0mWQ= go.rtnl.ai/ulid v1.2.0 h1:EwIst7WZVdCmbtbDmIrRB15q03Qe9hmHHbxNej0V/wI= go.rtnl.ai/ulid v1.2.0/go.mod h1:F95yPYwEEZdz5sM4GC6buziff6xD+7XVcGZ/8n37Cn0= -go.rtnl.ai/x v1.11.0 h1:Dx/faeEVYNOEvlM4phVc6MmUONvCw5AfRjyb7EcwPgY= -go.rtnl.ai/x v1.11.0/go.mod h1:ciQ9PaXDtZDznzBrGDBV2yTElKX3aJgtQfi6V8613bo= +go.rtnl.ai/x v1.14.0 h1:6k25gpG1zx//w3Cd2dwdbZ+/8Wmts7GSfwGcvx7eoqw= +go.rtnl.ai/x v1.14.0/go.mod h1:ciQ9PaXDtZDznzBrGDBV2yTElKX3aJgtQfi6V8613bo= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= -golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= -golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE= +golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI= +google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/config/config.go b/pkg/config/config.go index 90cc282..c030f1d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,8 +1,11 @@ package config import ( - "github.com/rotationalio/confire" - "go.rtnl.ai/gimlet/logger" + "sync" + + "github.com/gin-gonic/gin" + "go.rtnl.ai/confire" + "go.rtnl.ai/x/rlog" ) // All environment variables will have this prefix unless otherwise defined in struct @@ -13,11 +16,13 @@ const Prefix = "uptime" // Config contains all of the configuration parameters for the Uptime server which // are loaded from the environment and should be validated before use. type Config struct { - Maintenance bool `default:"false" desc:"if true the server will start in maintenance mode"` - Mode string `default:"release" desc:"specify the mode of the gin server (release, debug, testing)"` - LogLevel logger.LevelDecoder `default:"info" desc:"set the log level for the server"` - BindAddr string `default:":8000" desc:"the ip address and port to bind the server to"` - Telemetry TelemetryConfig + Maintenance bool `default:"false" desc:"if true the server will start in maintenance mode"` + Mode string `default:"release" desc:"specify the mode of the gin server (release, debug, testing)"` + LogLevel rlog.LevelDecoder `default:"info" split_words:"true" desc:"set the log level for the server"` + ConsoleLog bool `default:"false" split_words:"true" desc:"if true the server will log to the console in text format"` + BindAddr string `default:":8000" split_words:"true" desc:"the ip address and port to bind the server to"` + AllowedOrigins []string `split_words:"true" default:"http://localhost:8000" desc:"a list of allowed origins for CORS"` + Telemetry TelemetryConfig } // Telemetry is primarily configured via the open telemetry sdk environment variables. @@ -40,14 +45,59 @@ func New() (conf *Config, err error) { if err = confire.Process(Prefix, conf); err != nil { return nil, err } + return conf, nil +} - if err = conf.Validate(); err != nil { - return nil, err +func (c Config) Validate() (err error) { + if c.Mode != gin.ReleaseMode && c.Mode != gin.DebugMode && c.Mode != gin.TestMode { + return confire.Invalid("", "mode", "gin mode must be one of: release, debug, test") } + return nil +} - return conf, nil +//============================================================================ +// Config Package Management +//============================================================================ + +var ( + mu sync.RWMutex + err error + load sync.Once + conf *Config +) + +func Get() (Config, error) { + load.Do(func() { + mu.Lock() + defer mu.Unlock() + + if conf == nil { + conf, err = New() + } + }) + mu.RLock() + defer mu.RUnlock() + return *conf, err } -func (c *Config) Validate() (err error) { - return nil +func MustGet() Config { + conf, err := Get() + if err != nil { + panic(err) + } + return conf +} + +func Set(c Config) { + mu.Lock() + defer mu.Unlock() + conf = &c +} + +func Reset() { + mu.Lock() + defer mu.Unlock() + conf = nil + err = nil + load = sync.Once{} } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000..035f99a --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,69 @@ +package config_test + +import ( + "log/slog" + "testing" + + "github.com/stretchr/testify/require" + "go.rtnl.ai/confire/contest" + "go.rtnl.ai/uptime/pkg/config" + "go.rtnl.ai/x/rlog" +) + +var testEnv = contest.Env{ + "UPTIME_MAINTENANCE": "true", + "UPTIME_MODE": "debug", + "UPTIME_LOG_LEVEL": "debug", + "UPTIME_CONSOLE_LOG": "true", + "UPTIME_BIND_ADDR": ":8888", + "UPTIME_ALLOWED_ORIGINS": "http://localhost:8888", + "UPTIME_TELEMETRY_ENABLED": "false", + "OTEL_SERVICE_NAME": "uptime", + "GIMLET_OTEL_SERVICE_ADDR": "localhost:8888", +} + +var validConfig = config.Config{ + Maintenance: true, + Mode: "debug", + LogLevel: rlog.LevelDecoder(slog.LevelDebug), + ConsoleLog: true, + BindAddr: ":8888", + AllowedOrigins: []string{"http://localhost:8888"}, + Telemetry: config.TelemetryConfig{ + Enabled: false, + ServiceName: "uptime", + ServiceAddr: "localhost:8888", + }, +} + +func TestConfig(t *testing.T) { + t.Run("Valid", func(t *testing.T) { + t.Cleanup(testEnv.Set()) + + conf, err := config.New() + require.NoError(t, err) + require.Equal(t, validConfig, *conf) + }) + + t.Run("InvalidMode", func(t *testing.T) { + t.Cleanup(testEnv.Set()) + + invalid := contest.Env{ + "UPTIME_MODE": "invalid", + } + + errs := map[string]string{ + "UPTIME_MODE": "invalid configuration: mode gin mode must be one of: release, debug, test", + } + + for key := range invalid { + cleanup := invalid.Set(key) + + _, err := config.New() + require.EqualError(t, err, errs[key]) + + cleanup() + } + + }) +} diff --git a/pkg/config/cors.go b/pkg/config/cors.go new file mode 100644 index 0000000..ab64a50 --- /dev/null +++ b/pkg/config/cors.go @@ -0,0 +1,23 @@ +package config + +import ( + "time" + + "github.com/gin-contrib/cors" +) + +func (c Config) CORS() cors.Config { + // Create a CORS config with the configured allowed origins + return cors.Config{ + AllowOrigins: c.AllowedOrigins, + AllowMethods: []string{"GET", "HEAD", "OPTIONS"}, + AllowHeaders: []string{"Origin", "Content-Length", "Content-Type"}, + ExposeHeaders: []string{"Content-Length", "Access-Control-Allow-Origin"}, + AllowCredentials: false, + AllowWildcard: false, + AllowBrowserExtensions: false, + AllowWebSockets: false, + AllowPrivateNetwork: false, + MaxAge: 48 * time.Hour, + } +} diff --git a/pkg/server/routes.go b/pkg/server/routes.go index 203e0ee..0ba0864 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -4,12 +4,21 @@ import ( "github.com/gin-gonic/gin" "go.rtnl.ai/gimlet/logger" "go.rtnl.ai/uptime/pkg" + "go.rtnl.ai/uptime/pkg/telemetry" ) func (s *Server) setupRoutes() (err error) { + // Create observability middleware + var observability gin.HandlerFunc + if observability, err = telemetry.Middleware(); err != nil { + return err + } // Application Middleware middlewares := []gin.HandlerFunc{ + // o11y should be on the outside so we can record the correct latency of requests + // NOTE: o11y panics will not recover due to middleware ordering. + observability, // Panic recovery middleware gin.Recovery(), diff --git a/pkg/server/server.go b/pkg/server/server.go index 3cae1ba..cd65b60 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -20,6 +20,7 @@ import ( "go.rtnl.ai/uptime/pkg" "go.rtnl.ai/uptime/pkg/config" "go.rtnl.ai/x/probez" + "go.rtnl.ai/x/rlog" ) const ( @@ -45,16 +46,13 @@ type Server struct { errc chan error } -func New(conf *config.Config) (s *Server, err error) { - if conf == nil { - if conf, err = config.New(); err != nil { - return nil, fmt.Errorf("could not load configuration: %w", err) - } - } +func New() (s *Server, err error) { + // Create the server instance. + s = &Server{errc: make(chan error, 1)} - s = &Server{ - conf: *conf, - errc: make(chan error, 1), + // Load the configuration from the environment if not provided. + if s.conf, err = config.Get(); err != nil { + return nil, fmt.Errorf("could not load configuration: %w", err) } // Configure the gin router @@ -117,9 +115,9 @@ func (s *Server) Serve() (err error) { // Mark the server as live and ready s.Ready() - // TODO: setup logging with go.rtnl.ai/x/rlog - slog.Default().Info( - "server started", + rlog.InfoAttrs( + context.Background(), + "uptime server started", slog.String("url", s.URL()), slog.String("version", pkg.Version(true)), slog.Bool("maintenance", s.conf.Maintenance), @@ -135,7 +133,7 @@ func (s *Server) serve(sock net.Listener) (err error) { } func (s *Server) Shutdown() (err error) { - slog.Default().Info("gracefully shutting down server") + rlog.Info("gracefully shutting down server") s.NotReady() ctx, cancel := context.WithTimeout(context.Background(), ShutdownTimeout) @@ -148,7 +146,7 @@ func (s *Server) Shutdown() (err error) { err = errors.Join(serr, fmt.Errorf("could not shutdown server: %w", serr)) } - slog.Default().Debug("server shutdown", slog.Any("error", err)) + rlog.Debug("server shutdown", slog.Any("error", err)) return err } diff --git a/pkg/telemetry/applog.go b/pkg/telemetry/applog.go new file mode 100644 index 0000000..82a56ed --- /dev/null +++ b/pkg/telemetry/applog.go @@ -0,0 +1,41 @@ +package telemetry + +import ( + "log/slog" + "os" + + otelslog "go.opentelemetry.io/contrib/bridges/otelslog" + + "go.rtnl.ai/uptime/pkg/config" + "go.rtnl.ai/x/rlog" + "go.rtnl.ai/x/rlog/console" +) + +// initializeLogging configures the process root logger: stdout (JSON or console) with rlog +// level names, optionally fan-out to OpenTelemetry logs via otelslog when telemetry is on. +func initializeLogging() error { + // Get the configured log level, set it as the global log level, and merge + // custom level string replacer. + conf := config.MustGet() + rlog.SetLevel(slog.Level(conf.LogLevel)) + opts := rlog.MergeWithCustomLevels(rlog.WithGlobalLevel(nil)) + + // Create the stdout handler: text or JSON. + var stdout slog.Handler + if conf.ConsoleLog { + stdout = console.New(os.Stdout, &console.Options{HandlerOptions: opts}) + } else { + stdout = slog.NewJSONHandler(os.Stdout, opts) + } + + // If telemetry is enabled, fan-out to OpenTelemetry logs via otelslog. + var root slog.Handler = stdout + if !disabled && upLoggerProvider != nil { + otelHandler := otelslog.NewHandler(ServiceName()) + root = slog.NewMultiHandler(stdout, otelHandler) + } + + // Create the root logger and set it as the default rlog logger. + rlog.SetDefault(rlog.New(slog.New(root))) + return nil +} diff --git a/pkg/telemetry/metrics.go b/pkg/telemetry/metrics.go new file mode 100644 index 0000000..9b6622a --- /dev/null +++ b/pkg/telemetry/metrics.go @@ -0,0 +1,40 @@ +package telemetry + +import ( + "errors" + + "go.opentelemetry.io/otel/metric" +) + +var ( + GroupsMonitored metric.Int64Gauge // The number of groups that are being actively monitored and their statuses (operational, degraded, partial, major, maintenance) + ServicesMonitored metric.Int64Gauge // The number of services that are being actively monitored and their statuses (operational, degraded, partial, major, maintenance) + ServiceLatency metric.Int64Histogram // The latency of the uptime checks for monitored services (in milliseconds) +) + +func initializeMetrics(meter metric.Meter) (err error) { + var merr error + + if GroupsMonitored, merr = meter.Int64Gauge("groups.monitored", + metric.WithDescription("The number of groups that are being actively monitored"), + metric.WithUnit("groups"), + ); merr != nil { + err = errors.Join(err, merr) + } + + if ServicesMonitored, merr = meter.Int64Gauge("services.monitored", + metric.WithDescription("The number of services that are being actively monitored"), + metric.WithUnit("services"), + ); merr != nil { + err = errors.Join(err, merr) + } + + if ServiceLatency, merr = meter.Int64Histogram("services.latency", + metric.WithDescription("The latency of the uptime checks for monitored services (in milliseconds)"), + metric.WithUnit("ms"), + ); merr != nil { + err = errors.Join(err, merr) + } + + return err +} diff --git a/pkg/telemetry/noop.go b/pkg/telemetry/noop.go new file mode 100644 index 0000000..1f093ce --- /dev/null +++ b/pkg/telemetry/noop.go @@ -0,0 +1,35 @@ +package telemetry + +import ( + "context" + "errors" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/log/global" + nooplog "go.opentelemetry.io/otel/log/noop" + noopmeter "go.opentelemetry.io/otel/metric/noop" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + nooptrace "go.opentelemetry.io/otel/trace/noop" +) + +func disableTelemetry(ctx context.Context) { + var err error + if upResource, err = resource.New(ctx); err != nil { + initerr = errors.Join(initerr, err) + } + + upPropagator = propagation.TraceContext{} + otel.SetTextMapPropagator(upPropagator) + + otel.SetTracerProvider(nooptrace.NewTracerProvider()) + otel.SetMeterProvider(noopmeter.NewMeterProvider()) + global.SetLoggerProvider(nooplog.NewLoggerProvider()) + + disabled = true + + // Setup application logging with console only logging. + if err = initializeLogging(); err != nil { + initerr = errors.Join(initerr, err) + } +} diff --git a/pkg/telemetry/otel.go b/pkg/telemetry/otel.go new file mode 100644 index 0000000..b48bff6 --- /dev/null +++ b/pkg/telemetry/otel.go @@ -0,0 +1,117 @@ +package telemetry + +import ( + "context" + "time" + + "go.opentelemetry.io/contrib/exporters/autoexport" + "go.opentelemetry.io/contrib/propagators/autoprop" + "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" + "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/log" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.37.0" + + "go.rtnl.ai/uptime/pkg" +) + +func newResource(ctx context.Context) (*resource.Resource, error) { + return resource.New(ctx, + // Resource Detectors + resource.WithFromEnv(), + resource.WithTelemetrySDK(), + resource.WithOS(), + resource.WithHost(), + resource.WithContainer(), + + // NOTE: these custom attributes should go last so they override any + // attributes set by the resource detectors above. + resource.WithAttributes( + semconv.ServiceName(ServiceName()), + semconv.ServiceVersion(pkg.Version(false)), + ), + ) +} + +// Uses autoprop to automatically create the correct propagator based on the +// environment, defaulting to a combined trace context and baggage text map propagator. +func newPropagator() (propagation.TextMapPropagator, error) { + return autoprop.NewTextMapPropagator(), nil +} + +// Uses autoexport to automatically create the correct tracer exporter based on the +// environment, defaulting to a console exporter if $OTEL_TRACES_EXPORTER is empty. +// The traces sampler is also configured from the environment, with the default +// AlwaysSample as the sampler if no environment variables are set. +func newTracerProvider(ctx context.Context, resource *resource.Resource) (tracerProvider *trace.TracerProvider, err error) { + // Default to a console exporter if $OTEL_TRACES_EXPORTER is empty. + opts := []autoexport.SpanOption{ + autoexport.WithFallbackSpanExporter(func(context.Context) (trace.SpanExporter, error) { + return stdouttrace.New(stdouttrace.WithPrettyPrint()) + }), + } + + var tracerExporter trace.SpanExporter + if tracerExporter, err = autoexport.NewSpanExporter(ctx, opts...); err != nil { + return nil, err + } + + // NOTE: do not specify sampler in order for it to be configured from the environment. + tracerProvider = trace.NewTracerProvider( + trace.WithResource(resource), + trace.WithBatcher(tracerExporter), + ) + return tracerProvider, nil +} + +// Uses autoexport to automatically create the correct meter exporter based on the +// environment, defaulting to a console exporter if $OTEL_METRICS_EXPORTER is empty. +func newMeterProvider(ctx context.Context, resource *resource.Resource) (meterProvider *metric.MeterProvider, err error) { + // Default to a console exporter if $OTEL_METRICS_EXPORTER is empty. + opts := []autoexport.MetricOption{ + autoexport.WithFallbackMetricReader(func(context.Context) (_ metric.Reader, err error) { + var exporter metric.Exporter + if exporter, err = stdoutmetric.New(stdoutmetric.WithPrettyPrint()); err != nil { + return nil, err + } + return metric.NewPeriodicReader(exporter, metric.WithInterval(10*time.Second)), nil + }), + } + + var metricsReader metric.Reader + if metricsReader, err = autoexport.NewMetricReader(ctx, opts...); err != nil { + return nil, err + } + + meterProvider = metric.NewMeterProvider( + metric.WithResource(resource), + metric.WithReader(metricsReader), + ) + + return meterProvider, nil +} + +// Uses autoexport to automatically create the correct logger exporter based on the +// environment, defaulting to a console exporter if $OTEL_LOGS_EXPORTER is empty. +func newLoggerProvider(ctx context.Context, resource *resource.Resource) (loggerProvider *log.LoggerProvider, err error) { + opts := []autoexport.LogOption{ + autoexport.WithFallbackLogExporter(func(context.Context) (log.Exporter, error) { + return stdoutlog.New(stdoutlog.WithPrettyPrint()) + }), + } + + var loggerExporter log.Exporter + if loggerExporter, err = autoexport.NewLogExporter(ctx, opts...); err != nil { + return nil, err + } + + loggerProvider = log.NewLoggerProvider( + log.WithResource(resource), + log.WithProcessor(log.NewBatchProcessor(loggerExporter)), + ) + return loggerProvider, nil +} diff --git a/pkg/telemetry/telemetry.go b/pkg/telemetry/telemetry.go new file mode 100644 index 0000000..55db086 --- /dev/null +++ b/pkg/telemetry/telemetry.go @@ -0,0 +1,256 @@ +package telemetry + +import ( + "context" + "errors" + "fmt" + "net" + "os" + "sync" + + "github.com/gin-gonic/gin" + "go.rtnl.ai/gimlet/o11y" + "go.rtnl.ai/uptime/pkg/config" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/log/global" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/log" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/sdk/trace" +) + +const ( + DefaultServiceName = "uptime" +) + +type shutdownFn func(ctx context.Context) error + +var ( + disabled bool + shutdown []shutdownFn + initmu sync.Once + initerr error + closemu sync.Mutex + closeerr error + + upResource *resource.Resource + upPropagator propagation.TextMapPropagator + upTracerProvider *trace.TracerProvider + upMeterProvider *metric.MeterProvider + upLoggerProvider *log.LoggerProvider +) + +// Middleware returns the gimlet o11y middleware configured using the setup function. +func Middleware() (middleware gin.HandlerFunc, err error) { + // NOTE: setup is thread safe and can be called multiple times. + if err = Setup(context.Background()); err != nil { + return nil, err + } + + // If telemetry is disabled then do not activate the o11y middleware. + // NOTE: the setupRoutes() method must filter nil handlers. + if disabled { + return nil, nil + } + + opts := []o11y.Option{ + o11y.WithFilter(o11y.FilterStatus), + o11y.WithPropagators(upPropagator), + o11y.WithTracerProvider(upTracerProvider), + o11y.WithMeterProvider(upMeterProvider), + } + + // Return the gimlet o11y middleware configured using the setup function. + return o11y.Middleware(ServiceAddr(), opts...), nil +} + +// Setup initializes the opentelemetry sdk components and sets them as the global +// providers. In general, the setup is primarily configured via the standard OTEL_* +// environment variables, and to a limited extent via the config package. +// +// The setup function is idempotent and can be called multiple times, but will only be +// configured once; modifying the environment after the first call will have no effect. +func Setup(ctx context.Context) (err error) { + initmu.Do(func() { setup(ctx) }) + return initerr +} + +func setup(ctx context.Context) { + closemu.Lock() + defer closemu.Unlock() + + // Setup module telemetry handlers + shutdown = make([]shutdownFn, 0, 4) + initerr = nil + closeerr = nil + + // If telemetry is disabled then setup no-op handlers for opentelemetry sdk + // components. NOTE: this will override any OTEL_* environment variables. + conf := config.MustGet() + if !conf.Telemetry.Enabled { + disableTelemetry(ctx) + return + } + + // Cleanup is only called if there is an error during setup; shutting down any + // open telemetry sdk objects that have been created before the error occurred. + cleanup := func(ctx context.Context) error { + for _, fn := range shutdown { + closeerr = errors.Join(closeerr, fn(ctx)) + } + + shutdown = nil + return closeerr + } + + var err error + if upResource, err = newResource(ctx); err != nil { + initerr = errors.Join(err, cleanup(ctx)) + return + } + + // Set up propagator + if upPropagator, err = newPropagator(); err != nil { + initerr = errors.Join(err, cleanup(ctx)) + return + } + otel.SetTextMapPropagator(upPropagator) + + // Set up tracer provider + if upTracerProvider, err = newTracerProvider(ctx, upResource); err != nil { + initerr = errors.Join(err, cleanup(ctx)) + return + } + shutdown = append(shutdown, upTracerProvider.Shutdown) + otel.SetTracerProvider(upTracerProvider) + + // Set up meter provider + if upMeterProvider, err = newMeterProvider(ctx, upResource); err != nil { + initerr = errors.Join(err, cleanup(ctx)) + return + } + shutdown = append(shutdown, upMeterProvider.Shutdown) + otel.SetMeterProvider(upMeterProvider) + + // Initialize all custom metrics + meter := upMeterProvider.Meter(ServiceName()) + if err = initializeMetrics(meter); err != nil { + initerr = errors.Join(err, cleanup(ctx)) + return + } + + // Set up logger provider + if upLoggerProvider, err = newLoggerProvider(ctx, upResource); err != nil { + initerr = errors.Join(err, cleanup(ctx)) + return + } + shutdown = append(shutdown, upLoggerProvider.Shutdown) + global.SetLoggerProvider(upLoggerProvider) + + // Initialize application logging + if err = initializeLogging(); err != nil { + initerr = errors.Join(err, cleanup(ctx)) + return + } +} + +func Shutdown(ctx context.Context) error { + closemu.Lock() + if shutdown == nil { + closemu.Unlock() + return closeerr + } + + for _, fn := range shutdown { + closeerr = errors.Join(closeerr, fn(ctx)) + } + + shutdown = nil + closemu.Unlock() + + return closeerr +} + +func Disabled() bool { + return disabled +} + +func Propagator() propagation.TextMapPropagator { + return upPropagator +} + +func TracerProvider() *trace.TracerProvider { + return upTracerProvider +} + +func MeterProvider() *metric.MeterProvider { + return upMeterProvider +} + +func LoggerProvider() *log.LoggerProvider { + return upLoggerProvider +} + +// Returns the service name for use in the otel resource. By default it is "endeavor" +// but can be overridden by the `$OTEL_SERVICE_NAME` environment variable. This method +// is used to ensure the service name is consistent across all components including +// logging (which might use a separate resource). +func ServiceName() string { + conf := config.MustGet() + if conf.Telemetry.ServiceName == "" { + return DefaultServiceName + } + return conf.Telemetry.ServiceName +} + +// Returns the service address for use in otel http server tracing. This address +// can be set by the `$GIMLET_OTEL_SERVICE_ADDR` environment variable. If not set by +// this value then it is inferred from the bind address and the name of the pod or the +// hostname of the machine running the service. +// +// The service address must be the primary server name if it is known. E.g. the server +// name directive in an Apache or Nginx configuration. More generically, the primary +// server name would be the host header value that matches the default virtual host of +// an HTTP server. It should include the host identifier and if a port is used to route +// to the server that port identifier should be included as an appropriate port suffix. +// If this name is not known, server should be an empty string. +func ServiceAddr() string { + // If the service address is set in the configuration then return it. + conf := config.MustGet() + if conf.Telemetry.ServiceAddr != "" { + return conf.Telemetry.ServiceAddr + } + + // Attempt to infer the service address from the bind address. + var ( + host string + port string + err error + ) + + if host, port, err = net.SplitHostPort(conf.BindAddr); err != nil { + return "" + } + + // If the host is specified in the bind address then return it (normalizing localhost) + if host != "" { + if host == "127.0.0.1" { + return fmt.Sprintf("localhost:%s", port) + } + return conf.BindAddr + } + + // Attempt to get the pod name from the environment. + if pod, ok := os.LookupEnv("POD_NAME"); ok && pod != "" { + return fmt.Sprintf("%s:%s", pod, port) + } + + // Attempt to get the hostname from the environment. + if hostname, err := os.Hostname(); err == nil && hostname != "" { + return fmt.Sprintf("%s:%s", hostname, port) + } + + return "" +} From c0c42d34746297485ce67df6b0d02986854b8859 Mon Sep 17 00:00:00 2001 From: Benjamin Bengfort Date: Sun, 29 Mar 2026 12:45:48 -0500 Subject: [PATCH 2/2] review feedback --- pkg/config/config.go | 11 ++++++++++- pkg/server/server.go | 11 +++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index c030f1d..a977832 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -40,7 +40,13 @@ type TelemetryConfig struct { ServiceAddr string `split_words:"true" env:"GIMLET_OTEL_SERVICE_ADDR" desc:"the primary server name if it is known. E.g. the server name directive in an Nginx config. Should include host identifier and port if it is used; empty if not known."` } +// New creates a new Config instance and loads the configuration from the environment, +// validating the configuration and returning an error if the configuration is invalid +// or could not be parsed from environment variables. +// +// NOTE: New should only be used for testing, for module access to the config use Get(). func New() (conf *Config, err error) { + // NOTE: confire.Process calls Validate() internally. conf = &Config{} if err = confire.Process(Prefix, conf); err != nil { return nil, err @@ -77,7 +83,10 @@ func Get() (Config, error) { }) mu.RLock() defer mu.RUnlock() - return *conf, err + if conf != nil { + return *conf, err + } + return Config{}, err } func MustGet() Config { diff --git a/pkg/server/server.go b/pkg/server/server.go index cd65b60..4ede4e2 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -19,6 +19,7 @@ import ( "go.opentelemetry.io/otel/trace" "go.rtnl.ai/uptime/pkg" "go.rtnl.ai/uptime/pkg/config" + "go.rtnl.ai/uptime/pkg/telemetry" "go.rtnl.ai/x/probez" "go.rtnl.ai/x/rlog" ) @@ -47,6 +48,11 @@ type Server struct { } func New() (s *Server, err error) { + // Initialize telemetry when the server is created. + if err = telemetry.Setup(context.Background()); err != nil { + return nil, fmt.Errorf("could not initialize telemetry: %w", err) + } + // Create the server instance. s = &Server{errc: make(chan error, 1)} @@ -146,6 +152,11 @@ func (s *Server) Shutdown() (err error) { err = errors.Join(serr, fmt.Errorf("could not shutdown server: %w", serr)) } + // Shutdown and flush telemetry + if err = telemetry.Shutdown(ctx); err != nil { + err = errors.Join(err, fmt.Errorf("could not shutdown telemetry: %w", err)) + } + rlog.Debug("server shutdown", slog.Any("error", err)) return err }