> For the complete documentation index, see [llms.txt](https://docs.ipcheck.ing/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ipcheck.ing/developer/configuration/logging.md).

# Logging

Both Node processes — the backend API and the static file server — write to one shared [pino](https://getpino.io/) logger. Three environment variables control it. All three are optional.

| Variable     | Values                              | Default |
| ------------ | ----------------------------------- | ------- |
| `LOG_LEVEL`  | `debug` / `info` / `warn` / `error` | `info`  |
| `LOG_FORMAT` | `json`, or anything else            | pretty  |
| `LOG_HTTP`   | `"true"`                            | off     |

Everything goes to stdout. There is no built-in log file — your process manager (pm2, systemd, Docker) owns that.

## `LOG_LEVEL`

Sets the minimum severity that gets written. Anything below it is dropped before formatting, so a quiet level is genuinely cheap.

* `debug` — everything, including verbose dataset-update chatter. Useful when a MaxMind or CAIDA download misbehaves.
* `info` — the default. Startup lines, dataset loads, scheduled-job plans.
* `warn` — degradation only: rate-limited IPs, partial upstream failures, timeouts.
* `error` — handler failures only.

{% hint style="info" %}
`LOG_LEVEL` also controls what reaches Sentry. The Sentry bridge is installed inside the logger, so a line suppressed by the level never becomes a Sentry log or issue. See [Error Monitoring](/developer/configuration/error-monitoring.md).
{% endhint %}

## `LOG_FORMAT`

### Pretty (default)

Leave `LOG_FORMAT` empty and output goes through `pino-pretty`: colorized, one line per event, host-local timestamps with a UTC offset, `pid` and `hostname` omitted.

```
[2026-07-14 10:23:45.221 +0800] INFO: 🚀 Backend server ready on http://localhost:11966
```

This is what you want for `pnpm dev` and for reading `pm2 logs` or `docker logs` by eye.

### JSON

```bash
LOG_FORMAT="json"
```

Output becomes one raw JSON object per line — pino's native format — with no colors and no ANSI escapes:

```json
{"level":30,"time":1752459825221,"msg":"🚀 Backend server ready on http://localhost:11966"}
{"level":40,"time":1752460013887,"ip":"203.0.113.45","msg":"IP rate-limited"}
{"level":50,"time":1752460101044,"err":{"type":"Error","message":"Upstream responded 429"},"ip":"1.1.1.1","msg":"ipinfo-io handler failed"}
```

Field notes:

* `level` is numeric: `20` debug, `30` info, `40` warn, `50` error.
* `time` is epoch milliseconds.
* `msg` is the human-readable message.
* Any other keys are the structured context the call site attached — `ip`, `asn`, `prefix`, `err`, and so on.

Use JSON whenever something other than a human reads the logs. ANSI color codes in pretty mode confuse most parsers.

## `LOG_HTTP`

```bash
LOG_HTTP="true"
```

Turns on per-request logging for `/api/*` routes only. Off by default to keep process-manager logs readable.

```
INFO: GET /api/ipinfo?ip=1.1.1.1 → 200
WARN: GET /api/report/abc → 404
```

Details worth knowing:

* Each request logs method, URL, status code, and response time.
* The level maps to the outcome: `5xx` or a thrown error → `error`, `4xx` → `warn`, everything else → `info`.
* The middleware is mounted **before** the rate limiter, so `429` responses are logged too. See [Security Options](/developer/configuration/security-options.md).
* Handler-level failures are logged regardless of this flag — `LOG_HTTP` adds the successful requests, not the errors.

{% hint style="warning" %}
Request URLs contain query parameters, including the IP addresses visitors look up. On a public instance that is personal data. Turn `LOG_HTTP` on for debugging, then turn it back off — or make sure your retention policy covers it.
{% endhint %}

## Reading a healthy startup

Startup lines are emoji-prefixed so you can scan a boot at a glance.

| Line                                                                    | Meaning                                                                        |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `📝 HTTP request logging enabled (LOG_HTTP=true)`                       | `LOG_HTTP` is on                                                               |
| `🛡️ Rate limiter enabled — N requests per …`                           | `SECURITY_RATE_LIMIT` is set                                                   |
| `🐢 Speed limiter enabled — slow down after N requests`                 | `SECURITY_DELAY_AFTER` is set                                                  |
| `📥 MaxMind databases missing; attempting initial download …`           | First boot, GeoLite2 is being fetched                                          |
| `📦 MaxMind databases loaded (…)`                                       | Geolocation is ready                                                           |
| `📦 CAIDA as2org loaded (…)` / `📦 CAIDA as-rel loaded (…)`             | ASN org names and the connectivity graph are ready                             |
| `🗓️ … auto update plan: next check at …`                               | A scheduled dataset refresh is armed                                           |
| `📦 Service status cache primed`                                        | The service-status page has data                                               |
| `🛰️ Sentry backend monitoring enabled`                                 | `SENTRY_DSN_BACKEND` is set                                                    |
| `🚀 Backend server ready on http://localhost:11966`                     | API is accepting traffic                                                       |
| `🚀 Static file server ready on http://localhost:18966`                 | The SPA is being served                                                        |
| `❌ MaxMind API will return 503 until databases are loaded successfully` | **Problem** — see [MaxMind Setup](/developer/getting-started/maxmind-setup.md) |

Both `🚀` lines say `localhost` because that is the address the process binds inside its own host or container. It is not a hint about your public URL.

## Shipping JSON logs

Set `LOG_FORMAT="json"` and let your platform collect stdout. Nothing else in the app needs to change.

{% tabs %}
{% tab title="Docker" %}

```bash
docker run -d -p 18966:18966 \
  -e LOG_FORMAT="json" \
  -e LOG_LEVEL="info" \
  --log-driver=json-file \
  --name myip \
  jason5ng32/myip:latest
```

From there, any Docker logging driver or a sidecar collector (Vector, Fluent Bit, Promtail, the Datadog agent) picks the lines up. Each line is already valid JSON, so no multiline or regex parsing is needed.
{% endtab %}

{% tab title="pm2" %}
{% code title=".env" %}

```bash
LOG_FORMAT="json"
LOG_LEVEL="info"
```

{% endcode %}

pm2 writes stdout to its own log files; point your collector at those paths (`pm2 info <name>` shows them).

{% hint style="warning" %}
pm2 snapshots environment variables when a process is first started. `pm2 restart` replays the old snapshot. After changing `.env`, do `pm2 delete <name> && pm2 start ecosystem.config.cjs && pm2 save`.
{% endhint %}
{% endtab %}

{% tab title="Ad-hoc / jq" %}

```bash
# Errors only, most recent first
docker logs myip 2>&1 | jq -c 'select(.level >= 50)'

# Which IPs got rate-limited
docker logs myip 2>&1 | jq -r 'select(.msg == "IP rate-limited") | .ip' | sort | uniq -c

# Re-prettify a JSON stream for human reading
docker logs myip 2>&1 | npx pino-pretty
```

{% endtab %}
{% endtabs %}

A reasonable production baseline: `LOG_FORMAT="json"`, `LOG_LEVEL="info"`, `LOG_HTTP` off. Add `LOG_HTTP="true"` temporarily when you need per-request visibility.

## Related pages

* [Error Monitoring](/developer/configuration/error-monitoring.md) — how `warn` and `error` lines reach Sentry
* [Security Options](/developer/configuration/security-options.md) — the events behind the `IP rate-limited` warnings
* [Environment Variables](/developer/reference/environment-variables.md) — the full list
* [Backend](/developer/architecture/backend.md) — where the logger sits in the request path


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ipcheck.ing/developer/configuration/logging.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
