diff --git a/README.md b/README.md index bf4a8ae0..0ee45b83 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,8 @@ - [Required Capabilities](#required-capabilities) - [API](#api) - [Metrics](#metrics) + - [Prometheus Integration](#prometheus-integration) + - [Traces](#traces) - [Code of Conduct](#code-of-conduct) - [Working Language](#working-language) - [Support and Feedback](#support-and-feedback) @@ -268,6 +270,25 @@ targetManager: # The ID of your GitLab project. This is where Sparrow will register itself # and grab the list of other Sparrows from projectId: 18923 + +# Configures the telemetry exporter. +# Omitting this section will disable telemetry. +telemetry: + # The telemetry exporter to use. + # Options: + # grpc: Exports telemetry using OTLP via gRPC. + # http: Exports telemetry using OTLP via HTTP. + # stdout: Prints telemetry to stdout. + # noop | "": Disables telemetry. + exporter: grpc + # The address to export telemetry to. + url: localhost:4317 + # The token to use for authentication. + # If the exporter does not require a token, this can be left empty. + token: "" + # The path to the tls certificate to use. + # To disable tls, either set this to an empty string or set it to insecure. + certPath: "" ``` #### Loader @@ -409,7 +430,7 @@ latency: - `sparrow_latency_duration_seconds` - Type: Gauge - - Description: Latency with status information of targets. This metric is DEPRECATED. Use `sparrow_latency_seconds`. + - Description: Latency with status information of targets. This metric is DEPRECATED. Use `sparrow_latency_seconds`. - Labelled with `target` and `status` - `sparrow_latency_seconds` @@ -522,8 +543,54 @@ at `/v1/metrics/{check-name}`. The API's definition is available at `/openapi`. ## Metrics -The `sparrow` provides a `/metrics` endpoint to expose application metrics. In addition to runtime information, the -sparrow provides specific metrics for each check. Refer to the [Checks](#checks) section for more detailed information. +The `sparrow` provides a `/metrics` endpoint to expose application metrics. In addition to runtime information, the sparrow provides specific metrics for each check. Refer to the [Checks](#checks) section for more detailed information. + +### Prometheus Integration + +The `sparrow` metrics API is designed to be compatible with Prometheus. To integrate `sparrow` with Prometheus, add the following scrape configuration to your Prometheus configuration file: + +```yaml +scrape_configs: + - job_name: 'sparrow' + static_configs: + - targets: [':8080'] +``` + +Replace `` with the actual address of your `sparrow` instance. + +### Traces + +The `sparrow` supports exporting telemetry data using the OpenTelemetry Protocol (OTLP). This allows users to choose their preferred telemetry provider and collector. The following configuration options are available for setting up telemetry: + +| Field | Type | Description | +| ---------- | -------- | ------------------------------------------------------------------------ | +| `exporter` | `string` | The telemetry exporter to use. Options: `grpc`, `http`, `stdout`, `noop` | +| `url` | `string` | The address to export telemetry to | +| `token` | `string` | The token to use for authentication | +| `certPath` | `string` | The path to the TLS certificate to use | + +For example, to export telemetry data using OTLP via gRPC, you can add the following configuration to your [startup configuration](#startup): + +```yaml +telemetry: + # The telemetry exporter to use. + # Options: + # grpc: Exports telemetry using OTLP via gRPC. + # http: Exports telemetry using OTLP via HTTP. + # stdout: Prints telemetry to stdout. + # noop | "": Disables telemetry. + exporter: grpc + # The address to export telemetry to. + url: collector.example.com:4317 + # The token to use for authentication. + # If the exporter does not require a token, this can be left empty. + token: "" + # The path to the tls certificate to use. + # To disable tls, either set this to an empty string or set it to insecure. + certPath: "" +``` + +Since [OTLP](https://opentelemetry.io/docs/specs/otlp/) is a standard protocol, you can choose any collector that supports it. The `stdout` exporter can be used for debugging purposes to print telemetry data to the console, while the `noop` exporter disables telemetry. If an external collector is used, a bearer token for authentication and a TLS certificate path for secure communication can be provided. ## Code of Conduct diff --git a/go.mod b/go.mod index 3530c1da..59db68ab 100644 --- a/go.mod +++ b/go.mod @@ -11,9 +11,25 @@ require ( github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/otel v1.25.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.25.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.25.0 + go.opentelemetry.io/otel/sdk v1.25.0 + google.golang.org/grpc v1.63.0 gopkg.in/yaml.v3 v3.0.1 ) +require ( + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect + golang.org/x/net v0.23.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect +) + require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -45,6 +61,10 @@ require ( github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0 // indirect + go.opentelemetry.io/otel/metric v1.25.0 // indirect + go.opentelemetry.io/otel/trace v1.25.0 // indirect + go.opentelemetry.io/proto/otlp v1.1.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/sys v0.20.0 // indirect diff --git a/go.sum b/go.sum index a1db7cc1..87443758 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/aeden/traceroute v0.0.0-20210211061815-03f5f7cb7908 h1:6suDyKbvZ5r2G/ github.com/aeden/traceroute v0.0.0-20210211061815-03f5f7cb7908/go.mod h1:HPBB/4vaPt7NcN9l72/+IwsmDVQsa6AWM6ZDKJCLB9U= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= @@ -18,6 +20,11 @@ github.com/getkin/kin-openapi v0.120.0 h1:MqJcNJFrMDFNc07iwE8iFC5eT2k/NPUFDIpNei github.com/getkin/kin-openapi v0.120.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw= github.com/go-chi/chi/v5 v5.0.14 h1:PyEwo2Vudraa0x/Wl6eDRRW2NXBvekgfxyydcM0WGE0= github.com/go-chi/chi/v5 v5.0.14/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= @@ -26,6 +33,8 @@ github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -98,14 +107,42 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k= +go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0 h1:dT33yIHtmsqpixFsSQPwNeY5drM9wTcoL8h0FWF4oGM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0/go.mod h1:h95q0LBGh7hlAC08X2DhSeyIG02YQ0UyioTCVAqRPmc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.25.0 h1:vOL89uRfOCCNIjkisd0r7SEdJF3ZJFyCNY34fdZs8eU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.25.0/go.mod h1:8GlBGcDk8KKi7n+2S4BT/CPZQYH3erLu0/k64r1MYgo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0 h1:Mbi5PKN7u322woPa85d7ebZ+SOvEoPvoiBu+ryHWgfA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0/go.mod h1:e7ciERRhZaOZXVjx5MiL8TK5+Xv7G5Gv5PA2ZDEJdL8= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.25.0 h1:0vZZdECYzhTt9MKQZ5qQ0V+J3MFu4MQaQ3COfugF+FQ= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.25.0/go.mod h1:e7iXx3HjaSSBXfy9ykVUlupS2Vp7LBIBuT21ousM2Hk= +go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA= +go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s= +go.opentelemetry.io/otel/sdk v1.25.0 h1:PDryEJPC8YJZQSyLY5eqLeafHtG+X7FWnf3aXMtxbqo= +go.opentelemetry.io/otel/sdk v1.25.0/go.mod h1:oFgzCM2zdsxKzz6zwpTZYLLQsFwc+K0daArPdIhuxkw= +go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM= +go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I= +go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= +go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 h1:rIo7ocm2roD9DcFIX67Ym8icoGCKSARAiPljFhh5suQ= +google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.63.0 h1:WjKe+dnvABXyPJMD7KDNLxtoGk5tgk+YFWN6cBWjZE8= +google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/config/config.go b/pkg/config/config.go index 4719fc32..f6760513 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -21,6 +21,7 @@ package config import ( "time" + "github.com/caas-team/sparrow/pkg/sparrow/metrics" "github.com/caas-team/sparrow/pkg/sparrow/targets" "github.com/caas-team/sparrow/internal/helper" @@ -33,6 +34,7 @@ type Config struct { Loader LoaderConfig `yaml:"loader" mapstructure:"loader"` Api api.Config `yaml:"api" mapstructure:"api"` TargetManager targets.TargetManagerConfig `yaml:"targetManager" mapstructure:"targetManager"` + Telemetry metrics.Config `yaml:"telemetry" mapstructure:"telemetry"` } // LoaderConfig is the configuration for loader @@ -60,3 +62,7 @@ type FileLoaderConfig struct { func (c *Config) HasTargetManager() bool { return c.TargetManager.Enabled } + +func (c *Config) HasTelemetry() bool { + return c.Telemetry != metrics.Config{} +} diff --git a/pkg/config/validate.go b/pkg/config/validate.go index 39725705..d467e0f4 100644 --- a/pkg/config/validate.go +++ b/pkg/config/validate.go @@ -48,6 +48,13 @@ func (c *Config) Validate(ctx context.Context) (err error) { } } + if c.HasTelemetry() { + if vErr := c.Telemetry.Validate(ctx); vErr != nil { + log.Error("The telemetry configuration is invalid") + err = errors.Join(err, vErr) + } + } + if vErr := c.Api.Validate(); vErr != nil { log.Error("The api configuration is invalid") err = errors.Join(err, vErr) diff --git a/pkg/sparrow/controller.go b/pkg/sparrow/controller.go index 5ac06bec..50b4ef68 100644 --- a/pkg/sparrow/controller.go +++ b/pkg/sparrow/controller.go @@ -29,13 +29,14 @@ import ( "github.com/caas-team/sparrow/pkg/checks/runtime" "github.com/caas-team/sparrow/pkg/db" "github.com/caas-team/sparrow/pkg/factory" + "github.com/caas-team/sparrow/pkg/sparrow/metrics" "github.com/getkin/kin-openapi/openapi3" ) // ChecksController is responsible for managing checks. type ChecksController struct { db db.DB - metrics Metrics + metrics metrics.Provider checks runtime.Checks cResult chan checks.ResultDTO cErr chan error @@ -43,10 +44,10 @@ type ChecksController struct { } // NewChecksController creates a new ChecksController. -func NewChecksController(dbase db.DB, metrics Metrics) *ChecksController { +func NewChecksController(dbase db.DB, m metrics.Provider) *ChecksController { return &ChecksController{ db: dbase, - metrics: metrics, + metrics: m, checks: runtime.Checks{}, cResult: make(chan checks.ResultDTO, 8), //nolint:mnd // Buffered channel to avoid blocking the checks cErr: make(chan error, 1), diff --git a/pkg/sparrow/controller_test.go b/pkg/sparrow/controller_test.go index 56d2be17..902700e4 100644 --- a/pkg/sparrow/controller_test.go +++ b/pkg/sparrow/controller_test.go @@ -32,6 +32,7 @@ import ( "github.com/caas-team/sparrow/pkg/checks/latency" "github.com/caas-team/sparrow/pkg/checks/runtime" "github.com/caas-team/sparrow/pkg/db" + "github.com/caas-team/sparrow/pkg/sparrow/metrics" "github.com/getkin/kin-openapi/openapi3" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" @@ -41,7 +42,7 @@ func TestRun_CheckRunError(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - cc := NewChecksController(db.NewInMemory(), NewMetrics()) + cc := NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{})) mockCheck := &checks.CheckMock{ NameFunc: func() string { return "mockCheck" }, RunFunc: func(ctx context.Context, cResult chan checks.ResultDTO) error { @@ -81,7 +82,7 @@ func TestRun_CheckRunError(t *testing.T) { func TestRun_ContextCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) - cc := NewChecksController(db.NewInMemory(), NewMetrics()) + cc := NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{})) done := make(chan struct{}) go func() { @@ -205,7 +206,7 @@ func TestChecksController_Reconcile(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cc := NewChecksController(db.NewInMemory(), NewMetrics()) + cc := NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{})) for _, c := range tt.checks { cc.checks.Add(c) @@ -243,7 +244,7 @@ func TestChecksController_RegisterCheck(t *testing.T) { { name: "register one check", setup: func() *ChecksController { - return NewChecksController(db.NewInMemory(), NewMetrics()) + return NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{})) }, check: health.NewCheck(), }, @@ -273,7 +274,7 @@ func TestChecksController_UnregisterCheck(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cc := NewChecksController(db.NewInMemory(), NewMetrics()) + cc := NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{})) cc.UnregisterCheck(context.Background(), tt.check) diff --git a/pkg/sparrow/metrics.go b/pkg/sparrow/metrics.go deleted file mode 100644 index a57612be..00000000 --- a/pkg/sparrow/metrics.go +++ /dev/null @@ -1,52 +0,0 @@ -// sparrow -// (C) 2024, Deutsche Telekom IT GmbH -// -// Deutsche Telekom IT GmbH and all other contributors / -// copyright owners license this file to you under the Apache -// License, Version 2.0 (the "License"); you may not use this -// file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package sparrow - -import ( - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/collectors" -) - -//go:generate moq -out metrics_moq.go . Metrics -type Metrics interface { - // GetRegistry returns the prometheus registry instance - // containing the registered prometheus collectors - GetRegistry() *prometheus.Registry -} - -type PrometheusMetrics struct { - registry *prometheus.Registry -} - -// NewMetrics initializes the metrics and returns the PrometheusMetrics -func NewMetrics() Metrics { - registry := prometheus.NewRegistry() - - registry.MustRegister( - collectors.NewGoCollector(), - collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}), - ) - - return &PrometheusMetrics{registry: registry} -} - -// GetRegistry returns the registry to register prometheus metrics -func (m *PrometheusMetrics) GetRegistry() *prometheus.Registry { - return m.registry -} diff --git a/pkg/sparrow/metrics/config.go b/pkg/sparrow/metrics/config.go new file mode 100644 index 00000000..8deaa194 --- /dev/null +++ b/pkg/sparrow/metrics/config.go @@ -0,0 +1,54 @@ +// sparrow +// (C) 2024, Deutsche Telekom IT GmbH +// +// Deutsche Telekom IT GmbH and all other contributors / +// copyright owners license this file to you under the Apache +// License, Version 2.0 (the "License"); you may not use this +// file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package metrics + +import ( + "context" + "fmt" + + "github.com/caas-team/sparrow/internal/logger" +) + +// Config holds the configuration for OpenTelemetry +type Config struct { + // Exporter is the otlp exporter used to export the traces + Exporter Exporter `yaml:"exporter" mapstructure:"exporter"` + // Url is the Url of the collector to which the traces are exported + Url string `yaml:"url" mapstructure:"url"` + // Token is the token used to authenticate with the collector + Token string `yaml:"token" mapstructure:"token"` + // CertPath is the path to the tls certificate file + CertPath string `yaml:"certPath" mapstructure:"certPath"` +} + +func (c *Config) Validate(ctx context.Context) error { + log := logger.FromContext(ctx) + if err := c.Exporter.Validate(); err != nil { + log.ErrorContext(ctx, "Invalid exporter", "error", err) + return err + } + + if c.Exporter.IsExporting() { + if c.Url == "" { + log.ErrorContext(ctx, "Url is required for otlp exporter", "exporter", c.Exporter) + return fmt.Errorf("url is required for otlp exporter %q", c.Exporter) + } + } + return nil +} diff --git a/pkg/sparrow/metrics/exporter.go b/pkg/sparrow/metrics/exporter.go new file mode 100644 index 00000000..30706385 --- /dev/null +++ b/pkg/sparrow/metrics/exporter.go @@ -0,0 +1,197 @@ +// sparrow +// (C) 2024, Deutsche Telekom IT GmbH +// +// Deutsche Telekom IT GmbH and all other contributors / +// copyright owners license this file to you under the Apache +// License, Version 2.0 (the "License"); you may not use this +// file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package metrics + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io" + "io/fs" + "os" + + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "google.golang.org/grpc/credentials" +) + +// Exporter is the protocol used to export the traces +type Exporter string + +const ( + // HTTP is the protocol used to export the traces via HTTP/1.1 + HTTP Exporter = "http" + // GRPC is the protocol used to export the traces via HTTP/2 (gRPC) + GRPC Exporter = "grpc" + // STDOUT is the protocol used to export the traces to the standard output + STDOUT Exporter = "stdout" + // NOOP is the protocol used to not export the traces + NOOP Exporter = "noop" +) + +// String returns the string representation of the protocol +func (e Exporter) String() string { + return string(e) +} + +// Validate validates the protocol +func (e Exporter) Validate() error { + switch e { + case HTTP, GRPC, STDOUT, NOOP, "": + return nil + default: + return fmt.Errorf("unsupported exporter type: %s", e.String()) + } +} + +// IsExporting returns true if the protocol is exporting the traces +func (e Exporter) IsExporting() bool { + return e == HTTP || e == GRPC +} + +// exporterFactory is a function that creates a new exporter +type exporterFactory func(ctx context.Context, config *Config) (sdktrace.SpanExporter, error) + +// registry contains the mapping of the exporter to the factory function +var registry = map[Exporter]exporterFactory{ + HTTP: newHTTPExporter, + GRPC: newGRPCExporter, + STDOUT: newStdoutExporter, + NOOP: newNoopExporter, + "": newNoopExporter, +} + +// Create creates a new exporter based on the configuration +func (e Exporter) Create(ctx context.Context, config *Config) (sdktrace.SpanExporter, error) { + if factory, ok := registry[e]; ok { + return factory(ctx, config) + } + return nil, fmt.Errorf("unsupported exporter type: %s", config.Exporter.String()) +} + +// newHTTPExporter creates a new HTTP exporter +func newHTTPExporter(ctx context.Context, config *Config) (sdktrace.SpanExporter, error) { + headers, tlsCfg, err := getCommonConfig(config) + if err != nil { + return nil, err + } + + opts := []otlptracehttp.Option{ + otlptracehttp.WithEndpoint(config.Url), + otlptracehttp.WithHeaders(headers), + } + if tlsCfg != nil { + opts = append(opts, otlptracehttp.WithTLSClientConfig(tlsCfg)) + } else { + opts = append(opts, otlptracehttp.WithInsecure()) + } + + return otlptracehttp.New(ctx, opts...) +} + +// newGRPCExporter creates a new gRPC exporter +func newGRPCExporter(ctx context.Context, config *Config) (sdktrace.SpanExporter, error) { + headers, tlsCfg, err := getCommonConfig(config) + if err != nil { + return nil, err + } + + opts := []otlptracegrpc.Option{ + otlptracegrpc.WithEndpoint(config.Url), + otlptracegrpc.WithHeaders(headers), + } + if tlsCfg != nil { + opts = append(opts, otlptracegrpc.WithTLSCredentials(credentials.NewTLS(tlsCfg))) + } else { + opts = append(opts, otlptracegrpc.WithInsecure()) + } + + return otlptracegrpc.New(ctx, opts...) +} + +// newStdoutExporter creates a new stdout exporter +func newStdoutExporter(_ context.Context, _ *Config) (sdktrace.SpanExporter, error) { + return stdouttrace.New(stdouttrace.WithPrettyPrint()) +} + +// newNoopExporter creates a new noop exporter +func newNoopExporter(_ context.Context, _ *Config) (sdktrace.SpanExporter, error) { + return nil, nil +} + +// getCommonConfig returns the common configuration for the exporters +func getCommonConfig(config *Config) (map[string]string, *tls.Config, error) { + headers := make(map[string]string) + if config.Token != "" { + headers["Authorization"] = fmt.Sprintf("Bearer %s", config.Token) + } + + tlsCfg, err := getTLSConfig(config.CertPath) + if err != nil { + return nil, nil, fmt.Errorf("failed to create TLS configuration: %w", err) + } + + return headers, tlsCfg, nil +} + +// FileOpener is the function used to open a file +type FileOpener func(string) (fs.File, error) + +// openFile is the function used to open a file +var openFile FileOpener = func() FileOpener { + return func(name string) (fs.File, error) { + return os.Open(name) // #nosec G304 // How else to open the file? + } +}() + +func getTLSConfig(certFile string) (conf *tls.Config, err error) { + if certFile == "" || certFile == "insecure" { + return nil, nil + } + + file, err := openFile(certFile) + if err != nil { + return nil, fmt.Errorf("failed to open certificate file: %w", err) + } + defer func() { + if cErr := file.Close(); cErr != nil { + err = errors.Join(err, cErr) + } + }() + + b, err := io.ReadAll(file) + if err != nil { + return nil, fmt.Errorf("failed to read certificate file: %w", err) + } + + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(b) { + return nil, fmt.Errorf("failed to append certificate(s) from file: %s", certFile) + } + + return &tls.Config{ + RootCAs: pool, + InsecureSkipVerify: false, + MinVersion: tls.VersionTLS12, + }, nil +} diff --git a/pkg/sparrow/metrics/metrics.go b/pkg/sparrow/metrics/metrics.go new file mode 100644 index 00000000..6f3a7235 --- /dev/null +++ b/pkg/sparrow/metrics/metrics.go @@ -0,0 +1,122 @@ +// sparrow +// (C) 2024, Deutsche Telekom IT GmbH +// +// Deutsche Telekom IT GmbH and all other contributors / +// copyright owners license this file to you under the Apache +// License, Version 2.0 (the "License"); you may not use this +// file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package metrics + +import ( + "context" + "fmt" + + "github.com/caas-team/sparrow/internal/logger" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.24.0" +) + +var _ Provider = (*manager)(nil) + +//go:generate moq -out metrics_moq.go . Provider +type Provider interface { + // GetRegistry returns the prometheus registry instance + // containing the registered prometheus collectors + GetRegistry() *prometheus.Registry + // InitTracing initializes the OpenTelemetry tracing + InitTracing(ctx context.Context) error + // Shutdown closes the metrics and tracing + Shutdown(ctx context.Context) error +} + +type manager struct { + config Config + registry *prometheus.Registry + tp *sdktrace.TracerProvider +} + +// New initializes the metrics and returns the PrometheusMetrics +func New(config Config) Provider { + registry := prometheus.NewRegistry() + + registry.MustRegister( + collectors.NewGoCollector(), + collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}), + ) + + return &manager{ + config: config, + registry: registry, + } +} + +// GetRegistry returns the registry to register prometheus metrics +func (m *manager) GetRegistry() *prometheus.Registry { + return m.registry +} + +// InitTracing initializes the OpenTelemetry tracing +func (m *manager) InitTracing(ctx context.Context) error { + log := logger.FromContext(ctx) + res, err := resource.New(ctx, + resource.WithHost(), + resource.WithContainer(), + resource.WithAttributes( + semconv.ServiceNameKey.String("sparrow-metrics-api"), + // TODO: Maybe we should use the version that is set on build time in the main package + semconv.ServiceVersionKey.String("0.1.0"), + ), + ) + if err != nil { + log.ErrorContext(ctx, "Failed to create resource", "error", err) + return fmt.Errorf("failed to create resource: %v", err) + } + + exporter, err := m.config.Exporter.Create(ctx, &m.config) + if err != nil { + log.ErrorContext(ctx, "Failed to create exporter", "error", err) + return fmt.Errorf("failed to create exporter: %v", err) + } + + bsp := sdktrace.NewBatchSpanProcessor(exporter) + tp := sdktrace.NewTracerProvider( + // TODO: Keep track of the sampler if we run into traffic issues due to the high volume of data. + sdktrace.WithSampler(sdktrace.AlwaysSample()), + sdktrace.WithSpanProcessor(bsp), + sdktrace.WithResource(res), + ) + otel.SetTracerProvider(tp) + m.tp = tp + log.DebugContext(ctx, "Tracing initialized with new provider", "provider", m.config.Exporter) + return nil +} + +// Shutdown closes the metrics and tracing +func (m *manager) Shutdown(ctx context.Context) error { + log := logger.FromContext(ctx) + if m.tp != nil { + err := m.tp.Shutdown(ctx) + if err != nil { + log.ErrorContext(ctx, "Failed to shutdown tracer provider", "error", err) + return fmt.Errorf("failed to shutdown tracer provider: %w", err) + } + } + + log.DebugContext(ctx, "Tracing shutdown") + return nil +} diff --git a/pkg/sparrow/metrics/metrics_moq.go b/pkg/sparrow/metrics/metrics_moq.go new file mode 100644 index 00000000..bb67a4a6 --- /dev/null +++ b/pkg/sparrow/metrics/metrics_moq.go @@ -0,0 +1,157 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package metrics + +import ( + "context" + "github.com/prometheus/client_golang/prometheus" + "sync" +) + +// Ensure, that ProviderMock does implement Provider. +// If this is not the case, regenerate this file with moq. +var _ Provider = &ProviderMock{} + +// ProviderMock is a mock implementation of Provider. +// +// func TestSomethingThatUsesProvider(t *testing.T) { +// +// // make and configure a mocked Provider +// mockedProvider := &ProviderMock{ +// GetRegistryFunc: func() *prometheus.Registry { +// panic("mock out the GetRegistry method") +// }, +// InitTracingFunc: func(ctx context.Context) error { +// panic("mock out the InitTracing method") +// }, +// ShutdownFunc: func(ctx context.Context) error { +// panic("mock out the Shutdown method") +// }, +// } +// +// // use mockedProvider in code that requires Provider +// // and then make assertions. +// +// } +type ProviderMock struct { + // GetRegistryFunc mocks the GetRegistry method. + GetRegistryFunc func() *prometheus.Registry + + // InitTracingFunc mocks the InitTracing method. + InitTracingFunc func(ctx context.Context) error + + // ShutdownFunc mocks the Shutdown method. + ShutdownFunc func(ctx context.Context) error + + // calls tracks calls to the methods. + calls struct { + // GetRegistry holds details about calls to the GetRegistry method. + GetRegistry []struct { + } + // InitTracing holds details about calls to the InitTracing method. + InitTracing []struct { + // Ctx is the ctx argument value. + Ctx context.Context + } + // Shutdown holds details about calls to the Shutdown method. + Shutdown []struct { + // Ctx is the ctx argument value. + Ctx context.Context + } + } + lockGetRegistry sync.RWMutex + lockInitTracing sync.RWMutex + lockShutdown sync.RWMutex +} + +// GetRegistry calls GetRegistryFunc. +func (mock *ProviderMock) GetRegistry() *prometheus.Registry { + if mock.GetRegistryFunc == nil { + panic("ProviderMock.GetRegistryFunc: method is nil but Provider.GetRegistry was just called") + } + callInfo := struct { + }{} + mock.lockGetRegistry.Lock() + mock.calls.GetRegistry = append(mock.calls.GetRegistry, callInfo) + mock.lockGetRegistry.Unlock() + return mock.GetRegistryFunc() +} + +// GetRegistryCalls gets all the calls that were made to GetRegistry. +// Check the length with: +// +// len(mockedProvider.GetRegistryCalls()) +func (mock *ProviderMock) GetRegistryCalls() []struct { +} { + var calls []struct { + } + mock.lockGetRegistry.RLock() + calls = mock.calls.GetRegistry + mock.lockGetRegistry.RUnlock() + return calls +} + +// InitTracing calls InitTracingFunc. +func (mock *ProviderMock) InitTracing(ctx context.Context) error { + if mock.InitTracingFunc == nil { + panic("ProviderMock.InitTracingFunc: method is nil but Provider.InitTracing was just called") + } + callInfo := struct { + Ctx context.Context + }{ + Ctx: ctx, + } + mock.lockInitTracing.Lock() + mock.calls.InitTracing = append(mock.calls.InitTracing, callInfo) + mock.lockInitTracing.Unlock() + return mock.InitTracingFunc(ctx) +} + +// InitTracingCalls gets all the calls that were made to InitTracing. +// Check the length with: +// +// len(mockedProvider.InitTracingCalls()) +func (mock *ProviderMock) InitTracingCalls() []struct { + Ctx context.Context +} { + var calls []struct { + Ctx context.Context + } + mock.lockInitTracing.RLock() + calls = mock.calls.InitTracing + mock.lockInitTracing.RUnlock() + return calls +} + +// Shutdown calls ShutdownFunc. +func (mock *ProviderMock) Shutdown(ctx context.Context) error { + if mock.ShutdownFunc == nil { + panic("ProviderMock.ShutdownFunc: method is nil but Provider.Shutdown was just called") + } + callInfo := struct { + Ctx context.Context + }{ + Ctx: ctx, + } + mock.lockShutdown.Lock() + mock.calls.Shutdown = append(mock.calls.Shutdown, callInfo) + mock.lockShutdown.Unlock() + return mock.ShutdownFunc(ctx) +} + +// ShutdownCalls gets all the calls that were made to Shutdown. +// Check the length with: +// +// len(mockedProvider.ShutdownCalls()) +func (mock *ProviderMock) ShutdownCalls() []struct { + Ctx context.Context +} { + var calls []struct { + Ctx context.Context + } + mock.lockShutdown.RLock() + calls = mock.calls.Shutdown + mock.lockShutdown.RUnlock() + return calls +} diff --git a/pkg/sparrow/metrics/metrics_test.go b/pkg/sparrow/metrics/metrics_test.go new file mode 100644 index 00000000..80c0f566 --- /dev/null +++ b/pkg/sparrow/metrics/metrics_test.go @@ -0,0 +1,138 @@ +// sparrow +// (C) 2024, Deutsche Telekom IT GmbH +// +// Deutsche Telekom IT GmbH and all other contributors / +// copyright owners license this file to you under the Apache +// License, Version 2.0 (the "License"); you may not use this +// file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package metrics + +import ( + "context" + "reflect" + "testing" + + "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel" + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +func TestPrometheusMetrics_GetRegistry(t *testing.T) { + tests := []struct { + name string + registry *prometheus.Registry + want *prometheus.Registry + }{ + { + name: "simple registry", + registry: prometheus.NewRegistry(), + want: prometheus.NewRegistry(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &manager{ + registry: tt.registry, + } + if got := m.GetRegistry(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("PrometheusMetrics.GetRegistry() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewMetrics(t *testing.T) { + testMetrics := New(Config{}) + testGauge := prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: "TEST_GAUGE", + }, + ) + + t.Run("Register a collector", func(t *testing.T) { + testMetrics.(*manager).registry.MustRegister( + testGauge, + ) + }) +} + +func TestMetrics_InitTracing(t *testing.T) { + tests := []struct { + name string + config Config + wantErr bool + }{ + { + name: "success - stdout exporter", + config: Config{ + Exporter: STDOUT, + Url: "", + Token: "", + }, + wantErr: false, + }, + { + name: "success - otlp exporter", + config: Config{ + Exporter: HTTP, + Url: "http://localhost:4317", + Token: "", + }, + wantErr: false, + }, + { + name: "success - otlp exporter with token", + config: Config{ + Exporter: GRPC, + Url: "http://localhost:4317", + Token: "my-super-secret-token", + }, + wantErr: false, + }, + { + name: "success - no exporter", + config: Config{ + Exporter: NOOP, + Url: "", + Token: "", + }, + wantErr: false, + }, + { + name: "failure - unsupported exporter", + config: Config{ + Exporter: "unsupported", + Url: "", + Token: "", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := New(tt.config) + if err := m.InitTracing(context.Background()); (err != nil) != tt.wantErr { + t.Errorf("Metrics.InitTracing() error = %v", err) + } + + if tp, ok := otel.GetTracerProvider().(*sdktrace.TracerProvider); !ok { + t.Errorf("Metrics.InitTracing() type = %T, want = %T", tp, &sdktrace.TracerProvider{}) + } + + if err := m.Shutdown(context.Background()); err != nil { + t.Fatalf("Metrics.Shutdown() error = %v", err) + } + }) + } +} diff --git a/pkg/sparrow/metrics_moq.go b/pkg/sparrow/metrics_moq.go deleted file mode 100644 index 19cc3e8b..00000000 --- a/pkg/sparrow/metrics_moq.go +++ /dev/null @@ -1,68 +0,0 @@ -// Code generated by moq; DO NOT EDIT. -// github.com/matryer/moq - -package sparrow - -import ( - "github.com/prometheus/client_golang/prometheus" - "sync" -) - -// Ensure, that MetricsMock does implement Metrics. -// If this is not the case, regenerate this file with moq. -var _ Metrics = &MetricsMock{} - -// MetricsMock is a mock implementation of Metrics. -// -// func TestSomethingThatUsesMetrics(t *testing.T) { -// -// // make and configure a mocked Metrics -// mockedMetrics := &MetricsMock{ -// GetRegistryFunc: func() *prometheus.Registry { -// panic("mock out the GetRegistry method") -// }, -// } -// -// // use mockedMetrics in code that requires Metrics -// // and then make assertions. -// -// } -type MetricsMock struct { - // GetRegistryFunc mocks the GetRegistry method. - GetRegistryFunc func() *prometheus.Registry - - // calls tracks calls to the methods. - calls struct { - // GetRegistry holds details about calls to the GetRegistry method. - GetRegistry []struct { - } - } - lockGetRegistry sync.RWMutex -} - -// GetRegistry calls GetRegistryFunc. -func (mock *MetricsMock) GetRegistry() *prometheus.Registry { - if mock.GetRegistryFunc == nil { - panic("MetricsMock.GetRegistryFunc: method is nil but Metrics.GetRegistry was just called") - } - callInfo := struct { - }{} - mock.lockGetRegistry.Lock() - mock.calls.GetRegistry = append(mock.calls.GetRegistry, callInfo) - mock.lockGetRegistry.Unlock() - return mock.GetRegistryFunc() -} - -// GetRegistryCalls gets all the calls that were made to GetRegistry. -// Check the length with: -// -// len(mockedMetrics.GetRegistryCalls()) -func (mock *MetricsMock) GetRegistryCalls() []struct { -} { - var calls []struct { - } - mock.lockGetRegistry.RLock() - calls = mock.calls.GetRegistry - mock.lockGetRegistry.RUnlock() - return calls -} diff --git a/pkg/sparrow/metrics_test.go b/pkg/sparrow/metrics_test.go deleted file mode 100644 index 66e7dcf8..00000000 --- a/pkg/sparrow/metrics_test.go +++ /dev/null @@ -1,70 +0,0 @@ -// sparrow -// (C) 2024, Deutsche Telekom IT GmbH -// -// Deutsche Telekom IT GmbH and all other contributors / -// copyright owners license this file to you under the Apache -// License, Version 2.0 (the "License"); you may not use this -// file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package sparrow - -import ( - "reflect" - "testing" - - "github.com/prometheus/client_golang/prometheus" -) - -func TestPrometheusMetrics_GetRegistry(t *testing.T) { - type fields struct { - registry *prometheus.Registry - } - tests := []struct { - name string - fields fields - want *prometheus.Registry - }{ - { - name: "simple registry", - fields: fields{ - registry: prometheus.NewRegistry(), - }, - want: prometheus.NewRegistry(), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m := &PrometheusMetrics{ - registry: tt.fields.registry, - } - if got := m.GetRegistry(); !reflect.DeepEqual(got, tt.want) { - t.Errorf("PrometheusMetrics.GetRegistry() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestNewMetrics(t *testing.T) { - testMetrics := NewMetrics() - testGauge := prometheus.NewGauge( - prometheus.GaugeOpts{ - Name: "TEST_GAUGE", - }, - ) - - t.Run("Register a collector", func(t *testing.T) { - testMetrics.(*PrometheusMetrics).registry.MustRegister( - testGauge, - ) - }) -} diff --git a/pkg/sparrow/run.go b/pkg/sparrow/run.go index 2a78b806..742f95da 100644 --- a/pkg/sparrow/run.go +++ b/pkg/sparrow/run.go @@ -32,6 +32,7 @@ import ( "github.com/caas-team/sparrow/pkg/checks/runtime" "github.com/caas-team/sparrow/pkg/config" "github.com/caas-team/sparrow/pkg/db" + "github.com/caas-team/sparrow/pkg/sparrow/metrics" "github.com/caas-team/sparrow/pkg/sparrow/targets" ) @@ -50,7 +51,7 @@ type Sparrow struct { // tarMan is the target manager that is used to manage global targets tarMan targets.TargetManager // metrics is used to collect metrics - metrics Metrics + metrics metrics.Provider // controller is used to manage the checks controller *ChecksController // cRuntime is used to signal that the runtime configuration has changed @@ -65,15 +66,15 @@ type Sparrow struct { // New creates a new sparrow from a given configfile func New(cfg *config.Config) *Sparrow { - metrics := NewMetrics() + m := metrics.New(cfg.Telemetry) dbase := db.NewInMemory() sparrow := &Sparrow{ config: cfg, db: dbase, api: api.New(cfg.Api), - metrics: metrics, - controller: NewChecksController(dbase, metrics), + metrics: m, + controller: NewChecksController(dbase, m), cRuntime: make(chan runtime.Config, 1), cErr: make(chan error, 1), cDone: make(chan struct{}, 1), @@ -95,6 +96,11 @@ func (s *Sparrow) Run(ctx context.Context) error { log := logger.FromContext(ctx) defer cancel() + err := s.metrics.InitTracing(ctx) + if err != nil { + return fmt.Errorf("failed to initialize tracing: %w", err) + } + go func() { s.cErr <- s.loader.Run(ctx) }() @@ -181,6 +187,7 @@ func (s *Sparrow) shutdown(ctx context.Context) { sErrs.errTarMan = s.tarMan.Shutdown(ctx) } sErrs.errAPI = s.api.Shutdown(ctx) + sErrs.errMetrics = s.metrics.Shutdown(ctx) s.loader.Shutdown(ctx) s.controller.Shutdown(ctx) diff --git a/pkg/sparrow/run_errors.go b/pkg/sparrow/run_errors.go index 034a7967..e1f983ba 100644 --- a/pkg/sparrow/run_errors.go +++ b/pkg/sparrow/run_errors.go @@ -19,10 +19,11 @@ package sparrow type ErrShutdown struct { - errAPI error - errTarMan error + errAPI error + errTarMan error + errMetrics error } func (e ErrShutdown) HasError() bool { - return e.errAPI != nil || e.errTarMan != nil + return e.errAPI != nil || e.errTarMan != nil || e.errMetrics != nil }