-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e43b36d
commit 3b42cb7
Showing
12 changed files
with
507 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")) | ||
}) | ||
} |
Oops, something went wrong.