mdbkit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mdbkit/triage.py ADDED
@@ -0,0 +1,360 @@
1
+ """`mdbkit triage` — one-command incident snapshot (beta).
2
+
3
+ At 3 a.m., on a box with nothing installed but mdbkit, one command answers:
4
+ what is unhealthy, what changed, and where to look next.
5
+
6
+ Read-only, offline, single pass over the log plus optional local OS probes
7
+ (statvfs/proc — nothing leaves the machine). Never connects to a database,
8
+ never mutates anything; findings include next steps for a HUMAN to run.
9
+
10
+ Detector validation status:
11
+ stable : restarts, fatal errors, connection storms, hot collections
12
+ beta : elections/stepdowns, slow checkpoints, eviction pressure,
13
+ flow control — pattern-matched from documented log messages,
14
+ pending validation against real-cluster fixtures
15
+ (see docs/TESTING-PLAYBOOK.md).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import re
22
+ from collections import Counter, defaultdict
23
+ from dataclasses import dataclass, field
24
+ from datetime import timedelta
25
+ from typing import List, Optional
26
+
27
+ from .analysis import QueryAggregator
28
+ from .parser import ID_CONN_ACCEPTED, ID_STARTUP, LogEntry, ParseStats, iter_entries
29
+
30
+ SEV_ORDER = {"CRIT": 0, "WARN": 1, "INFO": 2, "OK": 3}
31
+
32
+
33
+ @dataclass
34
+ class Finding:
35
+ severity: str # CRIT | WARN | INFO | OK
36
+ title: str
37
+ detail: str
38
+ evidence: List[str] = field(default_factory=list)
39
+ next_step: str = ""
40
+ beta: bool = False
41
+
42
+ def to_dict(self) -> dict:
43
+ return {"severity": self.severity, "title": self.title,
44
+ "detail": self.detail, "evidence": self.evidence,
45
+ "nextStep": self.next_step, "beta": self.beta}
46
+
47
+
48
+ def _minute(ts) -> int:
49
+ return int(ts.timestamp() // 60)
50
+
51
+
52
+ def _fmt_min(m: int) -> str:
53
+ from datetime import datetime, timezone
54
+ return datetime.fromtimestamp(m * 60, tz=timezone.utc).strftime("%H:%M")
55
+
56
+
57
+ class TriageEngine:
58
+ """Single-pass log consumer feeding all detectors."""
59
+
60
+ ELECTION_EVENT = ("starting an election", "election succeeded",
61
+ "stepping down", "stepped down")
62
+ ELECTION_STATE = ("member is in new state", "replica set state transition",
63
+ "transition to")
64
+
65
+ def __init__(self):
66
+ self.qagg = QueryAggregator()
67
+ self.startups: List = []
68
+ self.errors: Counter = Counter()
69
+ self.error_examples = {}
70
+ self.conn_minutes: Counter = Counter()
71
+ self.conn_ips: defaultdict = defaultdict(Counter)
72
+ self.elections: List = [] # (ts, kind, msg)
73
+ self.state_changes: List = [] # (ts, msg, newState)
74
+ self.checkpoints: List = [] # (ts, duration_ms or None)
75
+ self.evictions = 0
76
+ self.flow_control = 0
77
+ self.dbpath: Optional[str] = None
78
+
79
+ # ---------------------------------------------------------- consume ----
80
+ def consume(self, entry: LogEntry):
81
+ self.qagg.consume(entry)
82
+ msg_l = entry.msg.lower()
83
+ wt_msg = str(entry.attr.get("message", "")) if entry.attr else ""
84
+
85
+ if entry.msg_id == ID_STARTUP:
86
+ self.startups.append(entry.ts)
87
+ self.dbpath = entry.attr.get("dbPath") or self.dbpath
88
+ elif entry.severity in ("E", "F"):
89
+ key = (entry.component, entry.msg)
90
+ self.errors[key] += 1
91
+ self.error_examples.setdefault(key, entry.ts)
92
+ if entry.msg_id == ID_CONN_ACCEPTED and entry.ts is not None:
93
+ m = _minute(entry.ts)
94
+ self.conn_minutes[m] += 1
95
+ remote = str(entry.attr.get("remote", "")).rsplit(":", 1)[0]
96
+ self.conn_ips[m][remote] += 1
97
+ if entry.component in ("REPL", "ELECTION"):
98
+ if any(p in msg_l for p in self.ELECTION_EVENT) and \
99
+ "not starting" not in msg_l:
100
+ self.elections.append((entry.ts, entry.msg))
101
+ elif any(p in msg_l for p in self.ELECTION_STATE):
102
+ new_state = str(entry.attr.get("newState",
103
+ entry.attr.get("memberState", "")))
104
+ self.state_changes.append((entry.ts, entry.msg, new_state))
105
+ if "flow control" in msg_l or "flow control" in wt_msg.lower():
106
+ self.flow_control += 1
107
+ if "checkpoint" in msg_l or "checkpoint" in wt_msg.lower():
108
+ dur = entry.attr.get("durationMillis")
109
+ if dur is None:
110
+ m = re.search(r"took (\d+) second", wt_msg.lower())
111
+ dur = int(m.group(1)) * 1000 if m else None
112
+ self.checkpoints.append((entry.ts, dur))
113
+ if "eviction" in msg_l or "eviction" in wt_msg.lower():
114
+ self.evictions += 1
115
+
116
+ # --------------------------------------------------------- findings ----
117
+ def findings(self) -> List[Finding]:
118
+ out: List[Finding] = []
119
+
120
+ # Elections / stepdowns (beta).
121
+ if self.elections:
122
+ times = [t.strftime("%H:%M:%S") if t else "?" for t, _ in self.elections]
123
+ out.append(Finding(
124
+ "CRIT", "Replica set instability",
125
+ f"{len(self.elections)} election/stepdown event(s) in window "
126
+ f"at {', '.join(times[:6])}.",
127
+ [m for _, m in self.elections[:6]],
128
+ "Check node health/network at those timestamps; correlate "
129
+ "with connection storms and slow checkpoints below. Timeline "
130
+ "of state changes: see --json output.", beta=True))
131
+ else:
132
+ out.append(Finding("OK", "Replica set",
133
+ "No election or stepdown messages found.",
134
+ beta=True))
135
+
136
+ # Restarts.
137
+ if self.startups:
138
+ ts = [t.strftime("%H:%M:%S") if t else "?" for t in self.startups]
139
+ out.append(Finding(
140
+ "WARN", "Process start(s) in window",
141
+ f"mongod startup marker seen {len(self.startups)}x "
142
+ f"(at {', '.join(ts[:5])}). Unplanned restarts are incidents.",
143
+ next_step="If unexpected, check system OOM killer "
144
+ "(journalctl / dmesg) and FatalError findings."))
145
+
146
+ # Fatal / error lines.
147
+ if self.errors:
148
+ total = sum(self.errors.values())
149
+ top = self.errors.most_common(5)
150
+ out.append(Finding(
151
+ "WARN", "Error-severity log lines",
152
+ f"{total} E/F line(s) in window.",
153
+ [f"{n}x [{c}] {m}" for (c, m), n in top],
154
+ "Read the raw lines: mdbkit filter <log> --severity E"))
155
+ else:
156
+ out.append(Finding("OK", "Errors",
157
+ "No error/fatal severity lines in window."))
158
+
159
+ # Connection storm.
160
+ storm = self._storm_finding()
161
+ out.append(storm)
162
+
163
+ # Hot collection.
164
+ out.append(self._hot_collection_finding())
165
+
166
+ # Checkpoints (beta).
167
+ slow_cp = [(t, d) for t, d in self.checkpoints if d and d >= 60_000]
168
+ if slow_cp:
169
+ worst = max(d for _, d in slow_cp)
170
+ out.append(Finding(
171
+ "WARN", "Slow WiredTiger checkpoints",
172
+ f"{len(slow_cp)} checkpoint(s) over 60s (worst "
173
+ f"{worst/1000:.1f}s). Often disk I/O saturation or huge "
174
+ "dirty cache.",
175
+ next_step="Check disk latency/utilization at those times "
176
+ "(FTDC will cover this in v0.2).", beta=True))
177
+ elif self.checkpoints:
178
+ out.append(Finding("INFO", "WiredTiger checkpoints",
179
+ f"{len(self.checkpoints)} checkpoint "
180
+ "message(s), none flagged slow (>60s).",
181
+ beta=True))
182
+
183
+ # Eviction / flow control (beta).
184
+ if self.evictions:
185
+ out.append(Finding(
186
+ "WARN", "Cache eviction pressure indicators",
187
+ f"{self.evictions} eviction-related message(s) — application "
188
+ "threads may be doing eviction work (cache too small or "
189
+ "workload spike).",
190
+ next_step="Check WT cache usage vs configured max "
191
+ "(db.serverStatus().wiredTiger.cache).", beta=True))
192
+ if self.flow_control:
193
+ out.append(Finding(
194
+ "WARN", "Flow control engaged",
195
+ f"{self.flow_control} flow-control message(s): the primary "
196
+ "throttled writes because majority-commit point lagged.",
197
+ next_step="Check secondary health/lag and network.",
198
+ beta=True))
199
+ return out
200
+
201
+ def _storm_finding(self) -> Finding:
202
+ if not self.conn_minutes:
203
+ return Finding("INFO", "Connections",
204
+ "No connection-accepted events in window.")
205
+ counts = sorted(self.conn_minutes.values())
206
+ baseline = counts[:-1] or counts # the busiest minute can't be its own baseline
207
+ median = baseline[(len(baseline) - 1) // 2]
208
+ threshold = max(60, 10 * max(1, median))
209
+ storms = {m: n for m, n in self.conn_minutes.items() if n >= threshold}
210
+ if not storms:
211
+ return Finding("OK", "Connections",
212
+ f"No connection storms (peak "
213
+ f"{max(counts)}/min, median {median}/min).")
214
+ worst_min = max(storms, key=storms.get)
215
+ top_ips = self.conn_ips[worst_min].most_common(3)
216
+ return Finding(
217
+ "WARN", "Connection storm",
218
+ f"{len(storms)} minute(s) at >= {threshold} new connections/min "
219
+ f"(baseline median {median}/min); worst {storms[worst_min]} at "
220
+ f"{_fmt_min(worst_min)} UTC.",
221
+ [f"{ip or 'unknown'}: {n} in worst minute" for ip, n in top_ips],
222
+ "Identify the client (appName via `mdbkit connections`); check "
223
+ "for pool misconfiguration or crash-loop reconnects.")
224
+
225
+ def _hot_collection_finding(self) -> Finding:
226
+ shapes = self.qagg.results()
227
+ if not shapes:
228
+ return Finding("INFO", "Slow queries",
229
+ "No slow queries logged in window (slowms "
230
+ "default is 100 ms).")
231
+ by_ns: Counter = Counter()
232
+ for s in shapes:
233
+ by_ns[s.shape.ns] += s.total_ms
234
+ total = sum(by_ns.values()) or 1
235
+ ns, ms = by_ns.most_common(1)[0]
236
+ share = 100.0 * ms / total
237
+ ns_shapes = [s for s in shapes if s.shape.ns == ns]
238
+ worst = max(ns_shapes, key=lambda s: s.total_ms)
239
+ sev = "WARN" if share >= 50 and len(shapes) >= 3 else "INFO"
240
+ return Finding(
241
+ sev, "Hot collection",
242
+ f"{ns} accounts for {share:.0f}% of slow-query time "
243
+ f"({ms/1000:.1f}s across {len(ns_shapes)} shape(s)).",
244
+ [f"worst shape: {worst.shape.pretty()} "
245
+ f"({worst.count}x, {worst.total_ms/1000:.1f}s total)"],
246
+ f"mdbkit advise <log> # candidates for {ns}")
247
+
248
+
249
+ # ------------------------------------------------------------- sysprobe ----
250
+
251
+ def sysprobe(dbpath: Optional[str]) -> List[Finding]:
252
+ """Local OS probes. Stdlib only, no shell-outs, everything try/except."""
253
+ out: List[Finding] = []
254
+
255
+ if dbpath and os.path.isdir(dbpath):
256
+ try:
257
+ st = os.statvfs(dbpath)
258
+ free = st.f_bavail * st.f_frsize
259
+ totalb = st.f_blocks * st.f_frsize or 1
260
+ used_pct = 100.0 * (1 - st.f_bavail / (st.f_blocks or 1))
261
+ sev = "CRIT" if used_pct >= 95 else "WARN" if used_pct >= 85 else "OK"
262
+ out.append(Finding(
263
+ sev, "Disk (dbPath volume)",
264
+ f"{dbpath}: {used_pct:.0f}% used, "
265
+ f"{free / 2**30:.1f} GiB free of {totalb / 2**30:.1f} GiB.",
266
+ next_step="" if sev == "OK" else
267
+ "Free space or extend the volume; a full dbPath volume "
268
+ "stops writes and can corrupt shutdown."))
269
+ except OSError as exc:
270
+ out.append(Finding("INFO", "Disk probe unavailable", str(exc)))
271
+ else:
272
+ out.append(Finding(
273
+ "INFO", "System probes skipped",
274
+ "dbPath from the log was not found on this machine — the log "
275
+ "appears to be from another host. Run triage on the DB host "
276
+ "for disk/memory/load checks, or pass --dbpath."))
277
+ return out
278
+
279
+ try:
280
+ info = {}
281
+ with open("/proc/meminfo") as fh:
282
+ for line in fh:
283
+ k, _, rest = line.partition(":")
284
+ info[k] = int(rest.strip().split()[0]) # kB
285
+ avail, total = info.get("MemAvailable", 0), info.get("MemTotal", 1)
286
+ pct = 100.0 * avail / total
287
+ sev = "WARN" if pct < 10 else "OK"
288
+ out.append(Finding(sev, "Memory",
289
+ f"{pct:.0f}% available "
290
+ f"({avail / 2**20:.1f} GiB of {total / 2**20:.1f} GiB).",
291
+ next_step="" if sev == "OK" else
292
+ "Check for OOM risk: page cache squeeze, other "
293
+ "processes, or WT cache oversized."))
294
+ except (OSError, ValueError, KeyError):
295
+ out.append(Finding("INFO", "Memory probe unavailable",
296
+ "/proc/meminfo not readable (non-Linux?)."))
297
+
298
+ try:
299
+ load1, _, _ = os.getloadavg()
300
+ cores = os.cpu_count() or 1
301
+ sev = "WARN" if load1 > 2 * cores else "OK"
302
+ out.append(Finding(sev, "CPU load",
303
+ f"load1={load1:.1f} on {cores} core(s).",
304
+ next_step="" if sev == "OK" else
305
+ "Identify hot queries: mdbkit queries <log> "
306
+ "--sort totalMs"))
307
+ except OSError:
308
+ out.append(Finding("INFO", "Load probe unavailable", ""))
309
+ return out
310
+
311
+
312
+ # ------------------------------------------------------------------ run ----
313
+
314
+ def run_triage(logfile: str, window_min: Optional[int] = None,
315
+ dbpath: Optional[str] = None, no_sysprobe: bool = False):
316
+ cutoff = None
317
+ if window_min and logfile != "-":
318
+ pre = ParseStats()
319
+ for _ in iter_entries(logfile, pre):
320
+ pass
321
+ if pre.last_ts:
322
+ cutoff = pre.last_ts - timedelta(minutes=window_min)
323
+
324
+ stats = ParseStats()
325
+ engine = TriageEngine()
326
+ for entry in iter_entries(logfile, stats):
327
+ if cutoff and entry.ts and entry.ts < cutoff:
328
+ continue
329
+ engine.consume(entry)
330
+
331
+ findings = engine.findings()
332
+ if not no_sysprobe:
333
+ findings += sysprobe(dbpath or engine.dbpath)
334
+ findings.sort(key=lambda f: SEV_ORDER.get(f.severity, 9))
335
+ return findings, stats, cutoff
336
+
337
+
338
+ def render_triage(findings: List[Finding], stats: ParseStats, cutoff) -> str:
339
+ parts = ["== mdbkit triage (beta) =="]
340
+ span = ""
341
+ if stats.first_ts and stats.last_ts:
342
+ start = cutoff or stats.first_ts
343
+ span = f"window: {start.strftime('%H:%M')} -> " \
344
+ f"{stats.last_ts.strftime('%H:%M')} ({stats.parsed:,} lines)"
345
+ parts.append(span)
346
+ parts.append("beta detectors (elections/checkpoints/eviction/flow-control) "
347
+ "are pattern-matched pending real-cluster validation — "
348
+ "see docs/TESTING-PLAYBOOK.md")
349
+ parts.append("")
350
+ for f in findings:
351
+ tag = f"[{f.severity}]"
352
+ parts.append(f"{tag:<6} {f.title}: {f.detail}")
353
+ for e in f.evidence:
354
+ parts.append(f" - {e}")
355
+ if f.next_step:
356
+ parts.append(f" next: {f.next_step}")
357
+ parts.append("")
358
+ parts.append("mdbkit is read-only: it never runs commands against your "
359
+ "cluster. Review every next step before acting.")
360
+ return "\n".join(parts)
@@ -0,0 +1,207 @@
1
+ Metadata-Version: 2.4
2
+ Name: mdbkit
3
+ Version: 0.1.0
4
+ Summary: Offline toolkit for MongoDB 4.4+ structured logs: log analysis, slow-query shapes, connection churn, and deterministic index advice. A spiritual successor to mtools' log tools.
5
+ Author: Saqib Ameen Subhan
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/saqibameen86/mdbkit
8
+ Project-URL: Issues, https://github.com/saqibameen86/mdbkit/issues
9
+ Keywords: mongodb,logs,logv2,mtools,index,dba,slow-query
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: System Administrators
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Database
17
+ Classifier: Topic :: System :: Systems Administration
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=7; extra == "dev"
23
+ Dynamic: license-file
24
+
25
+ # mdbkit
26
+
27
+ **An offline toolkit for MongoDB structured logs — log analysis, slow-query shapes, connection churn, and deterministic index advice.**
28
+
29
+ A spiritual successor to [mtools](https://github.com/rueckstiess/mtools)' log tools (`mloginfo`, `mlogfilter`) for the structured JSON log format MongoDB has used since 4.4 — the format mtools never supported. Built for DBAs and ops engineers running self-managed MongoDB 4.4 / 5.0 / 6.0 / 7.0 / 8.0.
30
+
31
+ > **Privacy by design: mdbkit never makes a network call.** It reads log files (or stdin) and writes to stdout. No telemetry, no phoning home, no cloud. Your logs never leave your machine. It is safe to run on air-gapped database hosts.
32
+
33
+ ## Why
34
+
35
+ MongoDB 4.4 switched to structured JSON logging, and the beloved mtools log commands stopped working — the issue has been open since 2020. Meanwhile, index recommendations from MongoDB's Performance Advisor require a paid Atlas tier or Cloud/Ops Manager. If you run Community Edition on your own infrastructure, you're back to reading raw JSON logs with `grep` and `jq`.
36
+
37
+ mdbkit fills that gap: a single, dependency-free CLI that turns structured logs into answers.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pipx install mdbkit # recommended
43
+ # or
44
+ pip install mdbkit
45
+ ```
46
+
47
+ Zero runtime dependencies (pure Python stdlib), so it also installs cleanly on air-gapped hosts from a single wheel:
48
+
49
+ ```bash
50
+ pip download mdbkit -d ./wheels # on a connected machine
51
+ pip install --no-index --find-links ./wheels mdbkit # on the DB host
52
+ ```
53
+
54
+ Requires Python 3.9+.
55
+
56
+ ## Quick start
57
+
58
+ ```bash
59
+ # Overall log summary: versions, restarts, connection counts, error/warning totals
60
+ mdbkit loginfo /var/log/mongodb/mongod.log
61
+
62
+ # Slow queries grouped by query shape (literals stripped), ranked by total time
63
+ mdbkit queries mongod.log
64
+ mdbkit queries mongod.log --sort scanRatio --limit 10 --json
65
+
66
+ # Connection churn by source IP, appName, and driver
67
+ mdbkit connections mongod.log
68
+
69
+ # Filter raw log lines (output stays valid logv2 JSON — chainable)
70
+ mdbkit filter mongod.log --slow 500 --component COMMAND --ns shop.orders
71
+ mdbkit filter mongod.log --from 2026-07-01T08:00:00Z --to 2026-07-01T09:00:00Z | mdbkit queries -
72
+
73
+ # Rotated/compressed logs work directly
74
+ mdbkit queries mongod.log.2.gz
75
+ ```
76
+
77
+ ### Index advice
78
+
79
+ ```bash
80
+ mdbkit advise mongod.log
81
+ ```
82
+
83
+ Produces **candidate** indexes from observed slow-query shapes using the ESR
84
+ (Equality → Sort → Range) guideline, with evidence, confidence, caveats, and a
85
+ validation step for every recommendation:
86
+
87
+ ```
88
+ [1] shop.orders — confidence: HIGH
89
+ query shape : {createdAt:gt, status:eq} sort:{createdAt:-1}
90
+ candidate : { status: 1, createdAt: -1 }
91
+ evidence : COLLSCAN observed in planSummary
92
+ evidence : in-memory sort (hasSortStage) observed
93
+ evidence : examined 251,400 docs to return 73 (3444:1)
94
+ caveat : Every index adds write and storage overhead ...
95
+ validate : Re-run the query with .explain('executionStats') ...
96
+ ```
97
+
98
+ The advice gets sharper if you export your existing indexes and a sampled
99
+ schema. mdbkit never connects to your database — instead it prints small
100
+ `mongosh` scripts you run yourself, so you can read exactly what they do:
101
+
102
+ ```bash
103
+ mdbkit export-script indexes > export_indexes.js
104
+ mdbkit export-script schema > export_schema.js
105
+ mongosh --quiet "mongodb://localhost/shop" export_indexes.js > indexes.json
106
+ mongosh --quiet "mongodb://localhost/shop" export_schema.js > schema.json
107
+
108
+ mdbkit advise mongod.log --indexes indexes.json --schema schema.json
109
+ ```
110
+
111
+ With `--indexes`, mdbkit checks each candidate against your existing indexes
112
+ (flagging when an existing index should already cover the query, or when a
113
+ candidate would make an existing index redundant — it flags, never suggests
114
+ dropping). With `--schema`, it warns about multikey (array) fields,
115
+ low-cardinality fields, and field-name typos. The schema export records field
116
+ **names and types only — no values.**
117
+
118
+ ### Incident triage (beta)
119
+
120
+ ```bash
121
+ mdbkit triage /var/log/mongodb/mongod.log # or a copied log + --no-sysprobe
122
+ ```
123
+
124
+ One command during an incident: election/stepdown timeline, connection
125
+ storms, hot collections, error clusters, slow checkpoints — plus local
126
+ disk/memory/load probes when run on the DB host. Read-only: it never
127
+ connects to the database, and every finding ends with a next step for a
128
+ human to review. Detectors marked beta are validated against real logs
129
+ where available and clearly labeled where broader validation is pending.
130
+
131
+ ### Explain-plan analysis
132
+
133
+ Got a slow query in hand rather than a log? Save its explain output and ask
134
+ mdbkit what's wrong:
135
+
136
+ ```bash
137
+ # in mongosh: EJSON.stringify(db.orders.find({...}).sort({...}).explain("executionStats"))
138
+ mdbkit explain explain.json
139
+ ```
140
+
141
+ You get the plan chain (`SORT -> COLLSCAN`), the examined/returned math, plain-
142
+ English verdicts (full collection scan, blocking in-memory sort, weakly
143
+ selective index, covered query), and — when the plan needs help — the same
144
+ evidence-backed candidate index the advisor would produce. Works with find and
145
+ aggregate explains, classic and SBE (6.0+) plans, and sharded winning plans.
146
+
147
+ ### Design principles
148
+
149
+ * **Deterministic.** Same log in, same advice out. Rules, not AI. Every
150
+ recommendation shows its evidence and its rule of reasoning.
151
+ * **Candidates, not commands.** mdbkit never tells you to blindly run
152
+ `createIndex`, and never advises dropping an index.
153
+ * **Honest about uncertainty.** Shapes seen once are labeled low-confidence;
154
+ `$or`, `$regex`, `$in` and low-selectivity operators carry explicit caveats.
155
+ * **Offline, always.** No network code exists in this codebase.
156
+
157
+ ## What it reads
158
+
159
+ Any MongoDB 4.4+ structured log: `mongod.log`, `mongos` logs, rotated `.gz`
160
+ files, or stdin (`-`). Slow-query lines (`"msg":"Slow query"`) are logged by
161
+ default for operations over `slowms` (100 ms); lower `slowms` or enable
162
+ profiling level 1 to capture more:
163
+
164
+ ```js
165
+ db.setProfilingLevel(1, { slowms: 50 })
166
+ ```
167
+
168
+ Pre-4.4 plain-text logs are detected and politely refused — for those, the
169
+ original mtools still works.
170
+
171
+ ## Roadmap
172
+
173
+ Terminal output is and will remain first-class — this tool is built for the
174
+ Linux box the database actually runs on.
175
+
176
+ * **v0.2** — FTDC (`diagnostic.data`) decoding: offline summaries of the
177
+ metrics MongoDB already records on every node; election/failover timeline
178
+ from REPL events; per-shape drill-down.
179
+ * **v0.3** — shareable Markdown/HTML report export (for tickets and
180
+ post-incident reviews — a convenience layer, never a replacement for the
181
+ terminal).
182
+
183
+ mdbkit is validated against real-world structured logs (tens of thousands of
184
+ lines) in addition to its synthetic test fixtures.
185
+
186
+ ## Bugs, feature requests, questions
187
+
188
+ Please use [GitHub Issues](../../issues) — it keeps problems and fixes public
189
+ so the next person can find them. Real-world log lines that parse wrongly are
190
+ the most valuable bug reports of all (redact literals first!).
191
+
192
+ ## Security
193
+
194
+ mdbkit is offline by design: the codebase contains no network code, never
195
+ executes or evaluates input, and treats every log line as untrusted data
196
+ (strict JSON parsing only — shell constructors are never evaluated). See
197
+ [SECURITY.md](SECURITY.md) for the reporting process.
198
+
199
+ ## Non-affiliation
200
+
201
+ mdbkit is an independent community project. It is **not affiliated with,
202
+ endorsed by, or sponsored by MongoDB, Inc.** "MongoDB" is a registered
203
+ trademark of MongoDB, Inc., used here only to describe compatibility.
204
+
205
+ ## License
206
+
207
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,16 @@
1
+ mdbkit/__init__.py,sha256=8TobwH0RfPVfJPFn8kkWbTNuMpMbGc5FvYqmNGNKv8o,105
2
+ mdbkit/advisor.py,sha256=g9T3hAk-L6yM1etD9ENoA6PQ7yEah-RmUmp4M8X35nU,11107
3
+ mdbkit/analysis.py,sha256=oSYJwJ7F3QU7f4GkmMAeyxL-xwJM1B1TUMZp-5Mn28c,16559
4
+ mdbkit/cli.py,sha256=g63_WHP9EJvWJhuLWZhSXXsdqV-YrAvCdpKgqs8V1VQ,9386
5
+ mdbkit/explain.py,sha256=j8znhhx_yxrMoTy9MMuuF3SMSX4aZswYMwXh2aQaSK0,9453
6
+ mdbkit/filtering.py,sha256=ebv8uS7OqEEJYEbYAthK7UCRB_mvyaj1YkJ5Y1eKSGc,1968
7
+ mdbkit/parser.py,sha256=sawbRqPRn-CofCe8rcHl6bxAESX9YLWGjAp3Jwt_SUM,4655
8
+ mdbkit/render.py,sha256=TMaIPegq_J8eOhDIivLnBoHc9RejYswGzwGjzgrH_WU,5533
9
+ mdbkit/scripts.py,sha256=MwHcC6pS1ENH-PNLYXnxkNt0z3CQ0CotKDnS61om-Q8,2835
10
+ mdbkit/triage.py,sha256=qBJo55WkYh814TY45MnZo-S0yrTx63yjB5R5sfdvxjM,15648
11
+ mdbkit-0.1.0.dist-info/licenses/LICENSE,sha256=9azApMITxLqMK28K4o6lCPfu8-SL9FCDX5z40ZuAsFw,1075
12
+ mdbkit-0.1.0.dist-info/METADATA,sha256=oUFG8wsqZxlw6IBSxMpmwAPJ_sxkt6lT46VRuw89vYE,8743
13
+ mdbkit-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
14
+ mdbkit-0.1.0.dist-info/entry_points.txt,sha256=-dooR5VbflzKZrXO_wXxxVcUOeymijPztzXwJ6x2sF0,43
15
+ mdbkit-0.1.0.dist-info/top_level.txt,sha256=aveAWwz7JTWVGDs1rDdEKFTTwBdVQA6qnJnN891nA88,7
16
+ mdbkit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mdbkit = mdbkit.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Saqib Ameen Subhan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ mdbkit