---
topic: logs
title: Reading the shop's four log sources
summary: read_logs over tjtllog, teinstellungenlog, tzahlungslog and auth.log — two inverted level scales, one normalised vocabulary, and what is never logged.
appliesTo:
  - payment.method.saveConfig
derivedFrom:
  - Plugin\tnt_mcp\src\Mcp\Log\LogSource::columns
  - Plugin\tnt_mcp\src\Mcp\Log\LogLevel::fromPayment
  - Plugin\tnt_mcp\src\Mcp\Log\LogReader::read
  - build/components/vue-installer/initial_schema.sql:1733-1741
  - build/components/vue-installer/initial_schema.sql:5153-5161
  - includes/src/Services/Factory.php:101-124
  - includes/src/Backend/AdminLoginConfig.php:13-15
---

## Domain model

`read_logs` is one tool over four unrelated stores, unified behind one shape. `source: "system"` is
`tjtllog` — `kLog`, `nLevel`, `cLog`, `cKey`, `kKey`, `dErstellt` (`initial_schema.sql:1733-1741`),
where `cKey` is the Monolog channel and `kKey` the id of whatever that channel names
(`LogSource.php:35-42`, `LogReader.php:390-396`). `source: "settings"` is `teinstellungenlog`, an
audit trail with no severity at all: `kEinstellungenLog`, `cEinstellungenName`,
`cEinstellungenWertAlt`, `cEinstellungenWertNeu`, `dDatum` (`Migration20210208144100.php:31-40`),
plus `cAdminname` (`Migration20210823135200.php:30-32`) and `cIP`
(`Migration20210915151919.php:30`). `source: "payment"` is `tzahlungslog` — `kZahlunglog`,
`cModulId`, `cLog`, `cLogData`, `nLevel`, `dDatum` (`initial_schema.sql:5153-5161`).
`source: "auth_file"` is not a table but `jtllogs/auth.log`, read by tailing the last 64 KB
(`AuthLogFile.php:27`). Time columns differ per source (`dErstellt` for system, `dDatum` for the
other two) — filter with `since`/`until` and never name the column yourself.

**The two level scales run in opposite directions.** `tjtllog.nLevel` is a Monolog int — 100 debug,
200 info, 300 warning, 400+ error, exactly the breakpoints the backend's own filter uses
(`SystemLogController.php:96-107`). `tzahlungslog.nLevel` is a `tinyint(1)` whose column comment is
`'1=Error, 2=Notice, 3=Debug'` (`initial_schema.sql:5158`) — **lower means worse**. "At least
warning" is therefore `nLevel >= 300` on one table and `nLevel <= 1` on the other
(`LogLevel.php:76-98`). `read_logs` normalises both into one vocabulary, `debug | info | warning |
error` (`LogLevel::ORDER`, `LogLevel.php:30`), via `fromMonolog()` (`:52-61`) and `fromPayment()`
(`:63-70`), and returns the untranslated int alongside as `rawLevel` (`LogReader.php:381-389`) so
you can always check the translation. The payment scale has **no warning member**, so `warning` and
`error` select the same rows there (`LogLevel::paymentAtMost`, `:91-98`).

## Reads first

1. `read_logs({"source": "system"})` with no other argument. Defaults fire and are declared in
   `notes`: `since` = last 24h, `level` = `warning`, `limit` = 20 entries
   (`LogQuery.php:27-30`, `:70`, `:79-82`). Read the header before the entries — `totalMatching`,
   `byLevel`, `byChannel`, `topMessages` cover the **whole window**, not the returned page
   (`LogReader.php:22-24`), so page one already carries trustworthy totals.
2. Pick a channel out of `byChannel` (that is `cKey`) and re-run with `channel` set. Do not guess
   channel names; they are whatever code passed to Monolog.
3. `read_logs({"source": "payment", "level": "error"})` — `byChannel` here is `cModulId`, the
   payment module id, which is the string you would need for `payment.method.saveConfig`.
4. `read_logs({"source": "settings", "since": "7d"})` — who changed what. `byLevel`/`byChannel` are
   absent by design; `byAdmin` replaces them and a `level` filter is ignored (`LogReader.php:66-74`).

## Walkthrough

Question: *"checkout has been failing since this morning — what broke?"*

Step 1 — the wide look:
`read_logs({"source": "system", "since": "12h", "level": "warning"})`. Read `topMessages` first: it
groups on the first 120 characters (`LogReader.php:35`, `:222-233`), so 1200 identical timeouts
collapse into one row with `count`, `firstSeen`, `lastSeen`. A `firstSeen` that matches "this
morning" is your candidate; `count` tells you whether it is the story or noise.

