Skip to content
Install · free

Any language

Write a LogNorth client in any language. What to send, when to send it, and how to retry.

LogNorth has SDKs for Go, Node and Bun, and Rails, and it accepts OpenTelemetry logs. For any other language, write a small client. This page is the full protocol. A client that follows it behaves like the official SDKs.

Using a coding agent? Let it write the client.

  1. Connect your agent to LogNorth with the one command on the MCP page. It installs the LogNorth plugin in Claude Code, Codex, and Gemini CLI.
  2. In your project, ask your agent to “add LogNorth”. The plugin’s lognorth-integrate skill does the rest.

The agent detects the language. Where an SDK exists, it installs it. Where none exists, it writes a small client that follows this page, with the buffering and retry rules, and tests it. Then it reads the first event back from LogNorth to prove it arrives.

POST {LOGNORTH_URL}/api/v1/events/batch
Authorization: Bearer lgn-...
Content-Type: application/json
{
"events": [
{
"message": "POST /checkout → 500",
"timestamp": "2026-10-01T12:00:05.123Z",
"duration_ms": 231,
"trace_id": "req-8f2c",
"context": {
"method": "POST",
"path": "/checkout",
"status": 500,
"environment": "production",
"error": "card declined",
"error_class": "CardError",
"error_file": "payments.py",
"error_line": 42,
"stack_trace": "..."
}
}
]
}
  • Read the URL and the key from the environment: LOGNORTH_URL and LOGNORTH_API_KEY. Never put a key in source code.
  • Each environment is its own app in LogNorth, with its own key (lgn-). The agent key (lgn-agent-) reads only and cannot send.
  • LogNorth accepts at most 1,000 events and 4 MB per request. Send smaller batches: at most 500 events and 1 MB.
  • You may compress the body: send Content-Encoding: gzip. LogNorth inflates it up to 16 MB.
Field Type Required Notes
message string yes What happened. Keep it under 1,000 characters.
timestamp string no RFC 3339 in UTC, for example 2026-10-01T12:00:05.123Z. Set it when the event happens, not when you send it. Default: the time LogNorth receives it.
duration_ms integer no How long it took.
trace_id string no Ties together the events of one request or job. Keep it under 100 characters.
context object no Everything else. Every key is searchable.

Keep one shape per kind of event, so the log reads the same across apps:

  • A request: GET /cart → 200
  • A job: SendInvoices completed or SendInvoices failed
  • Anything else: a short sentence, User signed up
Key Example What LogNorth does with it
method "POST" Shows the request method.
path "/checkout" Groups requests into endpoints, for stats and alerts. Send the path without the query string.
status 500 A number. 500 and above makes the event an error.
environment "production" Shows where the event came from.
error "card declined" Makes the event an error. Groups errors into issues.
error_class "CardError" Makes the event an error. Groups errors into issues.
error_file, error_line, error_caller "payments.py", 42, "charge" Points at the code. Agents use it to open the right file.
stack_trace "..." Shown on the error. Keep it to about 20 frames.
job "SendInvoices" Marks a background job.
user "42" Who made the request: an ID, never an email. The issue page counts the users an issue hit.
release "a1b2c3d" The version that runs, such as a git SHA. Send it on errors. The issue page shows the release an issue first and last appeared in, and error emails name it.
user_agent "Mozilla/5.0 …" Send it on failed requests only, to tell a bot from a browser.

An event is an error when error or error_class is a non-empty string, or when status is 500 or more. There are no levels. A 404 is not an error: it is the client’s mistake, not yours.

LogNorth drops events whose path starts with /. (scanners probing /.env, /.git). Skip other noise, such as health checks, in the client.

  1. Every request. Add a middleware that sends one event after the response: method, path, status, and duration. Give it a trace ID and pass that ID to everything else the request logs.
    • When the user is signed in, add user. Let the app name it with one call, such as set_user(id), that stores it with the request’s trace ID.
    • When the status is 500 or more, add user_agent.
  2. Every unhandled error. Catch it in the same middleware or in the framework’s error hook. Send error, error_class, the file and line where it was raised, and the stack trace. Add release and, inside a request, user.
  3. Every background job. One event when it ends, with its duration, and the error fields when it fails.
  4. A few business events, only where they earn their place: a signup, a payment.

