Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: axiomhq/axiom-go
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: e78ec17e550751d956afd7897b7fc0c7e7ad9bbb
Choose a base ref
..
head repository: axiomhq/axiom-go
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 5ae595fc020d270e075179d4168eac2d3f2803ba
Choose a head ref
4 changes: 4 additions & 0 deletions .github/workflows/test_examples.yaml
Original file line number Diff line number Diff line change
@@ -49,6 +49,7 @@ jobs:
# - slog
- slogx
- zap
- zerolog
include:
- example: apex
verify: |
@@ -92,6 +93,9 @@ jobs:
- example: zap
verify: |
axiom dataset info $AXIOM_DATASET -f=json | jq -e 'any( .numEvents ; . == 3 )'
- example: zerolog
verify: |
axiom dataset info $AXIOM_DATASET -f=json | jq -e 'any( .numEvents ; . == 3 )'
env:
AXIOM_URL: ${{ secrets.TESTING_STAGING_API_URL }}
AXIOM_TOKEN: ${{ secrets.TESTING_STAGING_TOKEN }}
53 changes: 53 additions & 0 deletions adapters/zerolog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Axiom Go Adapter for rs/zerolog

Adapter to ship logs generated by [rs/zerolog](https://github.com/rs/zerolog)
to Axiom.

## Quickstart

Follow the [Axiom Go Quickstart](https://github.com/axiomhq/axiom-go#quickstart)
to install the Axiom Go package and configure your environment.

Import the package:

```go
// Imported as "adapter" to not conflict with the "rs/zerolog" package.
import adapter "github.com/axiomhq/axiom-go/adapters/zerolog"
```

You can also configure the adapter using [options](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/zerolog#Option)
passed to the [New](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/zerolog#New)
function:

```go
writer, err := adapter.New(
WithDatasetName("logs"),
)
l.Logger = zerolog.New(io.MultiWriter(writer, os.Stderr)).With().Str("env", os.Getenv("ENV")).Timestamp().Logger()
```

To configure the underlying client manually either pass in a client that was
created according to the [Axiom Go Quickstart](https://github.com/axiomhq/axiom-go#quickstart)
using [SetClient](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/zerolog#SetClient)
or pass [client options](https://pkg.go.dev/github.com/axiomhq/axiom-go/axiom#Option)
to the adapter using [SetClientOptions](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/zerolog#SetClientOptions).

```go
import (
"github.com/axiomhq/axiom-go/axiom"
adapter "github.com/axiomhq/axiom-go/adapters/zerolog"
)

// ...
writer, err := adapter.New()
if err != nil {
log.Fatal(err)
}
l.Logger = zerolog.New(io.MultiWriter(writer, os.Stderr)).With().Str("env", os.Getenv("ENV")).Timestamp().Logger()
```

> [!IMPORTANT]
> The adapter uses a buffer to batch events before sending them to Axiom. This
> buffer can be flushed explicitly by calling [Close](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/zerolog#Writer.Close), and is necessary when terminating the program so as not to lose logs.
> If With().Timestamp() isn't passed, then the timestamp will be the batched timestamp on the server
> Checkout out the [example](../../examples/zerolog/main.go).
3 changes: 3 additions & 0 deletions adapters/zerolog/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package zerolog provides an adapter for the popular github.com/rs/zerolog
// logging library.
package zerolog
254 changes: 254 additions & 0 deletions adapters/zerolog/zerolog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
package zerolog

import (
"bytes"
"context"
"errors"
"io"
"log"
"os"
"sync"
"time"

"github.com/buger/jsonparser"
"github.com/rs/zerolog"

"github.com/axiomhq/axiom-go/axiom/ingest"

"github.com/axiomhq/axiom-go/axiom"
)

var (
_ = io.Writer(new(Writer))

// ErrMissingDataset is raised when a dataset name is not provided. Set it
// manually using the [SetDataset] option or export "AXIOM_DATASET".
ErrMissingDataset = errors.New("missing dataset name")

logger = log.New(os.Stderr, "[AXIOM|ZEROLOG]", 0)
loggerName = []byte(`"zerolog"`)
)

const (
defaultBatchSize = 1000
flushInterval = time.Second
)

// Writer is a axiom events writer with std io.Writer interface.
type Writer struct {
client *axiom.Client
dataset string

clientOptions []axiom.Option
ingestOptions []ingest.Option
levels map[zerolog.Level]struct{}

byteCh chan []byte
closeOnce sync.Once
closeCh chan struct{}
}

// Write must not modify the slice data, even temporarily.
func (w *Writer) Write(data []byte) (int, error) {
select {
case <-w.closeCh:
default:
b := make([]byte, len(data))
copy(b, data)
w.byteCh <- b
}

return len(data), nil
}

func (w *Writer) Close() {
w.closeOnce.Do(func() {
close(w.byteCh)
<-w.closeCh
})
}

// Option configures axiom events writer.
type Option func(*Writer)

// SetClient configures a custom axiom client.
func SetClient(client *axiom.Client) Option {
return Option(func(cfg *Writer) {
cfg.client = client
})
}

// SetLevels configures zerolog levels that have to be sent to Axiom.
func SetLevels(levels []zerolog.Level) Option {
return Option(func(cfg *Writer) {
for _, level := range levels {
cfg.levels[level] = struct{}{}
}
})
}

// SetDataset configures the axiom dataset name.
func SetDataset(dataset string) Option {
return Option(func(cfg *Writer) {
cfg.dataset = dataset
})
}

// SetClientOptions configures the axiom client options.
func SetClientOptions(clientOptions []axiom.Option) Option {
return Option(func(cfg *Writer) {
cfg.clientOptions = clientOptions
})
}

// SetIngestOptions configures the axiom ingest options.
func SetIngestOptions(ingestOptions []ingest.Option) Option {
return Option(func(cfg *Writer) {
cfg.ingestOptions = ingestOptions
})
}

// New creates a new Writer that ingests logs into Axiom. It automatically takes
// its configuration from the environment. To connect, export the following
// environment variables:
//
// - AXIOM_TOKEN
// - AXIOM_ORG_ID (only when using a personal token)
// - AXIOM_DATASET
//
// The configuration can be set manually using options which are prefixed with
// "Set".
//
// An API token with "ingest" permission is sufficient enough.
//
// A Writer needs to be closed properly to make sure all logs are sent by calling
// [Writer.Close].
func New(opts ...Option) (*Writer, error) {
w := &Writer{
levels: make(map[zerolog.Level]struct{}),
ingestOptions: []ingest.Option{ingest.SetTimestampField(zerolog.TimestampFieldName), ingest.SetTimestampFormat(zerolog.TimeFieldFormat)},
clientOptions: []axiom.Option{},
byteCh: make(chan []byte, defaultBatchSize),
closeCh: make(chan struct{}),
}

// func supplied options.
for _, option := range opts {
if option == nil {
continue
}
option(w)
}

if len(w.levels) == 0 {
for _, level := range []zerolog.Level{zerolog.InfoLevel, zerolog.WarnLevel, zerolog.ErrorLevel, zerolog.FatalLevel, zerolog.PanicLevel} {
w.levels[level] = struct{}{}
}
}

// Create client, if not set.
if w.client == nil {
var err error
if w.client, err = axiom.NewClient(w.clientOptions...); err != nil {
return nil, err
}
}

// When the dataset name is not set, use "AXIOM_DATASET".
if w.dataset == "" {
w.dataset = os.Getenv("AXIOM_DATASET")
if w.dataset == "" {
return nil, ErrMissingDataset
}
}

go w.runBackgroundJob()
return w, nil
}

func (w *Writer) runBackgroundJob() {
var (
counter = 0
buffer = &bytes.Buffer{}
t = time.NewTicker(flushInterval)
encoder = axiom.ZstdEncoder()
)
defer t.Stop()

flush := func() error {
defer func() {
counter = 0
t.Reset(flushInterval)
buffer.Reset()
}()

if buffer.Len() == 0 {
return nil
}

r, err := encoder(buffer)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()

res, err := w.client.Ingest(ctx, w.dataset, r, axiom.NDJSON, axiom.Zstd, w.ingestOptions...)
if err != nil {
return err
}
if res.Failed > 0 {
logger.Printf("event(s) [%v] at %s failed to ingest: %s\n", res.Failed, res.Failures[0].Timestamp, res.Failures[0].Error)
}
return nil
}

defer close(w.closeCh)

for {
select {
case data, ok := <-w.byteCh:
if !ok {
if err := flush(); err != nil {
logger.Printf("failed to ingest events: %s\n", err)
}
return
}
if len(data) == 0 {
continue
}

counter++

lvlStr, err := jsonparser.GetUnsafeString(data, zerolog.LevelFieldName)
if err != nil {
logger.Printf("failed to retrieve level field name from data: %s\n", err)
continue
}

lvl, err := zerolog.ParseLevel(lvlStr)
if err != nil {
logger.Printf("failed to parse level: %s\n", err)
continue
}

if _, enabled := w.levels[lvl]; !enabled {
continue
}

data, _ = jsonparser.Set(data, loggerName, "logger")

buffer.Write(data)

if counter >= defaultBatchSize {
if err := flush(); err != nil {
logger.Printf("failed to ingest events: %s\n", err)
}
}
case <-t.C:
if err := flush(); err != nil {
logger.Printf("failed to ingest events: %s\n", err)
}
}
}
}
27 changes: 27 additions & 0 deletions adapters/zerolog/zerolog_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package zerolog_test

import (
"io"
"log"
"os"

"github.com/rs/zerolog"
l "github.com/rs/zerolog/log"

adapter "github.com/axiomhq/axiom-go/adapters/zerolog"
)

func Example() {
// Export "AXIOM_DATASET" in addition to the required environment variables.

writer, err := adapter.New()
if err != nil {
log.Fatal(err)
}

l.Logger = zerolog.New(io.MultiWriter(writer, os.Stderr)).With().Timestamp().Logger()

l.Logger.Info().Str("mood", "hyped").Msg("This is awesome!")
l.Logger.Warn().Str("mood", "worried").Msg("This is no that awesome...")
l.Logger.Error().Str("mood", "depressed").Msg("This is rather bad.")
}
Loading