MongoDB Mastery
MongoDB Mastery lessons / Module 28: Monitoring & Diagnostics

Server logs: structured JSON, verbosity and log rotation

Level 4: Database Engineer22 min

mongod writes one JSON document per log line. Learn to read the fields, find slow queries and errors, raise verbosity for one component without flooding the disk, and rotate logs safely on a self-hosted server.

This lesson is interactive. Run every example against an in-browser MongoDB, see explain() plans drawn as trees, and take the quiz.
Open the interactive lesson
You will learn to
  • Read the t, s, c, id, ctx, msg and attr fields of a log line
  • Find slow queries with getLog in the shell and with jq on the file
  • Change slowms and component verbosity at runtime, then put them back
  • Configure log rotation with logrotate and systemLog.logRotate

conceptThe concept

Since 4.4 every line in mongod.log is a JSON document:

  • t: timestamp, s: severity (F fatal, E error, W warning, I info, D1 to D5 debug), c: component (COMMAND, NETWORK, REPL, STORAGE, ACCESS, CONTROL and others).
  • id: a stable numeric identifier for the message. Search and alert on the id, not the wording; the wording can change between releases. Slow operations are id 51803, "Slow query".
  • ctx: the thread or connection (conn1234), msg: the message, attr: structured details such as ns, durationMillis, planSummary, keysExamined, docsExamined.

What gets logged. At the default verbosity 0, an operation is logged when it takes longer than slowms (100 ms by default, set with db.setProfilingLevel(level, { slowms }) or operationProfiling.slowOpThresholdMs). The profiler level does not matter: level 0 still logs slow operations. slowOpSampleRate logs only a fraction of them on very busy servers.

Verbosity is set globally and per component, from 0 (info) to 5 (very chatty debug); -1 means "inherit from the parent". db.setLogLevel(2, "replication.heartbeats") raises one component at runtime; db.getLogComponents() shows the whole tree. With the command component at 1 or higher, every operation is logged regardless of duration, so raise it briefly and lower it again.

The server also keeps the most recent 1,024 lines in memory. db.adminCommand({ getLog: "global" }) returns them as JSON strings, and show log startupWarnings prints the warnings from startup, which is where problems like low ulimits or an unsupported filesystem first appear.

whyWhy it matters

Logs are the first place to look after an incident and the only record of slow queries when the profiler is off. Structured lines make them easy to filter, aggregate and ship to a log platform, but only if you know the fields and keep verbosity and rotation under control. A debug level left on for a week can fill the log volume and stop the server.

Use caseReal-world scenario

At 14:05 the checkout API slowed down for ten minutes. The on-call DBA filters mongod.log for id 51803 between 14:00 and 14:20, groups by attr.ns and attr.planSummary, and finds hundreds of COLLSCANs on orders from one appName: a new report that filtered on an unindexed field. The fix is an index and a meeting with the report owner; the evidence came from the log.

Try itExamples

Run and edit these in the interactive lesson.

List the in-memory logs and read the startup warnings
show logs
show log startupWarnings
A W line at startup (ulimits, filesystem, missing access control) is a configuration problem waiting to become an incident.
Find slow queries in the log
db.orders.find({ "shipping.city": "Pune" }).toArray().length
db.adminCommand({ getLog: "global" }).log
  .map(JSON.parse)
  .filter(l => l.id === 51803)
  .map(l => ({ ns: l.attr.ns, ms: l.attr.durationMillis, plan: l.attr.planSummary, docsExamined: l.attr.docsExamined, nreturned: l.attr.nreturned }))
shipping.city has no index, so the query scans all 5,000 orders and crosses the 100 ms threshold. The log line has everything needed to decide on an index.
Lower slowms to see more operations
db.setProfilingLevel(0, { slowms: 50 })
db.events.find({ type: "purchase" }).toArray().length
const slow = db.adminCommand({ getLog: "global" }).log.map(JSON.parse).filter(l => l.id === 51803)
slow.map(l => l.attr.ns + " " + l.attr.durationMillis + " ms")
db.setProfilingLevel(0, { slowms: 100 })
Profiler level 0 means nothing is written to system.profile, but slowms still decides what reaches the log.
Raise verbosity for one component, then put it back
db.setLogLevel(1, "command")
db.products.find({ sku: "SKU-0001" }).toArray().length
const fast = db.adminCommand({ getLog: "global" }).log.map(JSON.parse).filter(l => l.id === 51803 && l.attr.ns === "shop.products")
db.setLogLevel(-1, "command")
fast.map(l => l.attr.durationMillis + " ms, " + l.attr.planSummary)
At command verbosity 1 even a fast query is logged. -1 returns the component to inheriting the global level.
The component tree and the same change as a parameter
db.adminCommand({ setParameter: 1, logComponentVerbosity: { replication: { heartbeats: { verbosity: 2 } } } })
db.adminCommand({ getParameter: 1, logComponentVerbosity: 1 }).logComponentVerbosity.replication
db.setLogLevel(-1, "replication.heartbeats")
getLog only runs against admin
db.runCommand({ getLog: "global" })
Rotate the log
db.adminCommand({ logRotate: 1 })
mongod.conf and logrotate on a Linux host
# /etc/mongod.conf
systemLog:
  destination: file
  path: /var/log/mongodb/mongod.log
  logAppend: true
  logRotate: reopen          # logrotate moves the file, mongod reopens the path
