From 8abcfc1b2d0ed942da906a54fcd1ae93182995cf Mon Sep 17 00:00:00 2001 From: whitneygriffith Date: Mon, 14 Oct 2024 14:58:54 +0000 Subject: [PATCH] Add telemetryClient interface and update trace exporter unit tests Signed-off-by: whitneygriffith --- exporter/azuremonitorexporter/clients.go | 48 +++++++++++++++++ exporter/azuremonitorexporter/factory.go | 41 +++++++++++++- .../mock_telemetryClient.go | 53 +++++++++++++++++++ .../azuremonitorexporter/traceexporter.go | 15 +++--- .../traceexporter_test.go | 41 +++++++++----- 5 files changed, 176 insertions(+), 22 deletions(-) create mode 100644 exporter/azuremonitorexporter/clients.go create mode 100644 exporter/azuremonitorexporter/mock_telemetryClient.go diff --git a/exporter/azuremonitorexporter/clients.go b/exporter/azuremonitorexporter/clients.go new file mode 100644 index 000000000000..6c34f70dd4e5 --- /dev/null +++ b/exporter/azuremonitorexporter/clients.go @@ -0,0 +1,48 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package azuremonitorexporter + +import "github.com/microsoft/ApplicationInsights-Go/appinsights" + +// telemetryClient is an interface for telemetry clients. +type telemetryClient interface { + Track(telemetry appinsights.Telemetry) + Channel() telemetryChannel +} + +// telemetryChannel is an interface for telemetry channels. +type telemetryChannel interface { + Flush() + Close() +} + +// appInsightsTelemetryClient is a wrapper around appinsights.TelemetryClient. +type appInsightsTelemetryClient struct { + client appinsights.TelemetryClient +} + +// Track sends telemetry data using the underlying client. +func (t *appInsightsTelemetryClient) Track(telemetry appinsights.Telemetry) { + t.client.Track(telemetry) +} + +// Channel returns the telemetry channel of the underlying client. +func (t *appInsightsTelemetryClient) Channel() telemetryChannel { + return &appInsightsTelemetryChannel{channel: t.client.Channel()} +} + +// appInsightsTelemetryChannel is a wrapper around appinsights.TelemetryChannel. +type appInsightsTelemetryChannel struct { + channel appinsights.TelemetryChannel +} + +// Flush flushes the telemetry data. +func (t *appInsightsTelemetryChannel) Flush() { + t.channel.Flush() +} + +// Close closes the telemetry channel. +func (t *appInsightsTelemetryChannel) Close() { + t.channel.Close() +} \ No newline at end of file diff --git a/exporter/azuremonitorexporter/factory.go b/exporter/azuremonitorexporter/factory.go index 99545457c2cc..44240589741e 100644 --- a/exporter/azuremonitorexporter/factory.go +++ b/exporter/azuremonitorexporter/factory.go @@ -41,7 +41,8 @@ func NewFactory() exporter.Factory { // Implements the interface from go.opentelemetry.io/collector/exporter/factory.go type factory struct { - tChannel transportChannel + tChannel transportChannel + telemetryClient telemetryClient } func createDefaultConfig() component.Config { @@ -65,12 +66,18 @@ func (f *factory) createTracesExporter( return nil, errUnexpectedConfigurationType } + // TODO: deprecate in favor of telemetryClient tc, errInstrumentationKeyOrConnectionString := f.getTransportChannel(exporterConfig, set.Logger) if errInstrumentationKeyOrConnectionString != nil { return nil, errInstrumentationKeyOrConnectionString } - return newTracesExporter(exporterConfig, tc, set) + telemetryClient, errInstrumentationKeyOrConnectionString := f.getTelemetryClient(exporterConfig, set.Logger) + if errInstrumentationKeyOrConnectionString != nil { + return nil, errInstrumentationKeyOrConnectionString + } + + return newTracesExporter(exporterConfig, tc, telemetryClient, set) } func (f *factory) createLogsExporter( @@ -144,3 +151,33 @@ func (f *factory) getTransportChannel(exporterConfig *Config, logger *zap.Logger return f.tChannel, nil } + +// Configures the telemetry client. +func (f *factory) getTelemetryClient(exporterConfig *Config, logger *zap.Logger) (telemetryClient, error) { + + if f.telemetryClient == nil { + connectionVars, err := parseConnectionString(exporterConfig) + if err != nil { + return nil, err + } + + exporterConfig.InstrumentationKey = configopaque.String(connectionVars.InstrumentationKey) + exporterConfig.Endpoint = connectionVars.IngestionURL + telemetryConfiguration := appinsights.NewTelemetryConfiguration(string(exporterConfig.InstrumentationKey)) + telemetryConfiguration.EndpointUrl = exporterConfig.Endpoint + telemetryConfiguration.MaxBatchSize = exporterConfig.MaxBatchSize + telemetryConfiguration.MaxBatchInterval = exporterConfig.MaxBatchInterval + + f.telemetryClient = &appInsightsTelemetryClient{ + client: appinsights.NewTelemetryClientFromConfig(telemetryConfiguration), + } + + if checkedEntry := logger.Check(zap.DebugLevel, ""); checkedEntry != nil { + appinsights.NewDiagnosticsMessageListener(func(msg string) error { + logger.Debug(msg) + return nil + }) + } + } + return f.telemetryClient, nil +} \ No newline at end of file diff --git a/exporter/azuremonitorexporter/mock_telemetryClient.go b/exporter/azuremonitorexporter/mock_telemetryClient.go new file mode 100644 index 000000000000..6a47f736d9b4 --- /dev/null +++ b/exporter/azuremonitorexporter/mock_telemetryClient.go @@ -0,0 +1,53 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by mockery v2.46.3. DO NOT EDIT. + +package azuremonitorexporter + +import ( + appinsights "github.com/microsoft/ApplicationInsights-Go/appinsights" + + mock "github.com/stretchr/testify/mock" +) + +// mockTelemetryClient is an autogenerated mock type for the telemetryClient type +type mockTelemetryClient struct { + mock.Mock +} + +func (_m *mockTelemetryClient) Channel() telemetryChannel { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Channel") + } + + var r0 telemetryChannel + if rf, ok := ret.Get(0).(func() telemetryChannel); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(telemetryChannel) + } + } + + return r0 +} + +// Track provides a mock function with given fields: telemetry +func (_m *mockTelemetryClient) Track(telemetry appinsights.Telemetry) { + _m.Called(telemetry) +} + +type mockTelemetryChannel struct { + mock.Mock +} + +func (m *mockTelemetryChannel) Flush() { + m.Called() +} + +func (m *mockTelemetryChannel) Close() { + m.Called() +} \ No newline at end of file diff --git a/exporter/azuremonitorexporter/traceexporter.go b/exporter/azuremonitorexporter/traceexporter.go index 1cd40778e717..abde11c9fecf 100644 --- a/exporter/azuremonitorexporter/traceexporter.go +++ b/exporter/azuremonitorexporter/traceexporter.go @@ -6,7 +6,6 @@ package azuremonitorexporter // import "github.com/open-telemetry/opentelemetry- import ( "context" - "github.com/microsoft/ApplicationInsights-Go/appinsights" "go.opentelemetry.io/collector/consumer/consumererror" "go.opentelemetry.io/collector/exporter" "go.opentelemetry.io/collector/exporter/exporterhelper" @@ -16,8 +15,10 @@ import ( ) type traceExporter struct { - config *Config + config *Config + // TODO: deprecate in favor of telemetryClient transportChannel transportChannel + telemetryClient telemetryClient logger *zap.Logger } @@ -33,9 +34,6 @@ func (v *traceVisitor) visit( scope pcommon.InstrumentationScope, span ptrace.Span) (ok bool) { - instrumentationKey := string(v.exporter.config.InstrumentationKey) - client := appinsights.NewTelemetryClient(instrumentationKey) - telemetryTraces, err := spanToTelemetryTraces(resource, scope, span, v.exporter.config.SpanEventsEnabled, v.exporter.logger) if err != nil { // record the error and short-circuit @@ -45,11 +43,11 @@ func (v *traceVisitor) visit( for _, trace := range telemetryTraces { // This is a fire and forget operation - client.Track(trace) + v.exporter.telemetryClient.Track(trace) } // Flush the telemetry client to ensure all data is sent and take advantage of batching - client.Channel().Flush() + v.exporter.telemetryClient.Channel().Flush() v.processed++ return true @@ -67,10 +65,11 @@ func (exporter *traceExporter) onTraceData(_ context.Context, traceData ptrace.T } // Returns a new instance of the trace exporter -func newTracesExporter(config *Config, transportChannel transportChannel, set exporter.Settings) (exporter.Traces, error) { +func newTracesExporter(config *Config, transportChannel transportChannel, telemetryClient telemetryClient, set exporter.Settings) (exporter.Traces, error) { exporter := &traceExporter{ config: config, transportChannel: transportChannel, + telemetryClient: telemetryClient, logger: set.Logger, } diff --git a/exporter/azuremonitorexporter/traceexporter_test.go b/exporter/azuremonitorexporter/traceexporter_test.go index a4fc97be457d..6dae67f72de6 100644 --- a/exporter/azuremonitorexporter/traceexporter_test.go +++ b/exporter/azuremonitorexporter/traceexporter_test.go @@ -22,19 +22,22 @@ var ( // Tests the export onTraceData callback with no Spans func TestExporterTraceDataCallbackNoSpans(t *testing.T) { mockTransportChannel := getMockTransportChannel() - exporter := getExporter(defaultConfig, mockTransportChannel) + mockTelemetryClient := getMockTelemetryClient() + exporter := getExporter(defaultConfig, mockTransportChannel, mockTelemetryClient) traces := ptrace.NewTraces() assert.NoError(t, exporter.onTraceData(context.Background(), traces)) mockTransportChannel.AssertNumberOfCalls(t, "Send", 0) + mockTelemetryClient.AssertNumberOfCalls(t, "Track", 0) } // Tests the export onTraceData callback with a single Span func TestExporterTraceDataCallbackSingleSpan(t *testing.T) { mockTransportChannel := getMockTransportChannel() - exporter := getExporter(defaultConfig, mockTransportChannel) + mockTelemetryClient := getMockTelemetryClient() + exporter := getExporter(defaultConfig, mockTransportChannel, mockTelemetryClient) // re-use some test generation method(s) from trace_to_envelope_test resource := getResource() @@ -50,16 +53,16 @@ func TestExporterTraceDataCallbackSingleSpan(t *testing.T) { span.CopyTo(ilss.Spans().AppendEmpty()) assert.NoError(t, exporter.onTraceData(context.Background(), traces)) - - mockTransportChannel.AssertNumberOfCalls(t, "Send", 1) + mockTelemetryClient.Channel().(*mockTelemetryChannel).AssertNumberOfCalls(t, "Flush", 1) } // Tests the export onTraceData callback with a single Span with SpanEvents func TestExporterTraceDataCallbackSingleSpanWithSpanEvents(t *testing.T) { mockTransportChannel := getMockTransportChannel() + mockTelemetryClient := getMockTelemetryClient() config := createDefaultConfig().(*Config) config.SpanEventsEnabled = true - exporter := getExporter(config, mockTransportChannel) + exporter := getExporter(config, mockTransportChannel, mockTelemetryClient) // re-use some test generation method(s) from trace_to_envelope_test resource := getResource() @@ -83,13 +86,16 @@ func TestExporterTraceDataCallbackSingleSpanWithSpanEvents(t *testing.T) { assert.NoError(t, exporter.onTraceData(context.Background(), traces)) - mockTransportChannel.AssertNumberOfCalls(t, "Send", 3) + mockTransportChannel.AssertNumberOfCalls(t, "Send", 0) + mockTelemetryClient.AssertNumberOfCalls(t, "Track", 3) } // Tests the export onTraceData callback with a single Span that fails to produce an envelope +// TODO: Depercate this test when transport channel is removed as we will not be using envelopes anymore func TestExporterTraceDataCallbackSingleSpanNoEnvelope(t *testing.T) { mockTransportChannel := getMockTransportChannel() - exporter := getExporter(defaultConfig, mockTransportChannel) + mockTelemetryClient := getMockTelemetryClient() + exporter := getExporter(defaultConfig, mockTransportChannel, mockTelemetryClient) // re-use some test generation method(s) from trace_to_envelope_test resource := getResource() @@ -113,6 +119,7 @@ func TestExporterTraceDataCallbackSingleSpanNoEnvelope(t *testing.T) { assert.True(t, consumererror.IsPermanent(err), "error should be permanent") mockTransportChannel.AssertNumberOfCalls(t, "Send", 0) + mockTelemetryClient.AssertNumberOfCalls(t, "Track", 0) } func getMockTransportChannel() *mockTransportChannel { @@ -121,10 +128,20 @@ func getMockTransportChannel() *mockTransportChannel { return &transportChannelMock } -func getExporter(config *Config, transportChannel transportChannel) *traceExporter { +func getMockTelemetryClient() *mockTelemetryClient { + mockClient := mockTelemetryClient{} + mockChannel := &mockTelemetryChannel{} + mockClient.On("Track", mock.Anything) + mockClient.On("Channel").Return(mockChannel) + mockChannel.On("Flush") + return &mockClient +} + +func getExporter(config *Config, transportChannel transportChannel, telemetryClient telemetryClient) *traceExporter { return &traceExporter{ - config, - transportChannel, - zap.NewNop(), + config: config, + transportChannel: transportChannel, + telemetryClient: telemetryClient, + logger: zap.NewNop(), } -} +} \ No newline at end of file