Skip to content

Commit

Permalink
Adding a trace exporter to Azure Monitor (#39)
Browse files Browse the repository at this point in the history
* Adding a trace exporter to Azure Monitor

This exporter transforms the current wire format Spans into constructs
defined in
[ApplicationInsights-Go](github.com/Microsoft/ApplicationInsights-Go/appinsights/contracts).

Once a Span is transformed it is sent to Azure Monitor via a transportChannel
interface. There is an implementation of that interface method inside
ApplicationInsights-Go package that ultimately ultimately sent to Azure
Monitor via a REST API. This package takes care of appropriate retries,
batching, and buffering (in-memory only).

Once the OpenTelemetry wire format is incorporated into the collector I
will make the switch to that format. The changes should not be too bad.

This PR does not:
- export Span events
- export Span links,
- implement a metrics exporter

Test code coverage is > 90%

* Wiring up the Azure Monitor exporter via local module redirect

* Adding slightly more test coverage

* Addressing some PR comments

* Rename toHex -> idToHex

* Address more PR comments

* Plumb zap.Logger through to the exporter

* Satisfy impi import validation

* Sanitize envelope fix

* Numeric attribute values -> measurements collection

* Remove the exporter Debug configuration flag and just use the information available in the zap.Logger

* Adding a config validation check

* Addressing PR comments
  • Loading branch information
pcwiese authored and Paulo Janotti committed Dec 3, 2019
1 parent d018721 commit 27c7118
Show file tree
Hide file tree
Showing 17 changed files with 2,448 additions and 3 deletions.
7 changes: 6 additions & 1 deletion cmd/otelcontribcol/components.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/open-telemetry/opentelemetry-collector/oterr"
"github.com/open-telemetry/opentelemetry-collector/receiver"

"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azuremonitorexporter"
"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/stackdriverexporter"
"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinscribereceiver"
)
Expand All @@ -41,7 +42,11 @@ func components() (config.Factories, error) {
errs = append(errs, err)
}

exporters := []exporter.Factory{&stackdriverexporter.Factory{}}
exporters := []exporter.Factory{
&stackdriverexporter.Factory{},
&azuremonitorexporter.Factory{},
}

for _, exp := range factories.Exporters {
exporters = append(exporters, exp)
}
Expand Down
1 change: 1 addition & 0 deletions exporter/azuremonitorexporter/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ../../Makefile.Common
21 changes: 21 additions & 0 deletions exporter/azuremonitorexporter/channels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright 2019, 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/Microsoft/ApplicationInsights-Go/appinsights/contracts"

type transportChannel interface {
Send(*contracts.Envelope)
}
31 changes: 31 additions & 0 deletions exporter/azuremonitorexporter/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2019, 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 (
"time"

"github.com/open-telemetry/opentelemetry-collector/config/configmodels"
)

