Skip to content

Commit de3ef01

Browse files
ycombinatormx-psi
andauthored
[service] Validate pipeline type against component types (#9257)
**Description:** <Describe what has changed.> <!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> This change adds another layer of validation to pipelines. It validates that all the components in a pipeline are of the same type as the pipeline. For example, if a `metrics` pipeline contains a `traces`-only receiver, the `otelcol validate -config ...` command will fail. **Link to tracking Issue:** Fixes #8007. **Testing:** Added unit test + existing tests are passing. **Documentation:** godoc. --------- Co-authored-by: Pablo Baeyens <[email protected]>
1 parent cad5c63 commit de3ef01

File tree

8 files changed

+191
-47
lines changed

8 files changed

+191
-47
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
7+
component: service
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Validate pipeline type against component types
11+
12+
# One or more tracking issues or pull requests related to the change
13+
issues: [8007]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# Optional: The change log or logs in which this entry should be included.
21+
# e.g. '[user]' or '[user, api]'
22+
# Include 'user' if the change is relevant to end users.
23+
# Include 'api' if there is a change to a library API.
24+
# Default: '[user]'
25+
change_logs: []

otelcol/collector.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,11 @@ func (col *Collector) DryRun(ctx context.Context) error {
236236
return fmt.Errorf("failed to get config: %w", err)
237237
}
238238

239-
return cfg.Validate()
239+
if err := cfg.Validate(); err != nil {
240+
return err
241+
}
242+
243+
return col.validatePipelineCfg(ctx, cfg, factories)
240244
}
241245

