Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
e393c3af3f
commit
4978089aab
4963 changed files with 677545 additions and 0 deletions
69
plugins/outputs/application_insights/README.md
Normal file
69
plugins/outputs/application_insights/README.md
Normal file
|
@ -0,0 +1,69 @@
|
|||
# Azure Application Insights Output Plugin
|
||||
|
||||
This plugin writes metrics to the [Azure Application Insights][insights]
|
||||
service.
|
||||
|
||||
⭐ Telegraf v1.7.0
|
||||
🏷️ applications, cloud
|
||||
💻 all
|
||||
|
||||
[insights]: https://azure.microsoft.com/en-us/services/application-insights/
|
||||
|
||||
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
|
||||
|
||||
In addition to the plugin-specific configuration settings, plugins support
|
||||
additional global and plugin configuration settings. These settings are used to
|
||||
modify metrics, tags, and field or create aliases and configure ordering, etc.
|
||||
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
||||
|
||||
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
|
||||
|
||||
## Configuration
|
||||
|
||||
```toml @sample.conf
|
||||
# Send metrics to Azure Application Insights
|
||||
[[outputs.application_insights]]
|
||||
## Instrumentation key of the Application Insights resource.
|
||||
instrumentation_key = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx"
|
||||
|
||||
## Regions that require endpoint modification https://docs.microsoft.com/en-us/azure/azure-monitor/app/custom-endpoints
|
||||
# endpoint_url = "https://dc.services.visualstudio.com/v2/track"
|
||||
|
||||
## Timeout for closing (default: 5s).
|
||||
# timeout = "5s"
|
||||
|
||||
## Enable additional diagnostic logging.
|
||||
# enable_diagnostic_logging = false
|
||||
|
||||
## NOTE: Due to the way TOML is parsed, tables must be at the END of the
|
||||
## plugin definition, otherwise additional config options are read as part of
|
||||
## the table
|
||||
|
||||
## Context Tag Sources add Application Insights context tags to a tag value.
|
||||
##
|
||||
## For list of allowed context tag keys see:
|
||||
## https://github.com/microsoft/ApplicationInsights-Go/blob/master/appinsights/contracts/contexttagkeys.go
|
||||
# [outputs.application_insights.context_tag_sources]
|
||||
# "ai.cloud.role" = "kubernetes_container_name"
|
||||
# "ai.cloud.roleInstance" = "kubernetes_pod_name"
|
||||
```
|
||||
|
||||
## Metric Encoding
|
||||
|
||||
For each field an Application Insights Telemetry record is created named based
|
||||
on the measurement name and field.
|
||||
|
||||
**Example:** Create the telemetry records `foo_first` and `foo_second`:
|
||||
|
||||
```text
|
||||
foo,host=a first=42,second=43 1525293034000000000
|
||||
```
|
||||
|
||||
In the special case of a single field named `value`, a single telemetry record
|
||||
is created named using only the measurement name
|
||||
|
||||
**Example:** Create a telemetry record `bar`:
|
||||
|
||||
```text
|
||||
bar,host=a value=42 1525293034000000000
|
||||
```
|
321
plugins/outputs/application_insights/application_insights.go
Normal file
321
plugins/outputs/application_insights/application_insights.go
Normal file
|
@ -0,0 +1,321 @@
|
|||
//go:generate ../../../tools/readme_config_includer/generator
|
||||
package application_insights
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/microsoft/ApplicationInsights-Go/appinsights"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/plugins/outputs"
|
||||
)
|
||||
|
||||
//go:embed sample.conf
|
||||
var sampleConfig string
|
||||
|
||||
type TelemetryTransmitter interface {
|
||||
Track(appinsights.Telemetry)
|
||||
Close() <-chan struct{}
|
||||
}
|
||||
|
||||
type DiagnosticsMessageSubscriber interface {
|
||||
Subscribe(appinsights.DiagnosticsMessageHandler) appinsights.DiagnosticsMessageListener
|
||||
}
|
||||
|
||||
type ApplicationInsights struct {
|
||||
InstrumentationKey string `toml:"instrumentation_key"`
|
||||
EndpointURL string `toml:"endpoint_url"`
|
||||
Timeout config.Duration `toml:"timeout"`
|
||||
EnableDiagnosticLogging bool `toml:"enable_diagnostic_logging"`
|
||||
ContextTagSources map[string]string `toml:"context_tag_sources"`
|
||||
Log telegraf.Logger `toml:"-"`
|
||||
|
||||
diagMsgSubscriber DiagnosticsMessageSubscriber
|
||||
transmitter TelemetryTransmitter
|
||||
diagMsgListener appinsights.DiagnosticsMessageListener
|
||||
}
|
||||
|
||||
func (*ApplicationInsights) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (a *ApplicationInsights) Connect() error {
|
||||
if a.InstrumentationKey == "" {
|
||||
return errors.New("instrumentation key is required")
|
||||
}
|
||||
|
||||
if a.transmitter == nil {
|
||||
a.transmitter = NewTransmitter(a.InstrumentationKey, a.EndpointURL)
|
||||
}
|
||||
|
||||
if a.EnableDiagnosticLogging && a.diagMsgSubscriber != nil {
|
||||
a.diagMsgListener = a.diagMsgSubscriber.Subscribe(func(msg string) error {
|
||||
a.Log.Info(msg)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ApplicationInsights) Write(metrics []telegraf.Metric) error {
|
||||
for _, metric := range metrics {
|
||||
allMetricTelemetry := a.createTelemetry(metric)
|
||||
for _, telemetry := range allMetricTelemetry {
|
||||
a.transmitter.Track(telemetry)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ApplicationInsights) Close() error {
|
||||
if a.diagMsgListener != nil {
|
||||
// We want to listen to diagnostic messages during closing
|
||||
// That is why we stop listening only after Close() ends (or a timeout occurs)
|
||||
defer a.diagMsgListener.Remove()
|
||||
}
|
||||
|
||||
if a.transmitter == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
select {
|
||||
case <-a.transmitter.Close():
|
||||
a.Log.Info("Closed")
|
||||
case <-time.After(time.Duration(a.Timeout)):
|
||||
a.Log.Warnf("Close operation timed out after %v", time.Duration(a.Timeout))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ApplicationInsights) createTelemetry(metric telegraf.Metric) []appinsights.Telemetry {
|
||||
aggregateTelemetry, usedFields := a.createAggregateMetricTelemetry(metric)
|
||||
if aggregateTelemetry != nil {
|
||||
telemetry := a.createTelemetryForUnusedFields(metric, usedFields)
|
||||
telemetry = append(telemetry, aggregateTelemetry)
|
||||
return telemetry
|
||||
}
|
||||
|
||||
fields := metric.Fields()
|
||||
if len(fields) == 1 && metric.FieldList()[0].Key == "value" {
|
||||
// Just use metric name as the telemetry name
|
||||
telemetry := a.createSimpleMetricTelemetry(metric, "value", false)
|
||||
if telemetry != nil {
|
||||
return []appinsights.Telemetry{telemetry}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// AppInsights does not support multi-dimensional metrics at the moment, so we need to disambiguate resulting telemetry
|
||||
// by adding field name as the telemetry name suffix
|
||||
return a.createTelemetryForUnusedFields(metric, nil)
|
||||
}
|
||||
|
||||
func (a *ApplicationInsights) createSimpleMetricTelemetry(
|
||||
metric telegraf.Metric,
|
||||
fieldName string,
|
||||
useFieldNameInTelemetryName bool,
|
||||
) *appinsights.MetricTelemetry {
|
||||
telemetryValue, err := getFloat64TelemetryPropertyValue([]string{fieldName}, metric, nil)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var telemetryName string
|
||||
if useFieldNameInTelemetryName {
|
||||
telemetryName = metric.Name() + "_" + fieldName
|
||||
} else {
|
||||
telemetryName = metric.Name()
|
||||
}
|
||||
telemetry := appinsights.NewMetricTelemetry(telemetryName, telemetryValue)
|
||||
telemetry.Properties = metric.Tags()
|
||||
a.addContextTags(metric, telemetry)
|
||||
telemetry.Timestamp = metric.Time()
|
||||
return telemetry
|
||||
}
|
||||
|
||||
func (a *ApplicationInsights) createAggregateMetricTelemetry(metric telegraf.Metric) (*appinsights.AggregateMetricTelemetry, []string) {
|
||||
usedFields := make([]string, 0, 6) // We will use up to 6 fields
|
||||
|
||||
// Get the sum of all individual measurements(mandatory property)
|
||||
telemetryValue, err := getFloat64TelemetryPropertyValue([]string{"sum", "value"}, metric, &usedFields)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Get the count of measurements (mandatory property)
|
||||
telemetryCount, err := getIntTelemetryPropertyValue([]string{"count", "samples"}, metric, &usedFields)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
telemetry := appinsights.NewAggregateMetricTelemetry(metric.Name())
|
||||
telemetry.Value = telemetryValue
|
||||
telemetry.Count = telemetryCount
|
||||
telemetry.Properties = metric.Tags()
|
||||
a.addContextTags(metric, telemetry)
|
||||
telemetry.Timestamp = metric.Time()
|
||||
|
||||
// We attempt to set min, max, variance and stddev fields but do not really care if they are not present--
|
||||
// they are not essential for aggregate metric.
|
||||
// By convention AppInsights prefers stddev over variance, so to be consistent, we test for stddev after testing for variance.
|
||||
//nolint:errcheck // see above
|
||||
telemetry.Min, _ = getFloat64TelemetryPropertyValue([]string{"min"}, metric, &usedFields)
|
||||
//nolint:errcheck // see above
|
||||
telemetry.Max, _ = getFloat64TelemetryPropertyValue([]string{"max"}, metric, &usedFields)
|
||||
//nolint:errcheck // see above
|
||||
telemetry.Variance, _ = getFloat64TelemetryPropertyValue([]string{"variance"}, metric, &usedFields)
|
||||
//nolint:errcheck // see above
|
||||
telemetry.StdDev, _ = getFloat64TelemetryPropertyValue([]string{"stddev"}, metric, &usedFields)
|
||||
|
||||
return telemetry, usedFields
|
||||
}
|
||||
|
||||
func (a *ApplicationInsights) createTelemetryForUnusedFields(metric telegraf.Metric, usedFields []string) []appinsights.Telemetry {
|
||||
fields := metric.Fields()
|
||||
retval := make([]appinsights.Telemetry, 0, len(fields))
|
||||
|
||||
for fieldName := range fields {
|
||||
if contains(usedFields, fieldName) {
|
||||
continue
|
||||
}
|
||||
|
||||
telemetry := a.createSimpleMetricTelemetry(metric, fieldName, true)
|
||||
if telemetry != nil {
|
||||
retval = append(retval, telemetry)
|
||||
}
|
||||
}
|
||||
|
||||
return retval
|
||||
}
|
||||
|
||||
func (a *ApplicationInsights) addContextTags(metric telegraf.Metric, telemetry appinsights.Telemetry) {
|
||||
for contextTagName, tagSourceName := range a.ContextTagSources {
|
||||
if contextTagValue, found := metric.GetTag(tagSourceName); found {
|
||||
telemetry.ContextTags()[contextTagName] = contextTagValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getFloat64TelemetryPropertyValue(
|
||||
candidateFields []string,
|
||||
metric telegraf.Metric,
|
||||
usedFields *[]string,
|
||||
) (float64, error) {
|
||||
for _, fieldName := range candidateFields {
|
||||
fieldValue, found := metric.GetField(fieldName)
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
|
||||
metricValue, err := toFloat64(fieldValue)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if usedFields != nil {
|
||||
*usedFields = append(*usedFields, fieldName)
|
||||
}
|
||||
|
||||
return metricValue, nil
|
||||
}
|
||||
|
||||
return 0.0, errors.New("no field from the candidate list was found in the metric")
|
||||
}
|
||||
|
||||
func getIntTelemetryPropertyValue(
|
||||
candidateFields []string,
|
||||
metric telegraf.Metric,
|
||||
usedFields *[]string,
|
||||
) (int, error) {
|
||||
for _, fieldName := range candidateFields {
|
||||
fieldValue, found := metric.GetField(fieldName)
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
|
||||
metricValue, err := toInt(fieldValue)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if usedFields != nil {
|
||||
*usedFields = append(*usedFields, fieldName)
|
||||
}
|
||||
|
||||
return metricValue, nil
|
||||
}
|
||||
|
||||
return 0, errors.New("no field from the candidate list was found in the metric")
|
||||
}
|
||||
|
||||
func contains(set []string, val string) bool {
|
||||
for _, elem := range set {
|
||||
if elem == val {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func toFloat64(value interface{}) (float64, error) {
|
||||
// Out of all Golang numerical types Telegraf only uses int64, unit64 and float64 for fields
|
||||
switch v := value.(type) {
|
||||
case int64:
|
||||
return float64(v), nil
|
||||
case uint64:
|
||||
return float64(v), nil
|
||||
case float64:
|
||||
return v, nil
|
||||
}
|
||||
|
||||
return 0.0, fmt.Errorf("[%s] cannot be converted to a float64 value", value)
|
||||
}
|
||||
|
||||
func toInt(value interface{}) (int, error) {
|
||||
// Out of all Golang numerical types Telegraf only uses int64, unit64 and float64 for fields
|
||||
switch v := value.(type) {
|
||||
case uint64:
|
||||
if strconv.IntSize == 32 {
|
||||
if v > math.MaxInt32 {
|
||||
return 0, fmt.Errorf("value [%d] out of range of 32-bit integers", v)
|
||||
}
|
||||
} else {
|
||||
if v > math.MaxInt64 {
|
||||
return 0, fmt.Errorf("value [%d] out of range of 64-bit integers", v)
|
||||
}
|
||||
}
|
||||
|
||||
return int(v), nil
|
||||
|
||||
case int64:
|
||||
if strconv.IntSize == 32 {
|
||||
if v > math.MaxInt32 || v < math.MinInt32 {
|
||||
return 0, fmt.Errorf("value [%d] out of range of 32-bit integers", v)
|
||||
}
|
||||
}
|
||||
|
||||
return int(v), nil
|
||||
}
|
||||
|
||||
return 0.0, fmt.Errorf("[%s] cannot be converted to an int value", value)
|
||||
}
|
||||
|
||||
func init() {
|
||||
outputs.Add("application_insights", func() telegraf.Output {
|
||||
return &ApplicationInsights{
|
||||
Timeout: config.Duration(time.Second * 5),
|
||||
diagMsgSubscriber: diagnosticsMessageSubscriber{},
|
||||
// It is very common to set Cloud.RoleName and Cloud.RoleInstance context properties, hence initial capacity of two
|
||||
ContextTagSources: make(map[string]string, 2),
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,462 @@
|
|||
package application_insights
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/microsoft/ApplicationInsights-Go/appinsights"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/metric"
|
||||
"github.com/influxdata/telegraf/plugins/outputs/application_insights/mocks"
|
||||
"github.com/influxdata/telegraf/testutil"
|
||||
)
|
||||
|
||||
func TestConnectFailsIfNoIkey(t *testing.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: config.Duration(time.Hour),
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
|
||||
err := ai.Connect()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestOutputCloseTimesOut(t *testing.T) {
|
||||
transmitter := new(mocks.Transmitter)
|
||||
transmitter.On("Close").Return(unfinished)
|
||||
|
||||
ai := ApplicationInsights{
|
||||
transmitter: transmitter,
|
||||
Timeout: config.Duration(time.Millisecond * 50),
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
|
||||
err := ai.Close()
|
||||
require.NoError(t, err)
|
||||
transmitter.AssertCalled(t, "Close")
|
||||
}
|
||||
|
||||
func TestCloseRemovesDiagMsgListener(t *testing.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: config.Duration(time.Hour),
|
||||
EnableDiagnosticLogging: true,
|
||||
diagMsgSubscriber: diagMsgSubscriber,
|
||||
InstrumentationKey: "1234", // Fake, but necessary to enable tracking
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
|
||||
err := ai.Connect()
|
||||
require.NoError(t, err)
|
||||
diagMsgSubscriber.AssertCalled(t, "Subscribe", mock.AnythingOfType("appinsights.DiagnosticsMessageHandler"))
|
||||
|
||||
err = ai.Close()
|
||||
require.NoError(t, 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) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
transmitter := new(mocks.Transmitter)
|
||||
transmitter.On("Track", mock.Anything)
|
||||
metricName := "ShouldBeAggregateMetric"
|
||||
|
||||
m := metric.New(
|
||||
metricName,
|
||||
nil, // tags
|
||||
tt.fields,
|
||||
now,
|
||||
)
|
||||
|
||||
ai := ApplicationInsights{
|
||||
transmitter: transmitter,
|
||||
InstrumentationKey: "1234", // Fake, but necessary to enable tracking
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
|
||||
err := ai.Connect()
|
||||
require.NoError(t, err)
|
||||
|
||||
mSet := []telegraf.Metric{m}
|
||||
err = ai.Write(mSet)
|
||||
require.NoError(t, err)
|
||||
transmitter.AssertNumberOfCalls(t, "Track", 1+len(tt.additionalMetricValueFields))
|
||||
var pAggregateTelemetry *appinsights.AggregateMetricTelemetry
|
||||
require.IsType(
|
||||
t,
|
||||
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(t, m, tt.valueField, tt.countField, aggregateTelemetry)
|
||||
|
||||
verifyAdditionalTelemetry(t, 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": float64(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) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
transmitter := new(mocks.Transmitter)
|
||||
transmitter.On("Track", mock.Anything)
|
||||
metricName := "ShouldBeSimpleMetric"
|
||||
|
||||
m := metric.New(
|
||||
metricName,
|
||||
nil, // tags
|
||||
tt.fields,
|
||||
now,
|
||||
)
|
||||
|
||||
ai := ApplicationInsights{
|
||||
transmitter: transmitter,
|
||||
InstrumentationKey: "1234", // Fake, but necessary to enable tracking
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
|
||||
err := ai.Connect()
|
||||
require.NoError(t, err)
|
||||
|
||||
mSet := []telegraf.Metric{m}
|
||||
err = ai.Write(mSet)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedNumberOfCalls := len(tt.additionalMetricValueFields)
|
||||
if tt.primaryMetricValueField != "" {
|
||||
expectedNumberOfCalls++
|
||||
}
|
||||
|
||||
transmitter.AssertNumberOfCalls(t, "Track", expectedNumberOfCalls)
|
||||
if tt.primaryMetricValueField != "" {
|
||||
var pMetricTelemetry *appinsights.MetricTelemetry
|
||||
require.IsType(t, 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(t, m, tt.primaryMetricValueField, expectedTelemetryName, metricTelemetry)
|
||||
}
|
||||
|
||||
verifyAdditionalTelemetry(t, 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) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
transmitter := new(mocks.Transmitter)
|
||||
transmitter.On("Track", mock.Anything)
|
||||
metricName := "ShouldBeSimpleMetric"
|
||||
|
||||
m := metric.New(
|
||||
metricName,
|
||||
tt.tags,
|
||||
tt.fields,
|
||||
now,
|
||||
)
|
||||
|
||||
ai := ApplicationInsights{
|
||||
transmitter: transmitter,
|
||||
InstrumentationKey: "1234", // Fake, but necessary to enable tracking
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
|
||||
err := ai.Connect()
|
||||
require.NoError(t, err)
|
||||
|
||||
mSet := []telegraf.Metric{m}
|
||||
err = ai.Write(mSet)
|
||||
require.NoError(t, err)
|
||||
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.Properties map
|
||||
verifyAdditionalTelemetry(t, m, transmitter, tt.metricValueFields, metricName)
|
||||
}
|
||||
|
||||
t.Run(tt.name, tf)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextTagsSetOnSimpleTelemetry(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
transmitter := new(mocks.Transmitter)
|
||||
transmitter.On("Track", mock.Anything)
|
||||
|
||||
m := metric.New(
|
||||
"SimpleMetric",
|
||||
map[string]string{"kubernetes_container_name": "atcsvc", "kubernetes_pod_name": "bunkie17554"},
|
||||
map[string]interface{}{"value": 23.0},
|
||||
now,
|
||||
)
|
||||
|
||||
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",
|
||||
},
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
|
||||
err := ai.Connect()
|
||||
require.NoError(t, err)
|
||||
|
||||
mSet := []telegraf.Metric{m}
|
||||
err = ai.Write(mSet)
|
||||
require.NoError(t, err)
|
||||
transmitter.AssertNumberOfCalls(t, "Track", 1)
|
||||
metricTelemetry := transmitter.Calls[0].Arguments.Get(0).(*appinsights.MetricTelemetry)
|
||||
cloudTags := metricTelemetry.Tags.Cloud()
|
||||
require.Equal(t, "atcsvc", cloudTags.GetRole())
|
||||
require.Equal(t, "bunkie17554", cloudTags.GetRoleInstance())
|
||||
}
|
||||
|
||||
func TestContextTagsSetOnAggregateTelemetry(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
transmitter := new(mocks.Transmitter)
|
||||
transmitter.On("Track", mock.Anything)
|
||||
|
||||
m := metric.New(
|
||||
"AggregateMetric",
|
||||
map[string]string{"kubernetes_container_name": "atcsvc", "kubernetes_pod_name": "bunkie17554"},
|
||||
map[string]interface{}{"value": 23.0, "count": 5},
|
||||
now,
|
||||
)
|
||||
|
||||
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",
|
||||
},
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
|
||||
err := ai.Connect()
|
||||
require.NoError(t, err)
|
||||
|
||||
mSet := []telegraf.Metric{m}
|
||||
err = ai.Write(mSet)
|
||||
require.NoError(t, err)
|
||||
transmitter.AssertNumberOfCalls(t, "Track", 1)
|
||||
metricTelemetry := transmitter.Calls[0].Arguments.Get(0).(*appinsights.AggregateMetricTelemetry)
|
||||
cloudTags := metricTelemetry.Tags.Cloud()
|
||||
require.Equal(t, "atcsvc", cloudTags.GetRole())
|
||||
require.Equal(t, "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(t *testing.T, m telegraf.Metric, valueField, countField string, telemetry *appinsights.AggregateMetricTelemetry) {
|
||||
verifyAggregateField := func(fieldName string, telemetryValue float64) {
|
||||
metricRawFieldValue, found := m.Fields()[fieldName]
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := toFloat64(metricRawFieldValue); err == nil {
|
||||
require.InDelta(t, metricRawFieldValue, telemetryValue, testutil.DefaultDelta, "Telemetry property %s does not match the metric field", fieldName)
|
||||
}
|
||||
}
|
||||
require.Equal(t, m.Name(), telemetry.Name, "Telemetry name should be the same as metric name")
|
||||
require.InDelta(t, m.Fields()[valueField], telemetry.Value, testutil.DefaultDelta, "Telemetry value does not match metric value field")
|
||||
require.EqualValues(t, m.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)
|
||||
require.Equal(t, m.Time(), telemetry.Timestamp, "Telemetry and metric timestamps do not match")
|
||||
assertMapContains(t, m.Tags(), telemetry.Properties)
|
||||
}
|
||||
|
||||
func verifySimpleTelemetry(t *testing.T, m telegraf.Metric, valueField, expectedTelemetryName string, telemetry *appinsights.MetricTelemetry) {
|
||||
require.Equal(t, expectedTelemetryName, telemetry.Name, "Telemetry name is not what was expected")
|
||||
require.InDelta(t, m.Fields()[valueField], telemetry.Value, testutil.DefaultDelta, "Telemetry value does not match metric value field")
|
||||
require.Equal(t, m.Time(), telemetry.Timestamp, "Telemetry and metric timestamps do not match")
|
||||
assertMapContains(t, m.Tags(), telemetry.Properties)
|
||||
}
|
||||
|
||||
func verifyAdditionalTelemetry(
|
||||
t *testing.T,
|
||||
m telegraf.Metric,
|
||||
transmitter *mocks.Transmitter,
|
||||
additionalMetricValueFields []string,
|
||||
telemetryNamePrefix string,
|
||||
) {
|
||||
for _, fieldName := range additionalMetricValueFields {
|
||||
expectedTelemetryName := telemetryNamePrefix + "_" + fieldName
|
||||
telemetry := findTransmittedTelemetry(transmitter, expectedTelemetryName)
|
||||
require.NotNil(t, telemetry, "Expected telemetry named %s to be created, but could not find it", expectedTelemetryName)
|
||||
if telemetry != nil {
|
||||
verifySimpleTelemetry(t, m, 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 assertMapContains(t *testing.T, expected, actual map[string]string) {
|
||||
if expected == nil && actual == nil {
|
||||
return
|
||||
}
|
||||
|
||||
require.NotNil(t, expected, "Maps not equal: expected is nil but actual is not")
|
||||
require.NotNil(t, actual, "Maps not equal: actual is nil but expected is not")
|
||||
|
||||
for k, v := range expected {
|
||||
av, ok := actual[k]
|
||||
require.True(t, ok, "Actual map does not contain a value for key %q", k)
|
||||
require.Equal(t, v, av, "The expected value for key %q is %q but the actual value is %q", k, v, av)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package application_insights
|
||||
|
||||
import (
|
||||
"github.com/microsoft/ApplicationInsights-Go/appinsights"
|
||||
)
|
||||
|
||||
type diagnosticsMessageSubscriber struct {
|
||||
}
|
||||
|
||||
func (diagnosticsMessageSubscriber) Subscribe(handler appinsights.DiagnosticsMessageHandler) appinsights.DiagnosticsMessageListener {
|
||||
return appinsights.NewDiagnosticsMessageListener(handler)
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package mocks
|
||||
|
||||
import "github.com/stretchr/testify/mock"
|
||||
|
||||
// DiagnosticsMessageSubscriber is an autogenerated mock type for the DiagnosticsMessageSubscriber type
|
||||
type DiagnosticsMessageListener struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (_m *DiagnosticsMessageListener) Remove() {
|
||||
_m.Called()
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
// Code generated by mockery v1.0.0. DO NOT EDIT.
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"github.com/microsoft/ApplicationInsights-Go/appinsights"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// DiagnosticsMessageSubscriber is an autogenerated mock type for the DiagnosticsMessageSubscriber type
|
||||
type DiagnosticsMessageSubscriber struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Subscribe provides a mock function with given fields: _a0
|
||||
func (_m *DiagnosticsMessageSubscriber) Subscribe(_a0 appinsights.DiagnosticsMessageHandler) appinsights.DiagnosticsMessageListener {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 appinsights.DiagnosticsMessageListener
|
||||
if rf, ok := ret.Get(0).(func(appinsights.DiagnosticsMessageHandler) appinsights.DiagnosticsMessageListener); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(appinsights.DiagnosticsMessageListener)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
33
plugins/outputs/application_insights/mocks/transmitter.go
Normal file
33
plugins/outputs/application_insights/mocks/transmitter.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
// Code generated by mockery v1.0.0. DO NOT EDIT.
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"github.com/microsoft/ApplicationInsights-Go/appinsights"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// Transmitter is an autogenerated mock type for the Transmitter type
|
||||
type Transmitter struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Close provides a mock function with given fields:
|
||||
func (_m *Transmitter) Close() <-chan struct{} {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 <-chan struct{}
|
||||
if rf, ok := ret.Get(0).(func() <-chan struct{}); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(<-chan struct{})
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Track provides a mock function with given fields: _a0
|
||||
func (_m *Transmitter) Track(_a0 appinsights.Telemetry) {
|
||||
_m.Called(_a0)
|
||||
}
|
25
plugins/outputs/application_insights/sample.conf
Normal file
25
plugins/outputs/application_insights/sample.conf
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Send metrics to Azure Application Insights
|
||||
[[outputs.application_insights]]
|
||||
## Instrumentation key of the Application Insights resource.
|
||||
instrumentation_key = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx"
|
||||
|
||||
## Regions that require endpoint modification https://docs.microsoft.com/en-us/azure/azure-monitor/app/custom-endpoints
|
||||
# endpoint_url = "https://dc.services.visualstudio.com/v2/track"
|
||||
|
||||
## Timeout for closing (default: 5s).
|
||||
# timeout = "5s"
|
||||
|
||||
## Enable additional diagnostic logging.
|
||||
# enable_diagnostic_logging = false
|
||||
|
||||
## NOTE: Due to the way TOML is parsed, tables must be at the END of the
|
||||
## plugin definition, otherwise additional config options are read as part of
|
||||
## the table
|
||||
|
||||
## Context Tag Sources add Application Insights context tags to a tag value.
|
||||
##
|
||||
## For list of allowed context tag keys see:
|
||||
## https://github.com/microsoft/ApplicationInsights-Go/blob/master/appinsights/contracts/contexttagkeys.go
|
||||
# [outputs.application_insights.context_tag_sources]
|
||||
# "ai.cloud.role" = "kubernetes_container_name"
|
||||
# "ai.cloud.roleInstance" = "kubernetes_pod_name"
|
27
plugins/outputs/application_insights/transmitter.go
Normal file
27
plugins/outputs/application_insights/transmitter.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package application_insights
|
||||
|
||||
import (
|
||||
"github.com/microsoft/ApplicationInsights-Go/appinsights"
|
||||
)
|
||||
|
||||
type Transmitter struct {
|
||||
client appinsights.TelemetryClient
|
||||
}
|
||||
|
||||
func NewTransmitter(ikey, endpointURL string) *Transmitter {
|
||||
if len(endpointURL) == 0 {
|
||||
return &Transmitter{client: appinsights.NewTelemetryClient(ikey)}
|
||||
}
|
||||
|
||||
telemetryConfig := appinsights.NewTelemetryConfiguration(ikey)
|
||||
telemetryConfig.EndpointUrl = endpointURL
|
||||
return &Transmitter{client: appinsights.NewTelemetryClientFromConfig(telemetryConfig)}
|
||||
}
|
||||
|
||||
func (t *Transmitter) Track(telemetry appinsights.Telemetry) {
|
||||
t.client.Track(telemetry)
|
||||
}
|
||||
|
||||
func (t *Transmitter) Close() <-chan struct{} {
|
||||
return t.client.Channel().Close(0)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue