Skip to content

Commit

Permalink
Move exporter builder into internal service (#10783)
Browse files Browse the repository at this point in the history
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue.
Ex. Adding a feature - Explain what this achieves.-->
#### Description

This moves the exporter builder out of the `exporter` package, and into
`service/internal/builders`.
There's no real reason for this struct to be public (folks shouldn't
call it), and making it private will allow us to add profiling support
to it.

<!-- Issue number if applicable -->
#### Link to tracking issue

#10375 (review)
  • Loading branch information
dmathieu authored Aug 22, 2024
1 parent 549ee72 commit 7cd1579
Show file tree
Hide file tree
Showing 13 changed files with 361 additions and 18 deletions.
25 changes: 25 additions & 0 deletions .chloggen/private-exporter-builder.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: deprecation

# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
component: exporter

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Deprecate exporter.Builder, and move it into an internal package of the service module

# One or more tracking issues or pull requests related to the change
issues: [10783]

# (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:

# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: [api]
6 changes: 6 additions & 0 deletions exporter/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@ import (
)

// Builder exporter is a helper struct that given a set of Configs and Factories helps with creating exporters.
//
// Deprecated: [v0.108.0] this builder is being internalized within the service module,
// and will be removed soon.
type Builder struct {
cfgs map[component.ID]component.Config
factories map[component.Type]Factory
}

// NewBuilder creates a new exporter.Builder to help with creating components form a set of configs and factories.
//
// Deprecated: [v0.108.0] this builder is being internalized within the service module,
// and will be removed soon.
func NewBuilder(cfgs map[component.ID]component.Config, factories map[component.Type]Factory) *Builder {
return &Builder{cfgs: cfgs, factories: factories}
}
Expand Down
3 changes: 3 additions & 0 deletions exporter/exportertest/nop_exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ type nopExporter struct {
}

