opentelemetry-collector/service/internal/graph/lifecycle_test.go

496 lines
20 KiB
Go

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package graph
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gonum.org/v1/gonum/graph/simple"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/component/componentstatus"
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/connector"
"go.opentelemetry.io/collector/connector/connectortest"
"go.opentelemetry.io/collector/exporter"
"go.opentelemetry.io/collector/exporter/exportertest"
"go.opentelemetry.io/collector/pipeline"
"go.opentelemetry.io/collector/processor"
"go.opentelemetry.io/collector/processor/processortest"
"go.opentelemetry.io/collector/receiver"
"go.opentelemetry.io/collector/receiver/receivertest"
"go.opentelemetry.io/collector/service/internal/builders"
"go.opentelemetry.io/collector/service/internal/status"
"go.opentelemetry.io/collector/service/internal/status/statustest"
"go.opentelemetry.io/collector/service/pipelines"
)
func TestGraphStartStop(t *testing.T) {
t.Run("with_internal_telemetry", func(t *testing.T) {
setObsConsumerGateForTest(t, true)
testGraphStartStop(t)
})
t.Run("without_internal_telemetry", func(t *testing.T) {
setObsConsumerGateForTest(t, false)
testGraphStartStop(t)
})
}
func testGraphStartStop(t *testing.T) {
testCases := []struct {
name string
edges [][2]component.ID
}{
{
name: "single",
edges: [][2]component.ID{
{component.MustNewIDWithName("r", "1"), component.MustNewIDWithName("p", "1")},
{component.MustNewIDWithName("r", "2"), component.MustNewIDWithName("p", "1")},
{component.MustNewIDWithName("p", "1"), component.MustNewIDWithName("p", "2")},
{component.MustNewIDWithName("p", "2"), component.MustNewIDWithName("e", "1")},
{component.MustNewIDWithName("p", "1"), component.MustNewIDWithName("e", "2")},
},
},
{
name: "multi",
edges: [][2]component.ID{
// Pipeline 1
{component.MustNewIDWithName("r", "1"), component.MustNewIDWithName("p", "1")},
{component.MustNewIDWithName("r", "2"), component.MustNewIDWithName("p", "1")},
{component.MustNewIDWithName("p", "1"), component.MustNewIDWithName("p", "2")},
{component.MustNewIDWithName("p", "2"), component.MustNewIDWithName("e", "1")},
{component.MustNewIDWithName("p", "1"), component.MustNewIDWithName("e", "2")},
// Pipeline 2, shares r1 and e2
{component.MustNewIDWithName("r", "1"), component.MustNewIDWithName("p", "3")},
{component.MustNewIDWithName("p", "3"), component.MustNewIDWithName("e", "2")},
},
},
{
name: "connected",
edges: [][2]component.ID{
// Pipeline 1
{component.MustNewIDWithName("r", "1"), component.MustNewIDWithName("p", "1")},
{component.MustNewIDWithName("r", "2"), component.MustNewIDWithName("p", "1")},
{component.MustNewIDWithName("p", "1"), component.MustNewIDWithName("p", "2")},
{component.MustNewIDWithName("p", "2"), component.MustNewIDWithName("e", "1")},
{component.MustNewIDWithName("p", "1"), component.MustNewIDWithName("c", "1")},
// Pipeline 2, shares r1 and c1
{component.MustNewIDWithName("r", "1"), component.MustNewIDWithName("p", "3")},
{component.MustNewIDWithName("p", "3"), component.MustNewIDWithName("c", "1")},
// Pipeline 3, emits to e2 and c2
{component.MustNewIDWithName("c", "1"), component.MustNewIDWithName("e", "2")},
{component.MustNewIDWithName("c", "1"), component.MustNewIDWithName("c", "2")},
// Pipeline 4, also emits to e2
{component.MustNewIDWithName("c", "2"), component.MustNewIDWithName("e", "2")},
},
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
ctx := &contextWithOrder{
Context: context.Background(),
order: map[component.ID]int{},
}
pg := &Graph{componentGraph: simple.NewDirectedGraph()}
pg.telemetry = componenttest.NewNopTelemetrySettings()
pg.instanceIDs = make(map[int64]*componentstatus.InstanceID)
for _, edge := range tt.edges {
f, t := &testNode{id: edge[0]}, &testNode{id: edge[1]}
pg.instanceIDs[f.ID()] = &componentstatus.InstanceID{}
pg.instanceIDs[t.ID()] = &componentstatus.InstanceID{}
pg.componentGraph.SetEdge(simple.Edge{F: f, T: t})
}
require.NoError(t, pg.StartAll(ctx, &Host{Reporter: status.NewReporter(func(*componentstatus.InstanceID, *componentstatus.Event) {}, func(error) {})}))
for _, edge := range tt.edges {
assert.Greater(t, ctx.order[edge[0]], ctx.order[edge[1]])
}
ctx.order = map[component.ID]int{}
require.NoError(t, pg.ShutdownAll(ctx, statustest.NewNopStatusReporter()))
for _, edge := range tt.edges {
assert.Less(t, ctx.order[edge[0]], ctx.order[edge[1]])
}
})
}
}
func TestGraphStartStopCycle(t *testing.T) {
t.Run("with_internal_telemetry", func(t *testing.T) {
setObsConsumerGateForTest(t, true)
testGraphStartStopCycle(t)
})
t.Run("without_internal_telemetry", func(t *testing.T) {
setObsConsumerGateForTest(t, false)
testGraphStartStopCycle(t)
})
}
func testGraphStartStopCycle(t *testing.T) {
pg := &Graph{componentGraph: simple.NewDirectedGraph()}
r1 := &testNode{id: component.MustNewIDWithName("r", "1")}
p1 := &testNode{id: component.MustNewIDWithName("p", "1")}
c1 := &testNode{id: component.MustNewIDWithName("c", "1")}
e1 := &testNode{id: component.MustNewIDWithName("e", "1")}
pg.instanceIDs = map[int64]*componentstatus.InstanceID{
r1.ID(): {},
p1.ID(): {},
c1.ID(): {},
e1.ID(): {},
}
pg.componentGraph.SetEdge(simple.Edge{F: r1, T: p1})
pg.componentGraph.SetEdge(simple.Edge{F: p1, T: c1})
pg.componentGraph.SetEdge(simple.Edge{F: c1, T: e1})
pg.componentGraph.SetEdge(simple.Edge{F: c1, T: p1}) // loop back
err := pg.StartAll(context.Background(), &Host{Reporter: status.NewReporter(func(*componentstatus.InstanceID, *componentstatus.Event) {}, func(error) {})})
require.ErrorContains(t, err, `topo: no topological ordering: cyclic components`)
err = pg.ShutdownAll(context.Background(), statustest.NewNopStatusReporter())
assert.ErrorContains(t, err, `topo: no topological ordering: cyclic components`)
}
func TestGraphStartStopComponentError(t *testing.T) {
t.Run("with_internal_telemetry", func(t *testing.T) {
setObsConsumerGateForTest(t, true)
testGraphStartStopComponentError(t)
})
t.Run("without_internal_telemetry", func(t *testing.T) {
setObsConsumerGateForTest(t, false)
testGraphStartStopComponentError(t)
})
}
func testGraphStartStopComponentError(t *testing.T) {
pg := &Graph{componentGraph: simple.NewDirectedGraph()}
pg.telemetry = componenttest.NewNopTelemetrySettings()
r1 := &testNode{
id: component.MustNewIDWithName("r", "1"),
startErr: errors.New("foo"),
}
e1 := &testNode{
id: component.MustNewIDWithName("e", "1"),
shutdownErr: errors.New("bar"),
}
pg.instanceIDs = map[int64]*componentstatus.InstanceID{
r1.ID(): {},
e1.ID(): {},
}
pg.componentGraph.SetEdge(simple.Edge{
F: r1,
T: e1,
})
require.ErrorIs(t, pg.StartAll(context.Background(), &Host{Reporter: status.NewReporter(func(*componentstatus.InstanceID, *componentstatus.Event) {}, func(error) {})}), r1.startErr)
assert.EqualError(t, pg.ShutdownAll(context.Background(), statustest.NewNopStatusReporter()), "bar")
}
// This includes all tests from the previous implementation, plus a new one
// relevant only to the new graph-based implementation.
func TestGraphFailToStartAndShutdown(t *testing.T) {
t.Run("with_internal_telemetry", func(t *testing.T) {
setObsConsumerGateForTest(t, true)
testGraphFailToStartAndShutdown(t)
})
t.Run("without_internal_telemetry", func(t *testing.T) {
setObsConsumerGateForTest(t, false)
testGraphFailToStartAndShutdown(t)
})
}
func testGraphFailToStartAndShutdown(t *testing.T) {
errReceiverFactory := newErrReceiverFactory()
errProcessorFactory := newErrProcessorFactory()
errExporterFactory := newErrExporterFactory()
errConnectorFactory := newErrConnectorFactory()
nopReceiverFactory := receivertest.NewNopFactory()
nopProcessorFactory := processortest.NewNopFactory()
nopExporterFactory := exportertest.NewNopFactory()
nopConnectorFactory := connectortest.NewNopFactory()
set := Settings{
Telemetry: componenttest.NewNopTelemetrySettings(),
BuildInfo: component.NewDefaultBuildInfo(),
ReceiverBuilder: builders.NewReceiver(
map[component.ID]component.Config{
component.NewID(nopReceiverFactory.Type()): nopReceiverFactory.CreateDefaultConfig(),
component.NewID(errReceiverFactory.Type()): errReceiverFactory.CreateDefaultConfig(),
},
map[component.Type]receiver.Factory{
nopReceiverFactory.Type(): nopReceiverFactory,
errReceiverFactory.Type(): errReceiverFactory,
}),
ProcessorBuilder: builders.NewProcessor(
map[component.ID]component.Config{
component.NewID(nopProcessorFactory.Type()): nopProcessorFactory.CreateDefaultConfig(),
component.NewID(errProcessorFactory.Type()): errProcessorFactory.CreateDefaultConfig(),
},
map[component.Type]processor.Factory{
nopProcessorFactory.Type(): nopProcessorFactory,
errProcessorFactory.Type(): errProcessorFactory,
}),
ExporterBuilder: builders.NewExporter(
map[component.ID]component.Config{
component.NewID(nopExporterFactory.Type()): nopExporterFactory.CreateDefaultConfig(),
component.NewID(errExporterFactory.Type()): errExporterFactory.CreateDefaultConfig(),
},
map[component.Type]exporter.Factory{
nopExporterFactory.Type(): nopExporterFactory,
errExporterFactory.Type(): errExporterFactory,
}),
ConnectorBuilder: builders.NewConnector(
map[component.ID]component.Config{
component.NewIDWithName(nopConnectorFactory.Type(), "conn"): nopConnectorFactory.CreateDefaultConfig(),
component.NewIDWithName(errConnectorFactory.Type(), "conn"): errConnectorFactory.CreateDefaultConfig(),
},
map[component.Type]connector.Factory{
nopConnectorFactory.Type(): nopConnectorFactory,
errConnectorFactory.Type(): errConnectorFactory,
}),
}
dataTypes := []pipeline.Signal{pipeline.SignalTraces, pipeline.SignalMetrics, pipeline.SignalLogs}
for _, dt := range dataTypes {
t.Run(dt.String()+"/receiver", func(t *testing.T) {
set.PipelineConfigs = pipelines.Config{
pipeline.NewID(dt): {
Receivers: []component.ID{component.MustNewID("nop"), component.MustNewID("err")},
Processors: []component.ID{component.MustNewID("nop")},
Exporters: []component.ID{component.MustNewID("nop")},
},
}
pipelines, err := Build(context.Background(), set)
require.NoError(t, err)
require.Error(t, pipelines.StartAll(context.Background(), &Host{Reporter: status.NewReporter(func(*componentstatus.InstanceID, *componentstatus.Event) {}, func(error) {})}))
assert.Error(t, pipelines.ShutdownAll(context.Background(), statustest.NewNopStatusReporter()))
})
t.Run(dt.String()+"/processor", func(t *testing.T) {
set.PipelineConfigs = pipelines.Config{
pipeline.NewID(dt): {
Receivers: []component.ID{component.MustNewID("nop")},
Processors: []component.ID{component.MustNewID("nop"), component.MustNewID("err")},
Exporters: []component.ID{component.MustNewID("nop")},
},
}
pipelines, err := Build(context.Background(), set)
require.NoError(t, err)
require.Error(t, pipelines.StartAll(context.Background(), &Host{Reporter: status.NewReporter(func(*componentstatus.InstanceID, *componentstatus.Event) {}, func(error) {})}))
assert.Error(t, pipelines.ShutdownAll(context.Background(), statustest.NewNopStatusReporter()))
})
t.Run(dt.String()+"/exporter", func(t *testing.T) {
set.PipelineConfigs = pipelines.Config{
pipeline.NewID(dt): {
Receivers: []component.ID{component.MustNewID("nop")},
Processors: []component.ID{component.MustNewID("nop")},
Exporters: []component.ID{component.MustNewID("nop"), component.MustNewID("err")},
},
}
pipelines, err := Build(context.Background(), set)
require.NoError(t, err)
require.Error(t, pipelines.StartAll(context.Background(), &Host{Reporter: status.NewReporter(func(*componentstatus.InstanceID, *componentstatus.Event) {}, func(error) {})}))
assert.Error(t, pipelines.ShutdownAll(context.Background(), statustest.NewNopStatusReporter()))
})
for _, dt2 := range dataTypes {
t.Run(dt.String()+"/"+dt2.String()+"/connector", func(t *testing.T) {
set.PipelineConfigs = pipelines.Config{
pipeline.NewIDWithName(dt, "in"): {
Receivers: []component.ID{component.MustNewID("nop")},
Processors: []component.ID{component.MustNewID("nop")},
Exporters: []component.ID{component.MustNewID("nop"), component.MustNewIDWithName("err", "conn")},
},
pipeline.NewIDWithName(dt2, "out"): {
Receivers: []component.ID{component.MustNewID("nop"), component.MustNewIDWithName("err", "conn")},
Processors: []component.ID{component.MustNewID("nop")},
Exporters: []component.ID{component.MustNewID("nop")},
},
}
pipelines, err := Build(context.Background(), set)
require.NoError(t, err)
require.Error(t, pipelines.StartAll(context.Background(), &Host{Reporter: status.NewReporter(func(*componentstatus.InstanceID, *componentstatus.Event) {}, func(error) {})}))
assert.Error(t, pipelines.ShutdownAll(context.Background(), statustest.NewNopStatusReporter()))
})
}
}
}
func TestStatusReportedOnStartupShutdown(t *testing.T) {
t.Run("with_internal_telemetry", func(t *testing.T) {
setObsConsumerGateForTest(t, true)
testStatusReportedOnStartupShutdown(t)
})
t.Run("without_internal_telemetry", func(t *testing.T) {
setObsConsumerGateForTest(t, false)
testStatusReportedOnStartupShutdown(t)
})
}
func testStatusReportedOnStartupShutdown(t *testing.T) {
rNoErr := &testNode{id: component.MustNewIDWithName("r_no_err", "1")}
rStErr := &testNode{id: component.MustNewIDWithName("r_st_err", "1"), startErr: assert.AnError}
rSdErr := &testNode{id: component.MustNewIDWithName("r_sd_err", "1"), shutdownErr: assert.AnError}
eNoErr := &testNode{id: component.MustNewIDWithName("e_no_err", "1")}
eStErr := &testNode{id: component.MustNewIDWithName("e_st_err", "1"), startErr: assert.AnError}
eSdErr := &testNode{id: component.MustNewIDWithName("e_sd_err", "1"), shutdownErr: assert.AnError}
instanceIDs := map[*testNode]*componentstatus.InstanceID{
rNoErr: componentstatus.NewInstanceID(rNoErr.id, component.KindReceiver),
rStErr: componentstatus.NewInstanceID(rStErr.id, component.KindReceiver),
rSdErr: componentstatus.NewInstanceID(rSdErr.id, component.KindReceiver),
eNoErr: componentstatus.NewInstanceID(eNoErr.id, component.KindExporter),
eStErr: componentstatus.NewInstanceID(eStErr.id, component.KindExporter),
eSdErr: componentstatus.NewInstanceID(eSdErr.id, component.KindExporter),
}
// compare two maps of status events ignoring timestamp
assertEqualStatuses := func(t *testing.T, evMap1, evMap2 map[*componentstatus.InstanceID][]*componentstatus.Event) {
assert.Len(t, evMap2, len(evMap1))
for id, evts1 := range evMap1 {
evts2 := evMap2[id]
assert.Len(t, evts2, len(evts1))
for i := 0; i < len(evts1); i++ {
ev1 := evts1[i]
ev2 := evts2[i]
assert.Equal(t, ev1.Status(), ev2.Status())
assert.Equal(t, ev1.Err(), ev2.Err())
}
}
}
for _, tt := range []struct {
name string
edge [2]*testNode
expectedStatuses map[*componentstatus.InstanceID][]*componentstatus.Event
startupErr error
shutdownErr error
}{
{
name: "successful startup/shutdown",
edge: [2]*testNode{rNoErr, eNoErr},
expectedStatuses: map[*componentstatus.InstanceID][]*componentstatus.Event{
instanceIDs[rNoErr]: {
componentstatus.NewEvent(componentstatus.StatusStarting),
componentstatus.NewEvent(componentstatus.StatusOK),
componentstatus.NewEvent(componentstatus.StatusStopping),
componentstatus.NewEvent(componentstatus.StatusStopped),
},
instanceIDs[eNoErr]: {
componentstatus.NewEvent(componentstatus.StatusStarting),
componentstatus.NewEvent(componentstatus.StatusOK),
componentstatus.NewEvent(componentstatus.StatusStopping),
componentstatus.NewEvent(componentstatus.StatusStopped),
},
},
},
{
name: "early startup error",
edge: [2]*testNode{rNoErr, eStErr},
expectedStatuses: map[*componentstatus.InstanceID][]*componentstatus.Event{
instanceIDs[eStErr]: {
componentstatus.NewEvent(componentstatus.StatusStarting),
componentstatus.NewPermanentErrorEvent(assert.AnError),
componentstatus.NewEvent(componentstatus.StatusStopping),
componentstatus.NewEvent(componentstatus.StatusStopped),
},
},
startupErr: assert.AnError,
},
{
name: "late startup error",
edge: [2]*testNode{rStErr, eNoErr},
expectedStatuses: map[*componentstatus.InstanceID][]*componentstatus.Event{
instanceIDs[rStErr]: {
componentstatus.NewEvent(componentstatus.StatusStarting),
componentstatus.NewPermanentErrorEvent(assert.AnError),
componentstatus.NewEvent(componentstatus.StatusStopping),
componentstatus.NewEvent(componentstatus.StatusStopped),
},
instanceIDs[eNoErr]: {
componentstatus.NewEvent(componentstatus.StatusStarting),
componentstatus.NewEvent(componentstatus.StatusOK),
componentstatus.NewEvent(componentstatus.StatusStopping),
componentstatus.NewEvent(componentstatus.StatusStopped),
},
},
startupErr: assert.AnError,
},
{
name: "early shutdown error",
edge: [2]*testNode{rSdErr, eNoErr},
expectedStatuses: map[*componentstatus.InstanceID][]*componentstatus.Event{
instanceIDs[rSdErr]: {
componentstatus.NewEvent(componentstatus.StatusStarting),
componentstatus.NewEvent(componentstatus.StatusOK),
componentstatus.NewEvent(componentstatus.StatusStopping),
componentstatus.NewPermanentErrorEvent(assert.AnError),
},
instanceIDs[eNoErr]: {
componentstatus.NewEvent(componentstatus.StatusStarting),
componentstatus.NewEvent(componentstatus.StatusOK),
componentstatus.NewEvent(componentstatus.StatusStopping),
componentstatus.NewEvent(componentstatus.StatusStopped),
},
},
shutdownErr: assert.AnError,
},
{
name: "late shutdown error",
edge: [2]*testNode{rNoErr, eSdErr},
expectedStatuses: map[*componentstatus.InstanceID][]*componentstatus.Event{
instanceIDs[rNoErr]: {
componentstatus.NewEvent(componentstatus.StatusStarting),
componentstatus.NewEvent(componentstatus.StatusOK),
componentstatus.NewEvent(componentstatus.StatusStopping),
componentstatus.NewEvent(componentstatus.StatusStopped),
},
instanceIDs[eSdErr]: {
componentstatus.NewEvent(componentstatus.StatusStarting),
componentstatus.NewEvent(componentstatus.StatusOK),
componentstatus.NewEvent(componentstatus.StatusStopping),
componentstatus.NewPermanentErrorEvent(assert.AnError),
},
},
shutdownErr: assert.AnError,
},
} {
t.Run(tt.name, func(t *testing.T) {
pg := &Graph{componentGraph: simple.NewDirectedGraph()}
pg.telemetry = componenttest.NewNopTelemetrySettings()
actualStatuses := make(map[*componentstatus.InstanceID][]*componentstatus.Event)
rep := status.NewReporter(func(id *componentstatus.InstanceID, ev *componentstatus.Event) {
actualStatuses[id] = append(actualStatuses[id], ev)
}, func(error) {
})
e0, e1 := tt.edge[0], tt.edge[1]
pg.instanceIDs = map[int64]*componentstatus.InstanceID{
e0.ID(): instanceIDs[e0],
e1.ID(): instanceIDs[e1],
}
pg.componentGraph.SetEdge(simple.Edge{F: e0, T: e1})
require.ErrorIs(t, pg.StartAll(context.Background(), &Host{Reporter: rep}), tt.startupErr)
assert.Equal(t, tt.shutdownErr, pg.ShutdownAll(context.Background(), rep))
assertEqualStatuses(t, tt.expectedStatuses, actualStatuses)
})
}
}