Send in every environment except development and test. Staging and previews report too. Put the environment name in context.environment.

Read the release once, at startup: an option the app passes, or else the first of LOGNORTH_RELEASE, GIT_SHA, GIT_COMMIT, SOURCE_COMMIT, KAMAL_VERSION, RENDER_GIT_COMMIT, HEROKU_SLUG_COMMIT, SOURCE_VERSION, RAILWAY_GIT_COMMIT_SHA, and VERCEL_GIT_COMMIT_SHA that is set. Put it on error events only, so other events stay small. Never overwrite a release the app set itself.

LogNorth shows events by the time they happened, not by the time they arrived. Two rules make that hold:

  • Set timestamp the moment the event happens, in UTC with milliseconds, for example 2026-10-01T12:00:05.123Z. For a request, use the time the response was sent. Never set it at send time: a batch can wait in the buffer, or go out again after an outage. A retried event keeps its original timestamp.
  • Keep the order you logged in. The buffer is first in, first out. A failed batch goes back to the front. One request is in flight at a time. A flush stops at the first failed batch instead of sending the ones behind it.

So a batch that waited out a ten-minute outage shows up in the log at its own time, ten minutes back, not on top. Events with the same timestamp keep the order the client logged them in.

Two things work by arrival instead: following the log live (north tail, the live view) shows events as they arrive, each with its own timestamp, and an event without a timestamp gets the time LogNorth received it. So always send one.

LogNorth does not correct client clocks. Keep the server’s clock in sync (NTP), as for any log.

A logging client must never slow down or break the app it watches, and it must not lose events it can still deliver.

  • Never send on the request path. Put the event in a memory buffer and return. A background thread, task, or timer sends.
  • Never raise into the app. A failure to send is the client’s problem. Write one line to stderr when the state changes (failing, recovered, dropped), not one line per attempt.
  • Trim each event before it enters the buffer, so one huge event cannot use up the memory. At most 64 KB per event:
    • message: 1,000 characters.
    • context.stack_trace: 16 KB, keeping the start (the top of the stack).
    • Every other string in context: 8 KB.
    • Still over 64 KB: keep only error, error_class, error_file, error_line, method, path, status, environment, release, and user.
    • When anything was trimmed, set context.truncated = true.
  • Limit the buffer by count and by size: 10,000 events or 10 MB of JSON, whichever comes first. Measure each event once, when it enters, and keep a running total. A LogNorth that is down for an hour must not take the app’s memory with it.
  • When a limit is reached, drop the oldest event that is not an error. Drop an error only when the buffer holds nothing else. Count what you drop.
  • Send when the buffer holds 10 events, when an error arrives, or when the buffer passes half of either limit. Otherwise send 5 seconds after the first event arrived.
  • Send one request at a time, with at most 500 events and 1 MB of JSON. That keeps the order, stays well under the server’s limits, and does not flood a server that is struggling.
  • Flush on shutdown. On SIGTERM, SIGINT, or process exit, send what is left within a few seconds. Containers stop with SIGTERM, so a client that skips this loses the last events of every deploy.

The rule: keep the events and send them again, after the right wait. LogNorth says how long to wait.

Answer What it means What the client does
201 Stored Done. The body says {"created": N}; "errors" lists events the server rejected as invalid. Do not resend those.
503 LogNorth could not store the events Keep the events. Wait the seconds in Retry-After (LogNorth sends 5), then send again.
429 Too many requests Keep the events. Wait the seconds in Retry-After, the time until the rate window resets, then send again.
Other 5xx, 408, network error, timeout No usable answer Keep the events. Back off: 1 second, then 2, 4, 8, up to 60 seconds, with up to 20% random jitter.
401, 403, 404 Wrong key or wrong URL Keep the events. Write one line to stderr. Retry every minute, then less often, up to every 5 minutes, until someone fixes the key or the URL.
413, 400, other 4xx This batch cannot be accepted as it is Split the batch in half and send each half. A single event that is still refused is dropped and counted: sending it again cannot work.