// Config defines configuration for Azure Monitor
type Config struct {
// squash ensures fields are correctly decoded in embedded struct.
configmodels.ExporterSettings `mapstructure:",squash"`
Endpoint string `mapstructure:"endpoint"`
InstrumentationKey string `mapstructure:"instrumentation_key"`
MaxBatchSize int `mapstructure:"maxbatchsize"`
MaxBatchInterval time.Duration `mapstructure:"maxbatchinterval"`
}
61 changes: 61 additions & 0 deletions exporter/azuremonitorexporter/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright 2019, 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 (
"path"
"testing"
"time"

"github.com/open-telemetry/opentelemetry-collector/config"
"github.com/open-telemetry/opentelemetry-collector/config/configcheck"
"github.com/open-telemetry/opentelemetry-collector/config/configmodels"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestLoadConfig(t *testing.T) {
factories, err := config.ExampleComponents()
assert.Nil(t, err)

factory := &Factory{}
factories.Exporters[typeStr] = factory
cfg, err := config.LoadConfigFile(
t, path.Join(".", "testdata", "config.yaml"), factories,
)

require.NoError(t, err)
require.NotNil(t, cfg)

assert.Equal(t, len(cfg.Exporters), 2)

exporterType := typeStr
exporter := cfg.Exporters[exporterType]
assert.Equal(t, factory.CreateDefaultConfig(), exporter)

exporterType = typeStr + "/2"
exporter = cfg.Exporters[exporterType].(*Config)
assert.NoError(t, configcheck.ValidateConfig(exporter))
assert.Equal(
t,
&Config{
ExporterSettings: configmodels.ExporterSettings{TypeVal: typeStr, NameVal: exporterType},
Endpoint: defaultEndpoint,
InstrumentationKey: "abcdefg",
MaxBatchSize: 100,
MaxBatchInterval: 10 * time.Second,
},
exporter)
}
108 changes: 108 additions & 0 deletions exporter/azuremonitorexporter/factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright 2019, 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 (
"errors"
"time"

"github.com/Microsoft/ApplicationInsights-Go/appinsights"
"github.com/open-telemetry/opentelemetry-collector/config/configerror"
"github.com/open-telemetry/opentelemetry-collector/config/configmodels"
"github.com/open-telemetry/opentelemetry-collector/exporter"
"go.uber.org/zap"
)

const (
// The value of "type" key in configuration.
typeStr = "azuremonitor"
defaultEndpoint = "https://dc.services.visualstudio.com/v2/track"
)

var (
errUnexpectedConfigurationType = errors.New("failed to cast configuration to Azure Monitor Config")
)

// Factory for Azure Monitor exporter.
// Implements the interface from github.com/open-telemetry/opentelemetry-collector/exporter/factory.go
type Factory struct {
TransportChannel transportChannel
}

// Type gets the type of the Exporter config created by this factory.
func (f *Factory) Type() string {
return typeStr
}

// CreateDefaultConfig creates the default configuration for exporter.
func (f *Factory) CreateDefaultConfig() configmodels.Exporter {

return &Config{
ExporterSettings: configmodels.ExporterSettings{
TypeVal: typeStr,
NameVal: typeStr,
},
Endpoint: defaultEndpoint,
MaxBatchSize: 1024,
MaxBatchInterval: 10 * time.Second,
}
}

// CreateTraceExporter creates a trace exporter based on this config.
func (f *Factory) CreateTraceExporter(logger *zap.Logger, config configmodels.Exporter) (exporter.TraceExporter, error) {
exporterConfig, ok := config.(*Config)

if !ok {
return nil, errUnexpectedConfigurationType
}

transportChannel := f.getTransportChannel(exporterConfig, logger)
return newTraceExporter(exporterConfig, transportChannel, logger)
}

// CreateMetricsExporter creates a metrics exporter based on this config.
func (f *Factory) CreateMetricsExporter(
logger *zap.Logger,
cfg configmodels.Exporter,
) (exporter.MetricsExporter, error) {
return nil, configerror.ErrDataTypeIsNotSupported
}

// Configures the transport channel.
// This method is not thread-safe
func (f *Factory) getTransportChannel(exporterConfig *Config, logger *zap.Logger) transportChannel {

// The default transport channel uses the default send mechanism from the AppInsights telemetry client.
// This default channel handles batching, appropriate retries, and is backed by memory.
if f.TransportChannel == nil {
telemetryConfiguration := appinsights.NewTelemetryConfiguration(exporterConfig.InstrumentationKey)
telemetryConfiguration.EndpointUrl = exporterConfig.Endpoint
telemetryConfiguration.MaxBatchSize = exporterConfig.MaxBatchSize
telemetryConfiguration.MaxBatchInterval = exporterConfig.MaxBatchInterval
telemetryClient := appinsights.NewTelemetryClientFromConfig(telemetryConfiguration)

f.TransportChannel = telemetryClient.Channel()

// Don't even bother enabling the AppInsights diagnostics listener unless debug logging is enabled
if checkedEntry := logger.Check(zap.DebugLevel, ""); checkedEntry != nil {
appinsights.NewDiagnosticsMessageListener(func(msg string) error {
logger.Debug(msg)
return nil
})
}
}

return f.TransportChannel
}
57 changes: 57 additions & 0 deletions exporter/azuremonitorexporter/factory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2019, 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 (
"testing"

"github.com/open-telemetry/opentelemetry-collector/config/configerror"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
)

func TestExporterTypeKey(t *testing.T) {
factory := Factory{}

assert.Equal(t, typeStr, factory.Type())
}

func TestCreateMetricsExporter(t *testing.T) {
factory := Factory{}

exporter, err := factory.CreateMetricsExporter(zap.NewNop(), &Config{})

// unsupported
assert.Nil(t, exporter)
assert.Equal(t, configerror.ErrDataTypeIsNotSupported, err)
}

func TestCreateTraceExporterUsingSpecificTransportChannel(t *testing.T) {
// mock transport channel creation
factory := Factory{TransportChannel: &mockTransportChannel{}}
exporter, err := factory.CreateTraceExporter(zap.NewNop(), factory.CreateDefaultConfig())
assert.NotNil(t, exporter)
assert.Nil(t, err)
}

func TestCreateTraceExporterUsingDefaultTransportChannel(t *testing.T) {
// We get the default transport channel creation, if we dont't specify one during factory creation
factory := Factory{}
assert.Nil(t, factory.TransportChannel)
exporter, err := factory.CreateTraceExporter(zap.NewNop(), factory.CreateDefaultConfig())
assert.NotNil(t, exporter)
assert.Nil(t, err)
assert.NotNil(t, factory.TransportChannel)
}
27 changes: 27 additions & 0 deletions exporter/azuremonitorexporter/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module azuremonitorexporter

go 1.12

require (
cloud.google.com/go v0.45.1 // indirect
code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c // indirect
github.com/Microsoft/ApplicationInsights-Go v0.4.2
github.com/aws/aws-sdk-go v1.23.20 // indirect
github.com/census-instrumentation/opencensus-proto v0.2.1
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 // indirect
github.com/golang/protobuf v1.3.2
github.com/open-telemetry/opentelemetry-collector v0.2.1-0.20191016224815-dfabfb0c1d1e
github.com/satori/go.uuid v1.2.0 // indirect
github.com/stretchr/testify v1.4.0
github.com/tedsuo/ifrit v0.0.0-20191009134036-9a97d0632f00 // indirect
go.opencensus.io v0.22.2 // indirect
go.uber.org/multierr v1.4.0 // indirect
go.uber.org/zap v1.13.0
golang.org/x/net v0.0.0-20191119073136-fc4aabc6c914 // indirect
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
golang.org/x/sys v0.0.0-20191119195528-f068ffe820e4 // indirect
golang.org/x/tools v0.0.0-20191119175705-11e13f1c3fd7 // indirect
google.golang.org/appengine v1.6.2 // indirect
google.golang.org/genproto v0.0.0-20191115221424-83cc0476cb11 // indirect
google.golang.org/grpc v1.25.1
)
Loading

0 comments on commit 27c7118

Please sign in to comment.