Step 2 — the payment side, which is a different table entirely:
`read_logs({"source": "payment", "since": "12h", "level": "error"})`. `byChannel` names the failing
module. Expect a note that the payment scale has no warning level and that `cLogData` — the raw
module payload — is deliberately not returned because it is unbounded (`LogReader.php:75-80`); fetch
it with `run_sql_read` for one specific `kZahlunglog` if you truly need it.

Step 3 — one transaction end to end:
`read_logs({"source": "payment", "transactionId": "<merchant-facing id>", "since": "7d"})`. This
joins `paymentlog_transaction_entries` and `paymentlog_transactions`
(`LogReader.php:136-145`); the merchant-facing id is the `VARCHAR` on `paymentlog_transactions`, not
the integer key on the entries table. Passing `transactionId` with any other source is ignored, with
a note (`LogReader.php:81-83`).

Step 4 — did a human cause it?
`read_logs({"source": "settings", "since": "12h"})`. Entries are `{setting, oldValue, newValue,
admin, ip}` (`LogReader.php:369-378`). A payment setting changed at 08:40 and errors starting 08:41
is a cause; correlate on time before claiming it.

Step 5 — page only if you must. `nextCursor` is keyset, not offset (`LogReader.php:248-256`); pass
it back verbatim. Narrowing beats paging.

## Three most common failures

1. **Treating `tzahlungslog.nLevel = 3` as more severe than 1.** It is the least severe (debug); the
   scale is inverted relative to `tjtllog` (`initial_schema.sql:5158` vs `LogLevel.php:33`). If you
   bypass `read_logs` and write your own `run_sql_read` against `tzahlungslog`, a `nLevel >= 300`
   filter copied from `tjtllog` matches **nothing** and you will report "no payment errors" to a
   shop that is drowning in them.

2. **Reading "3 entries" as "3 problems".** `read_logs` defaults to the last 24 hours, severity
   `warning` and 20 rows (`LogQuery.php:27-30`); everything below that threshold is not counted
   anywhere in the response, not even in `totalMatching`. Every default that fires is listed in
   `notes` — read them, and pass `level: "debug"` before concluding a source is quiet.

3. **Expecting `auth_file` to exist.** It usually does not (see below). When it is missing the tool
   returns `available: false` with a `reason`, not an error (`LogReader.php:421-429`) — a fact about
   the shop's configuration, not something to retry.

## What you cannot do here

**Admin logins are logged nowhere on a stock shop — this is the landmine.** The handler set for the
`auth` channel is built by looping over the setting `admin_login_logger_mode` and comparing each
value against `AdminLoginConfig::CONFIG_DB` (`'db'`) and `CONFIG_FILE` (`'file'`)
(`Factory.php:101-114`, `AdminLoginConfig.php:13-15`). The shipped default value of that setting is
`'1'` (`initial_schema.sql:1253`; same default in `Migration20180319103900.php:62-73`), which
matches neither constant, so **zero handlers** are attached and `new Logger('auth', [])` discards
every record. Consequence: `auth_file` is unavailable *and* `system` contains no login records
either. When the setting is set to `db`, records land in `tjtllog` with `cKey = 'auth'` — the
Monolog channel name written straight into `cKey` by `NiceDBHandler.php:38` — at INFO level, which
the default `level: "warning"` filter would hide. So the working query is
`read_logs({"source": "system", "channel": "auth", "level": "info"})`, and if it returns nothing the
honest answer is "this shop does not record admin logins", not "there were no logins".

**Most of `tjtllog` is never written either.** The shop logger's minimum level comes from the
setting `systemlog_flag`, which ships as `400` (`initial_schema.sql:1253`) and is passed straight to
the DB handler (`Factory.php:117-124`). On a default shop only **errors** reach `tjtllog`; asking
for `level: "debug"` cannot recover warnings that were never recorded. Raising `systemlog_flag` is a
settings change, not a log query.

**No writes, no deletes, no rotation.** `read_logs` is `logs:read` and `isWrite() === false`
(`ReadLogs.php:110-118`). You cannot clear a log, change a log level, or enable file logging from
this tool — those are settings writes, and only the settings tools can make them.

**No PHP error log, no web-server log, no cron output.** The four sources are all there is.
`auth.log` is the only file; it is tailed at 64 KB and does **not** paginate (`AuthLogFile.php:27`,
`LogReader.php:477-486`), so older lines are missing from its counts too. Messages are cut at 400
characters (`LogReader.php:32`) and secret-looking values become `***` before truncation
(`LogRedactor.php:21`, `LogReader.php:401-415`) — a redacted value is not retrievable here.
