froe

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 15, 2026 License: MIT Imports: 14 Imported by: 0

README

froe

Push selected logs from your Go service to a Froe instance. Clients and agents fetch them back with a read key over a plain REST API.

Install

go get github.com/froe-run/froe-client-go

Requires Go 1.24+. Standard library only, no dependencies.

Quickstart

import froe "github.com/froe-run/froe-client-go"

log := froe.New(froe.Options{Key: "fw_..."})
defer log.Close(context.Background()) // flushes what is still buffered

log.Info("payment ok", froe.Meta{"order": 42})
log.Warn("retrying payment", froe.Meta{"order": 42, "attempt": 2})
log.Error("payment failed", froe.Meta{"order": 42, "code": "card_declined"})

All six level methods take a message and a meta map, which may be nil: Trace, Debug, Info, Warn, Error, Fatal.

log.Trace("cache miss", froe.Meta{"key": "user:42"})
log.Fatal("out of memory, exiting", nil)

Options

froe.New(froe.Options{
    Key:                "fw_...",            // required, write key
    URL:                "https://froe.run",  // your Froe instance
    BatchSize:          50,                  // send after this many buffered entries
    FlushInterval:      2 * time.Second,     // or after this long, whichever comes first
    MaxBufferedEntries: 10000,               // memory ceiling, buffered plus queued; oldest drop past it
    RequestTimeout:     10 * time.Second,    // abort a hung send after this long
    HTTPClient:         myClient,            // custom transport
    Warn:               myWarnFunc,          // SDK diagnostics, default one line to stderr
})

Only Key is required; every other field falls back to the default shown. Warn is called from your logging goroutine and from the sender goroutine, so it must be safe for concurrent use, and it must not log back into the same client.

Give URL the address your instance answers on directly. The default transport refuses redirects instead of following them, because Go replays a 301, 302, or 303 POST as a bodiless GET, which arrives as a fetch carrying a write key and comes back 403. A redirect is warned about and names its own status. A HTTPClient you supply keeps its own policy.

Delivery guarantees

