package opentelemetry import ( "context" "net" "os" "path/filepath" "runtime" "sort" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/influxdata/influxdb-observability/otel2influx" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" otlplogs "go.opentelemetry.io/proto/otlp/collector/logs/v1" otlpmetrics "go.opentelemetry.io/proto/otlp/collector/metrics/v1" otlpprofiles "go.opentelemetry.io/proto/otlp/collector/profiles/v1experimental" otlptrace "go.opentelemetry.io/proto/otlp/collector/trace/v1" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/protobuf/encoding/protojson" "github.com/influxdata/telegraf" "github.com/influxdata/telegraf/config" "github.com/influxdata/telegraf/plugins/inputs" "github.com/influxdata/telegraf/plugins/parsers/influx" "github.com/influxdata/telegraf/testutil" ) func TestOpenTelemetry(t *testing.T) { // Setup and start the plugin plugin := &OpenTelemetry{ MetricsSchema: "prometheus-v1", } require.NoError(t, plugin.Init()) var acc testutil.Accumulator require.NoError(t, plugin.Start(&acc)) defer plugin.Stop() // Setup the OpenTelemetry exporter ctx, cancel := context.WithTimeout(t.Context(), time.Second) defer cancel() exporter, err := otlpmetricgrpc.New(ctx, otlpmetricgrpc.WithInsecure(), otlpmetricgrpc.WithDialOption( grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return net.Dial("tcp", plugin.listener.Addr().String()) })), ) require.NoError(t, err) defer exporter.Shutdown(ctx) //nolint:errcheck // We cannot do anything if the shutdown fails // Setup the metric to send reader := metric.NewManualReader() defer reader.Shutdown(ctx) //nolint:errcheck // We cannot do anything if the shutdown fails provider := metric.NewMeterProvider(metric.WithReader(reader)) meter := provider.Meter("library-name") counter, err := meter.Int64Counter("measurement-counter") require.NoError(t, err) counter.Add(ctx, 7) // Write the OpenTelemetry metrics var rm metricdata.ResourceMetrics require.NoError(t, reader.Collect(ctx, &rm)) require.NoError(t, exporter.Export(ctx, &rm)) // Shutdown require.NoError(t, reader.Shutdown(ctx)) require.NoError(t, exporter.Shutdown(ctx)) plugin.Stop() // Check require.Empty(t, acc.Errors) var exesuffix string if runtime.GOOS == "windows" { exesuffix = ".exe" } expected := []telegraf.Metric{ testutil.MustMetric( "measurement-counter", map[string]string{ "otel.library.name": "library-name", "service.name": "unknown_service:opentelemetry.test" + exesuffix, "telemetry.sdk.language": "go", "telemetry.sdk.name": "opentelemetry", "telemetry.sdk.version": "1.27.0", }, map[string]interface{}{ "counter": 7, }, time.Unix(0, 0), telegraf.Counter, ), } options := []cmp.Option{ testutil.IgnoreTime(), testutil.IgnoreFields("start_time_unix_nano"), testutil.IgnoreTags("telemetry.sdk.version"), } actual := acc.GetTelegrafMetrics() testutil.RequireMetricsEqual(t, expected, actual, options...) } func TestCases(t *testing.T) { // Get all directories in testdata folders, err := os.ReadDir("testcases") require.NoError(t, err) // Register the plugin inputs.Add("opentelemetry", func() telegraf.Input { return &OpenTelemetry{ ServiceAddress: "127.0.0.1:0", SpanDimensions: otel2influx.DefaultOtelTracesToLineProtocolConfig().SpanDimensions, LogRecordDimensions: otel2influx.DefaultOtelLogsToLineProtocolConfig().LogRecordDimensions, ProfileDimensions: []string{"host.name"}, Timeout: config.Duration(5 * time.Second), } }) // Prepare the influx parser for expectations parser := &influx.Parser{} require.NoError(t, parser.Init()) for _, f := range folders { // Only handle folders if !f.IsDir() { continue } testcasePath := filepath.Join("testcases", f.Name()) configFilename := filepath.Join(testcasePath, "telegraf.conf") inputFiles := filepath.Join(testcasePath, "*.json") expectedFilename := filepath.Join(testcasePath, "expected.out") expectedErrorFilename := filepath.Join(testcasePath, "expected.err") // Compare options options := []cmp.Option{ testutil.IgnoreTime(), testutil.SortMetrics(), testutil.IgnoreFields("start_time_unix_nano"), } t.Run(f.Name(), func(t *testing.T) { // Read the input data inputs := make(map[string][][]byte) matches, err := filepath.Glob(inputFiles) require.NoError(t, err) require.NotEmpty(t, matches) sort.Strings(matches) for _, fn := range matches { buf, err := os.ReadFile(fn) require.NoError(t, err) key := strings.TrimSuffix(filepath.Base(fn), ".json") key, _, _ = strings.Cut(key, "_") inputs[key] = append(inputs[key], buf) } // Read the expected output if any var expected []telegraf.Metric if _, err := os.Stat(expectedFilename); err == nil { var err error expected, err = testutil.ParseMetricsFromFile(expectedFilename, parser) require.NoError(t, err) } // Read the expected output if any var expectedErrors []string if _, err := os.Stat(expectedErrorFilename); err == nil { var err error expectedErrors, err = testutil.ParseLinesFromFile(expectedErrorFilename) require.NoError(t, err) require.NotEmpty(t, expectedErrors) } // Configure the plugin cfg := config.NewConfig() require.NoError(t, cfg.LoadConfig(configFilename)) require.Len(t, cfg.Inputs, 1) // Setup and start the plugin plugin := cfg.Inputs[0].Input.(*OpenTelemetry) require.NoError(t, plugin.Init()) var acc testutil.Accumulator require.NoError(t, plugin.Start(&acc)) defer plugin.Stop() // Send all data to the plugin addr := plugin.listener.Addr().String() ctx, cancel := context.WithTimeout(t.Context(), time.Second) defer cancel() grpcClient, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) require.NoError(t, err) defer grpcClient.Close() for msgtype, messages := range inputs { switch msgtype { case "logs": client := otlplogs.NewLogsServiceClient(grpcClient) for _, buf := range messages { var msg otlplogs.ExportLogsServiceRequest require.NoError(t, protojson.Unmarshal(buf, &msg)) _, err := client.Export(ctx, &msg) require.NoError(t, err) } case "metrics": client := otlpmetrics.NewMetricsServiceClient(grpcClient) for _, buf := range messages { var msg otlpmetrics.ExportMetricsServiceRequest require.NoError(t, protojson.Unmarshal(buf, &msg)) _, err := client.Export(ctx, &msg) require.NoError(t, err) } case "profiles": client := otlpprofiles.NewProfilesServiceClient(grpcClient) for _, buf := range messages { var msg otlpprofiles.ExportProfilesServiceRequest require.NoError(t, protojson.Unmarshal(buf, &msg)) _, err := client.Export(ctx, &msg) require.NoError(t, err) } case "traces": client := otlptrace.NewTraceServiceClient(grpcClient) for _, buf := range messages { var msg otlptrace.ExportTraceServiceRequest require.NoError(t, protojson.Unmarshal(buf, &msg)) _, err := client.Export(ctx, &msg) require.NoError(t, err) } } } // Close the plugin to make sure all data is flushed require.NoError(t, grpcClient.Close()) plugin.Stop() // Check the metrics require.Eventually(t, func() bool { return acc.NMetrics() >= uint64(len(expected)) }, 3*time.Second, 100*time.Millisecond) require.Empty(t, acc.Errors) actual := acc.GetTelegrafMetrics() testutil.RequireMetricsEqual(t, expected, actual, options...) }) } }