diff --git a/.chloggen/azuremonitorexporter-metrics.yaml b/.chloggen/azuremonitorexporter-metrics.yaml new file mode 100755 index 000000000000..48a439ff976c --- /dev/null +++ b/.chloggen/azuremonitorexporter-metrics.yaml @@ -0,0 +1,16 @@ +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: azuremonitorexporter + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Adds metrics exporting + +# One or more tracking issues related to the change +issues: [14915] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: diff --git a/exporter/azuremonitorexporter/README.md b/exporter/azuremonitorexporter/README.md index f09d93c4debb..1d7c8d93b4c6 100644 --- a/exporter/azuremonitorexporter/README.md +++ b/exporter/azuremonitorexporter/README.md @@ -1,12 +1,12 @@ # Azure Monitor Exporter -| Status | | -|--------------------------|--------------| -| Stability | [beta] | -| Supported pipeline types | logs, traces | -| Distributions | [contrib] | +| Status | | +|--------------------------|-----------------------| +| Stability | [beta] | +| Supported pipeline types | logs, traces, metrics | +| Distributions | [contrib] | -This exporter sends logs and trace data to [Azure Monitor](https://docs.microsoft.com/azure/azure-monitor/). +This exporter sends logs, traces and metrics to [Azure Monitor](https://docs.microsoft.com/azure/azure-monitor/). ## Configuration @@ -62,15 +62,19 @@ The exact mapping can be found [here](trace_to_envelope.go). All attributes are also mapped to custom properties if they are booleans or strings and to custom measurements if they are ints or doubles. +#### Span Events + +Span events are optionally saved to the Application Insights `traces` table. +Exception events are saved to the Application Insights `exception` table. + ### Logs This exporter saves log records to Application Insights `traces` table. [TraceId](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#field-traceid) is mapped to `operation_id` column and [SpanId](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#field-spanid) is mapped to `operation_parentId` column. -[beta]:https://github.com/open-telemetry/opentelemetry-collector#beta -[contrib]:https://github.com/open-telemetry/opentelemetry-collector-releases/tree/main/distributions/otelcol-contrib +### Metrics -### Span Events +This exporter saves metrics to Application Insights `customMetrics` table. -Span events are optionally saved to the Application Insights `traces` table. -Exception events are saved to the Application Insights `exception` table. +[beta]:https://github.com/open-telemetry/opentelemetry-collector#beta +[contrib]:https://github.com/open-telemetry/opentelemetry-collector-releases/tree/main/distributions/otelcol-contrib diff --git a/exporter/azuremonitorexporter/contracts_utils.go b/exporter/azuremonitorexporter/contracts_utils.go new file mode 100644 index 000000000000..36d32145e013 --- /dev/null +++ b/exporter/azuremonitorexporter/contracts_utils.go @@ -0,0 +1,66 @@ +// Copyright OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azuremonitorexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azuremonitorexporter" + +import ( + "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" + "go.opentelemetry.io/collector/pdata/pcommon" // Applies resource attributes values to data properties + conventions "go.opentelemetry.io/collector/semconv/v1.6.1" +) + +const ( + instrumentationLibraryName string = "instrumentationlibrary.name" + instrumentationLibraryVersion string = "instrumentationlibrary.version" +) + +// Applies resource attributes values to data properties +func applyResourcesToDataProperties(dataProperties map[string]string, resourceAttributes pcommon.Map) { + // Copy all the resource labels into the base data properties. Resource values are always strings + resourceAttributes.Range(func(k string, v pcommon.Value) bool { + dataProperties[k] = v.Str() + return true + }) +} + +// Sets important ai.cloud.* tags on the envelope +func applyCloudTagsToEnvelope(envelope *contracts.Envelope, resourceAttributes pcommon.Map) { + // Extract key service.* labels from the Resource labels and construct CloudRole and CloudRoleInstance envelope tags + // https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/resource/semantic_conventions + if serviceName, serviceNameExists := resourceAttributes.Get(conventions.AttributeServiceName); serviceNameExists { + cloudRole := serviceName.Str() + + if serviceNamespace, serviceNamespaceExists := resourceAttributes.Get(conventions.AttributeServiceNamespace); serviceNamespaceExists { + cloudRole = serviceNamespace.Str() + "." + cloudRole + } + + envelope.Tags[contracts.CloudRole] = cloudRole + } + + if serviceInstance, exists := resourceAttributes.Get(conventions.AttributeServiceInstanceID); exists { + envelope.Tags[contracts.CloudRoleInstance] = serviceInstance.Str() + } +} + +// Applies instrumentation values to data properties +func applyInstrumentationScopeValueToDataProperties(dataProperties map[string]string, instrumentationScope pcommon.InstrumentationScope) { + // Copy the instrumentation properties + if instrumentationScope.Name() != "" { + dataProperties[instrumentationLibraryName] = instrumentationScope.Name() + } + + if instrumentationScope.Version() != "" { + dataProperties[instrumentationLibraryVersion] = instrumentationScope.Version() + } +} diff --git a/exporter/azuremonitorexporter/factory.go b/exporter/azuremonitorexporter/factory.go index d3f2be04c66d..0f1986ea6649 100644 --- a/exporter/azuremonitorexporter/factory.go +++ b/exporter/azuremonitorexporter/factory.go @@ -44,7 +44,8 @@ func NewFactory() exporter.Factory { typeStr, createDefaultConfig, exporter.WithTraces(f.createTracesExporter, stability), - exporter.WithLogs(f.createLogsExporter, stability)) + exporter.WithLogs(f.createLogsExporter, stability), + exporter.WithMetrics(f.createMetricsExporter, stability)) } // Implements the interface from go.opentelemetry.io/collector/exporter/factory.go @@ -91,6 +92,21 @@ func (f *factory) createLogsExporter( return newLogsExporter(exporterConfig, tc, set) } +func (f *factory) createMetricsExporter( + ctx context.Context, + set exporter.CreateSettings, + cfg component.Config, +) (exporter.Metrics, error) { + exporterConfig, ok := cfg.(*Config) + + if !ok { + return nil, errUnexpectedConfigurationType + } + + tc := f.getTransportChannel(exporterConfig, set.Logger) + return newMetricsExporter(exporterConfig, tc, set) +} + // Configures the transport channel. // This method is not thread-safe func (f *factory) getTransportChannel(exporterConfig *Config, logger *zap.Logger) transportChannel { diff --git a/exporter/azuremonitorexporter/go.mod b/exporter/azuremonitorexporter/go.mod index 49486c496561..f04a763035b7 100644 --- a/exporter/azuremonitorexporter/go.mod +++ b/exporter/azuremonitorexporter/go.mod @@ -17,7 +17,7 @@ require ( ) require ( - code.cloudfoundry.org/clock v1.0.0 // indirect + code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c // indirect github.com/cenkalti/backoff/v4 v4.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/gofrs/uuid v4.0.0+incompatible // indirect @@ -31,6 +31,7 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.6.1 // indirect github.com/stretchr/objx v0.5.0 // indirect diff --git a/exporter/azuremonitorexporter/go.sum b/exporter/azuremonitorexporter/go.sum index fcdea4e8f866..995ed2ca4e8e 100644 --- a/exporter/azuremonitorexporter/go.sum +++ b/exporter/azuremonitorexporter/go.sum @@ -1,8 +1,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c h1:5eeuG0BHx1+DHeT3AP+ISKZ2ht1UjGhm581ljqYpVeQ= code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= -code.cloudfoundry.org/clock v1.0.0 h1:kFXWQM4bxYvdBw2X8BbBeXwQNgfoWv1vqAk2ZZyBN2o= -code.cloudfoundry.org/clock v1.0.0/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= contrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -223,8 +222,9 @@ github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/exporter/azuremonitorexporter/metric_to_envelopes.go b/exporter/azuremonitorexporter/metric_to_envelopes.go new file mode 100644 index 000000000000..4c64a4d84e89 --- /dev/null +++ b/exporter/azuremonitorexporter/metric_to_envelopes.go @@ -0,0 +1,238 @@ +// Copyright OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azuremonitorexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azuremonitorexporter" + +import ( + "time" + + "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.uber.org/zap" +) + +type metricPacker struct { + logger *zap.Logger +} + +type timedMetricDataPoint struct { + dataPoint *contracts.DataPoint + timestamp pcommon.Timestamp +} + +type metricTimedData interface { + getTimedDataPoints() []*timedMetricDataPoint +} + +// MetricToEnvelopes packages metrics into a slice of Application Insight envelopes. +func (packer *metricPacker) MetricToEnvelopes(metric pmetric.Metric, resource pcommon.Resource, instrumentationScope pcommon.InstrumentationScope) []*contracts.Envelope { + var envelopes []*contracts.Envelope + + mtd := packer.getMetricTimedData(metric) + + if mtd != nil { + + for _, timedDataPoint := range mtd.getTimedDataPoints() { + + envelope := contracts.NewEnvelope() + envelope.Tags = make(map[string]string) + envelope.Time = toTime(timedDataPoint.timestamp).Format(time.RFC3339Nano) + + metricData := contracts.NewMetricData() + dataPoint := timedDataPoint.dataPoint + metricData.Metrics = []*contracts.DataPoint{dataPoint} + metricData.Properties = make(map[string]string) + + envelope.Name = metricData.EnvelopeName("") + + data := contracts.NewData() + data.BaseData = metricData + data.BaseType = metricData.BaseType() + envelope.Data = data + + resourceAttributes := resource.Attributes() + applyResourcesToDataProperties(metricData.Properties, resourceAttributes) + applyInstrumentationScopeValueToDataProperties(metricData.Properties, instrumentationScope) + applyCloudTagsToEnvelope(envelope, resourceAttributes) + + packer.sanitize(func() []string { return metricData.Sanitize() }) + packer.sanitize(func() []string { return envelope.Sanitize() }) + packer.sanitize(func() []string { return contracts.SanitizeTags(envelope.Tags) }) + + packer.logger.Debug("Metric is packed", zap.String("name", dataPoint.Name), zap.Any("value", dataPoint.Value)) + + envelopes = append(envelopes, envelope) + + } + } + + return envelopes +} + +func (packer *metricPacker) sanitize(sanitizeFunc func() []string) { + for _, warning := range sanitizeFunc() { + packer.logger.Warn(warning) + } +} + +func newMetricPacker(logger *zap.Logger) *metricPacker { + packer := &metricPacker{ + logger: logger, + } + return packer +} + +func (packer metricPacker) getMetricTimedData(metric pmetric.Metric) metricTimedData { + switch metric.Type() { + case pmetric.MetricTypeGauge: + return newScalarMetric(metric.Name(), metric.Gauge().DataPoints()) + case pmetric.MetricTypeSum: + return newScalarMetric(metric.Name(), metric.Sum().DataPoints()) + case pmetric.MetricTypeHistogram: + return newHistogramMetric(metric.Name(), metric.Histogram().DataPoints()) + case pmetric.MetricTypeExponentialHistogram: + return newExponentialHistogramMetric(metric.Name(), metric.ExponentialHistogram().DataPoints()) + case pmetric.MetricTypeSummary: + return newSummaryMetric(metric.Name(), metric.Summary().DataPoints()) + } + + packer.logger.Debug("Unsupported metric type", zap.Any("Metric Type", metric.Type())) + return nil +} + +type scalarMetric struct { + name string + dataPointSlice pmetric.NumberDataPointSlice +} + +func newScalarMetric(name string, dataPointSlice pmetric.NumberDataPointSlice) *scalarMetric { + return &scalarMetric{ + name: name, + dataPointSlice: dataPointSlice, + } +} + +func (m scalarMetric) getTimedDataPoints() []*timedMetricDataPoint { + timedDataPoints := make([]*timedMetricDataPoint, m.dataPointSlice.Len()) + for i := 0; i < m.dataPointSlice.Len(); i++ { + numberDataPoint := m.dataPointSlice.At(i) + dataPoint := contracts.NewDataPoint() + dataPoint.Name = m.name + dataPoint.Value = numberDataPoint.DoubleValue() + dataPoint.Count = 1 + dataPoint.Kind = contracts.Measurement + timedDataPoints[i] = &timedMetricDataPoint{ + dataPoint: dataPoint, + timestamp: numberDataPoint.Timestamp(), + } + } + return timedDataPoints +} + +type histogramMetric struct { + name string + dataPointSlice pmetric.HistogramDataPointSlice +} + +func newHistogramMetric(name string, dataPointSlice pmetric.HistogramDataPointSlice) *histogramMetric { + return &histogramMetric{ + name: name, + dataPointSlice: dataPointSlice, + } +} + +func (m histogramMetric) getTimedDataPoints() []*timedMetricDataPoint { + timedDataPoints := make([]*timedMetricDataPoint, m.dataPointSlice.Len()) + for i := 0; i < m.dataPointSlice.Len(); i++ { + histogramDataPoint := m.dataPointSlice.At(i) + dataPoint := contracts.NewDataPoint() + dataPoint.Name = m.name + dataPoint.Value = histogramDataPoint.Sum() + dataPoint.Kind = contracts.Aggregation + dataPoint.Min = histogramDataPoint.Min() + dataPoint.Max = histogramDataPoint.Max() + dataPoint.Count = int(histogramDataPoint.Count()) + + timedDataPoints[i] = &timedMetricDataPoint{ + dataPoint: dataPoint, + timestamp: histogramDataPoint.Timestamp(), + } + + } + return timedDataPoints +} + +type exponentialHistogramMetric struct { + name string + dataPointSlice pmetric.ExponentialHistogramDataPointSlice +} + +func newExponentialHistogramMetric(name string, dataPointSlice pmetric.ExponentialHistogramDataPointSlice) *exponentialHistogramMetric { + return &exponentialHistogramMetric{ + name: name, + dataPointSlice: dataPointSlice, + } +} + +func (m exponentialHistogramMetric) getTimedDataPoints() []*timedMetricDataPoint { + timedDataPoints := make([]*timedMetricDataPoint, m.dataPointSlice.Len()) + for i := 0; i < m.dataPointSlice.Len(); i++ { + exponentialHistogramDataPoint := m.dataPointSlice.At(i) + dataPoint := contracts.NewDataPoint() + dataPoint.Name = m.name + dataPoint.Value = exponentialHistogramDataPoint.Sum() + dataPoint.Kind = contracts.Aggregation + dataPoint.Min = exponentialHistogramDataPoint.Min() + dataPoint.Max = exponentialHistogramDataPoint.Max() + dataPoint.Count = int(exponentialHistogramDataPoint.Count()) + + timedDataPoints[i] = &timedMetricDataPoint{ + dataPoint: dataPoint, + timestamp: exponentialHistogramDataPoint.Timestamp(), + } + } + return timedDataPoints +} + +type summaryMetric struct { + name string + dataPointSlice pmetric.SummaryDataPointSlice +} + +func newSummaryMetric(name string, dataPointSlice pmetric.SummaryDataPointSlice) *summaryMetric { + return &summaryMetric{ + name: name, + dataPointSlice: dataPointSlice, + } +} + +func (m summaryMetric) getTimedDataPoints() []*timedMetricDataPoint { + timedDataPoints := make([]*timedMetricDataPoint, m.dataPointSlice.Len()) + for i := 0; i < m.dataPointSlice.Len(); i++ { + summaryDataPoint := m.dataPointSlice.At(i) + dataPoint := contracts.NewDataPoint() + dataPoint.Name = m.name + dataPoint.Value = summaryDataPoint.Sum() + dataPoint.Kind = contracts.Aggregation + dataPoint.Count = int(summaryDataPoint.Count()) + + timedDataPoints[i] = &timedMetricDataPoint{ + dataPoint: dataPoint, + timestamp: summaryDataPoint.Timestamp(), + } + + } + return timedDataPoints +} diff --git a/exporter/azuremonitorexporter/metricexporter.go b/exporter/azuremonitorexporter/metricexporter.go new file mode 100644 index 000000000000..7bfd40c65457 --- /dev/null +++ b/exporter/azuremonitorexporter/metricexporter.go @@ -0,0 +1,64 @@ +// Copyright OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azuremonitorexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azuremonitorexporter" + +import ( + "context" + + "go.opentelemetry.io/collector/exporter" + "go.opentelemetry.io/collector/exporter/exporterhelper" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.uber.org/zap" +) + +type metricExporter struct { + config *Config + transportChannel transportChannel + logger *zap.Logger + packer *metricPacker +} + +func (exporter *metricExporter) onMetricData(context context.Context, metricData pmetric.Metrics) error { + resourceMetrics := metricData.ResourceMetrics() + + for i := 0; i < resourceMetrics.Len(); i++ { + scopeMetrics := resourceMetrics.At(i).ScopeMetrics() + resource := resourceMetrics.At(i).Resource() + for j := 0; j < scopeMetrics.Len(); j++ { + metrics := scopeMetrics.At(j).Metrics() + scope := scopeMetrics.At(j).Scope() + for k := 0; k < metrics.Len(); k++ { + for _, envelope := range exporter.packer.MetricToEnvelopes(metrics.At(k), resource, scope) { + envelope.IKey = exporter.config.InstrumentationKey + exporter.transportChannel.Send(envelope) + } + } + } + } + + return nil +} + +// Returns a new instance of the metric exporter +func newMetricsExporter(config *Config, transportChannel transportChannel, set exporter.CreateSettings) (exporter.Metrics, error) { + exporter := &metricExporter{ + config: config, + transportChannel: transportChannel, + logger: set.Logger, + packer: newMetricPacker(set.Logger), + } + + return exporterhelper.NewMetricsExporter(context.TODO(), set, config, exporter.onMetricData) +} diff --git a/exporter/azuremonitorexporter/metricexporter_test.go b/exporter/azuremonitorexporter/metricexporter_test.go new file mode 100644 index 000000000000..e2b06585061a --- /dev/null +++ b/exporter/azuremonitorexporter/metricexporter_test.go @@ -0,0 +1,222 @@ +// Copyright OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azuremonitorexporter + +/* +Contains tests for metricexporter.go and metric_to_envelopes.go +*/ + +import ( + "context" + "testing" + + "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.uber.org/zap" +) + +// Test onMetricData callback for the test metrics data +func TestExporterMetricDataCallback(t *testing.T) { + mockTransportChannel := getMockTransportChannel() + exporter := getMetricExporter(defaultConfig, mockTransportChannel) + + metrics := getTestMetrics() + + assert.NoError(t, exporter.onMetricData(context.Background(), metrics)) + + mockTransportChannel.AssertNumberOfCalls(t, "Send", 5) +} + +func TestGaugeEnvelopes(t *testing.T) { + gaugeMetric := getTestGaugeMetric() + dataPoint := getDataPoint(t, gaugeMetric) + + assert.Equal(t, dataPoint.Name, "Gauge") + assert.Equal(t, dataPoint.Value, float64(1)) + assert.Equal(t, dataPoint.Count, 1) + assert.Equal(t, dataPoint.Kind, contracts.Measurement) +} + +func TestSumEnvelopes(t *testing.T) { + sumMetric := getTestSumMetric() + dataPoint := getDataPoint(t, sumMetric) + + assert.Equal(t, dataPoint.Name, "Sum") + assert.Equal(t, dataPoint.Value, float64(2)) + assert.Equal(t, dataPoint.Count, 1) + assert.Equal(t, dataPoint.Kind, contracts.Measurement) +} + +func TestHistogramEnvelopes(t *testing.T) { + histogramMetric := getTestHistogramMetric() + dataPoint := getDataPoint(t, histogramMetric) + + assert.Equal(t, dataPoint.Name, "Histogram") + assert.Equal(t, dataPoint.Value, float64(3)) + assert.Equal(t, dataPoint.Count, 3) + assert.Equal(t, dataPoint.Min, float64(0)) + assert.Equal(t, dataPoint.Max, float64(2)) + assert.Equal(t, dataPoint.Kind, contracts.Aggregation) +} + +func TestExponentialHistogramEnvelopes(t *testing.T) { + exponentialHistogramMetric := getTestExponentialHistogramMetric() + dataPoint := getDataPoint(t, exponentialHistogramMetric) + + assert.Equal(t, dataPoint.Name, "ExponentialHistogram") + assert.Equal(t, dataPoint.Value, float64(4)) + assert.Equal(t, dataPoint.Count, 4) + assert.Equal(t, dataPoint.Min, float64(1)) + assert.Equal(t, dataPoint.Max, float64(3)) + assert.Equal(t, dataPoint.Kind, contracts.Aggregation) +} + +func TestSummaryEnvelopes(t *testing.T) { + summaryMetric := getTestSummaryMetric() + dataPoint := getDataPoint(t, summaryMetric) + + assert.Equal(t, dataPoint.Name, "Summary") + assert.Equal(t, dataPoint.Value, float64(5)) + assert.Equal(t, dataPoint.Count, 5) + assert.Equal(t, dataPoint.Kind, contracts.Aggregation) +} + +func getDataPoint(t testing.TB, metric pmetric.Metric) *contracts.DataPoint { + var envelopes []*contracts.Envelope = getMetricPacker().MetricToEnvelopes(metric, getResource(), getScope()) + require.Equal(t, len(envelopes), 1) + envelope := envelopes[0] + require.NotNil(t, envelope) + + assert.NotNil(t, envelope.Tags) + assert.NotNil(t, envelope.Time) + + require.NotNil(t, envelope.Data) + envelopeData := envelope.Data.(*contracts.Data) + assert.Equal(t, envelopeData.BaseType, "MetricData") + + require.NotNil(t, envelopeData.BaseData) + + metricData := envelopeData.BaseData.(*contracts.MetricData) + + require.Equal(t, len(metricData.Metrics), 1) + + dataPoint := metricData.Metrics[0] + require.NotNil(t, dataPoint) + + return dataPoint +} + +func getMetricExporter(config *Config, transportChannel transportChannel) *metricExporter { + return &metricExporter{ + config, + transportChannel, + zap.NewNop(), + newMetricPacker(zap.NewNop()), + } +} + +func getMetricPacker() *metricPacker { + return newMetricPacker(zap.NewNop()) +} + +func getTestMetrics() pmetric.Metrics { + metrics := pmetric.NewMetrics() + resourceMetricsSlice := metrics.ResourceMetrics() + resourceMetric := resourceMetricsSlice.AppendEmpty() + scopeMetricsSlice := resourceMetric.ScopeMetrics() + scopeMetrics := scopeMetricsSlice.AppendEmpty() + metricSlice := scopeMetrics.Metrics() + + metric := metricSlice.AppendEmpty() + gaugeMetric := getTestGaugeMetric() + gaugeMetric.CopyTo(metric) + + metric = metricSlice.AppendEmpty() + sumMetric := getTestSumMetric() + sumMetric.CopyTo(metric) + + metric = metricSlice.AppendEmpty() + histogramMetric := getTestHistogramMetric() + histogramMetric.CopyTo(metric) + + metric = metricSlice.AppendEmpty() + exponentialHistogramMetric := getTestExponentialHistogramMetric() + exponentialHistogramMetric.CopyTo(metric) + + metric = metricSlice.AppendEmpty() + summaryMetric := getTestSummaryMetric() + summaryMetric.CopyTo(metric) + + return metrics +} + +func getTestGaugeMetric() pmetric.Metric { + metric := pmetric.NewMetric() + metric.SetName("Gauge") + metric.SetEmptyGauge() + datapoints := metric.Gauge().DataPoints() + datapoint := datapoints.AppendEmpty() + datapoint.SetDoubleValue(1) + return metric +} + +func getTestSumMetric() pmetric.Metric { + metric := pmetric.NewMetric() + metric.SetName("Sum") + metric.SetEmptySum() + datapoints := metric.Sum().DataPoints() + datapoint := datapoints.AppendEmpty() + datapoint.SetDoubleValue(2) + return metric +} + +func getTestHistogramMetric() pmetric.Metric { + metric := pmetric.NewMetric() + metric.SetName("Histogram") + metric.SetEmptyHistogram() + datapoints := metric.Histogram().DataPoints() + datapoint := datapoints.AppendEmpty() + datapoint.SetSum(3) + datapoint.SetCount(3) + datapoint.SetMin(0) + datapoint.SetMax(2) + return metric +} + +func getTestExponentialHistogramMetric() pmetric.Metric { + metric := pmetric.NewMetric() + metric.SetName("ExponentialHistogram") + metric.SetEmptyExponentialHistogram() + datapoints := metric.ExponentialHistogram().DataPoints() + datapoint := datapoints.AppendEmpty() + datapoint.SetSum(4) + datapoint.SetCount(4) + datapoint.SetMin(1) + datapoint.SetMax(3) + return metric +} + +func getTestSummaryMetric() pmetric.Metric { + metric := pmetric.NewMetric() + metric.SetName("Summary") + metric.SetEmptySummary() + datapoints := metric.Summary().DataPoints() + datapoint := datapoints.AppendEmpty() + datapoint.SetSum(5) + datapoint.SetCount(5) + return metric +} diff --git a/exporter/azuremonitorexporter/trace_to_envelope.go b/exporter/azuremonitorexporter/trace_to_envelope.go index 043d67f1beb8..bcf27fb9fcfa 100644 --- a/exporter/azuremonitorexporter/trace_to_envelope.go +++ b/exporter/azuremonitorexporter/trace_to_envelope.go @@ -39,9 +39,7 @@ const ( messagingSpanType spanType = 4 faasSpanType spanType = 5 - exceptionSpanEventName string = "exception" - instrumentationLibraryName string = "instrumentationlibrary.name" - instrumentationLibraryVersion string = "instrumentationlibrary.version" + exceptionSpanEventName string = "exception" ) var ( @@ -188,46 +186,6 @@ func newEnvelope(span ptrace.Span, time string) *contracts.Envelope { return envelope } -// Applies resource attributes values to data properties -func applyResourcesToDataProperties(dataProperties map[string]string, resourceAttributes pcommon.Map) { - // Copy all the resource labels into the base data properties. Resource values are always strings - resourceAttributes.Range(func(k string, v pcommon.Value) bool { - dataProperties[k] = v.Str() - return true - }) -} - -// Sets important ai.cloud.* tags on the envelope -func applyCloudTagsToEnvelope(envelope *contracts.Envelope, resourceAttributes pcommon.Map) { - // Extract key service.* labels from the Resource labels and construct CloudRole and CloudRoleInstance envelope tags - // https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/resource/semantic_conventions - if serviceName, serviceNameExists := resourceAttributes.Get(conventions.AttributeServiceName); serviceNameExists { - cloudRole := serviceName.Str() - - if serviceNamespace, serviceNamespaceExists := resourceAttributes.Get(conventions.AttributeServiceNamespace); serviceNamespaceExists { - cloudRole = serviceNamespace.Str() + "." + cloudRole - } - - envelope.Tags[contracts.CloudRole] = cloudRole - } - - if serviceInstance, exists := resourceAttributes.Get(conventions.AttributeServiceInstanceID); exists { - envelope.Tags[contracts.CloudRoleInstance] = serviceInstance.Str() - } -} - -// Applies instrumentation values to data properties -func applyInstrumentationScopeValueToDataProperties(dataProperties map[string]string, instrumentationScope pcommon.InstrumentationScope) { - // Copy the instrumentation properties - if instrumentationScope.Name() != "" { - dataProperties[instrumentationLibraryName] = instrumentationScope.Name() - } - - if instrumentationScope.Version() != "" { - dataProperties[instrumentationLibraryVersion] = instrumentationScope.Version() - } -} - // Maps Server/Consumer Span to AppInsights RequestData func spanToRequestData(span ptrace.Span, incomingSpanType spanType) *contracts.RequestData { // See https://github.com/microsoft/ApplicationInsights-Go/blob/master/appinsights/contracts/requestdata.go