478 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			478 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Go
		
	
	
	
| package application_insights
 | |
| 
 | |
| import (
 | |
| 	"math"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/Microsoft/ApplicationInsights-Go/appinsights"
 | |
| 
 | |
| 	"github.com/influxdata/telegraf"
 | |
| 	"github.com/influxdata/telegraf/internal"
 | |
| 	"github.com/influxdata/telegraf/metric"
 | |
| 	"github.com/influxdata/telegraf/plugins/outputs/application_insights/mocks"
 | |
| 	"github.com/stretchr/testify/assert"
 | |
| 	"github.com/stretchr/testify/mock"
 | |
| )
 | |
| 
 | |
| func TestConnectFailsIfNoIkey(t *testing.T) {
 | |
| 	assert := assert.New(t)
 | |
| 
 | |
| 	transmitter := new(mocks.Transmitter)
 | |
| 	transmitter.On("Close").Return(closed)
 | |
| 
 | |
| 	ai := ApplicationInsights{
 | |
| 		transmitter: transmitter,
 | |
| 		// Very long timeout to ensure we do not rely on timeouts for closing the transmitter
 | |
| 		Timeout: internal.Duration{Duration: time.Hour},
 | |
| 	}
 | |
| 
 | |
| 	err := ai.Connect()
 | |
| 	assert.Error(err)
 | |
| }
 | |
| 
 | |
| func TestOutputCloseTimesOut(t *testing.T) {
 | |
| 	assert := assert.New(t)
 | |
| 
 | |
| 	transmitter := new(mocks.Transmitter)
 | |
| 	transmitter.On("Close").Return(unfinished)
 | |
| 
 | |
| 	ai := ApplicationInsights{
 | |
| 		transmitter: transmitter,
 | |
| 		Timeout:     internal.Duration{Duration: time.Millisecond * 50},
 | |
| 	}
 | |
| 
 | |
| 	err := ai.Close()
 | |
| 	assert.NoError(err)
 | |
| 	transmitter.AssertCalled(t, "Close")
 | |
| }
 | |
| 
 | |
| func TestCloseRemovesDiagMsgListener(t *testing.T) {
 | |
| 	assert := assert.New(t)
 | |
| 
 | |
| 	transmitter := new(mocks.Transmitter)
 | |
| 	transmitter.On("Close").Return(closed)
 | |
| 
 | |
| 	diagMsgListener := new(mocks.DiagnosticsMessageListener)
 | |
| 	diagMsgListener.On("Remove")
 | |
| 
 | |
| 	diagMsgSubscriber := new(mocks.DiagnosticsMessageSubscriber)
 | |
| 	diagMsgSubscriber.
 | |
| 		On("Subscribe", mock.AnythingOfType("appinsights.DiagnosticsMessageHandler")).
 | |
| 		Return(diagMsgListener)
 | |
| 
 | |
| 	ai := ApplicationInsights{
 | |
| 		transmitter:             transmitter,
 | |
| 		Timeout:                 internal.Duration{Duration: time.Hour},
 | |
| 		EnableDiagnosticLogging: true,
 | |
| 		diagMsgSubscriber:       diagMsgSubscriber,
 | |
| 		InstrumentationKey:      "1234", // Fake, but necessary to enable tracking
 | |
| 	}
 | |
| 
 | |
| 	err := ai.Connect()
 | |
| 	assert.NoError(err)
 | |
| 	diagMsgSubscriber.AssertCalled(t, "Subscribe", mock.AnythingOfType("appinsights.DiagnosticsMessageHandler"))
 | |
| 
 | |
| 	err = ai.Close()
 | |
| 	assert.NoError(err)
 | |
| 	transmitter.AssertCalled(t, "Close")
 | |
| 	diagMsgListener.AssertCalled(t, "Remove")
 | |
| }
 | |
| 
 | |
| func TestAggregateMetricCreated(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name                        string
 | |
| 		fields                      map[string]interface{}
 | |
| 		valueField                  string
 | |
| 		countField                  string
 | |
| 		additionalMetricValueFields []string
 | |
| 	}{
 | |
| 		{"value and count", map[string]interface{}{"value": 16.5, "count": 23}, "value", "count", nil},
 | |
| 		{"value and samples", map[string]interface{}{"value": 16.5, "samples": 23}, "value", "samples", nil},
 | |
| 		{"sum and count", map[string]interface{}{"sum": 16.5, "count": 23}, "sum", "count", nil},
 | |
| 		{"sum and samples", map[string]interface{}{"samples": 23, "sum": 16.5}, "sum", "samples", nil},
 | |
| 		{"value and count, sum is wrong type", map[string]interface{}{"sum": "J23", "value": 16.5, "count": 23}, "value", "count", nil},
 | |
| 		{
 | |
| 			"with aggregates",
 | |
| 			map[string]interface{}{
 | |
| 				"value":  16.5,
 | |
| 				"count":  23,
 | |
| 				"min":    -2.1,
 | |
| 				"max":    34,
 | |
| 				"stddev": 3.4,
 | |
| 			},
 | |
| 			"value",
 | |
| 			"count",
 | |
| 			nil,
 | |
| 		},
 | |
| 		{
 | |
| 			"some aggregates with invalid values",
 | |
| 			map[string]interface{}{
 | |
| 				"value": 16.5,
 | |
| 				"count": 23,
 | |
| 				"min":   "min",
 | |
| 				"max":   []float64{3.4, 5.6},
 | |
| 				"stddev": struct {
 | |
| 					name  string
 | |
| 					value float64
 | |
| 				}{"delta", 7.0},
 | |
| 			},
 | |
| 			"value",
 | |
| 			"count",
 | |
| 			nil,
 | |
| 		},
 | |
| 		{
 | |
| 			"aggregate with additional fields",
 | |
| 			map[string]interface{}{"value": 16.5, "samples": 23, "alpha": -34e12, "bravo": -3, "charlie": "charlie"},
 | |
| 			"value",
 | |
| 			"samples",
 | |
| 			[]string{"alpha", "bravo"},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		tf := func(t *testing.T) {
 | |
| 			assert := assert.New(t)
 | |
| 			now := time.Now().UTC()
 | |
| 
 | |
| 			transmitter := new(mocks.Transmitter)
 | |
| 			transmitter.On("Track", mock.Anything)
 | |
| 			metricName := "ShouldBeAggregateMetric"
 | |
| 
 | |
| 			m, err := metric.New(
 | |
| 				metricName,
 | |
| 				nil, // tags
 | |
| 				tt.fields,
 | |
| 				now,
 | |
| 			)
 | |
| 			assert.NoError(err)
 | |
| 
 | |
| 			ai := ApplicationInsights{
 | |
| 				transmitter:        transmitter,
 | |
| 				InstrumentationKey: "1234", // Fake, but necessary to enable tracking
 | |
| 			}
 | |
| 
 | |
| 			err = ai.Connect()
 | |
| 			assert.NoError(err)
 | |
| 
 | |
| 			mSet := []telegraf.Metric{m}
 | |
| 			ai.Write(mSet)
 | |
| 			transmitter.AssertNumberOfCalls(t, "Track", 1+len(tt.additionalMetricValueFields))
 | |
| 			var pAggregateTelemetry *appinsights.AggregateMetricTelemetry
 | |
| 			assert.IsType(pAggregateTelemetry, transmitter.Calls[len(transmitter.Calls)-1].Arguments.Get(0), "Expected last telemetry to be AggregateMetricTelemetry")
 | |
| 			aggregateTelemetry := transmitter.Calls[len(transmitter.Calls)-1].Arguments.Get(0).(*appinsights.AggregateMetricTelemetry)
 | |
| 			verifyAggregateTelemetry(assert, m, tt.valueField, tt.countField, aggregateTelemetry)
 | |
| 
 | |
| 			verifyAdditionalTelemetry(assert, m, transmitter, tt.additionalMetricValueFields, metricName)
 | |
| 		}
 | |
| 
 | |
| 		t.Run(tt.name, tf)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestSimpleMetricCreated(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name                        string
 | |
| 		fields                      map[string]interface{}
 | |
| 		primaryMetricValueField     string
 | |
| 		additionalMetricValueFields []string
 | |
| 	}{
 | |
| 		{"just a single value field", map[string]interface{}{"value": 16.5}, "value", nil},
 | |
| 		{"single field not named value", map[string]interface{}{"first": 32.9}, "first", nil},
 | |
| 		{"value but no count", map[string]interface{}{"value": 16.5, "other": "bulba"}, "", []string{"value"}},
 | |
| 		{"count but no value", map[string]interface{}{"v1": "v1Val", "count": 23}, "", []string{"count"}},
 | |
| 		{"neither value nor count", map[string]interface{}{"v1": "alpha", "v2": 45.8}, "", []string{"v2"}},
 | |
| 		{"value is of wrong type", map[string]interface{}{"value": "alpha", "count": 15}, "", []string{"count"}},
 | |
| 		{"count is of wrong type", map[string]interface{}{"value": 23.77, "count": 7.5}, "", []string{"count", "value"}},
 | |
| 		{"count is out of range", map[string]interface{}{"value": -98.45e4, "count": math.MaxUint64 - uint64(20)}, "", []string{"value", "count"}},
 | |
| 		{"several additional fields", map[string]interface{}{"alpha": 10, "bravo": "bravo", "charlie": 30, "delta": 40.7}, "", []string{"alpha", "charlie", "delta"}},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		tf := func(t *testing.T) {
 | |
| 			assert := assert.New(t)
 | |
| 			now := time.Now().UTC()
 | |
| 
 | |
| 			transmitter := new(mocks.Transmitter)
 | |
| 			transmitter.On("Track", mock.Anything)
 | |
| 			metricName := "ShouldBeSimpleMetric"
 | |
| 
 | |
| 			m, err := metric.New(
 | |
| 				metricName,
 | |
| 				nil, // tags
 | |
| 				tt.fields,
 | |
| 				now,
 | |
| 			)
 | |
| 			assert.NoError(err)
 | |
| 
 | |
| 			ai := ApplicationInsights{
 | |
| 				transmitter:        transmitter,
 | |
| 				InstrumentationKey: "1234", // Fake, but necessary to enable tracking
 | |
| 			}
 | |
| 
 | |
| 			err = ai.Connect()
 | |
| 			assert.NoError(err)
 | |
| 
 | |
| 			mSet := []telegraf.Metric{m}
 | |
| 			ai.Write(mSet)
 | |
| 
 | |
| 			expectedNumberOfCalls := len(tt.additionalMetricValueFields)
 | |
| 			if tt.primaryMetricValueField != "" {
 | |
| 				expectedNumberOfCalls++
 | |
| 			}
 | |
| 
 | |
| 			transmitter.AssertNumberOfCalls(t, "Track", expectedNumberOfCalls)
 | |
| 			if tt.primaryMetricValueField != "" {
 | |
| 				var pMetricTelemetry *appinsights.MetricTelemetry
 | |
| 				assert.IsType(pMetricTelemetry, transmitter.Calls[0].Arguments.Get(0), "First created telemetry should be simple MetricTelemetry")
 | |
| 				metricTelemetry := transmitter.Calls[0].Arguments.Get(0).(*appinsights.MetricTelemetry)
 | |
| 
 | |
| 				var expectedTelemetryName string
 | |
| 				if tt.primaryMetricValueField == "value" {
 | |
| 					expectedTelemetryName = m.Name()
 | |
| 				} else {
 | |
| 					expectedTelemetryName = m.Name() + "_" + tt.primaryMetricValueField
 | |
| 				}
 | |
| 				verifySimpleTelemetry(assert, m, tt.primaryMetricValueField, expectedTelemetryName, metricTelemetry)
 | |
| 			}
 | |
| 
 | |
| 			verifyAdditionalTelemetry(assert, m, transmitter, tt.additionalMetricValueFields, metricName)
 | |
| 		}
 | |
| 
 | |
| 		t.Run(tt.name, tf)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestTagsAppliedToTelemetry(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name              string
 | |
| 		fields            map[string]interface{}
 | |
| 		tags              map[string]string
 | |
| 		metricValueFields []string
 | |
| 	}{
 | |
| 		{
 | |
| 			"value but no count",
 | |
| 			map[string]interface{}{"value": 16.5, "alpha": 3.5, "bravo": 17},
 | |
| 			map[string]string{"alpha": "a tag is not a field", "charlie": "charlie"},
 | |
| 			[]string{"value", "alpha", "bravo"},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		tf := func(t *testing.T) {
 | |
| 			assert := assert.New(t)
 | |
| 			now := time.Now().UTC()
 | |
| 
 | |
| 			transmitter := new(mocks.Transmitter)
 | |
| 			transmitter.On("Track", mock.Anything)
 | |
| 			metricName := "ShouldBeSimpleMetric"
 | |
| 
 | |
| 			m, err := metric.New(
 | |
| 				metricName,
 | |
| 				tt.tags,
 | |
| 				tt.fields,
 | |
| 				now,
 | |
| 			)
 | |
| 			assert.NoError(err)
 | |
| 
 | |
| 			ai := ApplicationInsights{
 | |
| 				transmitter:        transmitter,
 | |
| 				InstrumentationKey: "1234", // Fake, but necessary to enable tracking
 | |
| 			}
 | |
| 
 | |
| 			err = ai.Connect()
 | |
| 			assert.NoError(err)
 | |
| 
 | |
| 			mSet := []telegraf.Metric{m}
 | |
| 			ai.Write(mSet)
 | |
| 			transmitter.AssertNumberOfCalls(t, "Track", len(tt.metricValueFields))
 | |
| 			transmitter.AssertCalled(t, "Track", mock.AnythingOfType("*appinsights.MetricTelemetry"))
 | |
| 
 | |
| 			// Will verify that all original tags are present in telemetry.Properies map
 | |
| 			verifyAdditionalTelemetry(assert, m, transmitter, tt.metricValueFields, metricName)
 | |
| 		}
 | |
| 
 | |
| 		t.Run(tt.name, tf)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestContextTagsSetOnSimpleTelemetry(t *testing.T) {
 | |
| 	assert := assert.New(t)
 | |
| 	now := time.Now().UTC()
 | |
| 
 | |
| 	transmitter := new(mocks.Transmitter)
 | |
| 	transmitter.On("Track", mock.Anything)
 | |
| 
 | |
| 	m, err := metric.New(
 | |
| 		"SimpleMetric",
 | |
| 		map[string]string{"kubernetes_container_name": "atcsvc", "kubernetes_pod_name": "bunkie17554"},
 | |
| 		map[string]interface{}{"value": 23.0},
 | |
| 		now,
 | |
| 	)
 | |
| 	assert.NoError(err)
 | |
| 
 | |
| 	ai := ApplicationInsights{
 | |
| 		transmitter:        transmitter,
 | |
| 		InstrumentationKey: "1234", // Fake, but necessary to enable tracking
 | |
| 		ContextTagSources: map[string]string{
 | |
| 			"ai.cloud.role":         "kubernetes_container_name",
 | |
| 			"ai.cloud.roleInstance": "kubernetes_pod_name",
 | |
| 			"ai.user.id":            "nonexistent",
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	err = ai.Connect()
 | |
| 	assert.NoError(err)
 | |
| 
 | |
| 	mSet := []telegraf.Metric{m}
 | |
| 	ai.Write(mSet)
 | |
| 	transmitter.AssertNumberOfCalls(t, "Track", 1)
 | |
| 	metricTelemetry := transmitter.Calls[0].Arguments.Get(0).(*appinsights.MetricTelemetry)
 | |
| 	cloudTags := metricTelemetry.Tags.Cloud()
 | |
| 	assert.Equal("atcsvc", cloudTags.GetRole())
 | |
| 	assert.Equal("bunkie17554", cloudTags.GetRoleInstance())
 | |
| }
 | |
| 
 | |
| func TestContextTagsSetOnAggregateTelemetry(t *testing.T) {
 | |
| 	assert := assert.New(t)
 | |
| 	now := time.Now().UTC()
 | |
| 
 | |
| 	transmitter := new(mocks.Transmitter)
 | |
| 	transmitter.On("Track", mock.Anything)
 | |
| 
 | |
| 	m, err := metric.New(
 | |
| 		"AggregateMetric",
 | |
| 		map[string]string{"kubernetes_container_name": "atcsvc", "kubernetes_pod_name": "bunkie17554"},
 | |
| 		map[string]interface{}{"value": 23.0, "count": 5},
 | |
| 		now,
 | |
| 	)
 | |
| 	assert.NoError(err)
 | |
| 
 | |
| 	ai := ApplicationInsights{
 | |
| 		transmitter:        transmitter,
 | |
| 		InstrumentationKey: "1234", // Fake, but necessary to enable tracking
 | |
| 		ContextTagSources: map[string]string{
 | |
| 			"ai.cloud.role":         "kubernetes_container_name",
 | |
| 			"ai.cloud.roleInstance": "kubernetes_pod_name",
 | |
| 			"ai.user.id":            "nonexistent",
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	err = ai.Connect()
 | |
| 	assert.NoError(err)
 | |
| 
 | |
| 	mSet := []telegraf.Metric{m}
 | |
| 	ai.Write(mSet)
 | |
| 	transmitter.AssertNumberOfCalls(t, "Track", 1)
 | |
| 	metricTelemetry := transmitter.Calls[0].Arguments.Get(0).(*appinsights.AggregateMetricTelemetry)
 | |
| 	cloudTags := metricTelemetry.Tags.Cloud()
 | |
| 	assert.Equal("atcsvc", cloudTags.GetRole())
 | |
| 	assert.Equal("bunkie17554", cloudTags.GetRoleInstance())
 | |
| }
 | |
| 
 | |
| func closed() <-chan struct{} {
 | |
| 	closed := make(chan struct{})
 | |
| 	close(closed)
 | |
| 	return closed
 | |
| }
 | |
| 
 | |
| func unfinished() <-chan struct{} {
 | |
| 	unfinished := make(chan struct{})
 | |
| 	return unfinished
 | |
| }
 | |
| 
 | |
| func verifyAggregateTelemetry(
 | |
| 	assert *assert.Assertions,
 | |
| 	metric telegraf.Metric,
 | |
| 	valueField string,
 | |
| 	countField string,
 | |
| 	telemetry *appinsights.AggregateMetricTelemetry,
 | |
| ) {
 | |
| 
 | |
| 	verifyAggregateField := func(fieldName string, telemetryValue float64) {
 | |
| 		metricRawFieldValue, found := metric.Fields()[fieldName]
 | |
| 		if !found {
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		if _, err := toFloat64(metricRawFieldValue); err == nil {
 | |
| 			assert.EqualValues(metricRawFieldValue, telemetryValue, "Telemetry property %s does not match the metric field", fieldName)
 | |
| 		}
 | |
| 	}
 | |
| 	assert.Equal(metric.Name(), telemetry.Name, "Telemetry name should be the same as metric name")
 | |
| 	assert.EqualValues(metric.Fields()[valueField], telemetry.Value, "Telemetry value does not match metric value field")
 | |
| 	assert.EqualValues(metric.Fields()[countField], telemetry.Count, "Telemetry sample count does not mach metric sample count field")
 | |
| 	verifyAggregateField("min", telemetry.Min)
 | |
| 	verifyAggregateField("max", telemetry.Max)
 | |
| 	verifyAggregateField("stdev", telemetry.StdDev)
 | |
| 	verifyAggregateField("variance", telemetry.Variance)
 | |
| 	assert.Equal(metric.Time(), telemetry.Timestamp, "Telemetry and metric timestamps do not match")
 | |
| 	assertMapContains(assert, metric.Tags(), telemetry.Properties)
 | |
| }
 | |
| 
 | |
| func verifySimpleTelemetry(
 | |
| 	assert *assert.Assertions,
 | |
| 	metric telegraf.Metric,
 | |
| 	valueField string,
 | |
| 	expectedTelemetryName string,
 | |
| 	telemetry *appinsights.MetricTelemetry,
 | |
| ) {
 | |
| 
 | |
| 	assert.Equal(expectedTelemetryName, telemetry.Name, "Telemetry name is not what was expected")
 | |
| 	assert.EqualValues(metric.Fields()[valueField], telemetry.Value, "Telemetry value does not match metric value field")
 | |
| 	assert.Equal(metric.Time(), telemetry.Timestamp, "Telemetry and metric timestamps do not match")
 | |
| 	assertMapContains(assert, metric.Tags(), telemetry.Properties)
 | |
| }
 | |
| 
 | |
| func verifyAdditionalTelemetry(
 | |
| 	assert *assert.Assertions,
 | |
| 	metric telegraf.Metric,
 | |
| 	transmitter *mocks.Transmitter,
 | |
| 	additionalMetricValueFields []string,
 | |
| 	telemetryNamePrefix string,
 | |
| ) {
 | |
| 	for _, fieldName := range additionalMetricValueFields {
 | |
| 		expectedTelemetryName := telemetryNamePrefix + "_" + fieldName
 | |
| 		telemetry := findTransmittedTelemetry(transmitter, expectedTelemetryName)
 | |
| 		assert.NotNil(telemetry, "Expected telemetry named %s to be created, but could not find it", expectedTelemetryName)
 | |
| 		if telemetry != nil {
 | |
| 			verifySimpleTelemetry(assert, metric, fieldName, expectedTelemetryName, telemetry)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func findTransmittedTelemetry(transmitter *mocks.Transmitter, telemetryName string) *appinsights.MetricTelemetry {
 | |
| 	for _, call := range transmitter.Calls {
 | |
| 		telemetry, isMetricTelemetry := call.Arguments.Get(0).(*appinsights.MetricTelemetry)
 | |
| 		if isMetricTelemetry && telemetry.Name == telemetryName {
 | |
| 			return telemetry
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func keys(m map[string]string) []string {
 | |
| 	keys := make([]string, 0, len(m))
 | |
| 	for k := range m {
 | |
| 		keys = append(keys, k)
 | |
| 	}
 | |
| 
 | |
| 	return keys
 | |
| }
 | |
| 
 | |
| func assertMapContains(assert *assert.Assertions, expected, actual map[string]string) {
 | |
| 	if expected == nil && actual == nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	assert.NotNil(expected, "Maps not equal: expected is nil but actual is not")
 | |
| 	assert.NotNil(actual, "Maps not equal: actual is nil but expected is not")
 | |
| 
 | |
| 	for k, v := range expected {
 | |
| 		av, ok := actual[k]
 | |
| 		assert.True(ok, "Actual map does not contain a value for key '%s'", k)
 | |
| 		assert.Equal(v, av, "The expected value for key '%s' is '%s' but the actual value is '%s", k, v, av)
 | |
| 	}
 | |
| }
 |