---
title: Adapters Overview
description: Ship logs to observability platforms with built-in adapters
---

Send your logs to external observability platforms with built-in adapters. Each adapter is a regular [transport](/docs/features/transports) — batched, retried, and non-blocking — so you can mix them with console and file logging or run them exclusively.

## Available Adapters

### Cloud Platforms

| Platform | Import | Best for |
| --- | --- | --- |
| [Axiom](/docs/adapters/axiom) | `logixlysia/axiom` | Schema-free log analytics — every field is queryable |
| [Better Stack](/docs/adapters/better-stack) | `logixlysia/better-stack` | Logs, uptime, and alerting in one place |
| [Datadog](/docs/adapters/datadog) | `logixlysia/datadog` | Enterprise observability with facets and pipelines |
| [Sentry](/docs/adapters/sentry) | `logixlysia/sentry` | Structured logs next to your errors and traces |
| [PostHog](/docs/adapters/posthog) | `logixlysia/posthog` | Product analytics — link logs to persons and funnels |

### Self-Hosted & Open Standards

| Platform | Import | Best for |
| --- | --- | --- |
| [OTLP](/docs/adapters/otlp) | `logixlysia/otlp` | Any OpenTelemetry backend — collectors, Grafana Cloud, New Relic, Honeycomb, SigNoz |
| [HyperDX](/docs/adapters/hyperdx) | `logixlysia/hyperdx` | Open-source observability via OTLP |
| [Grafana Loki](/docs/adapters/loki) | `logixlysia/loki` | Label-indexed logs for the Grafana stack |
| [ClickHouse](/docs/adapters/clickhouse) | `logixlysia/clickhouse` | Your own SQL log warehouse, no pipeline in between |

## Quick Start

Set the platform's environment variables, create the transport, and pass it to `transports`:

```ts
import { Elysia } from 'elysia'
import logixlysia from 'logixlysia'
import { createAxiomTransport } from 'logixlysia/axiom'

const app = new Elysia()
  .use(
    logixlysia({
      config: {
        transports: [createAxiomTransport()]
      }
    })
  )
  .get('/', () => 'ok')
  .listen(3000)
```

Trigger a request and the access log appears in your platform's log explorer.

## Shared Behavior

All adapters share the same core:

- **Batching** — entries buffer and flush either when `maxBatchSize` is reached (default 20) or after `flushIntervalMs` (default 2000 ms), whichever comes first.
- **Retries** — network errors, `429`, and `5xx` responses retry with linear backoff (default 2 retries). Other `4xx` responses fail immediately.
- **Timeout** — each request aborts after `timeout` ms (default 5000).
- **Non-blocking** — sends run in the background and never delay your HTTP responses. Failures are reported through [`onError`](/docs/features/transports) (sink `'transport'`) or rate-limited to stderr.
- **Credentials** — read from environment variables by default; options passed to the factory always win. Missing credentials throw at startup with an actionable message, not silently at runtime.
- **Endpoint safety** — endpoint URLs are validated when the transport is created and must use `http:` or `https:`, and requests never follow redirects, so a credential header can't be forwarded to another origin.

Every adapter accepts these options on top of its platform-specific ones:

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `maxBatchSize` | `number` | `20` | Entries buffered before an immediate flush |
| `flushIntervalMs` | `number` | `2000` | Max time an entry waits before the buffer is sent |
| `timeout` | `number` | `5000` | Per-request timeout in milliseconds |
| `retries` | `number` | `2` | Retry attempts on network errors, 429, and 5xx |
| `maxPendingBatches` | `number` | `32` | Batches allowed to be waiting on the backend at once |
| `onError` | `(error: unknown) => void` | — | Called when a batch fails after retries or is dropped |

Batches are sent one at a time, so they arrive in the order they were logged and a slow backend applies backpressure instead of piling up requests. Once `maxPendingBatches` batches are waiting, new batches are dropped rather than buffered forever. Pass the same function you give [`config.onError`](/docs/features/transports) as the adapter's `onError` to see every batch failure — including the ones from the interval timer, which no caller is awaiting — in one place.

## Multiple Destinations

Adapters compose — fan the same logs out to several platforms:

```ts
import { createAxiomTransport } from 'logixlysia/axiom'
import { createSentryTransport } from 'logixlysia/sentry'

app.use(
  logixlysia({
    config: {
      transports: [createAxiomTransport(), createSentryTransport()]
    }
  })
)
```

## Production-Only External Logging

Use `useTransportsOnly` to disable console and file output and send logs exclusively to your platform:

```ts
app.use(
  logixlysia({
    config: {
      transports: [createAxiomTransport()],
      useTransportsOnly: process.env.NODE_ENV === 'production'
    }
  })
)
```

## Graceful Shutdown

When the app stops, the plugin starts flushing every transport and the file sink. Elysia's `app.stop()` does not wait for plugin stop hooks, so a process that exits right after `await app.stop()` can cut that flush short. Before you exit, await `flushLogixlysia` with the same `options` object you passed to the plugin. The stop hook's own wait is bounded by [`flushTimeoutMs`](/docs/configuration) (5000 ms by default; `0` starts the flush without waiting), and a flush that runs out of time is reported through `onError` with `sink: 'shutdown'`.

What that does not cover:

- `SIGKILL`, or a crash — nothing runs.
- A process that exits without calling `app.stop()`, including a bare `SIGINT` with no handler.
- A serverless function frozen between invocations.

Signals are yours to handle, because a library that installs its own handlers fights the rest of your process. This handler stops the app, waits for the flush, then exits:

```ts
import { Elysia } from 'elysia'
import logixlysia, { flushLogixlysia } from 'logixlysia'
import { createAxiomTransport } from 'logixlysia/axiom'

const options = { config: { transports: [createAxiomTransport()] } }
const app = new Elysia().use(logixlysia(options)).listen(3000)

process.on('SIGTERM', async () => {
  await app.stop()
  await flushLogixlysia(options)
  process.exit(0)
})
```

For workers, scripts, and anything else that logs outside the Elysia lifecycle, drain the sinks directly:

```ts
import { flushLogixlysia } from 'logixlysia'

await flushLogixlysia(options, { close: true })
```

`close: true` also releases sockets, timers, and the file handle; omit it to flush and keep logging.

## What Gets Sent

Each log carries its level, message, and the full meta object: the request method and URL, the response status, `durationMs`, and everything merged into the [request context](/docs/features/request-context) — request IDs, trace IDs, user IDs, and your own fields. Platforms that prefer flat attributes (HyperDX, Sentry, PostHog) receive dot-notation keys like `request.method` and `context.requestId`; Axiom receives the nested structure as-is.

[Redaction](/docs/configuration) runs before transports, so `autoRedact` and `redactKeys` apply to everything an adapter ships off-box.
