Server Logging
The server logger runs in Node.js and is safe to use in Server Components, Server Actions, API routes, and Edge/Worker functions. It is imported from @simplelogs/next/server.
Import
import { serverLogger } from '@simplelogs/next/server';
Only import from @simplelogs/next/server in server-side code. This module uses fs and path to load the optional config file — importing it in a browser bundle will error.
If you used <SimpleLogsProvider> in your layout, server-side configuration is applied automatically. Otherwise call configureSDK() before using the logger:
import { configureSDK, serverLogger } from '@simplelogs/next/server';
configureSDK({ serverKey: process.env.SIMPLELOGS_SERVER_KEY! });
serverLogger.log(), start(), and end() now return Promise<void> instead of void. Before enqueueing an entry, they read the incoming request's correlation headers via next/headers() (see Page & Session Correlation) — that lookup is asynchronous, so the methods are too.
Always await these calls. See A note on awaiting below for why this matters more than it might seem.
:::
serverLogger.log(options)
Logs a discrete event.
await serverLogger.log({
touchpoint: 'api/users/login',
message: 'User logged in',
level: 'info',
metadata: { userId: 'u_abc123', method: 'oauth' },
});
LogOptions
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
touchpoint | string | No | — | The touchpoint name to associate this log with |
key | string | No | — | A unique correlation key for this specific event |
message | string | No | — | Free-form log message |
level | "info" | "warn" | "error" | "debug" | No | "info" | Severity level |
metadata | Record<string, unknown> | No | — | Arbitrary structured data |
pageIdentifier | string | No | — | Override the auto-detected page identifier. See Manual override. |
sessionIdentifier | string | No | — | Override the auto-detected session identifier |
requestHeaders | Headers | Record<string, string | string[]> | No | — | Forward an incoming request's headers directly; takes precedence over the two fields above |
Automatic page/session correlation
Every call to log(), start(), and end() automatically tries to pick up the x-simplelogs-page-id / x-simplelogs-session-id headers from the incoming request (via next/headers()) and merges them into metadata — no code changes needed in a Route Handler, Server Component, or Server Action under the App Router. This is what ties a server-side entry back to the client-side page load that triggered it.
See Page & Session Correlation for the full picture, including how to override it manually outside the App Router.
serverLogger.start(options) and serverLogger.end(options)
Measure the duration of an operation using a matched start / end pair.
await serverLogger.start({
touchpoint: 'db/query/products',
metadata: { category: 'electronics' },
});
const products = await db.query('SELECT ...');
await serverLogger.end({
touchpoint: 'db/query/products',
metadata: { count: products.length },
});
Matching start to end
The SDK matches end() to a pending start() using one of two strategies:
By key — explicit pairing:
await serverLogger.start({ key: 'req-123', touchpoint: 'api/checkout' });
// ... work ...
await serverLogger.end({ key: 'req-123' });
By touchpoint — implicit pairing (FIFO if multiple outstanding):
await serverLogger.start({ touchpoint: 'api/checkout' });
// ... work ...
await serverLogger.end({ touchpoint: 'api/checkout' });
If you call start() with the same key while a previous start() with that key is still open, the orphaned timing is flushed as an incomplete entry and a new start is recorded.
StartOptions
| Field | Type | Required | Description |
|---|---|---|---|
touchpoint | string | No | Touchpoint name |
key | string | Req. if standalone: true | Unique identifier for this start/end pair |
metadata | Record<string, unknown> | No | Metadata attached to the timing entry |
standalone | boolean | No | See Standalone starts |
pageIdentifier / sessionIdentifier / requestHeaders | — | No | Same correlation overrides as LogOptions, above |
EndOptions
| Field | Type | Required | Description |
|---|---|---|---|
key | string | No | Must match the key used in start() |
touchpoint | string | No | Used to match a keyless start |
metadata | Record<string, unknown> | No | Merged with or replaces start metadata |
pageIdentifier / sessionIdentifier / requestHeaders | — | No | Same correlation overrides as LogOptions, above |
Standalone starts
A standalone start sends the start time to SimpleLogs immediately rather than holding it in memory until end() is called. This is useful for long-running or cross-request operations where you can't guarantee end() runs in the same process as start().
// In request A
await serverLogger.start({
key: 'job-456',
touchpoint: 'worker/email/send',
standalone: true,
});
// Later, in request B or a different process
await serverLogger.end({ key: 'job-456' });
standalone: true requires a key. Enable it per-call or globally via configureSDK({ allowStandaloneStart: true }).
Standalone starts have a tradeoff: the initial start event reaches SimpleLogs immediately, but if end() is never called the entry remains open in the UI indefinitely.
flushServer()
Forces an immediate flush of all queued server-side entries. Useful in serverless environments where you want to ensure delivery before a function returns:
import { flushServer } from '@simplelogs/next/server';
export async function GET() {
await serverLogger.log({ message: 'Request received' });
await flushServer();
return Response.json({ ok: true });
}
In serverless mode (serverless: true) every call already flushes immediately, so flushServer() is a no-op.
Serverless / Vercel considerations
On Vercel, serverless mode is enabled automatically. This means:
- Every
log(),start(), andend()call triggers an immediatefetchto the ingestion endpoint - There is no in-memory batching between calls
flushServer()is a no-op
If you're running on a long-lived server (not serverless), the batch interval (batchInterval, default 250 ms) and max batch size (maxBatchSize, default 100) control when entries are sent.
A note on awaiting
serverLogger.log(), start(), and end() resolve their correlation-header lookup before the entry is added to the queue — this is what makes serverless-mode immediate flushing safe (there's no race between "read the headers" and "send the entry"). But it also means the entry genuinely isn't queued until the returned promise resolves.
If you don't await the call, two things can go wrong:
- In serverless/edge functions, the runtime can freeze or tear down the function as soon as your handler returns a response — an un-awaited call may never get a chance to finish, and the log is silently dropped.
- In
serverless: truemode specifically, since flushing happens immediately on enqueue, an un-awaited call also risks the surrounding function exiting before the network request even starts.
// Risky — the promise is never awaited
serverLogger.log({ touchpoint: 'api/checkout' });
return Response.json({ ok: true }); // function may exit before the log is sent
// Safe
await serverLogger.log({ touchpoint: 'api/checkout' });
return Response.json({ ok: true });
On a long-lived (non-serverless) server this is lower-stakes — the process keeps running and the batch interval will flush eventually — but awaiting is still the recommended default for predictable behavior everywhere.