From 69f360ac28817a9d58cd16b37169cc78bec7efc9 Mon Sep 17 00:00:00 2001 From: Gerry Agbobada <10496163+gagbo@users.noreply.github.com> Date: Mon, 5 Jun 2023 17:16:26 +0200 Subject: [PATCH] Add net/http middleware library (#55) * refactor: use vanilla context for all autometrics values The change makes autometrics easier to plug into any API/external packages, as the functions to manipulate the values in an autometrics context only take "context.Context" as argument now. * Add net/http middleware * Capture the function name in the middleware * Slim down README * Make the go:generate cookie stand out * refactor!: change import paths to be more idiomatic The change includes removing `pkg` from the path, and also providing a package name that does not need renaming/aliasing for the common usecase. This is a stopgap measure until a vanity URL can be used. --- CHANGELOG.md | 10 + README.md | 144 +++++--- examples/otel/cmd/main.go | 2 +- examples/web/cmd/main.go | 29 +- examples/web/cmd/main.go.orig | 22 +- internal/autometrics/prometheus_link_gen.go | 2 +- internal/generate/defer_test.go | 60 ++-- internal/generate/generate.go | 8 +- internal/generate/generate_test.go | 56 ++-- otel/autometrics/ctx.go | 46 +++ .../otel => otel/autometrics}/doc.go | 4 +- otel/autometrics/instrument.go | 117 +++++++ .../otel => otel/autometrics}/otel.go | 2 +- .../otel => otel/autometrics}/utils.go | 2 +- otel/midhttp/http.go | 38 +++ pkg/autometrics/ctx.go | 307 +++++++++++++----- pkg/autometrics/ctx_opt.go | 98 ++++++ pkg/autometrics/doc.go | 4 +- pkg/autometrics/instrument.go | 34 +- pkg/autometrics/middleware.go | 31 -- pkg/autometrics/otel/ctx.go | 99 ------ pkg/autometrics/otel/instrument.go | 110 ------- pkg/autometrics/prometheus/ctx.go | 99 ------ pkg/autometrics/prometheus/instrument.go | 128 -------- pkg/midhttp/middleware.go | 28 ++ prometheus/autometrics/ctx.go | 46 +++ .../autometrics}/doc.go | 4 +- prometheus/autometrics/instrument.go | 135 ++++++++ .../autometrics}/prometheus.go | 2 +- .../autometrics}/utils.go | 2 +- prometheus/midhttp/http.go | 38 +++ 31 files changed, 1000 insertions(+), 707 deletions(-) create mode 100644 otel/autometrics/ctx.go rename {pkg/autometrics/otel => otel/autometrics}/doc.go (61%) create mode 100644 otel/autometrics/instrument.go rename {pkg/autometrics/otel => otel/autometrics}/otel.go (98%) rename {pkg/autometrics/otel => otel/autometrics}/utils.go (85%) create mode 100644 otel/midhttp/http.go create mode 100644 pkg/autometrics/ctx_opt.go delete mode 100644 pkg/autometrics/middleware.go delete mode 100644 pkg/autometrics/otel/ctx.go delete mode 100644 pkg/autometrics/otel/instrument.go delete mode 100644 pkg/autometrics/prometheus/ctx.go delete mode 100644 pkg/autometrics/prometheus/instrument.go create mode 100644 pkg/midhttp/middleware.go create mode 100644 prometheus/autometrics/ctx.go rename {pkg/autometrics/prometheus => prometheus/autometrics}/doc.go (63%) create mode 100644 prometheus/autometrics/instrument.go rename {pkg/autometrics/prometheus => prometheus/autometrics}/prometheus.go (98%) rename {pkg/autometrics/prometheus => prometheus/autometrics}/utils.go (84%) create mode 100644 prometheus/midhttp/http.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 47d36c3..f282f75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ versioning](https://go.dev/doc/modules/version-numbers). to read the exemplars. - Added new options to context constructors to manipulate the tracing information. +- Add a middleware for `net/http` handlers ### Changed @@ -26,6 +27,15 @@ versioning](https://go.dev/doc/modules/version-numbers). present in the annotated function arguments, when relevant. - The Context constructor changed signature to allow inclusion of a parent context. +- Refactor imports to become more idiomatic. The imports changed as follows +```patch +import ( +- autometrics "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" ++ "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" +- middleware "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus/middleware/http" ++ "github.com/autometrics-dev/autometrics-go/prometheus/midhttp" +) +``` ### Deprecated diff --git a/README.md b/README.md index 8e8f010..2341faa 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ docker compose -f docker-compose.prometheus-example.yaml up And then explore the generated links by opening the [main file](./examples/web/cmd/main.go) in your editor. -## How to use +## Quickstart There is a one-time setup phase to prime the code for autometrics. Once this phase is accomplished, only calling `go generate` is necessary. @@ -38,6 +38,8 @@ to install it through go: go install github.com/autometrics-dev/autometrics-go/cmd/autometrics@latest ``` +
+ Make sure your `$PATH` is set up In order to have `autometrics` visible then, make sure that the directory `$GOBIN` (or the default `$GOPATH/bin`) is in your `$PATH`: @@ -45,6 +47,7 @@ In order to have `autometrics` visible then, make sure that the directory $ echo "$PATH" | grep -q "${GOBIN:-$GOPATH/bin}" && echo "GOBIN in PATH" || echo "GOBIN not in PATH, please add it" GOBIN in PATH ``` +
### Import the libraries and initialize the metrics @@ -52,46 +55,39 @@ In the main entrypoint of your program, you need to both add package ``` go import ( - autometrics "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) ``` And then in your main function initialize the metrics ``` go - // Everything in BuildInfo is optional. - // You can also use any string variable whose value is - // injected at build time by ldflags. + // Everything in BuildInfo is optional. It will add + // relevant information on the metrics for better intelligence. + // You can use any string variable whose value is injected at build time by ldflags for example. autometrics.Init( nil, autometrics.DefBuckets, - autometrics.BuildInfo{ - Version: "0.4.0", - Commit: "anySHA", - Branch: "", - }, + autometrics.BuildInfo{Version: "0.4.0", Commit: "anySHA", Branch: ""}, ) ``` -> **Warning** -> If you want to enable alerting from Autometrics, you **MUST** -have the `--latency-ms` values to match the values given in your buckets. The -values in the buckets are given in _seconds_. By default, the generator will -error and tell you the valid default values if they don't match. -If the default values do not match your use case, you can change the buckets in -the init call, and add a `--custom-latency` argument to the `//go:generate` invocation. +### Add cookies in your code -```patch --//go:generate autometrics -+//go:generate autometrics --custom-latency +On top of each file you want to use Autometrics in, you need to have a `go generate` cookie: + +``` go +//go:generate autometrics ``` -### Add cookies in your code +Then instrumenting functions depend on their signature: +
+For error-returning functions Given a starting function like: ```go -func RouteHandler(args interface{}) error { +func AddUser(args interface{}) error { // Do stuff return nil } @@ -100,38 +96,68 @@ func RouteHandler(args interface{}) error { The manual changes you need to do are: ```go -//go:generate autometrics - -//autometrics:doc -func RouteHandler(args interface{}) (err error) { // Name the error return value; this is an optional but recommended change +//autometrics:inst +func AddUser(args interface{}) (err error) { // Name the error return value; this is an optional but recommended change // Do stuff return nil } ``` -If you want the generated metrics to contain the function success rate, you +> **Warning** +> If you want the generated metrics to contain the function success rate, you _must_ name the error return value. This is why we recommend to name the error value you return for the function you want to instrument. +
-### Generate the documentation and instrumentation code +
+For HTTP handler functions +Autometrics comes with a middleware library for `net.http` handler functions. -Install the go generator using `go install` as usual: +- Import the middleware library -``` console -go install https://github.com/autometrics-dev/autometrics-go/cmd/autometrics +``` go +import "github.com/autometrics-dev/autometrics-go/prometheus/midhttp" ``` -Once you've done this, the `autometrics` generator takes care of the rest, and you can -simply call `go generate` with an optional environment variable: +- Wrap your handlers in `Autometrics` handler + +``` patch + +- http.Handle("/path", http.HandlerFunc(routeHandler)) ++ http.Handle("/path", midhttp.Autometrics( ++ http.HandlerFunc(routeHandler), ++ // Optional: override what is considered a success (default is 100-399) ++ autometrics.WithValidHttpCodes([]autometrics.ValidHttpRange{{Min: 200, Max: 299}}), ++ // Optional: Alerting rules ++ autometrics.WithSloName("API"), ++ autometrics.WithAlertSuccess(90), ++ )) +``` + +There is only middleware for `net/http` handlers for now, but support for other web frameworks will +come soon! +
+ +### Generate the documentation and instrumentation code + +You can now call `go generate`: ```console -$ AM_PROMETHEUS_URL=http://localhost:9090/ go generate ./... +$ go generate ./... ``` The generator will augment your doc comment to add quick links to metrics (using the Prometheus URL as base URL), and add a unique defer statement that will take care of instrumenting your code. +`autometrics --help` will show you all the different arguments that can control behaviour +through environment variables. + +
+Make the links point to specific Prometheus instances +By default, the generated links will point to `localhost:9090`, which the default location +of Prometheus when run locally. + The environment variable `AM_PROMETHEUS_URL` controls the base URL of the instance that is scraping the deployed version of your code. Having an environment variable means you can change the generated links without touching your code. The default value, if absent, @@ -139,16 +165,19 @@ is `http://localhost:9090/`. You can have any value here, the only adverse impact it can have is that the links in the doc comment might lead nowhere useful. +
### Expose metrics outside The last step now is to actually expose the generated metrics to the Prometheus instance. -For Prometheus the shortest way is to add the handler code in your main entrypoint: +
+Add a Prometheus handler to expose autometrics metrics +The shortest way is to add the handler code in your main entrypoint: ``` go import ( - autometrics "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" "github.com/prometheus/client_golang/prometheus/promhttp" ) @@ -157,11 +186,7 @@ func main() { autometrics.Init( nil, autometrics.DefBuckets, - autometrics.BuildInfo{ - Version: "0.4.0", - Commit: "anySHA", - Branch: "", - }, + autometrics.BuildInfo{Version: "0.4.0", Commit: "anySHA", Branch: ""}, ) http.Handle("/metrics", promhttp.Handler()) } @@ -169,14 +194,18 @@ func main() { This is the shortest way to initialize and expose the metrics that autometrics will use in the generated code. +
-### (OPTIONAL) Generate alerts automatically +A Prometheus server can be configured to poll the application, and the autometrics will be +available! (See the [Web App example](./examples/web) for a simple, complete setup) + +## (OPTIONAL) Generate alerts automatically Change the annotation of the function to automatically generate alerts for it: ``` go -//autometrics:doc --slo "Api" --success-target 90 -func RouteHandler(args interface{}) (err error) { +//autometrics:inst --slo "Api" --success-target 90 +func AddUser(args interface{}) (err error) { // Do stuff return nil } @@ -195,9 +224,24 @@ The valid arguments for alert generation are: latency options, or none. > **Warning** -> The generator will error out if you use targets that are not +> The generator will error out if you use percentile targets that are not supported by the bundled [Alerting rules file](./configs/shared/autometrics.rules.yml). Support for custom target is planned but not present at the moment + + +> **Warning** +> You **MUST** have the `--latency-ms` values to match the values + given in the buckets given in the `autometrics.Init` call. The values in the + buckets are given in _seconds_. By default, the generator will error and tell + you the valid default values if they don't match. If the default values in + `autometrics.DefBuckets` do not match your use case, you can change the + buckets in the init call, and add a `--custom-latency` argument to the + `//go:generate` invocation. +```patch +-//go:generate autometrics ++//go:generate autometrics --custom-latency +``` + ## (OPTIONAL) OpenTelemetry Support @@ -207,8 +251,8 @@ Prometheus to publish the metrics. The changes you need to make are: - change where the `autometrics` import points to ```patch import ( -- autometrics "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" -+ autometrics "github.com/autometrics-dev/autometrics-go/pkg/autometrics/otel" +- "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ++ "github.com/autometrics-dev/autometrics-go/otel/autometrics" ) ``` - change the call to `autometrics.Init` to the new signature: instead of a registry, @@ -220,11 +264,7 @@ metric. You can use the name of the application or its version for example - nil, + "myApp/v2/prod", autometrics.DefBuckets, - autometrics.BuildInfo{ - Version: "2.1.37", - Commit: "anySHA", - Branch: "", - }, + autometrics.BuildInfo{ Version: "2.1.37", Commit: "anySHA", Branch: "" }, ) ``` diff --git a/examples/otel/cmd/main.go b/examples/otel/cmd/main.go index 7ff0cc4..dbedc51 100644 --- a/examples/otel/cmd/main.go +++ b/examples/otel/cmd/main.go @@ -8,7 +8,7 @@ import ( "net/http" "time" - autometrics "github.com/autometrics-dev/autometrics-go/pkg/autometrics/otel" + "github.com/autometrics-dev/autometrics-go/otel/autometrics" "github.com/prometheus/client_golang/prometheus/promhttp" ) diff --git a/examples/web/cmd/main.go b/examples/web/cmd/main.go index e6097e6..a854b4c 100644 --- a/examples/web/cmd/main.go +++ b/examples/web/cmd/main.go @@ -8,7 +8,8 @@ import ( "net/http" "time" - autometrics "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" + "github.com/autometrics-dev/autometrics-go/prometheus/midhttp" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) @@ -40,7 +41,13 @@ func main() { ) http.HandleFunc("/", errorable(indexHandler)) - http.HandleFunc("/random-error", errorable(randomErrorHandler)) + // Wrapping a route in Autometrics middleware + http.Handle("/random-error", midhttp.Autometrics( + http.HandlerFunc(randomErrorHandler), + autometrics.WithValidHttpCodes([]autometrics.ValidHttpRange{{Min: 200, Max: 299}}), + autometrics.WithSloName("API"), + autometrics.WithAlertSuccess(90), + )) http.Handle("/metrics", promhttp.HandlerFor( prometheus.DefaultGatherer, promhttp.HandlerOpts{ @@ -96,9 +103,7 @@ func indexHandler(w http.ResponseWriter, r *http.Request) error { fmt.Fprintf(w, "Hello, World!\n") - err := randomErrorHandler(w, r) - - return err + return nil } var handlerError = errors.New("failed to handle request") @@ -106,21 +111,11 @@ var handlerError = errors.New("failed to handle request") // randomErrorHandler handles the /random-error route. // // It returns an error around 90% of the time. -// -//autometrics:inst --no-doc --slo "API" --success-target 90 -func randomErrorHandler(w http.ResponseWriter, r *http.Request) (err error) { - defer autometrics.Instrument(autometrics.PreInstrument(autometrics.NewContext( - r.Context(), - autometrics.WithConcurrentCalls(true), - autometrics.WithCallerName(true), - autometrics.WithSloName("API"), - autometrics.WithAlertSuccess(90), - )), &err) //autometrics:defer - +func randomErrorHandler(w http.ResponseWriter, r *http.Request) { isOk := rand.Intn(10) == 0 if !isOk { - err = handlerError + http.Error(w, handlerError.Error(), http.StatusInternalServerError) } else { w.WriteHeader(http.StatusOK) } diff --git a/examples/web/cmd/main.go.orig b/examples/web/cmd/main.go.orig index cf59db1..3d1e093 100644 --- a/examples/web/cmd/main.go.orig +++ b/examples/web/cmd/main.go.orig @@ -8,7 +8,8 @@ import ( "net/http" "time" - autometrics "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" + "github.com/autometrics-dev/autometrics-go/prometheus/midhttp" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) @@ -40,7 +41,12 @@ func main() { ) http.HandleFunc("/", errorable(indexHandler)) - http.HandleFunc("/random-error", errorable(randomErrorHandler)) + // Wrapping a route in Autometrics middleware + http.Handle("/random-error", midhttp.Autometrics( + http.HandlerFunc(randomErrorHandler), + autometrics.WithSloName("API"), + autometrics.WithAlertSuccess(90), + )) http.Handle("/metrics", promhttp.HandlerFor( prometheus.DefaultGatherer, promhttp.HandlerOpts{ @@ -60,11 +66,9 @@ func indexHandler(w http.ResponseWriter, r *http.Request) error { time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond) - _, err := fmt.Fprintf(w, "Hello, World!\n") + fmt.Fprintf(w, "Hello, World!\n") - err := randomErrorHandler(w, r) - - return err + return nil } var handlerError = errors.New("failed to handle request") @@ -72,13 +76,11 @@ var handlerError = errors.New("failed to handle request") // randomErrorHandler handles the /random-error route. // // It returns an error around 90% of the time. -// -//autometrics:inst --no-doc --slo "API" --success-target 90 -func randomErrorHandler(w http.ResponseWriter, r *http.Request) (err error) { +func randomErrorHandler(w http.ResponseWriter, r *http.Request) { isOk := rand.Intn(10) == 0 if !isOk { - err = handlerError + http.Error(w, handlerError.Error(), http.StatusInternalServerError) } else { w.WriteHeader(http.StatusOK) } diff --git a/internal/autometrics/prometheus_link_gen.go b/internal/autometrics/prometheus_link_gen.go index d6f99e8..32691aa 100644 --- a/internal/autometrics/prometheus_link_gen.go +++ b/internal/autometrics/prometheus_link_gen.go @@ -4,7 +4,7 @@ import ( "fmt" "net/url" - "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prometheus "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) type AutometricsLinkCommentGenerator interface { diff --git a/internal/generate/defer_test.go b/internal/generate/defer_test.go index 4a366ba..9b215d8 100644 --- a/internal/generate/defer_test.go +++ b/internal/generate/defer_test.go @@ -18,7 +18,7 @@ package main import ( "context" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -35,7 +35,7 @@ func main(thisIsAContext context.Context) { "import (\n" + "\t\"context\"\n" + "\n" + - "\tprom \"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"\n" + + "\tprom \"github.com/autometrics-dev/autometrics-go/prometheus/autometrics\"\n" + ")\n" + "\n" + "// This comment is associated with the main function.\n" + @@ -75,7 +75,7 @@ package main import ( vanilla "context" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -92,7 +92,7 @@ func main(thisIsAContext vanilla.Context) { "import (\n" + "\tvanilla \"context\"\n" + "\n" + - "\tprom \"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"\n" + + "\tprom \"github.com/autometrics-dev/autometrics-go/prometheus/autometrics\"\n" + ")\n" + "\n" + "// This comment is associated with the main function.\n" + @@ -132,7 +132,7 @@ package main import ( . "context" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -149,7 +149,7 @@ func main(thisIsAContext Context) { "import (\n" + "\t. \"context\"\n" + "\n" + - "\tprom \"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"\n" + + "\tprom \"github.com/autometrics-dev/autometrics-go/prometheus/autometrics\"\n" + ")\n" + "\n" + "// This comment is associated with the main function.\n" + @@ -189,7 +189,7 @@ package main import ( "net/http" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -206,7 +206,7 @@ func main(w http.ResponseWriter, req *http.Request) { "import (\n" + "\t\"net/http\"\n" + "\n" + - "\tprom \"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"\n" + + "\tprom \"github.com/autometrics-dev/autometrics-go/prometheus/autometrics\"\n" + ")\n" + "\n" + "// This comment is associated with the main function.\n" + @@ -246,7 +246,7 @@ package main import ( vanilla "net/http" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -263,7 +263,7 @@ func main(w vanilla.ResponseWriter, req *vanilla.Request) { "import (\n" + "\tvanilla \"net/http\"\n" + "\n" + - "\tprom \"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"\n" + + "\tprom \"github.com/autometrics-dev/autometrics-go/prometheus/autometrics\"\n" + ")\n" + "\n" + "// This comment is associated with the main function.\n" + @@ -303,7 +303,7 @@ package main import ( . "net/http" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -320,7 +320,7 @@ func main(w ResponseWriter, req *Request) { "import (\n" + "\t. \"net/http\"\n" + "\n" + - "\tprom \"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"\n" + + "\tprom \"github.com/autometrics-dev/autometrics-go/prometheus/autometrics\"\n" + ")\n" + "\n" + "// This comment is associated with the main function.\n" + @@ -360,7 +360,7 @@ package main import ( "github.com/gobuffalo/buffalo" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -377,7 +377,7 @@ func main(thisIsAContext buffalo.Context) { "import (\n" + "\t\"github.com/gobuffalo/buffalo\"\n" + "\n" + - "\tprom \"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"\n" + + "\tprom \"github.com/autometrics-dev/autometrics-go/prometheus/autometrics\"\n" + ")\n" + "\n" + "// This comment is associated with the main function.\n" + @@ -417,7 +417,7 @@ package main import ( vanilla "github.com/gobuffalo/buffalo" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -434,7 +434,7 @@ func main(thisIsAContext vanilla.Context) { "import (\n" + "\tvanilla \"github.com/gobuffalo/buffalo\"\n" + "\n" + - "\tprom \"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"\n" + + "\tprom \"github.com/autometrics-dev/autometrics-go/prometheus/autometrics\"\n" + ")\n" + "\n" + "// This comment is associated with the main function.\n" + @@ -474,7 +474,7 @@ package main import ( . "github.com/gobuffalo/buffalo" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -491,7 +491,7 @@ func main(thisIsAContext Context) { "import (\n" + "\t. \"github.com/gobuffalo/buffalo\"\n" + "\n" + - "\tprom \"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"\n" + + "\tprom \"github.com/autometrics-dev/autometrics-go/prometheus/autometrics\"\n" + ")\n" + "\n" + "// This comment is associated with the main function.\n" + @@ -531,7 +531,7 @@ package main import ( "github.com/labstack/echo/v4" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -548,7 +548,7 @@ func main(thisIsAContext echo.Context) { "import (\n" + "\t\"github.com/labstack/echo/v4\"\n" + "\n" + - "\tprom \"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"\n" + + "\tprom \"github.com/autometrics-dev/autometrics-go/prometheus/autometrics\"\n" + ")\n" + "\n" + "// This comment is associated with the main function.\n" + @@ -590,7 +590,7 @@ package main import ( vanilla "github.com/labstack/echo/v4" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -607,7 +607,7 @@ func main(thisIsAContext vanilla.Context) { "import (\n" + "\tvanilla \"github.com/labstack/echo/v4\"\n" + "\n" + - "\tprom \"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"\n" + + "\tprom \"github.com/autometrics-dev/autometrics-go/prometheus/autometrics\"\n" + ")\n" + "\n" + "// This comment is associated with the main function.\n" + @@ -649,7 +649,7 @@ package main import ( . "github.com/labstack/echo/v4" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -666,7 +666,7 @@ func main(thisIsAContext Context) { "import (\n" + "\t. \"github.com/labstack/echo/v4\"\n" + "\n" + - "\tprom \"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"\n" + + "\tprom \"github.com/autometrics-dev/autometrics-go/prometheus/autometrics\"\n" + ")\n" + "\n" + "// This comment is associated with the main function.\n" + @@ -708,7 +708,7 @@ package main import ( "github.com/gin-gonic/gin" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -725,7 +725,7 @@ func main(thisIsAContext *gin.Context) { "import (\n" + "\t\"github.com/gin-gonic/gin\"\n" + "\n" + - "\tprom \"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"\n" + + "\tprom \"github.com/autometrics-dev/autometrics-go/prometheus/autometrics\"\n" + ")\n" + "\n" + "// This comment is associated with the main function.\n" + @@ -767,7 +767,7 @@ package main import ( vanilla "github.com/gin-gonic/gin" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -784,7 +784,7 @@ func main(thisIsAContext *vanilla.Context) { "import (\n" + "\tvanilla \"github.com/gin-gonic/gin\"\n" + "\n" + - "\tprom \"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"\n" + + "\tprom \"github.com/autometrics-dev/autometrics-go/prometheus/autometrics\"\n" + ")\n" + "\n" + "// This comment is associated with the main function.\n" + @@ -826,7 +826,7 @@ package main import ( . "github.com/gin-gonic/gin" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -843,7 +843,7 @@ func main(thisIsAContext *Context) { "import (\n" + "\t. \"github.com/gin-gonic/gin\"\n" + "\n" + - "\tprom \"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"\n" + + "\tprom \"github.com/autometrics-dev/autometrics-go/prometheus/autometrics\"\n" + ")\n" + "\n" + "// This comment is associated with the main function.\n" + diff --git a/internal/generate/generate.go b/internal/generate/generate.go index 1064423..806d71f 100644 --- a/internal/generate/generate.go +++ b/internal/generate/generate.go @@ -23,8 +23,8 @@ const ( LatencyObjArgument = "--latency-target" NoDocArgument = "--no-doc" - AmPromPackage = "\"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"" - AmOtelPackage = "\"github.com/autometrics-dev/autometrics-go/pkg/autometrics/otel\"" + AmPromPackage = "\"github.com/autometrics-dev/autometrics-go/prometheus/autometrics\"" + AmOtelPackage = "\"github.com/autometrics-dev/autometrics-go/otel/autometrics\"" ) // TransformFile takes a file path and generates the documentation @@ -82,7 +82,7 @@ func GenerateDocumentationAndInstrumentation(ctx internal.GeneratorContext, sour if importSpec.Name != nil { ctx.FuncCtx.ImplImportName = importSpec.Name.Name } else { - ctx.FuncCtx.ImplImportName = "prometheus" + ctx.FuncCtx.ImplImportName = "autometrics" } } } @@ -92,7 +92,7 @@ func GenerateDocumentationAndInstrumentation(ctx internal.GeneratorContext, sour if importSpec.Name != nil { ctx.FuncCtx.ImplImportName = importSpec.Name.Name } else { - ctx.FuncCtx.ImplImportName = "otel" + ctx.FuncCtx.ImplImportName = "autometrics" } } } diff --git a/internal/generate/generate_test.go b/internal/generate/generate_test.go index e678fb6..971e9b7 100644 --- a/internal/generate/generate_test.go +++ b/internal/generate/generate_test.go @@ -22,7 +22,7 @@ func TestCommentDirective(t *testing.T) { package main import ( - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -37,7 +37,7 @@ func main() { "package main\n" + "\n" + "import (\n" + - "\tprom \"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"\n" + + "\tprom \"github.com/autometrics-dev/autometrics-go/prometheus/autometrics\"\n" + ")\n" + "\n" + "// This comment is associated with the main function.\n" + @@ -101,7 +101,7 @@ func TestCommentRefresh(t *testing.T) { package main import ( - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -122,7 +122,7 @@ func main() { "package main\n" + "\n" + "import (\n" + - "\tprom \"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"\n" + + "\tprom \"github.com/autometrics-dev/autometrics-go/prometheus/autometrics\"\n" + ")\n" + "\n" + "// This comment is associated with the main function.\n" + @@ -186,7 +186,7 @@ func TestCommentDelete(t *testing.T) { sourceCode := `// This is the package comment. package main -import "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" +import "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" // This comment is associated with the main function. // @@ -206,17 +206,17 @@ func main() { "package main\n" + "\n" + "import " + - "\"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"\n" + + "\"github.com/autometrics-dev/autometrics-go/prometheus/autometrics\"\n" + "\n" + "// This comment is associated with the main function.\n" + "//autometrics:inst --no-doc --slo \"API\" --latency-target 99.9 --latency-ms 500\n" + "func main() {\n" + - "\tdefer prometheus.Instrument(prometheus.PreInstrument(prometheus.NewContext(\n" + + "\tdefer autometrics.Instrument(autometrics.PreInstrument(autometrics.NewContext(\n" + "\t\tnil,\n" + - "\t\tprometheus.WithConcurrentCalls(true),\n" + - "\t\tprometheus.WithCallerName(true),\n" + - "\t\tprometheus.WithSloName(\"API\"),\n" + - "\t\tprometheus.WithAlertLatency(500000000*time.Nanosecond, 99.9),\n" + + "\t\tautometrics.WithConcurrentCalls(true),\n" + + "\t\tautometrics.WithCallerName(true),\n" + + "\t\tautometrics.WithSloName(\"API\"),\n" + + "\t\tautometrics.WithAlertLatency(500000000*time.Nanosecond, 99.9),\n" + "\t)), nil) //autometrics:defer\n" + "\n" + " fmt.Println(hello) // line comment 3\n" + @@ -245,7 +245,7 @@ package main import ( "github.com/autometrics-dev/autometrics-go/pkg/autometrics" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -267,7 +267,7 @@ func main() { "\n" + "import (\n" + "\t\"github.com/autometrics-dev/autometrics-go/pkg/autometrics\"\n" + - "\tprom \"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"\n" + + "\tprom \"github.com/autometrics-dev/autometrics-go/prometheus/autometrics\"\n" + ")\n" + "\n" + "// This comment is associated with the main function.\n" + @@ -304,7 +304,7 @@ package main import ( "github.com/autometrics-dev/autometrics-go/pkg/autometrics" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -327,7 +327,7 @@ package main import ( "github.com/autometrics-dev/autometrics-go/pkg/autometrics" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -349,7 +349,7 @@ package main import ( "github.com/autometrics-dev/autometrics-go/pkg/autometrics" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -375,7 +375,7 @@ package main import ( "github.com/autometrics-dev/autometrics-go/pkg/autometrics" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -398,7 +398,7 @@ package main import ( "github.com/autometrics-dev/autometrics-go/pkg/autometrics" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -421,7 +421,7 @@ package main import ( "github.com/autometrics-dev/autometrics-go/pkg/autometrics" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -444,7 +444,7 @@ package main import ( "github.com/autometrics-dev/autometrics-go/pkg/autometrics" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -467,7 +467,7 @@ package main import ( "github.com/autometrics-dev/autometrics-go/pkg/autometrics" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -490,7 +490,7 @@ package main import ( "github.com/autometrics-dev/autometrics-go/pkg/autometrics" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -513,7 +513,7 @@ package main import ( "github.com/autometrics-dev/autometrics-go/pkg/autometrics" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -548,7 +548,7 @@ package main import ( "github.com/autometrics-dev/autometrics-go/pkg/autometrics" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -572,7 +572,7 @@ package main import ( "github.com/autometrics-dev/autometrics-go/pkg/autometrics" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -596,7 +596,7 @@ package main import ( "github.com/autometrics-dev/autometrics-go/pkg/autometrics" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -622,7 +622,7 @@ package main import ( "github.com/autometrics-dev/autometrics-go/pkg/autometrics" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. @@ -648,7 +648,7 @@ package main import ( "github.com/autometrics-dev/autometrics-go/pkg/autometrics" - prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" ) // This comment is associated with the main function. diff --git a/otel/autometrics/ctx.go b/otel/autometrics/ctx.go new file mode 100644 index 0000000..ec9ad65 --- /dev/null +++ b/otel/autometrics/ctx.go @@ -0,0 +1,46 @@ +package autometrics // import "github.com/autometrics-dev/autometrics-go/otel/autometrics" + +import ( + "context" + "time" + + "github.com/autometrics-dev/autometrics-go/pkg/autometrics" +) + +type ValidHttpRange = autometrics.InclusiveIntRange + +func NewContext(ctx context.Context, opts ...autometrics.Option) context.Context { + return autometrics.NewContextWithOpts(ctx, opts...) +} + +func WithTraceID(tid []byte) autometrics.Option { + return autometrics.WithTraceID(tid) +} + +func WithSpanID(sid []byte) autometrics.Option { + return autometrics.WithSpanID(sid) +} + +func WithAlertLatency(target time.Duration, objective float64) autometrics.Option { + return autometrics.WithAlertLatency(target, objective) +} + +func WithAlertSuccess(objective float64) autometrics.Option { + return autometrics.WithAlertSuccess(objective) +} + +func WithSloName(name string) autometrics.Option { + return autometrics.WithSloName(name) +} + +func WithConcurrentCalls(enabled bool) autometrics.Option { + return autometrics.WithConcurrentCalls(enabled) +} + +func WithCallerName(enabled bool) autometrics.Option { + return autometrics.WithCallerName(enabled) +} + +func WithValidHttpCodes(ranges []ValidHttpRange) autometrics.Option { + return autometrics.WithValidHttpCodes(ranges) +} diff --git a/pkg/autometrics/otel/doc.go b/otel/autometrics/doc.go similarity index 61% rename from pkg/autometrics/otel/doc.go rename to otel/autometrics/doc.go index 35d99a2..4e92554 100644 --- a/pkg/autometrics/otel/doc.go +++ b/otel/autometrics/doc.go @@ -1,8 +1,8 @@ -// Package otel implements the automatic metric registration and collection for autometrics using [OpenTelemetry metrics] and a Prometheus exporter. +// Package autometrics implements the automatic metric registration and collection for autometrics using [OpenTelemetry metrics] and a Prometheus exporter. // // The package contains the function implementations for the generated calls, see // the main project's [Readme] for more detail. // // [Readme]: https://github.com/autometrics-dev/autometrics-go // [OpenTelemetry metrics]: https://opentelemetry.io/docs/instrumentation/go/ -package otel +package autometrics diff --git a/otel/autometrics/instrument.go b/otel/autometrics/instrument.go new file mode 100644 index 0000000..da2c835 --- /dev/null +++ b/otel/autometrics/instrument.go @@ -0,0 +1,117 @@ +package autometrics // import "github.com/autometrics-dev/autometrics-go/otel/autometrics" + +import ( + "context" + "fmt" + "strconv" + "time" + + am "github.com/autometrics-dev/autometrics-go/pkg/autometrics" + "go.opentelemetry.io/otel/attribute" +) + +// Instrument called in a defer statement wraps the body of a function +// with automatic instrumentation. +// +// The first argument SHOULD be a call to PreInstrument so that +// the "concurrent calls" gauge is correctly setup. +func Instrument(ctx context.Context, err *error) { + result := "ok" + + if err != nil && *err != nil { + result = "error" + } + + var callerLabel, sloName, latencyTarget, latencyObjective, successObjective string + + callInfo := am.GetCallInfo(ctx) + buildInfo := am.GetBuildInfo(ctx) + slo := am.GetAlertConfiguration(ctx) + + if am.GetTrackCallerName(ctx) { + callerLabel = fmt.Sprintf("%s.%s", callInfo.ParentModuleName, callInfo.ParentFuncName) + } + + if slo.ServiceName != "" { + sloName = slo.ServiceName + + if slo.Latency != nil { + latencyTarget = strconv.FormatFloat(slo.Latency.Target.Seconds(), 'f', -1, 64) + latencyObjective = strconv.FormatFloat(slo.Latency.Objective, 'f', -1, 64) + } + + if slo.Success != nil { + successObjective = strconv.FormatFloat(slo.Success.Objective, 'f', -1, 64) + } + } + + functionCallsCount.Add(ctx, 1, + []attribute.KeyValue{ + attribute.Key(FunctionLabel).String(callInfo.FuncName), + attribute.Key(ModuleLabel).String(callInfo.ModuleName), + attribute.Key(CallerLabel).String(callerLabel), + attribute.Key(ResultLabel).String(result), + attribute.Key(TargetSuccessRateLabel).String(successObjective), + attribute.Key(SloNameLabel).String(sloName), + attribute.Key(CommitLabel).String(buildInfo.Commit), + attribute.Key(VersionLabel).String(buildInfo.Version), + attribute.Key(BranchLabel).String(buildInfo.Branch), + }...) + functionCallsDuration.Record(ctx, time.Since(am.GetStartTime(ctx)).Seconds(), + []attribute.KeyValue{ + attribute.Key(FunctionLabel).String(callInfo.FuncName), + attribute.Key(ModuleLabel).String(callInfo.ModuleName), + attribute.Key(CallerLabel).String(callerLabel), + attribute.Key(TargetLatencyLabel).String(latencyTarget), + attribute.Key(TargetSuccessRateLabel).String(latencyObjective), + attribute.Key(SloNameLabel).String(sloName), + attribute.Key(CommitLabel).String(buildInfo.Commit), + attribute.Key(VersionLabel).String(buildInfo.Version), + attribute.Key(BranchLabel).String(buildInfo.Branch), + }...) + + if am.GetTrackConcurrentCalls(ctx) { + functionCallsConcurrent.Add(ctx, -1, + []attribute.KeyValue{ + attribute.Key(FunctionLabel).String(callInfo.FuncName), + attribute.Key(ModuleLabel).String(callInfo.ModuleName), + attribute.Key(CallerLabel).String(callerLabel), + attribute.Key(CommitLabel).String(buildInfo.Commit), + attribute.Key(VersionLabel).String(buildInfo.Version), + attribute.Key(BranchLabel).String(buildInfo.Branch), + }...) + } +} + +// PreInstrument runs the "before wrappee" part of instrumentation. +// +// It is meant to be called as the first argument to Instrument in a +// defer call. +func PreInstrument(ctx context.Context) context.Context { + callInfo := am.CallerInfo() + ctx = am.SetCallInfo(ctx, callInfo) + ctx = am.FillBuildInfo(ctx) + ctx = am.FillTracingInfo(ctx) + + var callerLabel string + if am.GetTrackCallerName(ctx) { + callerLabel = fmt.Sprintf("%s.%s", callInfo.ParentModuleName, callInfo.ParentFuncName) + } + + if am.GetTrackConcurrentCalls(ctx) { + buildInfo := am.GetBuildInfo(ctx) + functionCallsConcurrent.Add(ctx, 1, + []attribute.KeyValue{ + attribute.Key(FunctionLabel).String(callInfo.FuncName), + attribute.Key(ModuleLabel).String(callInfo.ModuleName), + attribute.Key(CallerLabel).String(callerLabel), + attribute.Key(CommitLabel).String(buildInfo.Commit), + attribute.Key(VersionLabel).String(buildInfo.Version), + attribute.Key(BranchLabel).String(buildInfo.Branch), + }...) + } + + ctx = am.SetStartTime(ctx, time.Now()) + + return ctx +} diff --git a/pkg/autometrics/otel/otel.go b/otel/autometrics/otel.go similarity index 98% rename from pkg/autometrics/otel/otel.go rename to otel/autometrics/otel.go index cb3ff29..c7d043b 100644 --- a/pkg/autometrics/otel/otel.go +++ b/otel/autometrics/otel.go @@ -1,4 +1,4 @@ -package otel // import "github.com/autometrics-dev/autometrics-go/pkg/autometrics/otel" +package autometrics // import "github.com/autometrics-dev/autometrics-go/otel/autometrics" import ( "context" diff --git a/pkg/autometrics/otel/utils.go b/otel/autometrics/utils.go similarity index 85% rename from pkg/autometrics/otel/utils.go rename to otel/autometrics/utils.go index d52cbfd..ec0f563 100644 --- a/pkg/autometrics/otel/utils.go +++ b/otel/autometrics/utils.go @@ -1,4 +1,4 @@ -package otel // import "github.com/autometrics-dev/autometrics-go/pkg/autometrics/otel" +package autometrics // import "github.com/autometrics-dev/autometrics-go/otel/autometrics" import ( "context" diff --git a/otel/midhttp/http.go b/otel/midhttp/http.go new file mode 100644 index 0000000..d646d43 --- /dev/null +++ b/otel/midhttp/http.go @@ -0,0 +1,38 @@ +package midhttp // import "github.com/autometrics-dev/autometrics-go/otel/midhttp" + +import ( + "errors" + "net/http" + + otel "github.com/autometrics-dev/autometrics-go/otel/autometrics" + am "github.com/autometrics-dev/autometrics-go/pkg/autometrics" + mid "github.com/autometrics-dev/autometrics-go/pkg/midhttp" +) + +func Autometrics(next http.HandlerFunc, opts ...am.Option) http.HandlerFunc { + fn := func(rw http.ResponseWriter, r *http.Request) { + arw := mid.NewResponseWriter(rw) + ctx := otel.PreInstrument(otel.NewContext(r.Context(), opts...)) + + // Compute then set the function name and module name labels + ctx = am.SetCallInfo(ctx, am.ReflectFunctionModuleName(next)) + + err := errors.New("Unfinished handler") + + defer otel.Instrument(ctx, &err) + + r = r.WithContext(ctx) + next.ServeHTTP(arw, r) + + // Check the status code of the handler to reset the error before the Instrument deferred call + ranges := am.GetValidHttpCodeRanges(ctx) + for _, codeRange := range ranges { + if codeRange.Contains(arw.CurrentStatusCode()) { + err = nil + break + } + } + } + + return http.HandlerFunc(fn) +} diff --git a/pkg/autometrics/ctx.go b/pkg/autometrics/ctx.go index cb5c59b..1d8e02b 100644 --- a/pkg/autometrics/ctx.go +++ b/pkg/autometrics/ctx.go @@ -12,6 +12,13 @@ const ( currentTraceId contextKey = iota currentSpanId parentSpanId + trackConcurrentCalls + trackCallerName + alertConfiguration + startTime + callInfo + buildInfo + validHttpCodeRanges ) var randSource *rand.Rand @@ -22,108 +29,198 @@ type TraceID [16]byte // Open Telemetry-compatible span ID type SpanID [8]byte -// Context holds the configuration -// to instrument properly a function. -// -// This can be viewed as a context for the instrumentation calls -type Context struct { - // Embedded context of the currently instrumented function. - // - // This allows the context to be passed around wherever a [context.Context] is expected. - context.Context - // TrackConcurrentCalls triggers the collection of the gauge for concurrent calls of the function. - TrackConcurrentCalls bool - // TrackCallerName adds a label with the caller name in all the collected metrics. - TrackCallerName bool - // AlertConf is an optional configuration to add alerting capabilities to the metrics. - AlertConf *AlertConfiguration - // StartTime is the start time of a single function execution. - // Only amImpl.Instrument should read this value. - // Only amImpl.PreInstrument should write this value. - // - // (amImpl is either the [Prometheus] or the [Open Telemetry] implementation) - // - // This value is only exported for the child packages [Prometheus] and [Open Telemetry] - // - // [Prometheus]: https://godoc.org/github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus - // [Open Telemetry]: https://godoc.org/github.com/autometrics-dev/autometrics-go/pkg/autometrics/otel - StartTime time.Time - // CallInfo contains all the relevant data for caller information. - // Only amImpl.Instrument should read this value. - // Only amImpl.PreInstrument should write/read this value. - // - // (amImpl is either the [Prometheus] or the [Open Telemetry] implementation) - // - // This value is only exported for the child packages [Prometheus] and [Open Telemetry] - // - // [Prometheus]: https://godoc.org/github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus - // [Open Telemetry]: https://godoc.org/github.com/autometrics-dev/autometrics-go/pkg/autometrics/otel - CallInfo CallInfo - // BuildInfo contains all the relevant data for caller information. - // Only amImpl.Instrument and PreInstrument should read this value. - // Only amImpl.Init should write/read this value. - // - // (amImpl is either the [Prometheus] or the [Open Telemetry] implementation) - // - // This value is only exported for the child packages [Prometheus] and [Open Telemetry] - // - // [Prometheus]: https://godoc.org/github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus - // [Open Telemetry]: https://godoc.org/github.com/autometrics-dev/autometrics-go/pkg/autometrics/otel - BuildInfo BuildInfo -} - // NewContext is a constructor taking the parent context as argument. // // It accepts 'nil' as the parent context. In this case the constructor // acts as if it received a new, fresh context.Background(). -func NewContext(parentCtx context.Context) Context { +func NewContext(parentCtx context.Context) context.Context { if parentCtx == nil { parentCtx = context.Background() } - return Context{ - TrackConcurrentCalls: true, - TrackCallerName: true, - AlertConf: nil, - Context: parentCtx, + ctx := SetTrackConcurrentCalls(parentCtx, true) + ctx = SetTrackCallerName(ctx, true) + ctx = SetValidHttpCodeRanges(ctx, []InclusiveIntRange{{Min: 100, Max: 399}}) + return ctx +} + +// SetTrackConcurrentCalls sets a flag in the context deciding whether to track how many concurrent calls the instrumented functions observe. +// +// TrackConcurrentCalls triggers the collection of the gauge for concurrent calls of the function. +// The flag defaults to true. +func SetTrackConcurrentCalls(ctx context.Context, track bool) context.Context { + return context.WithValue(ctx, trackConcurrentCalls, track) +} + +// GetTrackConcurrentCalls returns whether autometrics should track how many concurrent calls the instrumented function observe. +// +// TrackConcurrentCalls triggers the collection of the gauge for concurrent calls of the function. +// It defaults to true. +func GetTrackConcurrentCalls(c context.Context) bool { + if c == nil { + return true } + + track, ok := c.Value(trackConcurrentCalls).(bool) + if !ok { + return true + } + + return track } -// SetTraceID sets the context's [TraceID] -func (ctx *Context) SetTraceID(tid TraceID) { - ctx.Context = context.WithValue(ctx.Context, currentTraceId, tid) +// SetTrackCallerName sets a flag in the context deciding whether to track the names of the callers of instrumented functions. +// +// TrackCallerName adds a label with the caller name in all the collected metrics. +// The flag defaults to true. +func SetTrackCallerName(ctx context.Context, track bool) context.Context { + return context.WithValue(ctx, trackCallerName, track) } -// SetSpanID sets the context's [SpanID] -func (ctx *Context) SetSpanID(sid SpanID) { - ctx.Context = context.WithValue(ctx.Context, currentSpanId, sid) +// GetTrackCallerName returns default information if the context did not contain any build information. +// +// TrackCallerName adds a label with the caller name in all the collected metrics. +// It defaults to true. +func GetTrackCallerName(c context.Context) bool { + if c == nil { + return true + } + + track, ok := c.Value(trackCallerName).(bool) + if !ok { + return true + } + + return track } -// SetParentSpanID sets the context's span's parent [SpanID] -func (ctx *Context) SetParentSpanID(sid SpanID) { - ctx.Context = context.WithValue(ctx.Context, parentSpanId, sid) +// SetAlertConfiguration sets the context's [AlertConfiguration] +// +// AlertConfiguration is an optional configuration to add alerting capabilities to the metrics. +func SetAlertConfiguration(ctx context.Context, slo AlertConfiguration) context.Context { + return context.WithValue(ctx, alertConfiguration, slo) +} + +// GetAlertConfiguration returns default information if the context did not contain any alerting configuration. +// +// AlertConfiguration is an optional configuration to add alerting capabilities to the metrics. +func GetAlertConfiguration(c context.Context) AlertConfiguration { + if c == nil { + return AlertConfiguration{} + } + + slo, ok := c.Value(alertConfiguration).(AlertConfiguration) + if !ok { + return AlertConfiguration{} + } + + return slo +} + +// SetCallInfo sets the context's [CallInfo] +// +// CallInfo contains all the relevant data for caller information. +func SetCallInfo(ctx context.Context, build CallInfo) context.Context { + return context.WithValue(ctx, callInfo, build) +} + +// GetCallInfo returns default information if the context did not contain any build information. +// +// CallInfo contains all the relevant data for caller information. +func GetCallInfo(c context.Context) CallInfo { + if c == nil { + return CallInfo{} + } + + build, ok := c.Value(callInfo).(CallInfo) + if !ok { + return CallInfo{} + } + + return build +} + +// SetStartTime sets the context's [StartTime] +// +// StartTime is the start time of a single function execution. +func SetStartTime(ctx context.Context, startTime time.Time) context.Context { + return context.WithValue(ctx, startTime, startTime) +} + +// GetStartTime returns default current time if the context did not contain any start time. +// +// StartTime is the start time of a single function execution. +func GetStartTime(c context.Context) time.Time { + if c == nil { + return time.Now() + } + + startTime, ok := c.Value(startTime).(time.Time) + if !ok { + return time.Now() + } + + return startTime +} + +// SetBuildInfo sets the context's [BuildInfo] +// +// BuildInfo contains all the relevant data for caller information. +func SetBuildInfo(ctx context.Context, build BuildInfo) context.Context { + return context.WithValue(ctx, buildInfo, build) +} + +// GetBuildInfo returns default information if the context did not contain any build information. +// +// BuildInfo contains all the relevant data for caller information. +func GetBuildInfo(c context.Context) BuildInfo { + if c == nil { + return BuildInfo{} + } + + build, ok := c.Value(buildInfo).(BuildInfo) + if !ok { + return BuildInfo{} + } + + return build +} + +// SetTraceID sets the context's [TraceID] +func SetTraceID(ctx context.Context, tid TraceID) context.Context { + return context.WithValue(ctx, currentTraceId, tid) } // GetTraceID returns (_, false) if the context did not contain any trace id. -func (c Context) GetTraceID() (TraceID, bool) { - if c.Context == nil { +func GetTraceID(c context.Context) (TraceID, bool) { + if c == nil { return TraceID{}, false } tid, ok := c.Value(currentTraceId).(TraceID) return tid, ok } +// SetSpanID sets the context's [SpanID] +func SetSpanID(ctx context.Context, sid SpanID) context.Context { + return context.WithValue(ctx, currentSpanId, sid) +} + // GetSpanID returns (_, false) if the context did not contain the current span id. -func (c Context) GetSpanID() (SpanID, bool) { - if c.Context == nil { +func GetSpanID(c context.Context) (SpanID, bool) { + if c == nil { return SpanID{}, false } sid, ok := c.Value(currentSpanId).(SpanID) return sid, ok } +// SetParentSpanID sets the context's span's parent [SpanID] +func SetParentSpanID(ctx context.Context, sid SpanID) context.Context { + return context.WithValue(ctx, parentSpanId, sid) +} + // GetParentSpanID returns (_, false) if the context did not contain the parent's span id (including when we are in the root span). -func (c Context) GetParentSpanID() (SpanID, bool) { - if c.Context == nil { +func GetParentSpanID(c context.Context) (SpanID, bool) { + if c == nil { return SpanID{}, false } sid, ok := c.Value(parentSpanId).(SpanID) @@ -135,7 +232,7 @@ func (c Context) GetParentSpanID() (SpanID, bool) { // generated IDs in the context to be used later for exemplars // // The random generator is a PRNG, seeded with the timestamp of the first time new IDs are needed. -func (c *Context) FillTracingInfo() { +func FillTracingInfo(ctx context.Context) context.Context { // We are using a PRNG because FillTracingInfo is expected to be called in PreInstrument. // Therefore it can have a noticeable impact on the performance of instrumented code. // Pseudo randomness should be enough for our use cases, true randomness might introduce too much latency. @@ -145,19 +242,21 @@ func (c *Context) FillTracingInfo() { randSource = rand.New(rand.NewSource(time.Now().UnixNano())) } - if parentSpanId, ok := c.GetSpanID(); ok { - c.SetParentSpanID(parentSpanId) + if parentSpanId, ok := GetSpanID(ctx); ok { + ctx = SetParentSpanID(ctx, parentSpanId) } sid := SpanID{} _, _ = randSource.Read(sid[:]) - c.SetSpanID(sid) + ctx = SetSpanID(ctx, sid) - if _, ok := c.GetTraceID(); !ok { + if _, ok := GetTraceID(ctx); !ok { tid := TraceID{} _, _ = randSource.Read(tid[:]) - c.SetTraceID(tid) + ctx = SetTraceID(ctx, tid) } + + return ctx } // GenerateTraceId generates a new TraceID with a Pseudo-random number generator. @@ -183,8 +282,54 @@ func WithNewTraceId(ctx context.Context) context.Context { } // FillBuildInfo adds the relevant build information to the current context. -func (c *Context) FillBuildInfo() { - c.BuildInfo.Version = GetVersion() - c.BuildInfo.Commit = GetCommit() - c.BuildInfo.Branch = GetBranch() +func FillBuildInfo(ctx context.Context) context.Context { + b := BuildInfo{ + Version: GetVersion(), + Commit: GetCommit(), + Branch: GetBranch(), + } + + return SetBuildInfo(ctx, b) +} + +type InclusiveIntRange struct { + Min int + Max int +} + +func (r InclusiveIntRange) Contains(value int) bool { + return value >= r.Min && value <= r.Max +} + +// SetValidHttpCodeRanges sets the values of http codes that Autometrics should consider as "ok" results on calls. +// +// The value to set is an array of `(int, int)` pairs, where each pair contains the inclusive minimum and inclusive maximum of a valid range. +// For example, `[(100,399)]` is a good default, if we only want 4xx and 5xx status codes to be errors for Autometrics reporting. Another +// option that might be popular is `[(100, 499)]` to only see server-side errors as errors in Autometrics metrics/dashboards. +// +// The ability to specify multiple ranges allow for disjoint sets: `[(100, 399), (418,418)]` would make Autometrics report an error on +// any 4xx or 5xx status code, _except_ if that code is 418 (I'm a teapot). +// +// This setting is only useful when used in conjunction with the [github.com/autometrics-dev/autometrics-go/pkg/middleware/http/middleware.Autometrics] wrapper. +func SetValidHttpCodeRanges(ctx context.Context, ranges []InclusiveIntRange) context.Context { + return context.WithValue(ctx, parentSpanId, ranges) +} + +// GetValidHttpCodeRanges returns the list of values that should be considered as "ok" by Autometrics when computing the success rate of a handler. +// +// Look at the documentation of [SetValidHttpCodeRanges] for more information about the semantics of the returned value. +func GetValidHttpCodeRanges(c context.Context) []InclusiveIntRange { + if c == nil { + return []InclusiveIntRange{{ + Min: 100, + Max: 399, + }} + } + + ranges, ok := c.Value(parentSpanId).([]InclusiveIntRange) + if !ok { + return []InclusiveIntRange{} + } + + return ranges } diff --git a/pkg/autometrics/ctx_opt.go b/pkg/autometrics/ctx_opt.go new file mode 100644 index 0000000..f6af815 --- /dev/null +++ b/pkg/autometrics/ctx_opt.go @@ -0,0 +1,98 @@ +package autometrics // import "github.com/autometrics-dev/autometrics-go/pkg/autometrics" + +import ( + "context" + "time" +) + +type Option interface { + // Apply the option to the currently created context + Apply(context.Context) context.Context +} + +type optionFunc func(context.Context) context.Context + +func (fn optionFunc) Apply(ctx context.Context) context.Context { + return fn(ctx) +} + +func NewContextWithOpts(ctx context.Context, opts ...Option) context.Context { + amCtx := NewContext(ctx) + + for _, o := range opts { + amCtx = o.Apply(amCtx) + } + + return amCtx +} + +func WithTraceID(tid []byte) Option { + return optionFunc(func(ctx context.Context) context.Context { + if tid != nil { + var truncatedTid TraceID + copy(truncatedTid[:], tid) + return SetTraceID(ctx, truncatedTid) + } + return ctx + }) +} + +func WithSpanID(sid []byte) Option { + return optionFunc(func(ctx context.Context) context.Context { + if sid != nil { + var truncatedSid SpanID + copy(truncatedSid[:], sid) + return SetSpanID(ctx, truncatedSid) + } + return ctx + }) +} + +func WithAlertLatency(target time.Duration, objective float64) Option { + return optionFunc(func(ctx context.Context) context.Context { + latencySlo := &LatencySlo{ + Target: target, + Objective: objective, + } + slo := GetAlertConfiguration(ctx) + slo.Latency = latencySlo + return SetAlertConfiguration(ctx, slo) + }) +} + +func WithAlertSuccess(objective float64) Option { + return optionFunc(func(ctx context.Context) context.Context { + successSlo := &SuccessSlo{ + Objective: objective, + } + slo := GetAlertConfiguration(ctx) + slo.Success = successSlo + return SetAlertConfiguration(ctx, slo) + }) +} + +func WithSloName(name string) Option { + return optionFunc(func(ctx context.Context) context.Context { + slo := GetAlertConfiguration(ctx) + slo.ServiceName = name + return SetAlertConfiguration(ctx, slo) + }) +} + +func WithConcurrentCalls(enabled bool) Option { + return optionFunc(func(ctx context.Context) context.Context { + return SetTrackConcurrentCalls(ctx, enabled) + }) +} + +func WithCallerName(enabled bool) Option { + return optionFunc(func(ctx context.Context) context.Context { + return SetTrackCallerName(ctx, enabled) + }) +} + +func WithValidHttpCodes(ranges []InclusiveIntRange) Option { + return optionFunc(func(ctx context.Context) context.Context { + return SetValidHttpCodeRanges(ctx, ranges) + }) +} diff --git a/pkg/autometrics/doc.go b/pkg/autometrics/doc.go index fd0ecc8..cd84f0a 100644 --- a/pkg/autometrics/doc.go +++ b/pkg/autometrics/doc.go @@ -1,9 +1,11 @@ // Package autometrics provides automatic metric collection and reporting to functions. // -// Depending on the implementation you want to use for metric collection (currently, [autometrics/prometheus] and [autometrics/otel] are supported), you can initialise the metrics collector, and then use a defer statement to automatically instrument a function body. +// Depending on the implementation you want to use for metric collection (currently, [Prometheus] and [Open Telemetry] are supported), you can initialise the metrics collector, and then use a defer statement to automatically instrument a function body. // // The generator associated with autometrics generates the collection defer statement from argument in a directive comment, see // the main project's [Readme] for more detail. // // [Readme]: https://github.com/autometrics-dev/autometrics-go +// [Prometheus]: https://godoc.org/github.com/autometrics-dev/autometrics-go/prometheus/autometrics +// [Open Telemetry]: https://godoc.org/github.com/autometrics-dev/autometrics-go/otel/autometrics package autometrics diff --git a/pkg/autometrics/instrument.go b/pkg/autometrics/instrument.go index 5311175..17b8982 100644 --- a/pkg/autometrics/instrument.go +++ b/pkg/autometrics/instrument.go @@ -1,15 +1,11 @@ package autometrics import ( + "reflect" "runtime" "strings" ) -type Option interface { - // Apply the option to the currently created context - Apply(*Context) -} - // CallerInfo returns the (method name, module name) of the function that called the function that called this function. // // It also returns the information about its grandparent. @@ -37,7 +33,7 @@ func CallerInfo() (callInfo CallInfo) { index := strings.LastIndex(functionName, ".") if index == -1 { - callInfo.FuncName = frame.Func.Name() + callInfo.FuncName = functionName } else { moduleIndex := strings.LastIndex(functionName[:index], ".") if moduleIndex == -1 { @@ -60,7 +56,7 @@ func CallerInfo() (callInfo CallInfo) { index = strings.LastIndex(parentFunctionName, ".") if index == -1 { - callInfo.ParentFuncName = parentFrame.Func.Name() + callInfo.ParentFuncName = parentFunctionName } else { moduleIndex := strings.LastIndex(parentFunctionName[:index], ".") if moduleIndex == -1 { @@ -74,3 +70,27 @@ func CallerInfo() (callInfo CallInfo) { return } + +// ReflectFunctionModuleName takes any function and returns it's name and module split. +// +// There is no `caller` in this context (we just use reflection to extract the information +// from the function pointer), therefore the caller-related fields in the return value are +// empty. +func ReflectFunctionModuleName(f interface{}) (callInfo CallInfo) { + functionName := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() + + index := strings.LastIndex(functionName, ".") + if index == -1 { + callInfo.FuncName = functionName + } else { + moduleIndex := strings.LastIndex(functionName[:index], ".") + if moduleIndex == -1 { + callInfo.ModuleName = functionName[:index] + } else { + callInfo.ModuleName = functionName[moduleIndex+1 : index] + } + callInfo.FuncName = functionName[index+1:] + } + + return callInfo +} diff --git a/pkg/autometrics/middleware.go b/pkg/autometrics/middleware.go deleted file mode 100644 index 2866aac..0000000 --- a/pkg/autometrics/middleware.go +++ /dev/null @@ -1,31 +0,0 @@ -package autometrics // import "github.com/autometrics-dev/autometrics-go/pkg/autometrics" - -import ( - "fmt" - "net/http" -) - -type autometricsResponseWriter struct { - http.ResponseWriter - statusCode int -} - -func NewResponseWriter(w http.ResponseWriter) *autometricsResponseWriter { - return &autometricsResponseWriter{w, http.StatusOK} -} - -func (amrw *autometricsResponseWriter) WriteHeader(code int) { - amrw.statusCode = code - amrw.ResponseWriter.WriteHeader(code) -} - -// HasHttpError returns non-nil if the response writer is an autometrics wrapper, -// and if the inner status code is outside of the 200-399 range. -func HasHttpError(rw http.ResponseWriter) error { - if amrw, ok := rw.(*autometricsResponseWriter); ok { - if amrw.statusCode < 200 || amrw.statusCode >= 400 { - return fmt.Errorf("HTTP error %s (%d)", http.StatusText(amrw.statusCode), amrw.statusCode) - } - } - return nil -} diff --git a/pkg/autometrics/otel/ctx.go b/pkg/autometrics/otel/ctx.go deleted file mode 100644 index 9f27044..0000000 --- a/pkg/autometrics/otel/ctx.go +++ /dev/null @@ -1,99 +0,0 @@ -package otel // import "github.com/autometrics-dev/autometrics-go/pkg/autometrics/otel" - -import ( - "context" - "time" - - "github.com/autometrics-dev/autometrics-go/pkg/autometrics" -) - -type optionFunc func(*autometrics.Context) - -func (fn optionFunc) Apply(ctx *autometrics.Context) { - fn(ctx) -} - -func NewContext(ctx context.Context, opts ...autometrics.Option) *autometrics.Context { - amCtx := autometrics.NewContext(ctx) - - for _, o := range opts { - o.Apply(&amCtx) - } - - return &amCtx -} - -func WithTraceID(tid []byte) autometrics.Option { - return optionFunc(func(ctx *autometrics.Context) { - if tid != nil { - var truncatedTid autometrics.TraceID - copy(truncatedTid[:], tid) - ctx.SetTraceID(truncatedTid) - } - }) -} - -func WithSpanID(sid []byte) autometrics.Option { - return optionFunc(func(ctx *autometrics.Context) { - if sid != nil { - var truncatedSid autometrics.SpanID - copy(truncatedSid[:], sid) - ctx.SetSpanID(truncatedSid) - } - }) -} - -func WithAlertLatency(target time.Duration, objective float64) autometrics.Option { - return optionFunc(func(ctx *autometrics.Context) { - latencySlo := &autometrics.LatencySlo{ - Target: target, - Objective: objective, - } - if ctx.AlertConf != nil { - ctx.AlertConf.Latency = latencySlo - } else { - ctx.AlertConf = &autometrics.AlertConfiguration{ - Latency: latencySlo, - } - } - }) -} - -func WithAlertSuccess(objective float64) autometrics.Option { - return optionFunc(func(ctx *autometrics.Context) { - successSlo := &autometrics.SuccessSlo{ - Objective: objective, - } - if ctx.AlertConf != nil { - ctx.AlertConf.Success = successSlo - } else { - ctx.AlertConf = &autometrics.AlertConfiguration{ - Success: successSlo, - } - } - }) -} - -func WithSloName(name string) autometrics.Option { - return optionFunc(func(ctx *autometrics.Context) { - if ctx.AlertConf != nil { - ctx.AlertConf.ServiceName = name - } else { - ctx.AlertConf = &autometrics.AlertConfiguration{ - ServiceName: name, - } - } - }) -} - -func WithConcurrentCalls(enabled bool) autometrics.Option { - return optionFunc(func(ctx *autometrics.Context) { - ctx.TrackConcurrentCalls = enabled - }) -} - -func WithCallerName(enabled bool) autometrics.Option { - return optionFunc(func(ctx *autometrics.Context) { - ctx.TrackCallerName = enabled - }) -} diff --git a/pkg/autometrics/otel/instrument.go b/pkg/autometrics/otel/instrument.go deleted file mode 100644 index 91e306b..0000000 --- a/pkg/autometrics/otel/instrument.go +++ /dev/null @@ -1,110 +0,0 @@ -package otel // import "github.com/autometrics-dev/autometrics-go/pkg/autometrics/otel" - -import ( - "fmt" - "strconv" - "time" - - "github.com/autometrics-dev/autometrics-go/pkg/autometrics" - "go.opentelemetry.io/otel/attribute" -) - -// Instrument called in a defer statement wraps the body of a function -// with automatic instrumentation. -// -// The first argument SHOULD be a call to PreInstrument so that -// the "concurrent calls" gauge is correctly setup. -func Instrument(ctx *autometrics.Context, err *error) { - result := "ok" - - if err != nil && *err != nil { - result = "error" - } - - var callerLabel, sloName, latencyTarget, latencyObjective, successObjective string - - if ctx.TrackCallerName { - callerLabel = fmt.Sprintf("%s.%s", ctx.CallInfo.ParentModuleName, ctx.CallInfo.ParentFuncName) - } - - if ctx.AlertConf != nil { - sloName = ctx.AlertConf.ServiceName - - if ctx.AlertConf.Latency != nil { - latencyTarget = strconv.FormatFloat(ctx.AlertConf.Latency.Target.Seconds(), 'f', -1, 64) - latencyObjective = strconv.FormatFloat(ctx.AlertConf.Latency.Objective, 'f', -1, 64) - } - - if ctx.AlertConf.Success != nil { - successObjective = strconv.FormatFloat(ctx.AlertConf.Success.Objective, 'f', -1, 64) - } - } - - functionCallsCount.Add(ctx.Context, 1, - []attribute.KeyValue{ - attribute.Key(FunctionLabel).String(ctx.CallInfo.FuncName), - attribute.Key(ModuleLabel).String(ctx.CallInfo.ModuleName), - attribute.Key(CallerLabel).String(callerLabel), - attribute.Key(ResultLabel).String(result), - attribute.Key(TargetSuccessRateLabel).String(successObjective), - attribute.Key(SloNameLabel).String(sloName), - attribute.Key(CommitLabel).String(ctx.BuildInfo.Commit), - attribute.Key(VersionLabel).String(ctx.BuildInfo.Version), - attribute.Key(BranchLabel).String(ctx.BuildInfo.Branch), - }...) - functionCallsDuration.Record(ctx.Context, time.Since(ctx.StartTime).Seconds(), - []attribute.KeyValue{ - attribute.Key(FunctionLabel).String(ctx.CallInfo.FuncName), - attribute.Key(ModuleLabel).String(ctx.CallInfo.ModuleName), - attribute.Key(CallerLabel).String(callerLabel), - attribute.Key(TargetLatencyLabel).String(latencyTarget), - attribute.Key(TargetSuccessRateLabel).String(latencyObjective), - attribute.Key(SloNameLabel).String(sloName), - attribute.Key(CommitLabel).String(ctx.BuildInfo.Commit), - attribute.Key(VersionLabel).String(ctx.BuildInfo.Version), - attribute.Key(BranchLabel).String(ctx.BuildInfo.Branch), - }...) - - if ctx.TrackConcurrentCalls { - functionCallsConcurrent.Add(ctx.Context, -1, - []attribute.KeyValue{ - attribute.Key(FunctionLabel).String(ctx.CallInfo.FuncName), - attribute.Key(ModuleLabel).String(ctx.CallInfo.ModuleName), - attribute.Key(CallerLabel).String(callerLabel), - attribute.Key(CommitLabel).String(ctx.BuildInfo.Commit), - attribute.Key(VersionLabel).String(ctx.BuildInfo.Version), - attribute.Key(BranchLabel).String(ctx.BuildInfo.Branch), - }...) - } -} - -// PreInstrument runs the "before wrappee" part of instrumentation. -// -// It is meant to be called as the first argument to Instrument in a -// defer call. -func PreInstrument(ctx *autometrics.Context) *autometrics.Context { - ctx.CallInfo = autometrics.CallerInfo() - ctx.FillBuildInfo() - ctx.FillTracingInfo() - - var callerLabel string - if ctx.TrackCallerName { - callerLabel = fmt.Sprintf("%s.%s", ctx.CallInfo.ParentModuleName, ctx.CallInfo.ParentFuncName) - } - - if ctx.TrackConcurrentCalls { - functionCallsConcurrent.Add(ctx.Context, 1, - []attribute.KeyValue{ - attribute.Key(FunctionLabel).String(ctx.CallInfo.FuncName), - attribute.Key(ModuleLabel).String(ctx.CallInfo.ModuleName), - attribute.Key(CallerLabel).String(callerLabel), - attribute.Key(CommitLabel).String(ctx.BuildInfo.Commit), - attribute.Key(VersionLabel).String(ctx.BuildInfo.Version), - attribute.Key(BranchLabel).String(ctx.BuildInfo.Branch), - }...) - } - - ctx.StartTime = time.Now() - - return ctx -} diff --git a/pkg/autometrics/prometheus/ctx.go b/pkg/autometrics/prometheus/ctx.go deleted file mode 100644 index faf5d0f..0000000 --- a/pkg/autometrics/prometheus/ctx.go +++ /dev/null @@ -1,99 +0,0 @@ -package prometheus // import "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" - -import ( - "context" - "time" - - "github.com/autometrics-dev/autometrics-go/pkg/autometrics" -) - -type optionFunc func(*autometrics.Context) - -func (fn optionFunc) Apply(ctx *autometrics.Context) { - fn(ctx) -} - -func NewContext(ctx context.Context, opts ...autometrics.Option) *autometrics.Context { - amCtx := autometrics.NewContext(ctx) - - for _, o := range opts { - o.Apply(&amCtx) - } - - return &amCtx -} - -func WithTraceID(tid []byte) autometrics.Option { - return optionFunc(func(ctx *autometrics.Context) { - if tid != nil { - var truncatedTid autometrics.TraceID - copy(truncatedTid[:], tid) - ctx.SetTraceID(truncatedTid) - } - }) -} - -func WithSpanID(sid []byte) autometrics.Option { - return optionFunc(func(ctx *autometrics.Context) { - if sid != nil { - var truncatedSid autometrics.SpanID - copy(truncatedSid[:], sid) - ctx.SetSpanID(truncatedSid) - } - }) -} - -func WithAlertLatency(target time.Duration, objective float64) autometrics.Option { - return optionFunc(func(ctx *autometrics.Context) { - latencySlo := &autometrics.LatencySlo{ - Target: target, - Objective: objective, - } - if ctx.AlertConf != nil { - ctx.AlertConf.Latency = latencySlo - } else { - ctx.AlertConf = &autometrics.AlertConfiguration{ - Latency: latencySlo, - } - } - }) -} - -func WithAlertSuccess(objective float64) autometrics.Option { - return optionFunc(func(ctx *autometrics.Context) { - successSlo := &autometrics.SuccessSlo{ - Objective: objective, - } - if ctx.AlertConf != nil { - ctx.AlertConf.Success = successSlo - } else { - ctx.AlertConf = &autometrics.AlertConfiguration{ - Success: successSlo, - } - } - }) -} - -func WithSloName(name string) autometrics.Option { - return optionFunc(func(ctx *autometrics.Context) { - if ctx.AlertConf != nil { - ctx.AlertConf.ServiceName = name - } else { - ctx.AlertConf = &autometrics.AlertConfiguration{ - ServiceName: name, - } - } - }) -} - -func WithConcurrentCalls(enabled bool) autometrics.Option { - return optionFunc(func(ctx *autometrics.Context) { - ctx.TrackConcurrentCalls = enabled - }) -} - -func WithCallerName(enabled bool) autometrics.Option { - return optionFunc(func(ctx *autometrics.Context) { - ctx.TrackCallerName = enabled - }) -} diff --git a/pkg/autometrics/prometheus/instrument.go b/pkg/autometrics/prometheus/instrument.go deleted file mode 100644 index ab1d514..0000000 --- a/pkg/autometrics/prometheus/instrument.go +++ /dev/null @@ -1,128 +0,0 @@ -package prometheus // import "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" - -import ( - "encoding/hex" - "fmt" - "strconv" - "time" - - "github.com/autometrics-dev/autometrics-go/pkg/autometrics" - "github.com/prometheus/client_golang/prometheus" -) - -// Instrument called in a defer statement wraps the body of a function -// with automatic instrumentation. -// -// The first argument SHOULD be a call to PreInstrument so that -// the "concurrent calls" gauge is correctly setup. -func Instrument(ctx *autometrics.Context, err *error) { - result := "ok" - - if err != nil && *err != nil { - result = "error" - } - - var callerLabel, sloName, latencyTarget, latencyObjective, successObjective string - - if ctx.TrackCallerName { - callerLabel = fmt.Sprintf("%s.%s", ctx.CallInfo.ParentModuleName, ctx.CallInfo.ParentFuncName) - } - - if ctx.AlertConf != nil { - sloName = ctx.AlertConf.ServiceName - - if ctx.AlertConf.Latency != nil { - latencyTarget = strconv.FormatFloat(ctx.AlertConf.Latency.Target.Seconds(), 'f', -1, 64) - latencyObjective = strconv.FormatFloat(ctx.AlertConf.Latency.Objective, 'f', -1, 64) - } - - if ctx.AlertConf.Success != nil { - successObjective = strconv.FormatFloat(ctx.AlertConf.Success.Objective, 'f', -1, 64) - } - } - - info := exemplars(ctx) - - functionCallsCount.With(prometheus.Labels{ - FunctionLabel: ctx.CallInfo.FuncName, - ModuleLabel: ctx.CallInfo.ModuleName, - CallerLabel: callerLabel, - ResultLabel: result, - TargetSuccessRateLabel: successObjective, - SloNameLabel: sloName, - BranchLabel: ctx.BuildInfo.Branch, - CommitLabel: ctx.BuildInfo.Commit, - VersionLabel: ctx.BuildInfo.Version, - }).(prometheus.ExemplarAdder).AddWithExemplar(1, info) - functionCallsDuration.With(prometheus.Labels{ - FunctionLabel: ctx.CallInfo.FuncName, - ModuleLabel: ctx.CallInfo.ModuleName, - CallerLabel: callerLabel, - TargetLatencyLabel: latencyTarget, - TargetSuccessRateLabel: latencyObjective, - SloNameLabel: sloName, - BranchLabel: ctx.BuildInfo.Branch, - CommitLabel: ctx.BuildInfo.Commit, - VersionLabel: ctx.BuildInfo.Version, - }).(prometheus.ExemplarObserver).ObserveWithExemplar(time.Since(ctx.StartTime).Seconds(), info) - - if ctx.TrackConcurrentCalls { - functionCallsConcurrent.With(prometheus.Labels{ - FunctionLabel: ctx.CallInfo.FuncName, - ModuleLabel: ctx.CallInfo.ModuleName, - CallerLabel: callerLabel, - BranchLabel: ctx.BuildInfo.Branch, - CommitLabel: ctx.BuildInfo.Commit, - VersionLabel: ctx.BuildInfo.Version, - }).Add(-1) - } -} - -// PreInstrument runs the "before wrappee" part of instrumentation. -// -// It is meant to be called as the first argument to Instrument in a -// defer call. -func PreInstrument(ctx *autometrics.Context) *autometrics.Context { - ctx.CallInfo = autometrics.CallerInfo() - ctx.FillBuildInfo() - ctx.FillTracingInfo() - - var callerLabel string - if ctx.TrackCallerName { - callerLabel = fmt.Sprintf("%s.%s", ctx.CallInfo.ParentModuleName, ctx.CallInfo.ParentFuncName) - } - - if ctx.TrackConcurrentCalls { - functionCallsConcurrent.With(prometheus.Labels{ - FunctionLabel: ctx.CallInfo.FuncName, - ModuleLabel: ctx.CallInfo.ModuleName, - CallerLabel: callerLabel, - BranchLabel: ctx.BuildInfo.Branch, - CommitLabel: ctx.BuildInfo.Commit, - VersionLabel: ctx.BuildInfo.Version, - }).Add(1) - } - - ctx.StartTime = time.Now() - - return ctx -} - -// Extract exemplars to add to metrics from the context -func exemplars(ctx *autometrics.Context) prometheus.Labels { - labels := make(prometheus.Labels) - - if tid, ok := ctx.GetTraceID(); ok { - labels[traceIdExemplar] = hex.EncodeToString(tid[:]) - } - - if sid, ok := ctx.GetSpanID(); ok { - labels[spanIdExemplar] = hex.EncodeToString(sid[:]) - } - - if psid, ok := ctx.GetParentSpanID(); ok { - labels[parentSpanIdExemplar] = hex.EncodeToString(psid[:]) - } - - return labels -} diff --git a/pkg/midhttp/middleware.go b/pkg/midhttp/middleware.go new file mode 100644 index 0000000..79cf1df --- /dev/null +++ b/pkg/midhttp/middleware.go @@ -0,0 +1,28 @@ +// Package midhttp contains common types used in the downstream implementations of the middleware for net/http handlers. +package midhttp // import "github.com/autometrics-dev/autometrics-go/pkg/middleware/midhttp" + +import ( + "net/http" +) + +const RequestIdHeader = "X-Request-Id" + +type autometricsResponseWriter struct { + http.ResponseWriter + statusCode int +} + +// NewResponseWriter creates a new ResponseWriter that keeps track of the status +// code of the query for reporting purposes. +func NewResponseWriter(w http.ResponseWriter) *autometricsResponseWriter { + return &autometricsResponseWriter{w, http.StatusOK} +} + +func (amrw *autometricsResponseWriter) CurrentStatusCode() int { + return amrw.statusCode +} + +func (amrw *autometricsResponseWriter) WriteHeader(code int) { + amrw.statusCode = code + amrw.ResponseWriter.WriteHeader(code) +} diff --git a/prometheus/autometrics/ctx.go b/prometheus/autometrics/ctx.go new file mode 100644 index 0000000..326a8e0 --- /dev/null +++ b/prometheus/autometrics/ctx.go @@ -0,0 +1,46 @@ +package autometrics // import "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" + +import ( + "context" + "time" + + "github.com/autometrics-dev/autometrics-go/pkg/autometrics" +) + +type ValidHttpRange = autometrics.InclusiveIntRange + +func NewContext(ctx context.Context, opts ...autometrics.Option) context.Context { + return autometrics.NewContextWithOpts(ctx, opts...) +} + +func WithTraceID(tid []byte) autometrics.Option { + return autometrics.WithTraceID(tid) +} + +func WithSpanID(sid []byte) autometrics.Option { + return autometrics.WithSpanID(sid) +} + +func WithAlertLatency(target time.Duration, objective float64) autometrics.Option { + return autometrics.WithAlertLatency(target, objective) +} + +func WithAlertSuccess(objective float64) autometrics.Option { + return autometrics.WithAlertSuccess(objective) +} + +func WithSloName(name string) autometrics.Option { + return autometrics.WithSloName(name) +} + +func WithConcurrentCalls(enabled bool) autometrics.Option { + return autometrics.WithConcurrentCalls(enabled) +} + +func WithCallerName(enabled bool) autometrics.Option { + return autometrics.WithCallerName(enabled) +} + +func WithValidHttpCodes(ranges []ValidHttpRange) autometrics.Option { + return autometrics.WithValidHttpCodes(ranges) +} diff --git a/pkg/autometrics/prometheus/doc.go b/prometheus/autometrics/doc.go similarity index 63% rename from pkg/autometrics/prometheus/doc.go rename to prometheus/autometrics/doc.go index 2ce65d0..121b85f 100644 --- a/pkg/autometrics/prometheus/doc.go +++ b/prometheus/autometrics/doc.go @@ -1,8 +1,8 @@ -// Package prometheus implements the automatic metric registration and collection for autometrics using the [Prometheus client library]. +// Package autometrics implements the automatic metric registration and collection for autometrics using the [Prometheus client library]. // // The package contains the function implementations for the generated calls, see // the main project's [Readme] for more detail. // // [Readme]: https://github.com/autometrics-dev/autometrics-go // [Prometheus client library]: https://github.com/prometheus/client_golang -package prometheus +package autometrics diff --git a/prometheus/autometrics/instrument.go b/prometheus/autometrics/instrument.go new file mode 100644 index 0000000..8b1a651 --- /dev/null +++ b/prometheus/autometrics/instrument.go @@ -0,0 +1,135 @@ +package autometrics // import "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" + +import ( + "context" + "encoding/hex" + "fmt" + "strconv" + "time" + + am "github.com/autometrics-dev/autometrics-go/pkg/autometrics" + "github.com/prometheus/client_golang/prometheus" +) + +// Instrument called in a defer statement wraps the body of a function +// with automatic instrumentation. +// +// The first argument SHOULD be a call to PreInstrument so that +// the "concurrent calls" gauge is correctly setup. +func Instrument(ctx context.Context, err *error) { + result := "ok" + + if err != nil && *err != nil { + result = "error" + } + + var callerLabel, sloName, latencyTarget, latencyObjective, successObjective string + + callInfo := am.GetCallInfo(ctx) + buildInfo := am.GetBuildInfo(ctx) + slo := am.GetAlertConfiguration(ctx) + + if am.GetTrackCallerName(ctx) { + callerLabel = fmt.Sprintf("%s.%s", callInfo.ParentModuleName, callInfo.ParentFuncName) + } + + if slo.ServiceName != "" { + sloName = slo.ServiceName + + if slo.Latency != nil { + latencyTarget = strconv.FormatFloat(slo.Latency.Target.Seconds(), 'f', -1, 64) + latencyObjective = strconv.FormatFloat(slo.Latency.Objective, 'f', -1, 64) + } + + if slo.Success != nil { + successObjective = strconv.FormatFloat(slo.Success.Objective, 'f', -1, 64) + } + } + + info := exemplars(ctx) + + functionCallsCount.With(prometheus.Labels{ + FunctionLabel: callInfo.FuncName, + ModuleLabel: callInfo.ModuleName, + CallerLabel: callerLabel, + ResultLabel: result, + TargetSuccessRateLabel: successObjective, + SloNameLabel: sloName, + BranchLabel: buildInfo.Branch, + CommitLabel: buildInfo.Commit, + VersionLabel: buildInfo.Version, + }).(prometheus.ExemplarAdder).AddWithExemplar(1, info) + functionCallsDuration.With(prometheus.Labels{ + FunctionLabel: callInfo.FuncName, + ModuleLabel: callInfo.ModuleName, + CallerLabel: callerLabel, + TargetLatencyLabel: latencyTarget, + TargetSuccessRateLabel: latencyObjective, + SloNameLabel: sloName, + BranchLabel: buildInfo.Branch, + CommitLabel: buildInfo.Commit, + VersionLabel: buildInfo.Version, + }).(prometheus.ExemplarObserver).ObserveWithExemplar(time.Since(am.GetStartTime(ctx)).Seconds(), info) + + if am.GetTrackConcurrentCalls(ctx) { + functionCallsConcurrent.With(prometheus.Labels{ + FunctionLabel: callInfo.FuncName, + ModuleLabel: callInfo.ModuleName, + CallerLabel: callerLabel, + BranchLabel: buildInfo.Branch, + CommitLabel: buildInfo.Commit, + VersionLabel: buildInfo.Version, + }).Add(-1) + } +} + +// PreInstrument runs the "before wrappee" part of instrumentation. +// +// It is meant to be called as the first argument to Instrument in a +// defer call. +func PreInstrument(ctx context.Context) context.Context { + callInfo := am.CallerInfo() + ctx = am.SetCallInfo(ctx, callInfo) + ctx = am.FillBuildInfo(ctx) + ctx = am.FillTracingInfo(ctx) + buildInfo := am.GetBuildInfo(ctx) + + var callerLabel string + if am.GetTrackCallerName(ctx) { + callerLabel = fmt.Sprintf("%s.%s", callInfo.ParentModuleName, callInfo.ParentFuncName) + } + + if am.GetTrackConcurrentCalls(ctx) { + functionCallsConcurrent.With(prometheus.Labels{ + FunctionLabel: callInfo.FuncName, + ModuleLabel: callInfo.ModuleName, + CallerLabel: callerLabel, + BranchLabel: buildInfo.Branch, + CommitLabel: buildInfo.Commit, + VersionLabel: buildInfo.Version, + }).Add(1) + } + + ctx = am.SetStartTime(ctx, time.Now()) + + return ctx +} + +// Extract exemplars to add to metrics from the context +func exemplars(ctx context.Context) prometheus.Labels { + labels := make(prometheus.Labels) + + if tid, ok := am.GetTraceID(ctx); ok { + labels[traceIdExemplar] = hex.EncodeToString(tid[:]) + } + + if sid, ok := am.GetSpanID(ctx); ok { + labels[spanIdExemplar] = hex.EncodeToString(sid[:]) + } + + if psid, ok := am.GetParentSpanID(ctx); ok { + labels[parentSpanIdExemplar] = hex.EncodeToString(psid[:]) + } + + return labels +} diff --git a/pkg/autometrics/prometheus/prometheus.go b/prometheus/autometrics/prometheus.go similarity index 98% rename from pkg/autometrics/prometheus/prometheus.go rename to prometheus/autometrics/prometheus.go index fbc9727..8fc83cc 100644 --- a/pkg/autometrics/prometheus/prometheus.go +++ b/prometheus/autometrics/prometheus.go @@ -1,4 +1,4 @@ -package prometheus // import "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" +package autometrics // import "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" import ( "github.com/autometrics-dev/autometrics-go/pkg/autometrics" diff --git a/pkg/autometrics/prometheus/utils.go b/prometheus/autometrics/utils.go similarity index 84% rename from pkg/autometrics/prometheus/utils.go rename to prometheus/autometrics/utils.go index a3b7d2b..3a4d690 100644 --- a/pkg/autometrics/prometheus/utils.go +++ b/prometheus/autometrics/utils.go @@ -1,4 +1,4 @@ -package prometheus // import "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" +package autometrics // import "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" import ( "context" diff --git a/prometheus/midhttp/http.go b/prometheus/midhttp/http.go new file mode 100644 index 0000000..d95590d --- /dev/null +++ b/prometheus/midhttp/http.go @@ -0,0 +1,38 @@ +package midhttp // import "github.com/autometrics-dev/autometrics-go/prometheus/midhttp" + +import ( + "errors" + "net/http" + + am "github.com/autometrics-dev/autometrics-go/pkg/autometrics" + mid "github.com/autometrics-dev/autometrics-go/pkg/midhttp" + prom "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" +) + +func Autometrics(next http.HandlerFunc, opts ...am.Option) http.HandlerFunc { + fn := func(rw http.ResponseWriter, r *http.Request) { + arw := mid.NewResponseWriter(rw) + ctx := prom.PreInstrument(prom.NewContext(r.Context(), opts...)) + + // Compute then set the function name and module name labels + ctx = am.SetCallInfo(ctx, am.ReflectFunctionModuleName(next)) + + err := errors.New("Unfinished handler") + + defer prom.Instrument(ctx, &err) + + r = r.WithContext(ctx) + next.ServeHTTP(arw, r) + + // Check the status code of the handler to reset the error before the Instrument deferred call + ranges := am.GetValidHttpCodeRanges(ctx) + for _, codeRange := range ranges { + if codeRange.Contains(arw.CurrentStatusCode()) { + err = nil + break + } + } + } + + return http.HandlerFunc(fn) +}