Rules for keeping events:

  • Put unsent events back at the front of the buffer, in the same order, so the oldest go out first.
  • Keep accepting new events while you wait. The memory limits still apply.
  • Reset the backoff after a success.
  • Retry-After does not grow the backoff. When LogNorth says how long to wait, wait exactly that. The backoff is for answers that do not say.
  • A retry takes a full batch again, up to 500 events from the front, so a backlog drains fast once LogNorth accepts.
  • Use timeouts: 5 seconds to connect and 10 seconds for the whole request. A client without timeouts can wait forever on a dead connection.
  • Accept rare duplicates. A retry after a timeout can store the same event twice, because the first request may have succeeded. That is better than losing it.
  • Report what you dropped. After the next success, send one event: LogNorth client dropped N events, with context.dropped and context.dropped_errors. Then a loss shows up in LogNorth itself, not only in a log file nobody reads.

In pseudocode:

buffer = [] # oldest first; limits: 10,000 events or 10 MB
backoff = 1s
dropped = 0, dropped_errors = 0
log(event):
if environment in ("development", "test"): return
event.timestamp = now_utc()
event.context.environment = environment
if is_error(event) and release and not event.context.release:
event.context.release = release # read once at startup
trim(event) # at most 64 KB, context.truncated = true
while over_limits(buffer + event):
drop the oldest non-error event (an error only if nothing else is left)
count it in dropped (and dropped_errors)
append(buffer, event)
if is_error(event) or len(buffer) >= 10 or buffer past half a limit: wake_sender()
else: wake_sender_in(5s)
sender loop (one request at a time, in the background):
batch = take_from_front(buffer, at most 500 events and 1 MB)
answer = POST /api/v1/events/batch {"events": batch}
with timeouts (connect 5s, total 10s)
case answer:
2xx: backoff = 1s
if dropped: append the "dropped N events" report
503, 429: put_back_front(buffer, batch)
if Retry-After: sleep(Retry-After) # backoff unchanged
else: sleep(backoff); backoff = min(backoff * 2, 60s)
401, 403, 404: put_back_front(buffer, batch); write_stderr once
sleep(1 minute, doubling up to 5 minutes)
413, 400, 4xx: if len(batch) > 1: put both halves back at the front
else: count the event in dropped
5xx, 408, none: put_back_front(buffer, batch)
sleep(backoff ± 20%); backoff = min(backoff * 2, 60s)
on SIGTERM, SIGINT, exit:
send what is left within a few seconds
  1. Send one event by hand:

    Terminal window
    curl -i -X POST "$LOGNORTH_URL/api/v1/events/batch" \
    -H "Authorization: Bearer $LOGNORTH_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"events":[{"message":"GET /hello → 200","context":{"path":"/hello","status":200}}]}'

    You should get 201 Created and {"created":1}.

  2. Run the app with an environment other than development, and make a request.

  3. Look at the app in LogNorth, or ask your agent: the search_logs tool with since: 5m returns the event. Events appear within seconds.

A client that follows this page:

  • Sends to /api/v1/events/batch with Authorization: Bearer, in batches of at most 500 events and 1 MB.
  • Reads the URL and key from LOGNORTH_URL and LOGNORTH_API_KEY.
  • Sets the timestamp when the event happens, in UTC with milliseconds, and keeps it on every retry.
  • Keeps the logged order: first in, first out, failed batches back at the front, one request at a time.
  • Sends one event per request, per unhandled error, and per finished job.
  • Sends nothing in development and test, and sets context.environment everywhere else.
  • Puts user on requests when one is signed in, release on errors, and user_agent on failed requests.
  • Never sends on the request path and never raises into the app.
  • Trims each event to 64 KB and marks it truncated.
  • Limits the buffer to 10,000 events or 10 MB, and drops the oldest non-error event first.
  • Sends at 10 events, on an error, past half a limit, or after 5 seconds, and flushes on shutdown.
  • Keeps the events on 503, 429, 5xx, 401, 403, 404, network errors, and timeouts, and waits Retry-After when LogNorth sends it.
  • Splits a refused batch, and drops only a single event that is still refused.
  • Reports dropped events to LogNorth after the next success.
  • Uses a 5-second connect timeout and a 10-second request timeout.