242246
// Run starts the collector according to the given configuration, and waits for it to complete.
@@ -314,3 +318,23 @@ func (col *Collector) shutdown(ctx context.Context) error {
314318
func (col *Collector) setCollectorState(state State) {
315319
col.state.Store(int32(state))
316320
}
321+
322+
// validatePipelineConfig validates that the components in a pipeline support the
323+
// signal type of the pipeline. For example, this function will return an error if
324+
// a metrics pipeline has non-metrics components.
325+
func (col *Collector) validatePipelineCfg(ctx context.Context, cfg *Config, factories Factories) error {
326+
set := service.Settings{
327+
Receivers: receiver.NewBuilder(cfg.Receivers, factories.Receivers),
328+
Processors: processor.NewBuilder(cfg.Processors, factories.Processors),
329+
Exporters: exporter.NewBuilder(cfg.Exporters, factories.Exporters),
330+
Connectors: connector.NewBuilder(cfg.Connectors, factories.Connectors),
331+
Extensions: extension.NewBuilder(cfg.Extensions, factories.Extensions),
332+
}
333+
334+
_, err := service.New(ctx, set, cfg.Service)
335+
if err != nil {
336+
return err
337+
}
338+
339+
return nil
340+
}

otelcol/collector_test.go

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -421,16 +421,41 @@ func TestCollectorClosedStateOnStartUpError(t *testing.T) {
421421
}
422422

423423
func TestCollectorDryRun(t *testing.T) {
424-
// Load a bad config causing startup to fail
425-
set := CollectorSettings{
426-
BuildInfo: component.NewDefaultBuildInfo(),
427-
Factories: nopFactories,
428-
ConfigProviderSettings: newDefaultConfigProviderSettings([]string{filepath.Join("testdata", "otelcol-invalid.yaml")}),
424+
tests := map[string]struct {
425+
settings CollectorSettings
426+
expectedErr string
427+
}{
428+
"invalid_processor": {
429+
settings: CollectorSettings{
430+
BuildInfo: component.NewDefaultBuildInfo(),
431+
Factories: nopFactories,
432+
ConfigProviderSettings: newDefaultConfigProviderSettings([]string{filepath.Join("testdata", "otelcol-invalid.yaml")}),
433+
},
434+
expectedErr: `service::pipelines::traces: references processor "invalid" which is not configured`,
435+
},
436+
"logs_receiver_traces_pipeline": {
437+
settings: CollectorSettings{
438+
BuildInfo: component.NewDefaultBuildInfo(),
439+
Factories: nopFactories,
440+
ConfigProviderSettings: newDefaultConfigProviderSettings([]string{filepath.Join("testdata", "otelcol-invalid-receiver-type.yaml")}),
441+
},
442+
expectedErr: `failed to build pipelines: failed to create "nop_logs" receiver for data type "traces": telemetry type is not supported`,
443+
},
429444
}
430-
col, err := NewCollector(set)
431-
require.NoError(t, err)
432445

433-
require.Error(t, col.DryRun(context.Background()))
446+
for name, test := range tests {
447+
t.Run(name, func(t *testing.T) {
448+
col, err := NewCollector(test.settings)
449+
require.NoError(t, err)
450+
451+
err = col.DryRun(context.Background())
452+
if test.expectedErr == "" {
453+
require.NoError(t, err)
454+
} else {
455+
require.EqualError(t, err, test.expectedErr)
456+
}
457+
})
458+
}
434459
}
435460

436461
func TestPassConfmapToServiceFailure(t *testing.T) {

otelcol/command_components.go

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@ package otelcol // import "go.opentelemetry.io/collector/otelcol"
55

66
import (
77
"fmt"
8+
"sort"
89

910
"github.com/spf13/cobra"
1011
"gopkg.in/yaml.v3"
1112

1213
"go.opentelemetry.io/collector/component"
14+
"go.opentelemetry.io/collector/connector"
15+
"go.opentelemetry.io/collector/exporter"
16+
"go.opentelemetry.io/collector/extension"
17+
"go.opentelemetry.io/collector/processor"
18+
"go.opentelemetry.io/collector/receiver"
1319
)
1420

1521
type componentWithStability struct {
@@ -41,59 +47,59 @@ func newComponentsCommand(set CollectorSettings) *cobra.Command {
4147
}
4248

4349
components := componentsOutput{}
44-
for con := range factories.Connectors {
50+
for _, con := range sortFactoriesByType[connector.Factory](factories.Connectors) {
4551
components.Connectors = append(components.Connectors, componentWithStability{
46-
Name: con,
52+
Name: con.Type(),
4753
Stability: map[string]string{
48-
"logs-to-logs": factories.Connectors[con].LogsToLogsStability().String(),
49-
"logs-to-metrics": factories.Connectors[con].LogsToMetricsStability().String(),
50-
"logs-to-traces": factories.Connectors[con].LogsToTracesStability().String(),
54+
"logs-to-logs": con.LogsToLogsStability().String(),
55+
"logs-to-metrics": con.LogsToMetricsStability().String(),
56+
"logs-to-traces": con.LogsToTracesStability().String(),
5157

52-
"metrics-to-logs": factories.Connectors[con].MetricsToLogsStability().String(),
53-
"metrics-to-metrics": factories.Connectors[con].MetricsToMetricsStability().String(),
54-
"metrics-to-traces": factories.Connectors[con].MetricsToTracesStability().String(),
58+
"metrics-to-logs": con.MetricsToLogsStability().String(),
59+
"metrics-to-metrics": con.MetricsToMetricsStability().String(),
60+
"metrics-to-traces": con.MetricsToTracesStability().String(),
5561

56-
"traces-to-logs": factories.Connectors[con].TracesToLogsStability().String(),
57-
"traces-to-metrics": factories.Connectors[con].TracesToMetricsStability().String(),
58-
"traces-to-traces": factories.Connectors[con].TracesToTracesStability().String(),
62+
"traces-to-logs": con.TracesToLogsStability().String(),
63+
"traces-to-metrics": con.TracesToMetricsStability().String(),
64+
"traces-to-traces": con.TracesToTracesStability().String(),
5965
},
6066
})
6167
}
62-
for ext := range factories.Extensions {
68+
for _, ext := range sortFactoriesByType[extension.Factory](factories.Extensions) {
6369
components.Extensions = append(components.Extensions, componentWithStability{
64-
Name: ext,
70+
Name: ext.Type(),
6571
Stability: map[string]string{
66-
"extension": factories.Extensions[ext].ExtensionStability().String(),
72+
"extension": ext.ExtensionStability().String(),
6773
},
6874
})
6975
}
70-
for prs := range factories.Processors {
76+
for _, prs := range sortFactoriesByType[processor.Factory](factories.Processors) {
7177
components.Processors = append(components.Processors, componentWithStability{
72-
Name: prs,
78+
Name: prs.Type(),
7379
Stability: map[string]string{
74-
"logs": factories.Processors[prs].LogsProcessorStability().String(),
75-
"metrics": factories.Processors[prs].MetricsProcessorStability().String(),
76-
"traces": factories.Processors[prs].TracesProcessorStability().String(),
80+
"logs": prs.LogsProcessorStability().String(),
81+
"metrics": prs.MetricsProcessorStability().String(),
82+
"traces": prs.TracesProcessorStability().String(),
7783
},
7884
})
7985
}
80-
for rcv := range factories.Receivers {
86+
for _, rcv := range sortFactoriesByType[receiver.Factory](factories.Receivers) {
8187
components.Receivers = append(components.Receivers, componentWithStability{
82-
Name: rcv,
88+
Name: rcv.Type(),
8389
Stability: map[string]string{
84-
"logs": factories.Receivers[rcv].LogsReceiverStability().String(),
85-
"metrics": factories.Receivers[rcv].MetricsReceiverStability().String(),
86-
"traces": factories.Receivers[rcv].TracesReceiverStability().String(),
90+
"logs": rcv.LogsReceiverStability().String(),
91+
"metrics": rcv.MetricsReceiverStability().String(),
92+
"traces": rcv.TracesReceiverStability().String(),
8793
},
8894
})
8995
}
90-
for exp := range factories.Exporters {
96+
for _, exp := range sortFactoriesByType[exporter.Factory](factories.Exporters) {
9197
components.Exporters = append(components.Exporters, componentWithStability{
92-
Name: exp,
98+
Name: exp.Type(),
9399
Stability: map[string]string{
94-
"logs": factories.Exporters[exp].LogsExporterStability().String(),
95-
"metrics": factories.Exporters[exp].MetricsExporterStability().String(),
96-
"traces": factories.Exporters[exp].TracesExporterStability().String(),
100+
"logs": exp.LogsExporterStability().String(),
101+
"metrics": exp.MetricsExporterStability().String(),
102+
"traces": exp.TracesExporterStability().String(),
97103
},
98104
})
99105
}
@@ -107,3 +113,24 @@ func newComponentsCommand(set CollectorSettings) *cobra.Command {
107113
},
108114
}
109115
}
116+
117+
func sortFactoriesByType[T component.Factory](factories map[component.Type]T) []T {
118+
// Gather component types (factories map keys)
119+
componentTypes := make([]component.Type, 0, len(factories))
120+
for componentType := range factories {
121+
componentTypes = append(componentTypes, componentType)
122+
}
123+
124+
// Sort component types as strings
125+
sort.Slice(componentTypes, func(i, j int) bool {
126+
return componentTypes[i].String() < componentTypes[j].String()
127+
})
128+
129+
// Build and return list of factories, sorted by component types
130+
sortedFactories := make([]T, 0, len(factories))
131+
for _, componentType := range componentTypes {
132+
sortedFactories = append(sortedFactories, factories[componentType])
133+
}
134+
135+
return sortedFactories
136+
}

otelcol/factories_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package otelcol
55

66
import (
7+
"go.opentelemetry.io/collector/component"
78
"go.opentelemetry.io/collector/connector"
89
"go.opentelemetry.io/collector/connector/connectortest"
910
"go.opentelemetry.io/collector/exporter"
@@ -28,7 +29,7 @@ func nopFactories() (Factories, error) {
2829
return Factories{}, err
2930
}
3031

31-
if factories.Receivers, err = receiver.MakeFactoryMap(receivertest.NewNopFactory()); err != nil {
32+
if factories.Receivers, err = receiver.MakeFactoryMap(receivertest.NewNopFactory(), receivertest.NewNopFactoryForType(component.DataTypeLogs)); err != nil {
3233
return Factories{}, err
3334
}
3435

otelcol/testdata/components-output.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ receivers:
88
logs: Stable
99
metrics: Stable
1010
traces: Stable
11+
- name: nop_logs
12+
stability:
13+
logs: Stable
14+
metrics: Undefined
15+
traces: Undefined
1116
processors:
1217
- name: nop
1318
stability:
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
receivers:
2+
nop_logs:
3+
4+
processors:
5+
nop:
6+
7+
exporters:
8+
nop:
9+
10+
service:
11+
telemetry:
12+
metrics:
13+
address: localhost:8888
14+
pipelines:
15+
traces:
16+
receivers: [nop_logs]
17+
processors: [nop]
18+
exporters: [nop]

receiver/receivertest/nop_receiver.go

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,48 @@ import (
1414
"go.opentelemetry.io/collector/receiver"
1515
)
1616

17-
var componentType = component.MustNewType("nop")
17+
var defaultComponentType = component.MustNewType("nop")
1818

1919
// NewNopCreateSettings returns a new nop settings for Create*Receiver functions.
2020
func NewNopCreateSettings() receiver.CreateSettings {
2121
return receiver.CreateSettings{
22-
ID: component.NewIDWithName(componentType, uuid.NewString()),
22+
ID: component.NewIDWithName(defaultComponentType, uuid.NewString()),
2323
TelemetrySettings: componenttest.NewNopTelemetrySettings(),
2424
BuildInfo: component.NewDefaultBuildInfo(),
2525
}
2626
}
2727

28-
// NewNopFactory returns a receiver.Factory that constructs nop receivers.
28+
// NewNopFactory returns a receiver.Factory that constructs nop receivers supporting all data types.
2929
func NewNopFactory() receiver.Factory {
3030
return receiver.NewFactory(
31-
componentType,
31+
defaultComponentType,
3232
func() component.Config { return &nopConfig{} },
3333
receiver.WithTraces(createTraces, component.StabilityLevelStable),
3434
receiver.WithMetrics(createMetrics, component.StabilityLevelStable),
3535
receiver.WithLogs(createLogs, component.StabilityLevelStable))
3636
}
3737

38+
// NewNopFactoryForType returns a receiver.Factory that constructs nop receivers supporting only the
39+
// given data type.
40+
func NewNopFactoryForType(dataType component.DataType) receiver.Factory {
41+
var factoryOpt receiver.FactoryOption
42+
switch dataType {
43+
case component.DataTypeTraces:
44+
factoryOpt = receiver.WithTraces(createTraces, component.StabilityLevelStable)
45+
case component.DataTypeMetrics:
46+
factoryOpt = receiver.WithMetrics(createMetrics, component.StabilityLevelStable)
47+
case component.DataTypeLogs:
48+
factoryOpt = receiver.WithLogs(createLogs, component.StabilityLevelStable)
49+
default:
50+
panic("unsupported data type for creating nop receiver factory: " + dataType.String())
51+
}
52+
53+
componentType := component.MustNewType(defaultComponentType.String() + "_" + dataType.String())
54+
return receiver.NewFactory(componentType, func() component.Config { return &nopConfig{} }, factoryOpt)
55+
}
56+
57+
type nopConfig struct{}
58+
3859
func createTraces(context.Context, receiver.CreateSettings, component.Config, consumer.Traces) (receiver.Traces, error) {
3960
return nopInstance, nil
4061
}
@@ -47,8 +68,6 @@ func createLogs(context.Context, receiver.CreateSettings, component.Config, cons
4768
return nopInstance, nil
4869
}
4970

50-
type nopConfig struct{}
51-
5271
var nopInstance = &nopReceiver{}
5372

5473
// nopReceiver acts as a receiver for testing purposes.
@@ -61,6 +80,6 @@ type nopReceiver struct {
6180
func NewNopBuilder() *receiver.Builder {
6281
nopFactory := NewNopFactory()
6382
return receiver.NewBuilder(
64-
map[component.ID]component.Config{component.NewID(componentType): nopFactory.CreateDefaultConfig()},
65-
map[component.Type]receiver.Factory{componentType: nopFactory})
83+
map[component.ID]component.Config{component.NewID(defaultComponentType): nopFactory.CreateDefaultConfig()},
84+
map[component.Type]receiver.Factory{defaultComponentType: nopFactory})
6685
}

0 commit comments

Comments
 (0)