Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions .changeset/tiny-wren.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"transports/betterstack": major
---

Initial release.
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ gtag('config', '${gaMeasurementId}');`,
text: 'Cloud',
items: [
{ text: 'Axiom', link: '/transports/axiom' },
{ text: 'Better Stack', link: '/transports/betterstack' },
{ text: 'Datadog', link: '/transports/datadog' },
{ text: 'Google Cloud Logging', link: '/transports/gcplogging' },
{ text: 'New Relic', link: '/transports/newrelic' },
Expand Down
18 changes: 10 additions & 8 deletions docs/src/public/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,9 @@ go get go.loglayer.dev/transports/cli/v2
go get go.loglayer.dev/transports/testing/v2
go get go.loglayer.dev/transports/blank/v2

# Wrappers around third-party loggers
go get go.loglayer.dev/transports/zerolog/v2
go get go.loglayer.dev/transports/zap/v2
go get go.loglayer.dev/transports/slog/v2
go get go.loglayer.dev/transports/phuslu/v2
go get go.loglayer.dev/transports/logrus/v2
go get go.loglayer.dev/transports/charmlog/v2

# Cloud (managed log services)
go get go.loglayer.dev/transports/axiom/v2
go get go.loglayer.dev/transports/betterstack/v2
go get go.loglayer.dev/transports/datadog/v2
go get go.loglayer.dev/transports/gcplogging/v2
go get go.loglayer.dev/transports/sentry/v2
Expand All @@ -39,6 +33,14 @@ go get go.loglayer.dev/transports/http/v2
go get go.loglayer.dev/transports/lumberjack/v2
go get go.loglayer.dev/transports/otellog/v2

# Wrappers around third-party loggers
go get go.loglayer.dev/transports/charmlog/v2
go get go.loglayer.dev/transports/logrus/v2
go get go.loglayer.dev/transports/phuslu/v2
go get go.loglayer.dev/transports/slog/v2
go get go.loglayer.dev/transports/zap/v2
go get go.loglayer.dev/transports/zerolog/v2

# Plugins
go get go.loglayer.dev/plugins/redact/v2
go get go.loglayer.dev/plugins/sampling/v2
Expand Down
4 changes: 3 additions & 1 deletion docs/src/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,8 @@ lines := lib.Lines() // []lltest.LogLine; assert on Level, Messages, Data, Meta
- `transports/blank`: user-supplied dispatch function

**Cloud** (managed log services):
- `transports/axiom`: ships logs to Axiom via caller-supplied `*axiom.Client`
- `transports/betterstack`: ships logs to Better Stack via HTTP intake
- `transports/datadog`: Datadog Logs HTTP intake
- `transports/gcplogging`: wraps a caller-supplied `*logging.Logger` from `cloud.google.com/go/logging`
- `transports/sentry`: wraps a caller-supplied `sentry.Logger`
Expand All @@ -374,7 +376,7 @@ lines := lib.Lines() // []lltest.LogLine; assert on Level, Messages, Data, Meta
- `transports/otellog`: emits to an OTel `log.Logger`; forwards `WithContext` for trace correlation

**Wrappers around third-party loggers**:
- `transports/zerolog`, `transports/zap`, `transports/slog`, `transports/phuslu`, `transports/logrus`, `transports/charmlog`
- `transports/charmlog`, `transports/logrus`, `transports/phuslu`, `transports/slog`, `transports/zap`, `transports/zerolog`

## Available Plugins

Expand Down
1 change: 1 addition & 0 deletions docs/src/transports/_partials/transport-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Managed log services. Async + batched by default; site-aware where applicable.
| Name | Version | Go Reference | Description |
|------|---------|--------------|-------------|
| [Axiom](/transports/axiom) | [![Version](https://img.shields.io/github/v/tag/loglayer/loglayer-go?filter=transports/axiom/v*&sort=date&label=version&style=flat-square&color=blue)](https://github.com/loglayer/loglayer-go/releases?q=transports/axiom/&expanded=true) | [![Go Reference](https://pkg.go.dev/badge/go.loglayer.dev/transports/axiom/v2.svg)](https://pkg.go.dev/go.loglayer.dev/transports/axiom/v2) | Ships logs to Axiom via caller-supplied `*axiom.Client`. NDJSON ingestion with configurable message field. |
| [Better Stack](/transports/betterstack) | [![Version](https://img.shields.io/github/v/tag/loglayer/loglayer-go?filter=transports/betterstack/v*&sort=date&label=version&style=flat-square&color=blue)](https://github.com/loglayer/loglayer-go/releases?q=transports/betterstack/&expanded=true) | [![Go Reference](https://pkg.go.dev/badge/go.loglayer.dev/transports/betterstack.svg)](https://pkg.go.dev/go.loglayer.dev/transports/betterstack) | Ships logs to Better Stack via HTTP intake. Source token auth, configurable timestamp field. |
| [Datadog](/transports/datadog) | [![Version](https://img.shields.io/github/v/tag/loglayer/loglayer-go?filter=transports/datadog/v*&sort=date&label=version&style=flat-square&color=blue)](https://github.com/loglayer/loglayer-go/releases?q=transports/datadog/&expanded=true) | [![Go Reference](https://pkg.go.dev/badge/go.loglayer.dev/transports/datadog/v2.svg)](https://pkg.go.dev/go.loglayer.dev/transports/datadog/v2) | Datadog Logs HTTP intake. Site-aware URL, DD-API-KEY header, status mapping. |
| [Google Cloud Logging](/transports/gcplogging) | [![Version](https://img.shields.io/github/v/tag/loglayer/loglayer-go?filter=transports/gcplogging/v*&sort=date&label=version&style=flat-square&color=blue)](https://github.com/loglayer/loglayer-go/releases?q=transports/gcplogging/&expanded=true) | [![Go Reference](https://pkg.go.dev/badge/go.loglayer.dev/transports/gcplogging/v2.svg)](https://pkg.go.dev/go.loglayer.dev/transports/gcplogging/v2) | Forwards entries to a caller-supplied `*logging.Logger` from `cloud.google.com/go/logging`. Severity mapping, root-level Entry skeleton, async + sync dispatch. |
| [New Relic](/transports/newrelic) | [![Version](https://img.shields.io/github/v/tag/loglayer/loglayer-go?filter=transports/newrelic/v*&sort=date&label=version&style=flat-square&color=blue)](https://github.com/loglayer/loglayer-go/releases?q=transports/newrelic/&expanded=true) | [![Go Reference](https://pkg.go.dev/badge/go.loglayer.dev/transports/newrelic.svg)](https://pkg.go.dev/go.loglayer.dev/transports/newrelic) | New Relic Log Ingest API. Site-aware URL, api-key header, LogEvent encoding. |
Expand Down
208 changes: 208 additions & 0 deletions docs/src/transports/betterstack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
---
title: Better Stack Transport
description: Ship logs to Better Stack's HTTP intake API.
---

# Better Stack Transport

<ModuleBadges path="transports/betterstack" />

Sends log entries to [Better Stack](https://betterstack.com) via their HTTP Logs Endpoint. Built on the [HTTP transport](/transports/http) with a Better Stack-specific encoder and bearer token authentication.

```sh
go get go.loglayer.dev/transports/betterstack
```

## Getting a Source Token

Better Stack identifies your log source with a **Source Token**. You need this to send logs.

To get the source token:

1. Sign in to Better Stack at [https://betterstack.com](https://betterstack.com).
2. Navigate to **Logs** → **Sources**.
3. Either copy an existing source's token or create a new log source and copy its token.
4. Store the token securely—treat it like a password.

The source token is a secret. Load it from an environment variable or secret manager rather than hard-coding it in source.

## Basic Usage

```go
import (
"os"
"go.loglayer.dev/v2"
"go.loglayer.dev/transports/betterstack"
)