// NewNopBuilder returns an exporter.Builder that constructs nop receivers.
//
// Deprecated: [v0.108.0] this builder is being internalized within the service module,
// and will be removed soon.
func NewNopBuilder() *exporter.Builder {
nopFactory := NewNopFactory()
return exporter.NewBuilder(
Expand Down
9 changes: 8 additions & 1 deletion internal/e2e/status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"go.opentelemetry.io/collector/connector"
"go.opentelemetry.io/collector/connector/connectortest"
"go.opentelemetry.io/collector/consumer"
"go.opentelemetry.io/collector/exporter"
"go.opentelemetry.io/collector/exporter/exportertest"
"go.opentelemetry.io/collector/extension"
"go.opentelemetry.io/collector/internal/sharedcomponent"
Expand All @@ -35,6 +36,7 @@ var nopType = component.MustNewType("nop")

func Test_ComponentStatusReporting_SharedInstance(t *testing.T) {
eventsReceived := make(map[*componentstatus.InstanceID][]*componentstatus.Event)
exporterFactory := exportertest.NewNopFactory()
connectorFactory := connectortest.NewNopFactory()
// Use a different ID than receivertest and exportertest to avoid ambiguous
// configuration scenarios. Ambiguous IDs are detected in the 'otelcol' package,
Expand All @@ -51,7 +53,12 @@ func Test_ComponentStatusReporting_SharedInstance(t *testing.T) {
component.MustNewType("test"): newReceiverFactory(),
},
Processors: processortest.NewNopBuilder(),
Exporters: exportertest.NewNopBuilder(),
ExportersConfigs: map[component.ID]component.Config{
component.NewID(nopType): exporterFactory.CreateDefaultConfig(),
},
ExportersFactories: map[component.Type]exporter.Factory{
nopType: exporterFactory,
},
ConnectorsConfigs: map[component.ID]component.Config{
connID: connectorFactory.CreateDefaultConfig(),
},
Expand Down
4 changes: 2 additions & 2 deletions otelcol/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/confmap"
"go.opentelemetry.io/collector/exporter"
"go.opentelemetry.io/collector/extension"
"go.opentelemetry.io/collector/otelcol/internal/grpclog"
"go.opentelemetry.io/collector/processor"
Expand Down Expand Up @@ -189,7 +188,8 @@ func (col *Collector) setupConfigurationComponents(ctx context.Context) error {
ReceiversConfigs: cfg.Receivers,
ReceiversFactories: factories.Receivers,
Processors: processor.NewBuilder(cfg.Processors, factories.Processors),
Exporters: exporter.NewBuilder(cfg.Exporters, factories.Exporters),
ExportersConfigs: cfg.Exporters,
ExportersFactories: factories.Exporters,
ConnectorsConfigs: cfg.Connectors,
ConnectorsFactories: factories.Connectors,
ExtensionsConfigs: cfg.Extensions,
Expand Down
98 changes: 98 additions & 0 deletions service/internal/builders/exporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package builders // import "go.opentelemetry.io/collector/service/internal/builders"

import (
"context"
"fmt"

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/exporter"
"go.opentelemetry.io/collector/exporter/exportertest"
)

// Exporter is an interface that allows using implementations of the builder
// from different packages.
type Exporter interface {
CreateTraces(context.Context, exporter.Settings) (exporter.Traces, error)
CreateMetrics(context.Context, exporter.Settings) (exporter.Metrics, error)
CreateLogs(context.Context, exporter.Settings) (exporter.Logs, error)
Factory(component.Type) component.Factory
}

// ExporterBuilder is a helper struct that given a set of Configs and Factories helps with creating exporters.
type ExporterBuilder struct {
cfgs map[component.ID]component.Config
factories map[component.Type]exporter.Factory
}

// NewExporter creates a new ExporterBuilder to help with creating components form a set of configs and factories.
func NewExporter(cfgs map[component.ID]component.Config, factories map[component.Type]exporter.Factory) *ExporterBuilder {
return &ExporterBuilder{cfgs: cfgs, factories: factories}
}

// CreateTraces creates a Traces exporter based on the settings and config.
func (b *ExporterBuilder) CreateTraces(ctx context.Context, set exporter.Settings) (exporter.Traces, error) {
cfg, existsCfg := b.cfgs[set.ID]
if !existsCfg {
return nil, fmt.Errorf("exporter %q is not configured", set.ID)
}

f, existsFactory := b.factories[set.ID.Type()]
if !existsFactory {
return nil, fmt.Errorf("exporter factory not available for: %q", set.ID)
}

logStabilityLevel(set.Logger, f.TracesExporterStability())
return f.CreateTracesExporter(ctx, set, cfg)
}

// CreateMetrics creates a Metrics exporter based on the settings and config.
func (b *ExporterBuilder) CreateMetrics(ctx context.Context, set exporter.Settings) (exporter.Metrics, error) {
cfg, existsCfg := b.cfgs[set.ID]
if !existsCfg {
return nil, fmt.Errorf("exporter %q is not configured", set.ID)
}

f, existsFactory := b.factories[set.ID.Type()]
if !existsFactory {
return nil, fmt.Errorf("exporter factory not available for: %q", set.ID)
}

logStabilityLevel(set.Logger, f.MetricsExporterStability())
return f.CreateMetricsExporter(ctx, set, cfg)
}

// CreateLogs creates a Logs exporter based on the settings and config.
func (b *ExporterBuilder) CreateLogs(ctx context.Context, set exporter.Settings) (exporter.Logs, error) {
cfg, existsCfg := b.cfgs[set.ID]
if !existsCfg {
return nil, fmt.Errorf("exporter %q is not configured", set.ID)
}

f, existsFactory := b.factories[set.ID.Type()]
if !existsFactory {
return nil, fmt.Errorf("exporter factory not available for: %q", set.ID)
}

logStabilityLevel(set.Logger, f.LogsExporterStability())
return f.CreateLogsExporter(ctx, set, cfg)
}

func (b *ExporterBuilder) Factory(componentType component.Type) component.Factory {
return b.factories[componentType]
}

// NewNopExporterConfigsAndFactories returns a configuration and factories that allows building a new nop exporter.
func NewNopExporterConfigsAndFactories() (map[component.ID]component.Config, map[component.Type]exporter.Factory) {
nopFactory := exportertest.NewNopFactory()
configs := map[component.ID]component.Config{
component.NewID(nopType): nopFactory.CreateDefaultConfig(),
}
factories := map[component.Type]exporter.Factory{
nopType: nopFactory,
}

return configs, factories
}
193 changes: 193 additions & 0 deletions service/internal/builders/exporter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package builders

import (
"context"
"testing"

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

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/consumer/consumertest"
"go.opentelemetry.io/collector/exporter"
"go.opentelemetry.io/collector/exporter/exportertest"
)

func TestExporterBuilder(t *testing.T) {
defaultCfg := struct{}{}
factories, err := exporter.MakeFactoryMap([]exporter.Factory{
exporter.NewFactory(component.MustNewType("err"), nil),
exporter.NewFactory(
component.MustNewType("all"),
func() component.Config { return &defaultCfg },
exporter.WithTraces(createExporterTraces, component.StabilityLevelDevelopment),
exporter.WithMetrics(createExporterMetrics, component.StabilityLevelAlpha),
exporter.WithLogs(createExporterLogs, component.StabilityLevelDeprecated),
),
}...)
require.NoError(t, err)

testCases := []struct {
name string
id component.ID
err string
}{
{
name: "unknown",
id: component.MustNewID("unknown"),
err: "exporter factory not available for: \"unknown\"",
},
{
name: "err",
id: component.MustNewID("err"),
err: "telemetry type is not supported",
},
{
name: "all",
id: component.MustNewID("all"),
},
{
name: "all/named",
id: component.MustNewIDWithName("all", "named"),
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
cfgs := map[component.ID]component.Config{tt.id: defaultCfg}
b := NewExporter(cfgs, factories)

te, err := b.CreateTraces(context.Background(), createExporterSettings(tt.id))
if tt.err != "" {
assert.EqualError(t, err, tt.err)
assert.Nil(t, te)
} else {
assert.NoError(t, err)
assert.Equal(t, nopExporterInstance, te)
}

me, err := b.CreateMetrics(context.Background(), createExporterSettings(tt.id))
if tt.err != "" {
assert.EqualError(t, err, tt.err)
assert.Nil(t, me)
} else {
assert.NoError(t, err)
assert.Equal(t, nopExporterInstance, me)
}

le, err := b.CreateLogs(context.Background(), createExporterSettings(tt.id))
if tt.err != "" {
assert.EqualError(t, err, tt.err)
assert.Nil(t, le)
} else {
assert.NoError(t, err)
assert.Equal(t, nopExporterInstance, le)
}
})
}
}

func TestExporterBuilderMissingConfig(t *testing.T) {
defaultCfg := struct{}{}
factories, err := exporter.MakeFactoryMap([]exporter.Factory{
exporter.NewFactory(
component.MustNewType("all"),
func() component.Config { return &defaultCfg },
exporter.WithTraces(createExporterTraces, component.StabilityLevelDevelopment),
exporter.WithMetrics(createExporterMetrics, component.StabilityLevelAlpha),
exporter.WithLogs(createExporterLogs, component.StabilityLevelDeprecated),
),
}...)

require.NoError(t, err)

bErr := NewExporter(map[component.ID]component.Config{}, factories)
missingID := component.MustNewIDWithName("all", "missing")

te, err := bErr.CreateTraces(context.Background(), createExporterSettings(missingID))
assert.EqualError(t, err, "exporter \"all/missing\" is not configured")
assert.Nil(t, te)

me, err := bErr.CreateMetrics(context.Background(), createExporterSettings(missingID))
assert.EqualError(t, err, "exporter \"all/missing\" is not configured")
assert.Nil(t, me)

le, err := bErr.CreateLogs(context.Background(), createExporterSettings(missingID))
assert.EqualError(t, err, "exporter \"all/missing\" is not configured")
assert.Nil(t, le)
}

func TestExporterBuilderFactory(t *testing.T) {
factories, err := exporter.MakeFactoryMap([]exporter.Factory{exporter.NewFactory(component.MustNewType("foo"), nil)}...)
require.NoError(t, err)

cfgs := map[component.ID]component.Config{component.MustNewID("foo"): struct{}{}}
b := NewExporter(cfgs, factories)

assert.NotNil(t, b.Factory(component.MustNewID("foo").Type()))
assert.Nil(t, b.Factory(component.MustNewID("bar").Type()))
}

func TestNewNopExporterConfigsAndFactories(t *testing.T) {
configs, factories := NewNopExporterConfigsAndFactories()
builder := NewExporter(configs, factories)
require.NotNil(t, builder)

factory := exportertest.NewNopFactory()
cfg := factory.CreateDefaultConfig()
set := exportertest.NewNopSettings()
set.ID = component.NewID(nopType)

traces, err := factory.CreateTracesExporter(context.Background(), set, cfg)
require.NoError(t, err)
bTraces, err := builder.CreateTraces(context.Background(), set)
require.NoError(t, err)
assert.IsType(t, traces, bTraces)

metrics, err := factory.CreateMetricsExporter(context.Background(), set, cfg)
require.NoError(t, err)
bMetrics, err := builder.CreateMetrics(context.Background(), set)
require.NoError(t, err)
assert.IsType(t, metrics, bMetrics)

logs, err := factory.CreateLogsExporter(context.Background(), set, cfg)
require.NoError(t, err)
bLogs, err := builder.CreateLogs(context.Background(), set)
require.NoError(t, err)
assert.IsType(t, logs, bLogs)
}

var nopExporterInstance = &nopExporter{
Consumer: consumertest.NewNop(),
}

// nopExporter stores consumed traces and metrics for testing purposes.
type nopExporter struct {
component.StartFunc
component.ShutdownFunc
consumertest.Consumer
}

func createExporterTraces(context.Context, exporter.Settings, component.Config) (exporter.Traces, error) {
return nopExporterInstance, nil
}

func createExporterMetrics(context.Context, exporter.Settings, component.Config) (exporter.Metrics, error) {
return nopExporterInstance, nil
}

func createExporterLogs(context.Context, exporter.Settings, component.Config) (exporter.Logs, error) {
return nopExporterInstance, nil
}

func createExporterSettings(id component.ID) exporter.Settings {
return exporter.Settings{
ID: id,
TelemetrySettings: componenttest.NewNopTelemetrySettings(),
BuildInfo: component.NewDefaultBuildInfo(),
}
}
Loading

0 comments on commit 7cd1579

Please sign in to comment.