Skip to content

Commit

Permalink
Added routing exporter
Browse files Browse the repository at this point in the history
Closes #1260

Signed-off-by: Juraci Paixão Kröhling <[email protected]>
  • Loading branch information
jpkrohling committed Aug 21, 2020
1 parent 1e65674 commit d45723c
Show file tree
Hide file tree
Showing 8 changed files with 1,006 additions and 0 deletions.
52 changes: 52 additions & 0 deletions exporter/routingexporter/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright The 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
//
// https://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 routingexporter

import (
"go.opentelemetry.io/collector/config/configmodels"
)

// Config defines configuration for the Routing exporter.
type Config struct {
configmodels.ExporterSettings `mapstructure:",squash"`

// DefaultExporters contains the list of exporters to use when a more specific record can't be found in the routing table.
// Optional.
DefaultExporters []string `mapstructure:"default_exporters"`

// FromAttribute contains the attribute name to look up the tenant value. This attribute should be part of the context propagated
// down from the previous receivers and/or processors. If all the receivers and processors are propagating the entire context correctly,
// this could be the HTTP/gRPC header from the original request/RPC. Typically, aggregation processors (batch, queued_retry, groupbytrace)
// will create a new context, so, those should be avoided when using this exporter. If you need those capabilities, add them as
// part of the table's pipeline. Although the HTTP spec allows headers to be repeated, this exporter will only use the first value.
// Required.
FromAttribute string `mapstructure:"from_attribute"`

// Table contains the routing table for this exporter.
// Required.
Table []RoutingTableItem `mapstructure:"table"`
}

// RoutingTableItem specifies how data should be routed to the different exporters
type RoutingTableItem struct {
// Value represents a possible value for the field specified under FromAttribute. Required.
Value string `mapstructure:"value"`

// Exporters contains the list of exporters to use when the value from the FromAttribute field matches this table item.
// When no exporters are specified, the ones specified under DefaultExporters are used, if any.
// The routing exporter will fail upon the first failure from these exporters.
// Optional.
Exporters []string `mapstructure:"exporters"`
}
67 changes: 67 additions & 0 deletions exporter/routingexporter/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright The 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
//
// https://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 routingexporter

import (
"path"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/config/configmodels"
"go.opentelemetry.io/collector/config/configtest"
"go.opentelemetry.io/collector/exporter/jaegerexporter"
"go.opentelemetry.io/collector/exporter/otlpexporter"
)

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

factory := NewFactory()
factories.Exporters[typeStr] = factory

// we don't need to use them in this test, but the config has them
factories.Exporters["otlp"] = otlpexporter.NewFactory()
factories.Exporters["jaeger"] = jaegerexporter.NewFactory()

cfg, err := configtest.LoadConfigFile(t, path.Join(".", "testdata", "config.yaml"), factories)

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

parsed := cfg.Exporters["routing"]
assert.Equal(t, parsed,
&Config{
ExporterSettings: configmodels.ExporterSettings{
NameVal: "routing",
TypeVal: "routing",
},
DefaultExporters: []string{"otlp"},
FromAttribute: "X-Tenant",
Table: []RoutingTableItem{
{
Value: "acme",
Exporters: []string{"jaeger/acme", "otlp/acme"},
},
{
Value: "globex",
Exporters: []string{"otlp/globex"},
},
},
})
}
55 changes: 55 additions & 0 deletions exporter/routingexporter/factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright The 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
//
// https://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 routingexporter

import (
"context"

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/config/configmodels"
"go.opentelemetry.io/collector/exporter/exporterhelper"
)

const (
// The value of "type" key in configuration.
typeStr = "routing"
)

// NewFactory creates a factory for OTLP exporter.
func NewFactory() component.ExporterFactory {
return exporterhelper.NewFactory(
typeStr,
createDefaultConfig,
exporterhelper.WithTraces(createTraceExporter),
)
}

func createDefaultConfig() configmodels.Exporter {
return &Config{}
}

func createTraceExporter(_ context.Context, params component.ExporterCreateParams, cfg configmodels.Exporter) (component.TraceExporter, error) {
oce, err := newExporter(params.Logger, cfg)
if err != nil {
return nil, err
}

return exporterhelper.NewTraceExporter(
cfg,
oce.pushTraceData,
exporterhelper.WithStart(oce.startTraceExporter),
exporterhelper.WithShutdown(oce.shutdown),
)
}
174 changes: 174 additions & 0 deletions exporter/routingexporter/factory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Copyright The 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
//
// https://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 routingexporter

