Skip to content

Commit

Permalink
feat(adapters): slog
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasmalkmus committed Jul 18, 2023
1 parent e43b36d commit 3b42cb7
Show file tree
Hide file tree
Showing 12 changed files with 507 additions and 5 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/test_examples.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ jobs:
- oteltraces
- query
- querylegacy
- slog
- zap
include:
- example: apex
Expand Down Expand Up @@ -71,6 +72,9 @@ jobs:
echo '[{"mood":"hyped","msg":"This is awesome!"}]' >> logs.json
axiom ingest $AXIOM_DATASET -f=logs.json -f=logs.json -f=logs.json
sleep 5
- example: slog
verify: |
axiom dataset info $AXIOM_DATASET -f=json | jq -e 'any( .numEvents ; . == 3 )'
- example: zap
verify: |
axiom dataset info $AXIOM_DATASET -f=json | jq -e 'any( .numEvents ; . == 3 )'
Expand Down
15 changes: 13 additions & 2 deletions adapters/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
# Adapters

Adapters integrate Axiom Go into well known Go logging libraries. We currently
support these adapters right out of the box:
Adapters integrate Axiom Go into well known Go logging libraries.

💡 _Go **1.21** will feature the `slog` package, a structured logging package in
the Go standard library. You can try it out know by importing
`golang.org/x/exp/slog` and we already provide [an adapter](slog)._

We currently support a bunch of adapters right out of the box.

## Standard Library

* [Slog](https://pkg.go.dev/golang.org/x/exp/slog): `import "github.com/axiomhq/axiom-go/adapters/slog"`

## Third Party Packages

* [Apex](https://github.com/apex/log): `import "github.com/axiomhq/axiom-go/adapters/apex"`
* [Logrus](https://github.com/sirupsen/logrus): `import "github.com/axiomhq/axiom-go/adapters/logrus"`
Expand Down
50 changes: 50 additions & 0 deletions adapters/slog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Axiom Go Adapter for slog

Adapter to ship logs generated by
[slog](https://pkg.go.dev/golang.org/x/exp/slog) 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 "slog" package.
import adapter "github.com/axiomhq/axiom-go/adapters/slog"
```

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

```go
handler, err := adapter.New(
SetDataset("AXIOM_DATASET"),
)
```

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/slog#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/slog#SetClientOptions).

```go
import adapter "github.com/axiomhq/axiom-go/axiom"

// ...

handler, err := adapter.New(
SetClientOptions(
axiom.SetPersonalTokenConfig("AXIOM_TOKEN", "AXIOM_ORG_ID"),
),
)
```

### ❗ Important ❗

The adapter uses a buffer to batch events before sending them to Axiom. This
buffer must be flushed explicitly by calling [Close](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/slog#Handler.Close).
Checkout the [example](../../examples/slog/main.go).
3 changes: 3 additions & 0 deletions adapters/slog/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package slog provides an adapter for the standard libraries structured
// logging package.
package slog
219 changes: 219 additions & 0 deletions adapters/slog/slog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
package slog

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

"golang.org/x/exp/slog"

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

var _ slog.Handler = (*Handler)(nil)

var logger = log.New(os.Stderr, "[AXIOM|SLOG]", 0)

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

// An Option modifies the behaviour of the Axiom handler.
type Option func(*Handler) error

// SetClient specifies the Axiom client to use for ingesting the logs.
func SetClient(client *axiom.Client) Option {
return func(h *Handler) error {
h.client = client
return nil
}
}

// SetClientOptions specifies the Axiom client options to pass to
// [axiom.NewClient] which is only called if no [axiom.Client] was specified by
// the [SetClient] option.
func SetClientOptions(options ...axiom.Option) Option {
return func(h *Handler) error {
h.clientOptions = options
return nil
}
}

// SetDataset specifies the dataset to ingest the logs into. Can also be
// specified using the "AXIOM_DATASET" environment variable.
func SetDataset(datasetName string) Option {
return func(h *Handler) error {
h.datasetName = datasetName
return nil
}
}

// SetHandlerOptions specifies the handler options to use.
func SetHandlerOptions(opts *slog.HandlerOptions) Option {
return func(h *Handler) error {
h.handlerOptions = opts
return nil
}
}

// SetIngestOptions specifies the ingestion options to use for ingesting the
// logs.
func SetIngestOptions(opts ...ingest.Option) Option {
return func(h *Handler) error {
h.ingestOptions = opts
return nil
}
}

// Handler implements a [slog.Handler] used for shipping logs to Axiom.
type Handler struct {
client *axiom.Client
datasetName string

clientOptions []axiom.Option
handlerOptions *slog.HandlerOptions
ingestOptions []ingest.Option

jsonHandler *slog.JSONHandler
buf bytes.Buffer
bufMtx sync.Mutex
closeCh chan struct{}
closeOnce sync.Once
}

// New creates a new handler 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 handler needs to be closed properly to make sure all logs are sent by
// calling [Handler.Close].
func New(options ...Option) (*Handler, error) {
handler := &Handler{
closeCh: make(chan struct{}),
}

// Apply supplied options.
for _, option := range options {
if err := option(handler); err != nil {
return nil, err
}
}

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

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

// Create a JSON handler.
handler.jsonHandler = slog.NewJSONHandler(&handler.buf, handler.handlerOptions)

// Run background ingest.
go func() {
if err := handler.sync(context.Background()); err != nil {
logger.Printf("failed to ingest events: %s\n", err)
}
}()

return handler, nil
}

// Close the handler and make sure all events are flushed. Closing the handler
// renders it unusable for further use.
func (h *Handler) Close() {
h.closeOnce.Do(func() {
close(h.closeCh)
})
}

// Enabled implements [slog.Handler].
func (h *Handler) Enabled(ctx context.Context, level slog.Level) bool {
return h.jsonHandler.Enabled(ctx, level)
}

// Handle implements [slog.Handler].
func (h *Handler) Handle(ctx context.Context, r slog.Record) error {
return h.jsonHandler.Handle(ctx, r)
}

// WithAttrs implements [slog.Handler].
func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
return h.jsonHandler.WithAttrs(attrs)
}

// WithGroup implements [slog.Handler].
func (h *Handler) WithGroup(name string) slog.Handler {
return h.jsonHandler.WithGroup(name)
}

func (h *Handler) sync(ctx context.Context) error {
// Flush on a per second basis.
const flushInterval = time.Second
t := time.NewTicker(flushInterval)
defer t.Stop()

flush := func() error {
h.bufMtx.Lock()
defer h.bufMtx.Unlock()

if h.buf.Len() == 0 {
return nil
}

r, err := axiom.ZstdEncoder()(&h.buf)
if err != nil {
return err
}

res, err := h.client.Ingest(ctx, h.datasetName, r, axiom.NDJSON, axiom.Zstd, h.ingestOptions...)
if err != nil {
return fmt.Errorf("failed to ingest events: %w", err)
} else if res.Failed > 0 {
// Best effort on notifying the user about the ingest failure.
logger.Printf("event at %s failed to ingest: %s\n",
res.Failures[0].Timestamp, res.Failures[0].Error)
}
t.Reset(flushInterval) // Reset the ticker.
h.buf.Reset() // Clear the buffer.

return nil
}

for {
select {
case <-ctx.Done():
return ctx.Err()
case <-h.closeCh:
return flush()
case <-t.C:
if err := flush(); err != nil {
return err
}
}
}
}
25 changes: 25 additions & 0 deletions adapters/slog/slog_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package slog_test

import (
"log"

"golang.org/x/exp/slog"

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

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

handler, err := adapter.New()
if err != nil {
log.Fatal(err.Error())
}
defer handler.Close()

logger := slog.New(handler)

logger.Info("This is awesome!", "mood", "hyped")
logger.With("mood", "worried").Warn("This is no that awesome...")
logger.Error("This is rather bad.", slog.String("mood", "depressed"))
}
33 changes: 33 additions & 0 deletions adapters/slog/slog_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//go:build integration

package slog_test

import (
"context"
"testing"

"github.com/stretchr/testify/require"
"golang.org/x/exp/slog"

adapter "github.com/axiomhq/axiom-go/adapters/slog"
"github.com/axiomhq/axiom-go/axiom"
"github.com/axiomhq/axiom-go/internal/test/adapters"
)

func Test(t *testing.T) {
adapters.IntegrationTest(t, "slog", func(_ context.Context, dataset string, client *axiom.Client) {
handler, err := adapter.New(
adapter.SetClient(client),
adapter.SetDataset(dataset),
)
require.NoError(t, err)

defer handler.Close()

logger := slog.New(handler)

logger.Info("This is awesome!", slog.String("mood", "hyped"))
logger.Warn("This is no that awesome...", slog.String("mood", "worried"))
logger.Error("This is rather bad.", slog.String("mood", "depressed"))
})
}
Loading

0 comments on commit 3b42cb7

Please sign in to comment.