Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions libbeat/otel/otelconsumer/otelconsumer.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@ const (
esDocumentIDAttribute = "elasticsearch.document_id"
)

func init() {
outputs.RegisterType("otelconsumer", makeOtelConsumer)
}

type otelConsumer struct {
observer outputs.Observer
logsConsumer consumer.Logs
Expand All @@ -61,7 +57,7 @@ type otelConsumer struct {
isReceiverTest bool // whether we are running in receivertest context
}

func makeOtelConsumer(_ outputs.IndexManager, beat beat.Info, observer outputs.Observer, cfg *config.C, beatPaths *paths.Path) (outputs.Group, error) {
func MakeOtelConsumer(beat beat.Info, observer outputs.Observer, cfg *config.C, beatPaths *paths.Path) (outputs.Group, error) {
ocConfig := defaultConfig()
if err := cfg.Unpack(&ocConfig); err != nil {
return outputs.Fail(err)
Expand Down
1 change: 0 additions & 1 deletion libbeat/publisher/includes/includes.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package includes

import (
// import queue types
_ "github.com/elastic/beats/v7/libbeat/otel/otelconsumer"
_ "github.com/elastic/beats/v7/libbeat/outputs/codec/format"
_ "github.com/elastic/beats/v7/libbeat/outputs/codec/json"
_ "github.com/elastic/beats/v7/libbeat/outputs/console"
Expand Down
141 changes: 141 additions & 0 deletions libbeat/publisher/pipeline/output_otel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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
//
// http://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 pipeline

import (
"context"
"fmt"

"github.com/elastic/beats/v7/libbeat/beat"
"github.com/elastic/beats/v7/libbeat/otel/otelconsumer"
"github.com/elastic/beats/v7/libbeat/outputs"
"github.com/elastic/beats/v7/libbeat/publisher"
"github.com/elastic/beats/v7/libbeat/publisher/queue"
"github.com/elastic/elastic-agent-libs/config"
"github.com/elastic/elastic-agent-libs/logp"
"github.com/elastic/elastic-agent-libs/monitoring"
"github.com/elastic/elastic-agent-libs/paths"
)

type otelOutputController struct {
beatInfo beat.Info
logger *logp.Logger
monitors Monitors
queue queue.Queue[publisher.Event]

// consumer is a helper goroutine that reads event batches from the queue
// and sends them to workerChan for an output worker to process.
consumer *eventConsumer

// Each worker is a goroutine that will read batches from workerChan and
// send them to the output.
workers []outputWorker
workerChan chan publisher.Batch
}

func newOTelOutputController(
beatInfo beat.Info,
paths *paths.Path,
monitors Monitors,
retryObserver retryObserver,
queueFactory queue.QueueFactory[publisher.Event],
) (*otelOutputController, error) {

// Queue metrics are reported under the pipeline namespace
var pipelineMetrics *monitoring.Registry
if monitors.Metrics != nil {
pipelineMetrics = monitors.Metrics.GetOrCreateRegistry("pipeline")
}
queueObserver := queue.NewQueueObserver(pipelineMetrics)

// Create the queue
queue, err := queueFactory(monitors.Logger, queueObserver, 0, nil)
if err != nil {
return nil, fmt.Errorf("queue creation failed: %w", err)
}

// Initialize output group
out, err := loadOutput(monitors, func(outStats outputs.Observer) (string, outputs.Group, error) {
out, err := otelconsumer.MakeOtelConsumer(beatInfo, outStats, config.NewConfig(), paths)
Comment thread
leehinman marked this conversation as resolved.
Outdated
return "otelconsumer", out, err
})
if err != nil {
return nil, err
}

// Create output workers
workerChan := make(chan publisher.Batch)
workers := make([]outputWorker, len(out.Clients))
logger := beatInfo.Logger.Named("otel_output_worker")
for i, client := range out.Clients {
workers[i] = makeClientWorker(workerChan, client, logger, monitors.Tracer)
}

// Create an event consumer pulling batches from the queue and sending
// them to the output worker channel.
consumer := newEventConsumer(monitors.Logger, retryObserver)
consumer.setTarget(
consumerTarget{
queue: queue,
ch: workerChan,
batchSize: out.BatchSize,
timeToLive: out.Retry + 1,
})

return &otelOutputController{
beatInfo: beatInfo,
logger: beatInfo.Logger.Named("otelOutputController"),
monitors: monitors,
queue: queue,
consumer: consumer,
workers: workers,
workerChan: workerChan,
}, nil
}

func (c *otelOutputController) waitClose(ctx context.Context, _ bool) error {
// First: signal the queue that we're shutting down, and allow it to drain
// and process ACKs until the given context terminates.
c.logger.Infof("Output shutdown started. Waiting for enqueued events to be published.")
c.queue.Close(false)
select {
case <-c.queue.Done():
c.logger.Infof("Continue shutdown: All enqueued events have been published.")
case <-ctx.Done():
c.logger.Infof("Continue shutdown: Time out waiting for events to be published.")
c.queue.Close(true)
<-c.queue.Done()
}

// We've drained the queue as much as we can, signal eventConsumer to
// close, and wait for it to finish. After consumer.close returns,
// there will be no more writes to c.workerChan, so it is safe to close.
c.consumer.close()
close(c.workerChan)

// Signal the output workers to close.
for _, out := range c.workers {
out.Close()
}

return nil
}

func (c *otelOutputController) queueProducer(config queue.ProducerConfig) queue.Producer[publisher.Event] {
return c.queue.Producer(config)
}
56 changes: 56 additions & 0 deletions libbeat/publisher/pipeline/output_otel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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
//
// http://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 pipeline

import (
"context"
"testing"

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

"github.com/elastic/beats/v7/libbeat/beat"
"github.com/elastic/beats/v7/libbeat/publisher"
"github.com/elastic/beats/v7/libbeat/publisher/queue/memqueue"
"github.com/elastic/elastic-agent-libs/logp/logptest"
"github.com/elastic/elastic-agent-libs/monitoring"
)

func TestOTelQueueMetrics(t *testing.T) {
// More thorough testing of queue metrics are in the queue package,
// here we just want to make sure that they appear under the right
// monitoring namespace.
reg := monitoring.NewRegistry()
logger := logptest.NewTestingLogger(t, "")
controller, err := newOTelOutputController(
beat.Info{Logger: logger},
nil,
Monitors{
Logger: logger,
Metrics: reg,
},
nilObserver,
memqueue.FactoryForSettings[publisher.Event](memqueue.Settings{Events: 1000}))
require.NoError(t, err, "creating OTel output controller should succeed")
defer controller.waitClose(context.Background(), true)
entry := reg.Get("pipeline.queue.max_events")
require.NotNil(t, entry, "pipeline.queue.max_events must exist")
value, ok := entry.(*monitoring.Uint)
require.True(t, ok, "pipeline.queue.max_events must be a *monitoring.Uint")
assert.Equal(t, uint64(1000), value.Get(), "pipeline.queue.max_events should match the events configuration key")
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ import (
"github.com/elastic/elastic-agent-libs/monitoring"
)

type outputController interface {
waitClose(ctx context.Context, force bool) error
queueProducer(config queue.ProducerConfig) queue.Producer[publisher.Event]
}

// processOutputController manages the pipelines output capabilities, like:
// - start
// - stop
Expand Down Expand Up @@ -98,7 +93,7 @@ func newProcessOutputController(
) (*processOutputController, error) {
controller := &processOutputController{
beat: beat,
logger: beat.Logger.Named("outputController"),
logger: beat.Logger.Named("processOutputController"),
monitors: monitors,
queueFactory: queueFactory,
workerChan: make(chan publisher.Batch),
Expand Down Expand Up @@ -195,8 +190,8 @@ func (c *processOutputController) Reload(
return nil
}

// Close the queue, waiting up to the specified timeout for pending events
// to complete.
// Close the queue and output, waiting for pending events until all are
// acknowledged or the provided context expires.
func (c *processOutputController) closeQueue(ctx context.Context, force bool) {
c.queueLock.Lock()
defer c.queueLock.Unlock()
Expand Down
58 changes: 51 additions & 7 deletions libbeat/publisher/pipeline/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
// When and how WaitClose is applied depends on WaitCloseMode.
WaitClose time.Duration

// This field has no effect when running as a Beats receiver.
WaitCloseMode WaitCloseMode

Processors processing.Supporter
Expand Down Expand Up @@ -118,6 +119,21 @@
WaitOnPipelineCloseThenForce
)

// outputController is the interface between the Pipeline and the output,
// which may be either the legacy Beats output pipeline (under the process
// runtime) or a bridge to the OTel Collector (when running as a Beats
// receiver under the otel runtime).
type outputController interface {
// queueProducer creates a queue producer with the given config, blocking
// until the queue is created if it does not yet exist.
queueProducer(config queue.ProducerConfig) queue.Producer[publisher.Event]

// Close the queue and output, waiting for pending events until all are
// acknowledged or the provided context expires.
// The force parameter has no effect when running as a Beats receiver.
waitClose(ctx context.Context, force bool) error
}

// OutputReloader interface, that can be queried from an active publisher pipeline.
// The output reloader can be used to change the active output.
type OutputReloader interface {
Expand All @@ -138,7 +154,7 @@
settings Settings,
) (*Pipeline, error) {
if monitors.Logger == nil {
monitors.Logger = logp.NewLogger("publish")

Check failure on line 157 in libbeat/publisher/pipeline/pipeline.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

use of `logp.NewLogger` forbidden because "accept a *logp.Logger as a parameter instead of creating one with logp.NewLogger" (forbidigo)

Check failure on line 157 in libbeat/publisher/pipeline/pipeline.go

View workflow job for this annotation

GitHub Actions / lint (windows-latest)

use of `logp.NewLogger` forbidden because "accept a *logp.Logger as a parameter instead of creating one with logp.NewLogger" (forbidigo)

Check failure on line 157 in libbeat/publisher/pipeline/pipeline.go

View workflow job for this annotation

GitHub Actions / lint (macos-latest)

use of `logp.NewLogger` forbidden because "accept a *logp.Logger as a parameter instead of creating one with logp.NewLogger" (forbidigo)
}

p := &Pipeline{
Expand All @@ -149,13 +165,6 @@
processors: settings.Processors,
paths: settings.Paths,
}
switch settings.WaitCloseMode {
case WaitOnPipelineClose, WaitOnPipelineCloseThenForce:
if settings.WaitClose > 0 {
p.waitCloseTimeout = settings.WaitClose
}
default:
}

p.forceCloseQueue = settings.WaitCloseMode == WaitOnPipelineCloseThenForce

Expand Down Expand Up @@ -185,6 +194,41 @@
return p, nil
}

func NewForReceiver(
beatInfo beat.Info,
monitors Monitors,
userQueueConfig conf.Namespace,
settings Settings,
) (*Pipeline, error) {
p := &Pipeline{
beatInfo: beatInfo,
monitors: monitors,
observer: newMetricsObserver(monitors.Metrics),
waitCloseTimeout: settings.WaitClose,
processors: settings.Processors,
paths: settings.Paths,
}

// Convert the raw queue config to a parsed Settings object that will
// be used during queue creation. This lets us fail immediately on startup
// if there's a configuration problem.
queueType := defaultQueueType
if b := userQueueConfig.Name(); b != "" {
queueType = b
}
queueFactory, err := queueFactoryForUserConfig(queueType, userQueueConfig.Config(), settings.Paths)
if err != nil {
return nil, err
}

p.outputController, err = newOTelOutputController(beatInfo, settings.Paths, monitors, p.observer, queueFactory)
if err != nil {
return nil, err
}

return p, nil
}

// Close stops the pipeline, outputs and queue.
// If WaitClose with WaitOnPipelineClose mode is configured, Close will block
// for a duration of WaitClose, if there are still active events in the pipeline.
Expand Down
5 changes: 1 addition & 4 deletions x-pack/libbeat/cmd/instance/beat.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
if err := instance.InitPaths(cfg); err != nil {
return nil, fmt.Errorf("error initializing paths: %w", err)
}
b.Paths = paths.Paths

Check failure on line 122 in x-pack/libbeat/cmd/instance/beat.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

use of `paths.Paths` forbidden because "use a per-beat *paths.Path instance instead of the global paths.Paths" (forbidigo)

Check failure on line 122 in x-pack/libbeat/cmd/instance/beat.go

View workflow job for this annotation

GitHub Actions / lint (windows-latest)

use of `paths.Paths` forbidden because "use a per-beat *paths.Path instance instead of the global paths.Paths" (forbidigo)

Check failure on line 122 in x-pack/libbeat/cmd/instance/beat.go

View workflow job for this annotation

GitHub Actions / lint (macos-latest)

use of `paths.Paths` forbidden because "use a per-beat *paths.Path instance instead of the global paths.Paths" (forbidigo)
}

// We have to initialize the keystore before any unpack or merging the cloud
Expand Down Expand Up @@ -269,20 +269,17 @@
Tracer: b.Instrumentation.Tracer(),
}

outputFactory := b.MakeOutputFactory(b.Config.Output)

pipelineSettings := pipeline.Settings{
Processors: b.GetProcessors(),
InputQueueSize: b.InputQueueSize,
WaitCloseMode: pipeline.WaitOnPipelineCloseThenForce,
WaitClose: receiverPublisherCloseTimeout,
Paths: b.Paths,
}
publisher, err := pipeline.LoadWithSettings(b.Info, monitors, b.Config.Pipeline, outputFactory, pipelineSettings)
publisher, err := pipeline.NewForReceiver(b.Info, monitors, b.Config.Pipeline.Queue, pipelineSettings)
if err != nil {
return nil, fmt.Errorf("error initializing publisher: %w", err)
}
b.Registry.MustRegisterOutput(b.MakeOutputReloader(publisher.OutputReloader()))
b.Publisher = publisher

return b, nil
Expand Down
Loading