operationProfiling:
  mode: off
  slowOpThresholdMs: 100
  slowOpSampleRate: 1.0

# /etc/logrotate.d/mongod
/var/log/mongodb/mongod.log {
  daily
  rotate 14
  compress
  delaycompress
  missingok
  notifempty
  sharedscripts
  postrotate
    /bin/kill -SIGUSR1 $(pidof mongod) 2>/dev/null || true
  endscript
}
Filtering the log file with jq
# slow queries: namespace, duration, plan
jq -c 'select(.id == 51803) | {t: .t["$date"], ns: .attr.ns, ms: .attr.durationMillis, plan: .attr.planSummary, docs: .attr.docsExamined}' /var/log/mongodb/mongod.log

# which namespaces and plans are slow most often
jq -r 'select(.id == 51803) | [.attr.ns, .attr.planSummary] | @tsv' /var/log/mongodb/mongod.log | sort | uniq -c | sort -rn | head

# errors and fatals only
jq -c 'select(.s == "E" or .s == "F") | {t: .t["$date"], c, id, msg}' /var/log/mongodb/mongod.log

internalsUnder the hood

Log lines are written synchronously by the thread that produced them, so extremely high verbosity costs real latency, not only disk. Long attributes are truncated (maxLogSizeKB, 10 KB by default), which is why very large queries appear with a truncated marker.

systemLog.logRotate decides what logRotate or SIGUSR1 does: rename (the default) renames the current file with a timestamp and opens a new one; reopen closes and reopens the same path, which is what Linux logrotate expects after it has moved the file (and requires logAppend: true).

Avoid / preferDo this, not that

Avoid
grep "Slow query" /var/log/mongodb/mongod.log | head
Prefer
jq -c 'select(.id == 51803) | {ns: .attr.ns, ms: .attr.durationMillis, plan: .attr.planSummary}' /var/log/mongodb/mongod.log

grep returns raw JSON that is hard to read and matches the text anywhere in the line. jq selects by id and extracts the fields you need, and the output can be sorted and counted.

Avoid
db.adminCommand({ setParameter: 1, logLevel: 3 })   // and then forget
Prefer
db.setLogLevel(2, "replication.election")
// reproduce, read the log
db.setLogLevel(-1, "replication.election")

A global debug level logs everything from every component, costs latency and disk, and is easy to forget. Raise one component and put it back.

PitfallsCommon mistakes

  • Searching logs for message text instead of the numeric id, then missing lines after an upgrade reworded them.
  • Setting the global log level to 2 or more to "see what is happening" and leaving it on until the disk fills.
  • Using logRotate: rename together with logrotate, so both rename the file and the server keeps writing to a moved file.
  • Treating profiler level 0 as "slow query logging off". slowms still applies to the log.

LimitsWhen not to use it

Logs are for events and slow operations, not for metrics. Do not raise verbosity to measure throughput or cache pressure; use serverStatus, FTDC or PMM, which sample continuously at almost no cost. For a full record of who did what, use auditing rather than a high log level.

SecuritySecurity notes

Slow query lines include the filter, so they can contain email addresses, names or tokens. Restrict the log directory to the mongod user, ship logs over TLS to a platform with access control, and consider security.redactClientLogData (MongoDB Enterprise and Percona Server for MongoDB), which replaces values in log messages with "###".

ChallengeTry it yourself

Run a query on reviews that filters on title (not indexed) after setting slowms to 20. Then, from getLog, return only the slow-query lines for shop.reviews as objects with plan and docsExamined. Finally set slowms back to 100.

Show one solution
One solution
db.setProfilingLevel(0, { slowms: 20 })
db.reviews.find({ title: "Exactly as described" }).toArray().length
const lines = db.adminCommand({ getLog: "global" }).log.map(JSON.parse)
const out = lines.filter(l => l.id === 51803 && l.attr.ns === "shop.reviews").map(l => ({ plan: l.attr.planSummary, docsExamined: l.attr.docsExamined }))
db.setProfilingLevel(0, { slowms: 100 })
out

InterviewQuestions you should be able to answer

How do you find what was slow on a self-hosted MongoDB at a specific time yesterday?

Filter mongod.log (and rotated files) for id 51803 in the time window, for example with jq. Group by attr.ns, attr.planSummary and attr.appName, and look at docsExamined versus nreturned. COLLSCANs or a large docsExamined to nreturned ratio point to missing or wrong indexes. Correlate with FTDC or PMM metrics for the same window to separate query problems from resource saturation, and confirm the fix with explain("executionStats").

Explain log verbosity in MongoDB and how you would debug elections without flooding the log.

Verbosity is a tree: a global level plus per-component levels, where -1 inherits from the parent. Raise only the component you need, for example db.setLogLevel(2, "replication.election") or replication.heartbeats, reproduce or wait for the event, then set it back to -1. Make the change at runtime rather than in the config file so a restart cannot leave it on, and watch log volume while it is raised.

QuizCheck your understanding

5 questions with instant feedback in the interactive lesson; a wrong answer goes into your review deck.