From 8249de986eccc6cb73a33a36183bf4c7c6315953 Mon Sep 17 00:00:00 2001 From: James Rhoat Date: Wed, 26 Jun 2024 11:16:21 +0200 Subject: [PATCH] Updating documentation around OTEL (#6519) Updating documentation around the opentelemetry endpoint for metrics and the "How to use OpenTelemetry metrics with VictoriaMetrics" guide so that it shows not only how to directly write but also how to write to the otel collector and view metrics in vmui. --------- Signed-off-by: Zakhar Bessarab Co-authored-by: Zakhar Bessarab (cherry picked from commit 6652fb630f60190e383d78ad68f19a6dd621b09d) Signed-off-by: hagen1778 --- docs/README.md | 19 ++ docs/Single-server-VictoriaMetrics.md | 19 ++ ...ith-opentelemetry-app.go-collector.example | 223 ++++++++++++++++++ .../getting-started-with-opentelemetry.md | 97 +++++++- docs/guides/vmui-dice-roll.webp | Bin 0 -> 17372 bytes 5 files changed, 349 insertions(+), 9 deletions(-) create mode 100644 docs/guides/getting-started-with-opentelemetry-app.go-collector.example create mode 100644 docs/guides/vmui-dice-roll.webp diff --git a/docs/README.md b/docs/README.md index d5ec365de..c09e77ab0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1627,6 +1627,25 @@ Set HTTP request header `Content-Encoding: gzip` when sending gzip-compressed da VictoriaMetrics stores the ingested OpenTelemetry [raw samples](https://docs.victoriametrics.com/keyconcepts/#raw-samples) as is without any transformations. Pass `-opentelemetry.usePrometheusNaming` command-line flag to VictoriaMetrics for automatic conversion of metric names and labels into Prometheus-compatible format. +Using the following exporter configuration in the opentelemetry collector will allow you to send metrics into VictoriaMetrics: + +```yaml +exporters: + otlphttp/victoriametrics: + compression: gzip + encoding: proto + endpoint: http://..svc.cluster.local:/opentelemetry +``` +Remember to add the exporter to the desired service pipeline in order to activate the exporter. +```yaml +service: + pipelines: + metrics: + exporters: + - otlphttp/victoriametrics + receivers: + - otlp +``` See [How to use OpenTelemetry metrics with VictoriaMetrics](https://docs.victoriametrics.com/guides/getting-started-with-opentelemetry/). ## JSON line format diff --git a/docs/Single-server-VictoriaMetrics.md b/docs/Single-server-VictoriaMetrics.md index 1536a9b72..32da7583e 100644 --- a/docs/Single-server-VictoriaMetrics.md +++ b/docs/Single-server-VictoriaMetrics.md @@ -1635,6 +1635,25 @@ Set HTTP request header `Content-Encoding: gzip` when sending gzip-compressed da VictoriaMetrics stores the ingested OpenTelemetry [raw samples](https://docs.victoriametrics.com/keyconcepts/#raw-samples) as is without any transformations. Pass `-opentelemetry.usePrometheusNaming` command-line flag to VictoriaMetrics for automatic conversion of metric names and labels into Prometheus-compatible format. +Using the following exporter configuration in the opentelemetry collector will allow you to send metrics into VictoriaMetrics: + +```yaml +exporters: + otlphttp/victoriametrics: + compression: gzip + encoding: proto + endpoint: http://..svc.cluster.local:/opentelemetry +``` +Remember to add the exporter to the desired service pipeline in order to activate the exporter. +```yaml +service: + pipelines: + metrics: + exporters: + - otlphttp/victoriametrics + receivers: + - otlp +``` See [How to use OpenTelemetry metrics with VictoriaMetrics](https://docs.victoriametrics.com/guides/getting-started-with-opentelemetry/). ## JSON line format diff --git a/docs/guides/getting-started-with-opentelemetry-app.go-collector.example b/docs/guides/getting-started-with-opentelemetry-app.go-collector.example new file mode 100644 index 000000000..24bc6e72c --- /dev/null +++ b/docs/guides/getting-started-with-opentelemetry-app.go-collector.example @@ -0,0 +1,223 @@ +package main + +import ( + "context" + "errors" + "io" + "log" + "math/rand" + "net" + "net/http" + "os" + "os/signal" + "strconv" + "time" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/propagation" + otelmetric "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.25.0" +) + +func main() { + if err := run(); err != nil { + log.Fatalln(err) + } +} + +func run() (err error) { + // Handle SIGINT (CTRL+C) gracefully. + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + // Set up OpenTelemetry. + otelShutdown, err := setupOTelSDK(ctx) + if err != nil { + return + } + // Handle shutdown properly so nothing leaks. + defer func() { + err = errors.Join(err, otelShutdown(context.Background())) + }() + + // Start HTTP server. + srv := &http.Server{ + Addr: ":8080", + BaseContext: func(_ net.Listener) context.Context { return ctx }, + ReadTimeout: time.Second, + WriteTimeout: 10 * time.Second, + Handler: newHTTPHandler(), + } + srvErr := make(chan error, 1) + go func() { + srvErr <- srv.ListenAndServe() + }() + + // Wait for interruption. + select { + case err = <-srvErr: + // Error when starting HTTP server. + return + case <-ctx.Done(): + // Wait for first CTRL+C. + // Stop receiving signal notifications as soon as possible. + stop() + } + + // When Shutdown is called, ListenAndServe immediately returns ErrServerClosed. + err = srv.Shutdown(context.Background()) + return +} + +func newHTTPHandler() http.Handler { + mux := http.NewServeMux() + + // handleFunc is a replacement for mux.HandleFunc + // which enriches the handler's HTTP instrumentation with the pattern as the http.route. + handleFunc := func(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) { + // Configure the "http.route" for the HTTP instrumentation. + handler := otelhttp.WithRouteTag(pattern, http.HandlerFunc(handlerFunc)) + mux.Handle(pattern, handler) + } + + // Register handlers. + handleFunc("/rolldice", rolldice) + + // Add HTTP instrumentation for the whole server. + handler := otelhttp.NewHandler(mux, "/") + return handler +} + +var ( + tracer = otel.Tracer("rolldice") + meter = otel.Meter("rolldice") + rollCnt otelmetric.Int64Counter +) + +func init() { + var err error + rollCnt, err = meter.Int64Counter("dice.rolls", + otelmetric.WithDescription("The number of rolls by roll value"), + otelmetric.WithUnit("{roll}")) + if err != nil { + panic(err) + } +} + +func rolldice(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "roll") + defer span.End() + + roll := 1 + rand.Intn(6) + + rollValueAttr := attribute.Int("roll.value", roll) + span.SetAttributes(rollValueAttr) + rollCnt.Add(ctx, 1, otelmetric.WithAttributes(rollValueAttr)) + + resp := strconv.Itoa(roll) + "\n" + if _, err := io.WriteString(w, resp); err != nil { + log.Printf("Write failed: %v\n", err) + } +} + +// setupOTelSDK bootstraps the OpenTelemetry pipeline. +// If it does not return an error, make sure to call shutdown for proper cleanup. +func setupOTelSDK(ctx context.Context) (shutdown func(context.Context) error, err error) { + var shutdownFuncs []func(context.Context) error + + // shutdown calls cleanup functions registered via shutdownFuncs. + // The errors from the calls are joined. + // Each registered cleanup will be invoked once. + shutdown = func(ctx context.Context) error { + var err error + for _, fn := range shutdownFuncs { + err = errors.Join(err, fn(ctx)) + } + shutdownFuncs = nil + return err + } + + // handleErr calls shutdown for cleanup and makes sure that all errors are returned. + handleErr := func(inErr error) { + err = errors.Join(inErr, shutdown(ctx)) + } + + // Set up propagator. + prop := newPropagator() + otel.SetTextMapPropagator(prop) + + // Set up trace provider. + tracerProvider, err := newTraceProvider(ctx) + if err != nil { + handleErr(err) + return + } + shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown) + otel.SetTracerProvider(tracerProvider) + + // Set up meter provider. + meterProvider, err := newMeterProvider(ctx) + if err != nil { + handleErr(err) + return + } + shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown) + otel.SetMeterProvider(meterProvider) + + return +} + +func newPropagator() propagation.TextMapPropagator { + return propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ) +} + +func newTraceProvider(ctx context.Context) (*trace.TracerProvider, error) { + traceExporter, err := otlptracehttp.New(ctx, otlptracehttp.WithInsecure(), otlptracehttp.WithEndpoint("localhost:4318")) + if err != nil { + return nil, err + } + + traceProvider := trace.NewTracerProvider( + trace.WithBatcher(traceExporter, + // Default is 5s. Set to 1s for demonstrative purposes. + trace.WithBatchTimeout(time.Second)), + ) + return traceProvider, nil +} + +func newMeterProvider(ctx context.Context) (*metric.MeterProvider, error) { + metricExporter, err := otlpmetrichttp.New(ctx, otlpmetrichttp.WithInsecure(), otlpmetrichttp.WithEndpoint("localhost:4318")) + if err != nil { + return nil, err + } + //metricExporter, err := stdoutmetric.New() + //if err != nil { + // return nil, err + //} + res, err := resource.Merge(resource.Default(), + resource.NewWithAttributes(semconv.SchemaURL, + semconv.ServiceName("dice-roller"), + semconv.ServiceVersion("0.1.0"), + )) + if err != nil { + return nil, err + } + meterProvider := metric.NewMeterProvider( + metric.WithResource(res), + metric.WithReader(metric.NewPeriodicReader(metricExporter, + // Default is 1m. Set to 3s for demonstrative purposes. + metric.WithInterval(3*time.Second))), + ) + + return meterProvider, nil +} diff --git a/docs/guides/getting-started-with-opentelemetry.md b/docs/guides/getting-started-with-opentelemetry.md index e8f444516..7d5afe912 100644 --- a/docs/guides/getting-started-with-opentelemetry.md +++ b/docs/guides/getting-started-with-opentelemetry.md @@ -55,23 +55,37 @@ helm repo update # add values cat << EOF > values.yaml +mode: deployment +image: + repository: "otel/opentelemetry-collector-contrib" presets: clusterMetrics: enabled: true config: + receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 exporters: - prometheusremotewrite: - endpoint: "http://victoria-metrics-victoria-metrics-single-server.default.svc.cluster.local:8428/api/v1/write" + otlphttp/victoriametrics: + compression: gzip + encoding: proto + endpoint: http://victoria-metrics-victoria-metrics-single-server.default.svc.cluster.local:8428/opentelemetry + tls: + insecure: true service: - pipelines: - metrics: - receivers: [otlp] - processors: [] - exporters: [prometheusremotewrite] + pipelines: + metrics: + receivers: [otlp] + processors: [] + exporters: [otlphttp/victoriametrics] EOF # install helm chart -helm upgrade -i otl-collector open-telemetry/opentelemetry-collector --set mode=deployment -f values.yaml +helm upgrade -i otl-collector open-telemetry/opentelemetry-collector -f values.yaml # check if pod is healthy kubectl get pod @@ -79,13 +93,78 @@ NAME READY STATUS RESTA otl-collector-opentelemetry-collector-7467bbb559-2pq2n 1/1 Running 0 23m # forward port to local machine to verify metrics are ingested -kubectl port-forward victoria-metrics-victoria-metrics-single-server-0 8428 +kubectl port-forward service/victoria-metrics-victoria-metrics-single-server 8428 # check metric `k8s_container_ready` via browser http://localhost:8428/vmui/#/?g0.expr=k8s_container_ready + +# forward port to local machine to setup opentelemetry-collector locally +kubectl port-forward otl-collector-opentelemetry-collector 4318 + ``` The full version of possible configuration options could be found in [OpenTelemetry docs](https://opentelemetry.io/docs/collector/configuration/). +## Sending to VictoriaMetrics via OpenTelemetry +Metrics could be sent to VictoriaMetrics via OpenTelemetry instrumentation libraries. You can use any compatible OpenTelemetry instrumentation [clients](https://opentelemetry.io/docs/languages/). +In our example, we'll create a WEB server in [Golang](https://go.dev/) and instrument it with metrics. + +### Building the Go application instrumented with metrics +Copy the go file from [here](/guides/getting-started-with-opentelemetry-app.go-collector.example). This will give you a basic implementation of a dice roll WEB server with the urls for opentelemetry-collector pointing to localhost:4318. +In the same directory run the following command to create the `go.mod` file: +```sh +go mod init vm/otel +``` + +For demo purposes, we'll add the following dependencies to `go.mod` file: +```go + +require ( + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 + go.opentelemetry.io/otel v1.27.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 + go.opentelemetry.io/otel/metric v1.27.0 + go.opentelemetry.io/otel/sdk v1.27.0 + go.opentelemetry.io/otel/sdk/metric v1.27.0 +) + +require ( + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.1 // indirectdice.rolls + github.com/go-logr/stdr v1.2.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect + go.opentelemetry.io/otel/trace v1.27.0 // indirect + go.opentelemetry.io/proto/otlp v1.2.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect + google.golang.org/grpc v1.64.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect +) +``` + +Once you have these in your `go.mod` file, you can run the following command to download the dependencies: +```sh +go mod tidy +``` + +Now you can run the application: +```sh +go run . +``` + +### Test metrics ingestion +By default, the application will be available at `localhost:8080`. You can start sending requests to /rolldice endpoint to generate metrics. The following command will send 20 requests to the /rolldice endpoint: +```sh +for i in `seq 1 20`; do curl http://localhost:8080/rolldice; done +``` + +After a few seconds you should start to see metrics sent over to the vmui interface by visiting `http://localhost:8428/vmui/#/?g0.expr=dice.rolls` in your browser or by querying the metric `dice.rolls` in the vmui interface. + ## Direct metrics push Metrics could be ingested into VictoriaMetrics directly with HTTP requests. You can use any compatible OpenTelemetry diff --git a/docs/guides/vmui-dice-roll.webp b/docs/guides/vmui-dice-roll.webp new file mode 100644 index 0000000000000000000000000000000000000000..300cca63f4ab83fe1f72eec5c19602b8166c40ca GIT binary patch literal 17372 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 literal 0 HcmV?d00001