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(options)
Logs a discrete event.
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 |
serverLogger.start(options) and serverLogger.end(options)
Measure the duration of an operation using a matched start / end pair.
serverLogger.start({
touchpoint: 'db/query/products',
metadata: { category: 'electronics' },
});
const products = await db.query('SELECT ...');
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:
serverLogger.start({ key: 'req-123', touchpoint: 'api/checkout' });
// ... work ...
serverLogger.end({ key: 'req-123' });
By touchpoint — implicit pairing (FIFO if multiple outstanding):
serverLogger.start({ touchpoint: 'api/checkout' });
// ... work ...
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 |
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 |
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
serverLogger.start({
key: 'job-456',
touchpoint: 'worker/email/send',
standalone: true,
});
// Later, in request B or a different process
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() {
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.