Skip to content

Commit c073ffb

Browse files
author
Fae Charlton
authored
Add otel-specific outputController to libbeat pipeline (#50075)
Add an OTel-specific implementation of `pipeline.outputController`, and create it via a separate pipeline initialization API for use with Beats receivers. Most of the core logic is unchanged in this pass (in particular this should cause no change in behavior), but the code paths are shorter/simpler than the ones for the process runtime since there are many complications and corner cases that aren't relevant when running as a Beats receiver (in particular Beats receivers do not need to dynamically reload their output configuration).
1 parent e9ec484 commit c073ffb

9 files changed

Lines changed: 250 additions & 63 deletions

File tree

libbeat/otel/otelconsumer/config.go

Lines changed: 0 additions & 30 deletions
This file was deleted.

libbeat/otel/otelconsumer/otelconsumer.go

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,8 @@ import (
3131
"github.com/elastic/beats/v7/libbeat/otel/otelmap"
3232
"github.com/elastic/beats/v7/libbeat/outputs"
3333
"github.com/elastic/beats/v7/libbeat/publisher"
34-
"github.com/elastic/elastic-agent-libs/config"
3534
"github.com/elastic/elastic-agent-libs/logp"
3635
"github.com/elastic/elastic-agent-libs/mapstr"
37-
"github.com/elastic/elastic-agent-libs/paths"
3836

3937
"go.opentelemetry.io/collector/consumer"
4038
"go.opentelemetry.io/collector/consumer/consumererror"
@@ -49,10 +47,6 @@ const (
4947
esDocumentIDAttribute = "elasticsearch.document_id"
5048
)
5149

52-
func init() {
53-
outputs.RegisterType("otelconsumer", makeOtelConsumer)
54-
}
55-
5650
type otelConsumer struct {
5751
observer outputs.Observer
5852
logsConsumer consumer.Logs
@@ -61,12 +55,7 @@ type otelConsumer struct {
6155
isReceiverTest bool // whether we are running in receivertest context
6256
}
6357

64-
func makeOtelConsumer(_ outputs.IndexManager, beat beat.Info, observer outputs.Observer, cfg *config.C, beatPaths *paths.Path) (outputs.Group, error) {
65-
ocConfig := defaultConfig()
66-
if err := cfg.Unpack(&ocConfig); err != nil {
67-
return outputs.Fail(err)
68-
}
69-
58+
func MakeOtelConsumer(beat beat.Info, observer outputs.Observer) (outputs.Group, error) {
7059
isReceiverTest := os.Getenv("OTELCONSUMER_RECEIVERTEST") == "1"
7160

7261
// Default to runtime.NumCPU() workers
@@ -81,7 +70,7 @@ func makeOtelConsumer(_ outputs.IndexManager, beat beat.Info, observer outputs.O
8170
})
8271
}
8372

84-
return outputs.Success(ocConfig.Queue, -1, 0, nil, beat.Logger, beatPaths, clients...)
73+
return outputs.Group{Clients: clients}, nil
8574
}
8675

8776
// Close is a noop for otelconsumer

libbeat/publisher/includes/includes.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package includes
1919

2020
import (
2121
// import queue types
22-
_ "github.com/elastic/beats/v7/libbeat/otel/otelconsumer"
2322
_ "github.com/elastic/beats/v7/libbeat/outputs/codec/format"
2423
_ "github.com/elastic/beats/v7/libbeat/outputs/codec/json"
2524
_ "github.com/elastic/beats/v7/libbeat/outputs/console"
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Licensed to Elasticsearch B.V. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. Elasticsearch B.V. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package pipeline
19+
20+
import (
21+
"context"
22+
"fmt"
23+
24+
"github.com/elastic/beats/v7/libbeat/beat"
25+
"github.com/elastic/beats/v7/libbeat/otel/otelconsumer"
26+
"github.com/elastic/beats/v7/libbeat/outputs"
27+
"github.com/elastic/beats/v7/libbeat/publisher"
28+
"github.com/elastic/beats/v7/libbeat/publisher/queue"
29+
"github.com/elastic/elastic-agent-libs/logp"
30+
"github.com/elastic/elastic-agent-libs/monitoring"
31+
)
32+
33+
type otelOutputController struct {
34+
beatInfo beat.Info
35+
logger *logp.Logger
36+
monitors Monitors
37+
queue queue.Queue[publisher.Event]
38+
39+
// consumer is a helper goroutine that reads event batches from the queue
40+
// and sends them to workerChan for an output worker to process.
41+
consumer *eventConsumer
42+
43+
// Each worker is a goroutine that will read batches from workerChan and
44+
// send them to the output.
45+
workers []outputWorker
46+
workerChan chan publisher.Batch
47+
}
48+
49+
func newOTelOutputController(
50+
beatInfo beat.Info,
51+
monitors Monitors,
52+
retryObserver retryObserver,
53+
queueFactory queue.QueueFactory[publisher.Event],
54+
) (*otelOutputController, error) {
55+
56+
// Queue metrics are reported under the pipeline namespace
57+
var pipelineMetrics *monitoring.Registry
58+
if monitors.Metrics != nil {
59+
pipelineMetrics = monitors.Metrics.GetOrCreateRegistry("pipeline")
60+
}
61+
queueObserver := queue.NewQueueObserver(pipelineMetrics)
62+
63+
// Create the queue
64+
queue, err := queueFactory(monitors.Logger, queueObserver, 0, nil)
65+
if err != nil {
66+
return nil, fmt.Errorf("queue creation failed: %w", err)
67+
}
68+
69+
// Initialize output group
70+
out, err := loadOutput(monitors, func(outStats outputs.Observer) (string, outputs.Group, error) {
71+
out, err := otelconsumer.MakeOtelConsumer(beatInfo, outStats)
72+
return "otelconsumer", out, err
73+
})
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
// Create output workers
79+
workerChan := make(chan publisher.Batch)
80+
workers := make([]outputWorker, len(out.Clients))
81+
logger := beatInfo.Logger.Named("otel_output_worker")
82+
for i, client := range out.Clients {
83+
workers[i] = makeClientWorker(workerChan, client, logger, monitors.Tracer)
84+
}
85+
86+
// Create an event consumer pulling batches from the queue and sending
87+
// them to the output worker channel.
88+
consumer := newEventConsumer(monitors.Logger, retryObserver)
89+
consumer.setTarget(
90+
consumerTarget{
91+
queue: queue,
92+
ch: workerChan,
93+
batchSize: out.BatchSize,
94+
timeToLive: out.Retry + 1,
95+
})
96+
97+
return &otelOutputController{
98+
beatInfo: beatInfo,
99+
logger: beatInfo.Logger.Named("otelOutputController"),
100+
monitors: monitors,
101+
queue: queue,
102+
consumer: consumer,
103+
workers: workers,
104+
workerChan: workerChan,
105+
}, nil
106+
}
107+
108+
func (c *otelOutputController) waitClose(ctx context.Context, _ bool) error {
109+
// First: signal the queue that we're shutting down, and allow it to drain
110+
// and process ACKs until the given context terminates.
111+
c.logger.Infof("Output shutdown started. Waiting for enqueued events to be published.")
112+
c.queue.Close(false)
113+
select {
114+
case <-c.queue.Done():
115+
c.logger.Infof("Continue shutdown: All enqueued events have been published.")
116+
case <-ctx.Done():
117+
c.logger.Infof("Continue shutdown: Time out waiting for events to be published.")
118+
c.queue.Close(true)
119+
<-c.queue.Done()
120+
}
121+
122+
// We've drained the queue as much as we can, signal eventConsumer to
123+
// close, and wait for it to finish. After consumer.close returns,
124+
// there will be no more writes to c.workerChan, so it is safe to close.
125+
c.consumer.close()
126+
close(c.workerChan)
127+
128+
// Signal the output workers to close.
129+
for _, out := range c.workers {
130+
out.Close()
131+
}
132+
133+
return nil
134+
}
135+
136+
func (c *otelOutputController) queueProducer(config queue.ProducerConfig) queue.Producer[publisher.Event] {
137+
return c.queue.Producer(config)
138+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Licensed to Elasticsearch B.V. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. Elasticsearch B.V. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package pipeline
19+
20+
import (
21+
"context"
22+
"testing"
23+
24+
"github.com/stretchr/testify/assert"
25+
"github.com/stretchr/testify/require"
26+
27+
"github.com/elastic/beats/v7/libbeat/beat"
28+
"github.com/elastic/beats/v7/libbeat/publisher"
29+
"github.com/elastic/beats/v7/libbeat/publisher/queue/memqueue"
30+
"github.com/elastic/elastic-agent-libs/logp/logptest"
31+
"github.com/elastic/elastic-agent-libs/monitoring"
32+
)
33+
34+
func TestOTelQueueMetrics(t *testing.T) {
35+
// More thorough testing of queue metrics are in the queue package,
36+
// here we just want to make sure that they appear under the right
37+
// monitoring namespace.
38+
reg := monitoring.NewRegistry()
39+
logger := logptest.NewTestingLogger(t, "")
40+
controller, err := newOTelOutputController(
41+
beat.Info{Logger: logger},
42+
Monitors{
43+
Logger: logger,
44+
Metrics: reg,
45+
},
46+
nilObserver,
47+
memqueue.FactoryForSettings[publisher.Event](memqueue.Settings{Events: 1000}))
48+
require.NoError(t, err, "creating OTel output controller should succeed")
49+
defer controller.waitClose(context.Background(), true)
50+
entry := reg.Get("pipeline.queue.max_events")
51+
require.NotNil(t, entry, "pipeline.queue.max_events must exist")
52+
value, ok := entry.(*monitoring.Uint)
53+
require.True(t, ok, "pipeline.queue.max_events must be a *monitoring.Uint")
54+
assert.Equal(t, uint64(1000), value.Get(), "pipeline.queue.max_events should match the events configuration key")
55+
}

libbeat/publisher/pipeline/controller.go renamed to libbeat/publisher/pipeline/output_process.go

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,6 @@ import (
3232
"github.com/elastic/elastic-agent-libs/monitoring"
3333
)
3434

35-
type outputController interface {
36-
waitClose(ctx context.Context, force bool) error
37-
queueProducer(config queue.ProducerConfig) queue.Producer[publisher.Event]
38-
}
39-
4035
// processOutputController manages the pipelines output capabilities, like:
4136
// - start
4237
// - stop
@@ -98,7 +93,7 @@ func newProcessOutputController(
9893
) (*processOutputController, error) {
9994
controller := &processOutputController{
10095
beat: beat,
101-
logger: beat.Logger.Named("outputController"),
96+
logger: beat.Logger.Named("processOutputController"),
10297
monitors: monitors,
10398
queueFactory: queueFactory,
10499
workerChan: make(chan publisher.Batch),
@@ -195,8 +190,8 @@ func (c *processOutputController) Reload(
195190
return nil
196191
}
197192

198-
// Close the queue, waiting up to the specified timeout for pending events
199-
// to complete.
193+
// Close the queue and output, waiting for pending events until all are
194+
// acknowledged or the provided context expires.
200195
func (c *processOutputController) closeQueue(ctx context.Context, force bool) {
201196
c.queueLock.Lock()
202197
defer c.queueLock.Unlock()
File renamed without changes.

libbeat/publisher/pipeline/pipeline.go

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ type Settings struct {
8888
// When and how WaitClose is applied depends on WaitCloseMode.
8989
WaitClose time.Duration
9090

91+
// This field has no effect when running as a Beats receiver.
9192
WaitCloseMode WaitCloseMode
9293

9394
Processors processing.Supporter
@@ -118,6 +119,21 @@ const (
118119
WaitOnPipelineCloseThenForce
119120
)
120121

122+
// outputController is the interface between the Pipeline and the output,
123+
// which may be either the legacy Beats output pipeline (under the process
124+
// runtime) or a bridge to the OTel Collector (when running as a Beats
125+
// receiver under the otel runtime).
126+
type outputController interface {
127+
// queueProducer creates a queue producer with the given config, blocking
128+
// until the queue is created if it does not yet exist.
129+
queueProducer(config queue.ProducerConfig) queue.Producer[publisher.Event]
130+
131+
// Close the queue and output, waiting for pending events until all are
132+
// acknowledged or the provided context expires.
133+
// The force parameter has no effect when running as a Beats receiver.
134+
waitClose(ctx context.Context, force bool) error
135+
}
136+
121137
// OutputReloader interface, that can be queried from an active publisher pipeline.
122138
// The output reloader can be used to change the active output.
123139
type OutputReloader interface {
@@ -149,13 +165,6 @@ func New(
149165
processors: settings.Processors,
150166
paths: settings.Paths,
151167
}
152-
switch settings.WaitCloseMode {
153-
case WaitOnPipelineClose, WaitOnPipelineCloseThenForce:
154-
if settings.WaitClose > 0 {
155-
p.waitCloseTimeout = settings.WaitClose
156-
}
157-
default:
158-
}
159168

160169
p.forceCloseQueue = settings.WaitCloseMode == WaitOnPipelineCloseThenForce
161170

@@ -185,6 +194,41 @@ func New(
185194
return p, nil
186195
}
187196

197+
func NewForReceiver(
198+
beatInfo beat.Info,
199+
monitors Monitors,
200+
userQueueConfig conf.Namespace,
201+
settings Settings,
202+
) (*Pipeline, error) {
203+
p := &Pipeline{
204+
beatInfo: beatInfo,
205+
monitors: monitors,
206+
observer: newMetricsObserver(monitors.Metrics),
207+
waitCloseTimeout: settings.WaitClose,
208+
processors: settings.Processors,
209+
paths: settings.Paths,
210+
}
211+
212+
// Convert the raw queue config to a parsed Settings object that will
213+
// be used during queue creation. This lets us fail immediately on startup
214+
// if there's a configuration problem.
215+
queueType := defaultQueueType
216+
if b := userQueueConfig.Name(); b != "" {
217+
queueType = b
218+
}
219+
queueFactory, err := queueFactoryForUserConfig(queueType, userQueueConfig.Config(), settings.Paths)
220+
if err != nil {
221+
return nil, err
222+
}
223+
224+
p.outputController, err = newOTelOutputController(beatInfo, monitors, p.observer, queueFactory)
225+
if err != nil {
226+
return nil, err
227+
}
228+
229+
return p, nil
230+
}
231+
188232
// Close stops the pipeline, outputs and queue.
189233
// If WaitClose with WaitOnPipelineClose mode is configured, Close will block
190234
// for a duration of WaitClose, if there are still active events in the pipeline.

0 commit comments

Comments
 (0)