tr := betterstack.New(betterstack.Config{
SourceToken: os.Getenv("BETTERSTACK_SOURCE_TOKEN"),
})
defer tr.Close()

log := loglayer.New(loglayer.Config{Transport: tr})
log = log.WithFields(loglayer.Fields{"requestId": "abc"})
log.WithMetadata(loglayer.Metadata{"durationMs": 42}).Info("served request")
```

The transport is async and batched (inherited from the HTTP transport, default 100 entries / 5 seconds). Always call `Close()` on shutdown to flush pending entries.

## URL

By default, logs are sent to Better Stack's public intake endpoint:

| Setting | Value |
|--------------|---------------------------------------|
| **URL** | `https://in.logs.betterstack.com` |

### On-prem / custom URL

When testing against a mock endpoint or using a proxy, set `Config.URL` directly:

```go
tr := betterstack.New(betterstack.Config{
SourceToken: "fake-for-tests",
URL: "http://localhost:8080/logs", // test server URL
})
```

The transport rejects non-HTTPS URLs by default. To point at an `httptest.Server` or a local plain-HTTP proxy, also set `AllowInsecureURL: true`:

```go
srv := httptest.NewServer(http.HandlerFunc(...))
defer srv.Close()

tr := betterstack.New(betterstack.Config{
SourceToken: "fake-for-tests",
URL: srv.URL, // http:// from httptest
AllowInsecureURL: true, // required for non-HTTPS URLs
})
```

