Skip to content

Commit

Permalink
Add the bufferExporter (#5119)
Browse files Browse the repository at this point in the history
* Add the bufferExporter

* Fix TestExportSync

Reset default ErrorHandler

* Comment

* Clean up tests

* Remove context arg from EnqueueExport

* Join wrapped exporter error
  • Loading branch information
MrAlias authored Apr 2, 2024
1 parent c4dffbf commit 5449f08
Show file tree
Hide file tree
Showing 2 changed files with 419 additions and 3 deletions.
133 changes: 133 additions & 0 deletions sdk/log/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ package log // import "go.opentelemetry.io/otel/sdk/log"

import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
"time"

"go.opentelemetry.io/otel"
Expand Down Expand Up @@ -158,3 +162,132 @@ func (e exportData) respond(err error) {
}
}
}

// bufferExporter provides asynchronous and synchronous export functionality by
// buffering export requests.
type bufferExporter struct {
Exporter

input chan exportData
inputMu sync.Mutex

done chan struct{}
stopped atomic.Bool
}

// newBufferExporter returns a new bufferExporter that wraps exporter. The
// returned bufferExporter will buffer at most size number of export requests.
// If size is less than zero, zero will be used (i.e. only synchronous
// exporting will be supported).
func newBufferExporter(exporter Exporter, size int) *bufferExporter {
if size < 0 {
size = 0
}
input := make(chan exportData, size)
return &bufferExporter{
Exporter: exporter,

input: input,
done: exportSync(input, exporter),
}
}

var errStopped = errors.New("exporter stopped")

func (e *bufferExporter) enqueue(ctx context.Context, records []Record, rCh chan<- error) error {
data := exportData{ctx, records, rCh}

e.inputMu.Lock()
defer e.inputMu.Unlock()

// Check stopped before enqueueing now that e.inputMu is held. This
// prevents sends on a closed chan when Shutdown is called concurrently.
if e.stopped.Load() {
return errStopped
}

select {
case e.input <- data:
case <-ctx.Done():
return ctx.Err()
}
return nil
}

// EnqueueExport enqueues an export of records in the context of ctx to be
// performed asynchronously. This will return true if the exported is
// successfully enqueued, false otherwise.
func (e *bufferExporter) EnqueueExport(records []Record) bool {
if len(records) == 0 {
// Nothing to enqueue, do not waste input space.
return true
}
return e.enqueue(context.Background(), records, nil) == nil
}

// Export synchronously exports records in the context of ctx. This will not
// return until the export has been completed.
func (e *bufferExporter) Export(ctx context.Context, records []Record) error {
if len(records) == 0 {
return nil
}

resp := make(chan error, 1)
err := e.enqueue(ctx, records, resp)
if err != nil {
if errors.Is(err, errStopped) {
return nil
}
return fmt.Errorf("%w: dropping %d records", err, len(records))
}

select {
case err := <-resp:
return err
case <-ctx.Done():
return ctx.Err()
}
}

// ForceFlush flushes buffered exports. Any existing exports that is buffered
// is flushed before this returns.
func (e *bufferExporter) ForceFlush(ctx context.Context) error {
resp := make(chan error, 1)
err := e.enqueue(ctx, nil, resp)
if err != nil {
if errors.Is(err, errStopped) {
return nil
}
return err
}

select {
case <-resp:
case <-ctx.Done():
return ctx.Err()
}
return e.Exporter.ForceFlush(ctx)
}

// Shutdown shuts down e.
//
// Any buffered exports are flushed before this returns.
//
// All calls to EnqueueExport or Exporter will return nil without any export
// after this is called.
func (e *bufferExporter) Shutdown(ctx context.Context) error {
if e.stopped.Swap(true) {
return nil
}
e.inputMu.Lock()
defer e.inputMu.Unlock()

// No more sends will be made.
close(e.input)
select {
case <-e.done:
case <-ctx.Done():
return errors.Join(ctx.Err(), e.Exporter.Shutdown(ctx))
}
return e.Exporter.Shutdown(ctx)
}
Loading

0 comments on commit 5449f08

Please sign in to comment.