Log calls never block the caller and never panic. Entries are buffered in memory and sent as batches, strictly in order, when the buffer reaches BatchSize, when FlushInterval elapses, or when you call Flush. A batch that fails with a network error, a timeout, a 5xx, or a 429 stays queued and retries with exponential backoff (250ms * 4^failures, capped at 30 seconds; a 429 honors the server's Retry-After). Only another 4xx drops the batch, with one warning, because no retry can fix a request the server rejected as wrong. Every batch carries a per-batch Idempotency-Key and an exact body, both fixed across all its retries, so a retry of a batch the server already accepted never stores duplicates.

An entry whose message plus meta exceeds 64 KB, or whose level is not one of the six, is dropped at the call site with a warning; it never enters the buffer. Meta the encoder refuses (a NaN, an infinity, a channel) costs the entry its meta but not its message, which is warned about and shipped without it: a ratio over an empty sample must not silently delete the log line reporting it. MaxBufferedEntries is the one memory knob: it caps buffered entries plus queued batch entries together, and on overflow the oldest go first (whole queued batches, then the oldest buffer entries), with one warning per overflow episode.

Flush(ctx) makes a single ordered delivery pass, ignoring any backoff, and returns even while the server is down; it reports only a ctx error, so it never holds your shutdown hook hostage. Whatever it could not deliver stays queued for the next interval. Close(ctx) flushes once and stops the background sender.

In short: logs are telemetry, not durable storage. Nothing here is meant to replace your application's own logging or an audit trail.

slog handler

client := froe.New(froe.Options{Key: "fw_..."})
defer client.Close(context.Background())

log := slog.New(froe.NewHandler(client, nil))

log.Info("just a local log line")
log.Info("shipped to Froe", "froe", true, "order", 42)
log.With("froe", true).Info("also shipped to Froe")

By default only records carrying froe=true are forwarded, whether set on a child logger or passed per call. A severity level is not a sharing decision: your error logs are not automatically things you want a client outside your trust boundary to read. Set ForwardAll: true in HandlerOptions to forward the whole stream instead. The marker is read at the top level only, and never reaches the entry's meta.

HandlerOptions also takes Level (a slog.Leveler, default slog.LevelInfo). Record attrs become the entry's meta, with slog groups as nested JSON objects; the record's own timestamp carries over. An attr holding an error keeps its text, since an error has no exported field for the JSON encoder to find and would otherwise arrive as {}. slog's four named levels map onto Froe's six: below Debug is trace, above Error is fatal.

To keep your local logs and ship a subset, put the handler behind a fanout of your own, or give Froe its own slog.Logger beside the one writing to stderr.

Reading logs

Consumers with a read key fetch entries with GET /v1/logs on your Froe instance, filtering by level, since, until, q (substring match), and paging with limit and cursor. The full wire contract, including request and response shapes, is served at GET /v1 on any Froe instance.

Server-side limits

A Froe instance accepts at most 1000 entries per push batch and 64 KB per entry (message plus meta). The SDK chunks large flushes into batches of at most 1000 automatically; the per-entry limit is enforced at the call site as described above.

Documentation

Overview

Package froe pushes selected logs to a Froe instance, the log-sharing service. Clients and agents fetch them back with a read key over a plain REST API; the wire contract is served at GET /v1 on any instance.

The hard rule of this package: log calls never block the caller and never panic. Entries are buffered in memory and shipped as batches, strictly in order, with capped exponential backoff on failure.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client buffers entries and ships them to one Froe instance. It is safe for concurrent use. Close it to stop its background goroutine.

func New

func New(opts Options) *Client

New starts a Client and its background sender.

func (*Client) Close

func (c *Client) Close(ctx context.Context) error

Close flushes once and stops the background sender. Later Log calls still buffer, but nothing ships them.

func (*Client) Debug

func (c *Client) Debug(message string, meta Meta)

func (*Client) Error

func (c *Client) Error(message string, meta Meta)

func (*Client) Fatal

func (c *Client) Fatal(message string, meta Meta)

func (*Client) Flush

func (c *Client) Flush(ctx context.Context) error

Flush makes one ordered delivery pass, ignoring any backoff, and returns when the pass settles. It reports only ctx errors: a batch it could not deliver stays queued for the next interval, so a shutdown hook is never held hostage by a dead server.

func (*Client) Info

func (c *Client) Info(message string, meta Meta)

func (*Client) Log

func (c *Client) Log(level Level, message string, meta Meta, t time.Time)

Log buffers one entry. A zero t stamps now; the parameter exists for adapters that carry the host logger's own timestamp.

func (*Client) Trace

func (c *Client) Trace(message string, meta Meta)

func (*Client) Warn

func (c *Client) Warn(message string, meta Meta)

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

Handler is a slog.Handler that feeds records into a Client. It is a forwarder, not a replacement for your local handler: wrap it in slog.New alongside whatever writes to stderr, or use it on its own if Froe is your only sink.

func NewHandler

func NewHandler(c *Client, opts *HandlerOptions) *Handler

NewHandler forwards records to c. A nil opts takes every default.

func (*Handler) Enabled

func (h *Handler) Enabled(_ context.Context, l slog.Level) bool

func (*Handler) Handle

func (h *Handler) Handle(ctx context.Context, r slog.Record) error

func (*Handler) WithAttrs

func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler

func (*Handler) WithGroup

func (h *Handler) WithGroup(name string) slog.Handler

type HandlerOptions

type HandlerOptions struct {
	// Level is the minimum record level forwarded. Default slog.LevelInfo.
	Level slog.Leveler
	// ForwardAll restores the firehose for the rare app whose whole
	// stream is meant to be shared, ignoring the froe=true marker.
	ForwardAll bool
}

HandlerOptions configures a Handler. The zero value is the default: forward records at slog's default level that carry froe=true.

type Level

type Level string

Level is a Froe severity. The set is fixed by API.md; the server rejects a whole batch on one unknown level, so unknown levels are dropped here.

const (
	LevelTrace Level = "trace"
	LevelDebug Level = "debug"
	LevelInfo  Level = "info"
	LevelWarn  Level = "warn"
	LevelError Level = "error"
	LevelFatal Level = "fatal"
)

type Meta

type Meta map[string]any

Meta is the free-form JSON object carried alongside a message.

type Options

type Options struct {
	// Key is the write key (fw_...). Required.
	Key string
	// URL of the Froe instance. Default https://froe.run.
	URL string
	// BatchSize is the buffered entry count that triggers a send.
	// Default 50.
	BatchSize int
	// FlushInterval sends whatever is buffered even below BatchSize.
	// Default 2s.
	FlushInterval time.Duration
	// MaxBufferedEntries is the ceiling on entries held in memory,
	// counting the unformed buffer and the batches waiting in the retry
	// queue together, so a single number bounds the SDK's whole
	// footprint. Past it the oldest entries go first. Default 10000.
	MaxBufferedEntries int
	// RequestTimeout caps how long one send may hang before it is
	// aborted and treated as a failed attempt. Without it, a server that
	// accepts the connection but never answers ties up the head batch,
	// and because the head blocks the queue nothing else would ship.
	// Default 10s.
	RequestTimeout time.Duration
	// HTTPClient is the transport. Default a plain http.Client; the
	// per-attempt deadline comes from RequestTimeout either way.
	HTTPClient *http.Client
	// Warn receives the SDK's own diagnostics (dropped entries, dropped
	// batches, overflow). Default writes one line to stderr. It is
	// called from the logging goroutine and from the sender goroutine,
	// so it must be safe for concurrent use, and it must not route back
	// into this Client, which would recurse.
	Warn func(msg string)
}

Options configures a Client. Only Key is required; every other field falls back to the default named in its comment.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL