Skip to main content

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';
warning

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

FieldTypeRequiredDefaultDescription
touchpointstringNoThe touchpoint name to associate this log with
keystringNoA unique correlation key for this specific event
messagestringNoFree-form log message
level"info" | "warn" | "error" | "debug"No"info"Severity level
metadataRecord<string, unknown>NoArbitrary 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

FieldTypeRequiredDescription
touchpointstringNoTouchpoint name
keystringReq. if standalone: trueUnique identifier for this start/end pair
metadataRecord<string, unknown>NoMetadata attached to the timing entry
standalonebooleanNoSee Standalone starts

EndOptions

FieldTypeRequiredDescription
keystringNoMust match the key used in start()
touchpointstringNoUsed to match a keyless start
metadataRecord<string, unknown>NoMerged 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' });
info

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(), and end() call triggers an immediate fetch to 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.