import (
"context"
"errors"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/config/configmodels"
)

func TestExporterGetsCreatedWithValidConfiguration(t *testing.T) {
// prepare
factory := NewFactory()
creationParams := component.ExporterCreateParams{Logger: zap.NewNop()}
cfg := &Config{
ExporterSettings: configmodels.ExporterSettings{
NameVal: "routing",
TypeVal: "routing",
},
DefaultExporters: []string{"otlp"},
FromAttribute: "X-Tenant",
Table: []RoutingTableItem{
{
Value: "acme",
Exporters: []string{"otlp"},
},
},
}

// test
exp, err := factory.CreateTraceExporter(context.Background(), creationParams, cfg)

// verify
assert.Nil(t, err)
assert.NotNil(t, exp)
}

func TestFailOnEmptyConfiguration(t *testing.T) {
// prepare
factory := NewFactory()
creationParams := component.ExporterCreateParams{Logger: zap.NewNop()}
cfg := factory.CreateDefaultConfig()

// test
exp, err := factory.CreateTraceExporter(context.Background(), creationParams, cfg)

// verify
assert.Error(t, err)
assert.Nil(t, exp)
}

func TestExporterFailsToBeCreatedWhenRouteHasNoExporters(t *testing.T) {
// prepare
factory := NewFactory()
creationParams := component.ExporterCreateParams{Logger: zap.NewNop()}
cfg := &Config{
ExporterSettings: configmodels.ExporterSettings{
NameVal: "routing",
TypeVal: "routing",
},
DefaultExporters: []string{"otlp"},
FromAttribute: "X-Tenant",
Table: []RoutingTableItem{
{
Value: "acme",
},
},
}

// test
exp, err := factory.CreateTraceExporter(context.Background(), creationParams, cfg)

// verify
assert.True(t, errors.Is(err, errNoExporters))
assert.Nil(t, exp)
}

func TestExporterFailsToBeCreatedWhenNoRoutesExist(t *testing.T) {
// prepare
factory := NewFactory()
creationParams := component.ExporterCreateParams{Logger: zap.NewNop()}
cfg := &Config{
ExporterSettings: configmodels.ExporterSettings{
NameVal: "routing",
TypeVal: "routing",
},
DefaultExporters: []string{"otlp"},
FromAttribute: "X-Tenant",
Table: []RoutingTableItem{},
}

// test
exp, err := factory.CreateTraceExporter(context.Background(), creationParams, cfg)

// verify
assert.True(t, errors.Is(err, errNoTableItems))
assert.Nil(t, exp)
}

func TestExporterFailsWithNoFromAttribute(t *testing.T) {
// prepare
factory := NewFactory()
creationParams := component.ExporterCreateParams{Logger: zap.NewNop()}
cfg := &Config{
ExporterSettings: configmodels.ExporterSettings{
NameVal: "routing",
TypeVal: "routing",
},
DefaultExporters: []string{"otlp"},
Table: []RoutingTableItem{
{
Value: "acme",
Exporters: []string{"otlp"},
},
},
}

// test
exp, err := factory.CreateTraceExporter(context.Background(), creationParams, cfg)

// verify
assert.True(t, errors.Is(err, errNoMissingFromAttribute))
assert.Nil(t, exp)
}

func TestShutdown(t *testing.T) {
// prepare
factory := NewFactory()
creationParams := component.ExporterCreateParams{Logger: zap.NewNop()}
cfg := &Config{
ExporterSettings: configmodels.ExporterSettings{
NameVal: "routing",
TypeVal: "routing",
},
DefaultExporters: []string{"otlp"},
FromAttribute: "X-Tenant",
Table: []RoutingTableItem{
{
Value: "acme",
Exporters: []string{"otlp"},
},
},
}

exp, err := factory.CreateTraceExporter(context.Background(), creationParams, cfg)
require.Nil(t, err)
require.NotNil(t, exp)

// test
err = exp.Shutdown(context.Background())

// verify
assert.NoError(t, err)

}
Loading

0 comments on commit d45723c

Please sign in to comment.