`AllowInsecureURL` is a test/debug ergonomic; leave it off for any URL that leaves your machine.

## Config

```go
type Config struct {
transport.BaseConfig

SourceToken string // required
URL string // default https://in.logs.betterstack.com
AllowInsecureURL bool // permit non-HTTPS URLs (httptest, local proxies)
TimestampField string // field name for timestamp, default "dt"

HTTP httptransport.Config // BatchSize, BatchInterval, Client timeout, OnError; URL, Encoder, and Headers cannot be overridden
}
```

### `SourceToken`

Required. Set as the Bearer token in the `Authorization` header on every request (`Bearer <source-token>`). `betterstack.New` panics with `betterstack.ErrSourceTokenRequired` when this is empty; use `betterstack.Build(cfg) (*Transport, error)` if you load the token from an environment variable and want to handle the missing-config case explicitly.

### `URL`

Optional. Overrides the default Better Stack intake endpoint. When set, the `AllowInsecureURL` config knob controls whether non-HTTPS URLs are permitted.

### `AllowInsecureURL`

Optional. When `true`, permits non-HTTPS URLs (useful for testing with `httptest.Server`). Defaults to `false` to prevent accidental plaintext logging in production.

### `TimestampField`

Optional. The field name used for timestamps in the log entry. Defaults to `"dt"` (Better Stack's convention). You can change this if your Better Stack source expects a different timestamp field name.

### `HTTP`

Embedded `httptransport.Config` for batching, client timeout, error handling, and other HTTP-layer concerns. The `URL`, `Encoder`, and `Authorization` header are set by the Better Stack wrapper and cannot be overridden via this field.

```go
tr := betterstack.New(betterstack.Config{
SourceToken: token,
HTTP: httptransport.Config{
BatchSize: 500,
BatchInterval: 2 * time.Second,
Client: &http.Client{Timeout: 10 * time.Second},
OnError: func(err error, entries []httptransport.Entry) {
metrics.Counter("betterstack.send.failed").Add(int64(len(entries)))
},
},
})
```

See the [HTTP transport docs](/transports/http) for the full HTTP config surface.

## Encoded Body Shape

Each log entry becomes one object in a JSON array:

```json
[
{
"dt": "2026-05-06T14:30:00.123Z",
"level": "info",
"message": "served request",
"requestId": "abc",
"durationMs": 42
}
]
```

- **Timestamp**: Controlled by `TimestampField` (default `"dt"`), formatted as ISO 8601 UTC.
- **Level**: String representation of the log level (trace, debug, info, warn, error, fatal, panic).
- **Message**: The log message string.
- **Fields**: Persistent fields from `WithFields()` and `Child()` are merged at the root level.
- **Metadata**: Map metadata merges at the root; non-map metadata is serialized as JSON under a `metadata` key (configurable via `loglayer.Config.MetadataFieldName`).

Persistent fields (`WithFields`) and metadata (`WithMetadata`) follow the [core placement rules](/configuration#fieldskey): when `FieldsKey` is empty, fields merge at the root of each log object; when `MetadataFieldName` is empty, map metadata merges at the root and non-map metadata nests under `metadata`. Set either knob on `loglayer.Config` to nest under a configured key instead.

## Level Mapping

Better Stack supports these log levels. The transport maps loglayer levels:

| LogLayer Level | Better Stack level |
|------------------|--------------------|
| `LogLevelTrace` | `"trace"` |
| `LogLevelDebug` | `"debug"` |
| `LogLevelInfo` | `"info"` |
| `LogLevelWarn` | `"warn"` |
| `LogLevelError` | `"error"` |
| `LogLevelFatal` | `"fatal"` |
| `LogLevelPanic` | `"panic"` |

## API Limits

Better Stack's intake has these limits:

- Max body size per request: **5MB**
- Max single log entry: **1MB**
- Max entries per array: **1,000**

The default `BatchSize` of 100 stays well under all of these. If you bump `BatchSize` for higher throughput, keep it under 1,000 and watch the body-size limit if your entries are large.

## Closing

`BetterStack.Transport` embeds `*httptransport.Transport`, so it has the same `Close() error` method. **Always call it on shutdown** so the in-flight batch is flushed:

```go
tr := betterstack.New(...)
defer tr.Close()
```

After `Close`, subsequent log calls drop the entry and invoke the underlying HTTP transport's `OnError` with `httptransport.ErrClosed`.

## Reaching the Underlying HTTP Transport

`betterstack.Transport` embeds `*httptransport.Transport`, so any HTTP-transport method works on it directly:

```go
tr := betterstack.New(...)
tr.Close() // from httptransport.Transport
tr.GetLoggerInstance() // from httptransport.Transport (returns nil)
```

## Fatal Behavior

<!--@include: ./_partials/fatal-passthrough.md-->

Same async caveat as the underlying [HTTP transport](/transports/http#fatal-behavior): set `DisableFatalExit: true` and call `tr.Close()` before `os.Exit(1)` if you need guaranteed delivery of the fatal entry.
12 changes: 12 additions & 0 deletions docs/src/transports/creating-transports.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,15 @@ Match the pattern the built-ins use ([`transports/structured`](https://github.co
## Testing

For testing a custom transport, see [Testing Transports](/transports/testing-transports). It covers the direct buffer assertion pattern and the `RunContract` helper that drives the same 14-test contract suite every built-in wrapper passes.

### Live Tests

Transports that ship to third-party services (HTTP endpoints, cloud APIs) include build-tagged live tests (`//go:build livetest`) that hit the real API. These are **never documented** in the transport's VitePress docs — they're internal developer documentation only.

Run them locally with the required credentials:

```sh
SOME_SERVICE_TOKEN=<token> go test -tags=livetest ./transports/yourservice/
```

CI runs them automatically on pushes to `main`. The test code itself includes a comment block at the top explaining how to run it, so contributors can discover it from the source.
4 changes: 4 additions & 0 deletions docs/src/whats-new.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ description: Latest features and improvements in LogLayer for Go.

Initial release. New [New Relic transport](/transports/newrelic).

`transports/betterstack`:

Initial release. New [Better Stack transport](/transports/betterstack).

## May 06, 2026

`transports/axiom`:
Expand Down
1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use (

./transports/axiom
./transports/blank
./transports/betterstack
./transports/central
./transports/charmlog
./transports/cli
Expand Down
1 change: 1 addition & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ pre-commit:
stage_fixed: true

pre-push:
parallel: true
commands:
# Catches go.mod / go.sum drift before the push reaches CI. CI
# runs the same check; this just shifts the failure left so devs
Expand Down
5 changes: 5 additions & 0 deletions monorel.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ tag_prefix = "transports/blank"
path = "transports/blank"
changelog = "transports/blank/CHANGELOG.md"

[packages."transports/betterstack"]
tag_prefix = "transports/betterstack"
path = "transports/betterstack"
changelog = "transports/betterstack/CHANGELOG.md"

[packages."transports/charmlog"]
tag_prefix = "transports/charmlog"
path = "transports/charmlog"
Expand Down
3 changes: 3 additions & 0 deletions scripts/foreach-module.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ ALL_MODULES=(
.
transports/axiom
transports/blank
transports/betterstack
transports/central
transports/charmlog
transports/cli
Expand Down Expand Up @@ -75,6 +76,7 @@ SHIPPED_MODULES=(
.
transports/axiom
transports/blank
transports/betterstack
transports/central
transports/charmlog
transports/cli
Expand Down Expand Up @@ -167,6 +169,7 @@ case "$op" in
.
transports/axiom
transports/blank
transports/betterstack
transports/central
transports/charmlog
transports/cli
Expand Down
7 changes: 7 additions & 0 deletions transports/betterstack/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Changelog

All notable changes to this project are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Loading
Loading