@shibbirweb/mcp-db-read-only 0.1.0 → 0.2.0
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.
- package/CHANGELOG.md +13 -1
- package/README.dockerhub.md +79 -2
- package/README.md +78 -1
- package/dist/ApplicationFactory.js +111 -14
- package/dist/config/EnvironmentConfigLoader.js +58 -0
- package/dist/drivers/BaseDriver.js +10 -1
- package/dist/drivers/document/MongoDriver.js +27 -30
- package/dist/drivers/keyvalue/RedisDriver.js +20 -5
- package/dist/drivers/search/ElasticsearchDriver.js +13 -9
- package/dist/drivers/sql/ClickHouseDriver.js +12 -13
- package/dist/drivers/sql/MsSqlDriver.js +6 -6
- package/dist/drivers/sql/MySqlDriver.js +7 -5
- package/dist/drivers/sql/MySqlSessionInitializer.js +17 -2
- package/dist/drivers/sql/PostgresDriver.js +6 -6
- package/dist/drivers/sql/SqliteDriver.js +3 -3
- package/dist/formatting/JsonSerializer.js +3 -2
- package/dist/logging/CallLogger.js +139 -0
- package/dist/logging/LogChannel.js +18 -0
- package/dist/logging/LogFormatter.js +80 -0
- package/dist/logging/LogRecords.js +7 -0
- package/dist/logging/LogSink.js +38 -0
- package/dist/logging/RecordJson.js +39 -0
- package/dist/logging/Redactor.js +95 -0
- package/dist/logging/StatementTracer.js +9 -0
- package/dist/logging/ToolCallObserver.js +6 -0
- package/dist/logging/store/FolderLogChannel.js +56 -0
- package/dist/logging/store/FolderLogStore.js +214 -0
- package/dist/logging/store/LogFileNames.js +57 -0
- package/dist/logging/store/LogStore.js +18 -0
- package/dist/logging/store/MemoryLogStore.js +70 -0
- package/dist/logging/viewer/LiveLogViewer.js +256 -0
- package/dist/logging/viewer/LiveViewerObserver.js +62 -0
- package/dist/logging/viewer/ViewerAssets.js +625 -0
- package/dist/server/BackgroundService.js +1 -0
- package/dist/server/McpDbServer.js +16 -2
- package/dist/tools/BaseTool.js +8 -2
- package/dist/tools/connection/CurrentConnectionTool.js +11 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,9 +6,21 @@ Each release is published to npm and Docker Hub from the same tag. Where a versi
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.0]
|
|
10
|
+
|
|
11
|
+
Not yet published to npm, Docker Hub or the MCP Registry.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Optional call logging (`DB_LOG`, `DB_LOG_FILE`, `DB_LOG_FORMAT`): every tool call with its input, each statement sent to the database, and the full output, in a readable boxed format or as JSON lines. Credentials are always redacted, and a logging failure never fails a call.
|
|
16
|
+
- A permanent log folder (`DB_LOG_DIR`): every entry saved as its own pretty JSON file, in a folder per day, never deleted, shared safely by every copy of the server.
|
|
17
|
+
- A live log viewer in the browser (`DB_LOG_PORT`, `DB_LOG_HISTORY`): paginated, 20 per page by default with 10, 20, 30 or 50 to choose from; filters across everything logged; a copy icon on every block; and live updates as each call finishes. With a log folder it shows every copy's calls, across restarts. Off unless a port is set; it has no access control and listens on all interfaces.
|
|
18
|
+
- The viewer binds its port on the first tool call, so the copy of the server actually in use gets it. A busy port is explained once in the chat, naming the process holding it, and retried on every call.
|
|
19
|
+
- `current_connection` reports the viewer's state.
|
|
20
|
+
|
|
9
21
|
## [0.1.0]
|
|
10
22
|
|
|
11
|
-
First
|
|
23
|
+
First version. Never published to npm, Docker Hub or the MCP Registry.
|
|
12
24
|
|
|
13
25
|
### Added
|
|
14
26
|
|
package/README.dockerhub.md
CHANGED
|
@@ -65,7 +65,7 @@ The server lives for the whole session, so the active connection is just state i
|
|
|
65
65
|
|
|
66
66
|
## Supported tags
|
|
67
67
|
|
|
68
|
-
`0.
|
|
68
|
+
`0.2.0`, `0.2`, `0`, `latest`, built for `linux/amd64` and `linux/arm64`.
|
|
69
69
|
|
|
70
70
|
---
|
|
71
71
|
|
|
@@ -271,6 +271,12 @@ Every reading tool also accepts an optional `database`, applied to that call onl
|
|
|
271
271
|
| `DB_DEFAULT_PROFILE` | none | Which profile starts active |
|
|
272
272
|
| `DB_QUERY_TIMEOUT_MS` | `30000` | Statement timeout, enforced by each server where it can be |
|
|
273
273
|
| `DB_CONNECT_TIMEOUT_MS` | `10000` | Connection timeout |
|
|
274
|
+
| `DB_LOG` | off | `true` logs every tool call to stderr. See [Call logging](https://github.com/shibbirweb/mcp-db-read-only#call-logging) |
|
|
275
|
+
| `DB_LOG_FILE` | none | Log every tool call to this file instead |
|
|
276
|
+
| `DB_LOG_DIR` | none | Save every tool call as its own JSON file in this folder, permanently |
|
|
277
|
+
| `DB_LOG_FORMAT` | `pretty` | `pretty` or `json` |
|
|
278
|
+
| `DB_LOG_PORT` | none | Serve a live log viewer in the browser on this port |
|
|
279
|
+
| `DB_LOG_HISTORY` | `500` | Entries the viewer keeps in memory when there is no `DB_LOG_DIR` |
|
|
274
280
|
| `MYSQL_*` | | The legacy MySQL-only variables, read unchanged. See above |
|
|
275
281
|
|
|
276
282
|
None of these are required: with no configuration at all the server still starts, and the tools tell you to call `connect`.
|
|
@@ -279,6 +285,77 @@ Starting profile: `DB_DEFAULT_PROFILE` (or `MYSQL_DEFAULT_PROFILE`) if it names
|
|
|
279
285
|
|
|
280
286
|
---
|
|
281
287
|
|
|
288
|
+
## Call logging
|
|
289
|
+
|
|
290
|
+
Off by default. Turn it on to see every tool call the assistant makes: its input, each statement the drivers sent to the database, and the full output.
|
|
291
|
+
|
|
292
|
+
| Variable | Effect |
|
|
293
|
+
| --- | --- |
|
|
294
|
+
| `DB_LOG=true` | Log to stderr, which your MCP client keeps (Claude Code shows it in its MCP logs) |
|
|
295
|
+
| `DB_LOG_FILE=/path/calls.log` | Log to that file instead, appended to, created readable by you only. Implies `DB_LOG` |
|
|
296
|
+
| `DB_LOG_DIR=/path/folder` | Save every entry as its own JSON file in that folder, permanently. Implies logging on its own, without also writing text |
|
|
297
|
+
| `DB_LOG_FORMAT=json` | One JSON object per line, for `jq` or a log shipper. The default is `pretty` |
|
|
298
|
+
|
|
299
|
+
A pretty entry:
|
|
300
|
+
|
|
301
|
+
```text
|
|
302
|
+
┌─ #3 run_query · ok · 38 ms · 2026-09-25T10:14:03.221Z
|
|
303
|
+
│ connection dev (MySQL) mysql://reader@127.0.0.1:3306/app
|
|
304
|
+
│ input
|
|
305
|
+
│ {
|
|
306
|
+
│ "query": "SELECT COUNT(*) AS n FROM members"
|
|
307
|
+
│ }
|
|
308
|
+
│ statements (1)
|
|
309
|
+
│ 1. MySQL · 12 ms · 1 row
|
|
310
|
+
│ SELECT COUNT(*) AS n FROM members
|
|
311
|
+
│ output
|
|
312
|
+
│ [
|
|
313
|
+
│ {
|
|
314
|
+
│ "n": 17440
|
|
315
|
+
│ }
|
|
316
|
+
│ ]
|
|
317
|
+
└─
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
- **Everything is logged, including the full output of every call.** Treat a log file like the data it contains.
|
|
321
|
+
- **Credentials never are.** A `password` argument, the password inside a connection URL, and secret-looking URL options such as `api_key` are always written as `***`, with no way to turn that off.
|
|
322
|
+
- **Statements** include the ones the server sends on its own behalf: PostgreSQL's `BEGIN READ ONLY` and `ROLLBACK`, Redis's `COMMAND INFO` checks, the catalog queries behind `describe_table`. Each shows its duration and outcome (a row count, or the error); the data itself is in the call's output. A statement sent outside any call, such as MySQL's per-connection setup, gets an entry of its own.
|
|
323
|
+
- An entry is written when its call finishes, and numbered when it starts, so calls handled concurrently can appear out of numeric order.
|
|
324
|
+
- With `DB_LOG_DIR`, each entry is its own file in a folder per UTC day, e.g. `2026-09-25/103014-221Z_p72440_c000012_run_query_ok.json`, holding the full record as pretty JSON: time, pid, sequence, tool and outcome are in the name, so `ls` and `grep` work without opening anything. Files and folders are readable by you only. Nothing is ever deleted or rotated; archive or remove old day folders yourself. Every copy of the server can share one folder, since names never collide.
|
|
325
|
+
- Logging can never break a call. If the log cannot be written (say, the disk is full), the call still succeeds, one warning is printed, and logging stops.
|
|
326
|
+
|
|
327
|
+
In Docker, a log file or folder must be on a mounted volume to outlive the container: `-v "$PWD/logs:/logs" -e DB_LOG_DIR=/logs`. `DB_LOG=true` needs no mount.
|
|
328
|
+
|
|
329
|
+
### Live viewer in the browser
|
|
330
|
+
|
|
331
|
+
Add `DB_LOG_PORT` to watch calls arrive in a browser page, updating the moment each one finishes:
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
DB_LOG=true DB_LOG_PORT=4800 npx -y @shibbirweb/mcp-db-read-only
|
|
335
|
+
# then open http://127.0.0.1:4800/
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
| Variable | Default | Effect |
|
|
339
|
+
| --- | --- | --- |
|
|
340
|
+
| `DB_LOG_PORT` | none | Serve the live viewer on this port. Needs `DB_LOG_DIR`, `DB_LOG` or `DB_LOG_FILE` as well; alone it only prints a warning |
|
|
341
|
+
| `DB_LOG_HISTORY` | `500` | Without `DB_LOG_DIR`, how many recent entries the viewer keeps in memory. `0` keeps none |
|
|
342
|
+
|
|
343
|
+
The page lists calls newest first, **20 per page by default**, with page controls and a choice of 10, 20, 30 or 50 per page (remembered in your browser). Each call is a card (tool, status, duration, connection, and which client and process made it) that expands to its input, statements and output, every block with its own copy icon. Filters (text, tool, failures only) apply across everything logged, not just the page shown. Page 1 updates itself as calls arrive; on any other page a "new entries" button appears instead, so the page you are reading does not shift. It follows your system's light or dark theme and loads nothing from the internet.
|
|
344
|
+
|
|
345
|
+
With `DB_LOG_DIR` the viewer reads the folder, so it shows everything ever saved there, across restarts, **including calls made by other copies of the server** sharing the folder (Claude Desktop runs several). Without it, it shows this copy's last `DB_LOG_HISTORY` calls from memory.
|
|
346
|
+
|
|
347
|
+
> **The viewer has no access control and listens on every network interface (`0.0.0.0`).** Anyone who can reach the port, including other devices on the same network, can read every query and every result in the log. Use it on a network you trust, or block the port at your firewall. The server prints a reminder of this when the viewer starts.
|
|
348
|
+
|
|
349
|
+
With no `DB_LOG_PORT`, nothing listens on any port.
|
|
350
|
+
|
|
351
|
+
The viewer takes its port on the **first tool call**, not at startup. MCP clients such as Claude Desktop start one copy of the server per chat surface, and most copies are never used; binding lazily means the copy your chat is using gets the port, and idle copies hold none (they open no database connections either until used).
|
|
352
|
+
|
|
353
|
+
If the port is taken when a call arrives (another chat is already using the viewer, say), the call still works, and its result carries one extra line saying which process holds the port: free it, or set `DB_LOG_PORT` to another one. Every later call retries, so once the port is free the viewer comes up on the next call and says so. `current_connection` always shows the viewer's state.
|
|
354
|
+
|
|
355
|
+
In Docker, publish the port as well: `-p 4800:4800 -e DB_LOG=true -e DB_LOG_PORT=4800`. Publishing it as `-p 127.0.0.1:4800:4800` keeps it reachable from this machine only.
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
282
359
|
## Security
|
|
283
360
|
|
|
284
361
|
Every engine is kept read-only by **two independent layers**, so a hole in one is not automatically a write. The first layer runs before any connection is used; the second is enforced by the database server itself wherever the engine offers a way, and structurally where it does not.
|
|
@@ -333,7 +410,7 @@ Other limits worth knowing:
|
|
|
333
410
|
|
|
334
411
|
**Parallel tool calls.** The active connection is a single piece of process state. If a client issues several tool calls in one batch they are handled concurrently, so a `use_database` batched alongside a query is not guaranteed to land first. When a read must be pinned to a particular database, pass the per-call `database` argument instead.
|
|
335
412
|
|
|
336
|
-
**Shutdown.** The server exits on `SIGINT`/`SIGTERM`, not when stdin closes. Open sockets keep the event loop alive, and stdin reaching EOF only means no further requests were buffered.
|
|
413
|
+
**Shutdown.** The server exits on `SIGINT`/`SIGTERM`, not when stdin closes. The live viewer, if running, closes its port with it. Open sockets keep the event loop alive, and stdin reaching EOF only means no further requests were buffered.
|
|
337
414
|
|
|
338
415
|
---
|
|
339
416
|
|
package/README.md
CHANGED
|
@@ -256,6 +256,12 @@ Every reading tool also accepts an optional `database`, applied to that call onl
|
|
|
256
256
|
| `DB_DEFAULT_PROFILE` | none | Which profile starts active |
|
|
257
257
|
| `DB_QUERY_TIMEOUT_MS` | `30000` | Statement timeout, enforced by each server where it can be |
|
|
258
258
|
| `DB_CONNECT_TIMEOUT_MS` | `10000` | Connection timeout |
|
|
259
|
+
| `DB_LOG` | off | `true` logs every tool call to stderr. See [Call logging](#call-logging) |
|
|
260
|
+
| `DB_LOG_FILE` | none | Log every tool call to this file instead |
|
|
261
|
+
| `DB_LOG_DIR` | none | Save every tool call as its own JSON file in this folder, permanently |
|
|
262
|
+
| `DB_LOG_FORMAT` | `pretty` | `pretty` or `json` |
|
|
263
|
+
| `DB_LOG_PORT` | none | Serve a live log viewer in the browser on this port |
|
|
264
|
+
| `DB_LOG_HISTORY` | `500` | Entries the viewer keeps in memory when there is no `DB_LOG_DIR` |
|
|
259
265
|
| `MYSQL_*` | | The legacy MySQL-only variables, read unchanged. See above |
|
|
260
266
|
|
|
261
267
|
None of these are required: with no configuration at all the server still starts, and the tools tell you to call `connect`.
|
|
@@ -264,6 +270,77 @@ Starting profile: `DB_DEFAULT_PROFILE` (or `MYSQL_DEFAULT_PROFILE`) if it names
|
|
|
264
270
|
|
|
265
271
|
---
|
|
266
272
|
|
|
273
|
+
## Call logging
|
|
274
|
+
|
|
275
|
+
Off by default. Turn it on to see every tool call the assistant makes: its input, each statement the drivers sent to the database, and the full output.
|
|
276
|
+
|
|
277
|
+
| Variable | Effect |
|
|
278
|
+
| --- | --- |
|
|
279
|
+
| `DB_LOG=true` | Log to stderr, which your MCP client keeps (Claude Code shows it in its MCP logs) |
|
|
280
|
+
| `DB_LOG_FILE=/path/calls.log` | Log to that file instead, appended to, created readable by you only. Implies `DB_LOG` |
|
|
281
|
+
| `DB_LOG_DIR=/path/folder` | Save every entry as its own JSON file in that folder, permanently. Implies logging on its own, without also writing text |
|
|
282
|
+
| `DB_LOG_FORMAT=json` | One JSON object per line, for `jq` or a log shipper. The default is `pretty` |
|
|
283
|
+
|
|
284
|
+
A pretty entry:
|
|
285
|
+
|
|
286
|
+
```text
|
|
287
|
+
┌─ #3 run_query · ok · 38 ms · 2026-09-25T10:14:03.221Z
|
|
288
|
+
│ connection dev (MySQL) mysql://reader@127.0.0.1:3306/app
|
|
289
|
+
│ input
|
|
290
|
+
│ {
|
|
291
|
+
│ "query": "SELECT COUNT(*) AS n FROM members"
|
|
292
|
+
│ }
|
|
293
|
+
│ statements (1)
|
|
294
|
+
│ 1. MySQL · 12 ms · 1 row
|
|
295
|
+
│ SELECT COUNT(*) AS n FROM members
|
|
296
|
+
│ output
|
|
297
|
+
│ [
|
|
298
|
+
│ {
|
|
299
|
+
│ "n": 17440
|
|
300
|
+
│ }
|
|
301
|
+
│ ]
|
|
302
|
+
└─
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
- **Everything is logged, including the full output of every call.** Treat a log file like the data it contains.
|
|
306
|
+
- **Credentials never are.** A `password` argument, the password inside a connection URL, and secret-looking URL options such as `api_key` are always written as `***`, with no way to turn that off.
|
|
307
|
+
- **Statements** include the ones the server sends on its own behalf: PostgreSQL's `BEGIN READ ONLY` and `ROLLBACK`, Redis's `COMMAND INFO` checks, the catalog queries behind `describe_table`. Each shows its duration and outcome (a row count, or the error); the data itself is in the call's output. A statement sent outside any call, such as MySQL's per-connection setup, gets an entry of its own.
|
|
308
|
+
- An entry is written when its call finishes, and numbered when it starts, so calls handled concurrently can appear out of numeric order.
|
|
309
|
+
- With `DB_LOG_DIR`, each entry is its own file in a folder per UTC day, e.g. `2026-09-25/103014-221Z_p72440_c000012_run_query_ok.json`, holding the full record as pretty JSON: time, pid, sequence, tool and outcome are in the name, so `ls` and `grep` work without opening anything. Files and folders are readable by you only. Nothing is ever deleted or rotated; archive or remove old day folders yourself. Every copy of the server can share one folder, since names never collide.
|
|
310
|
+
- Logging can never break a call. If the log cannot be written (say, the disk is full), the call still succeeds, one warning is printed, and logging stops.
|
|
311
|
+
|
|
312
|
+
In Docker, a log file or folder must be on a mounted volume to outlive the container: `-v "$PWD/logs:/logs" -e DB_LOG_DIR=/logs`. `DB_LOG=true` needs no mount.
|
|
313
|
+
|
|
314
|
+
### Live viewer in the browser
|
|
315
|
+
|
|
316
|
+
Add `DB_LOG_PORT` to watch calls arrive in a browser page, updating the moment each one finishes:
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
DB_LOG=true DB_LOG_PORT=4800 npx -y @shibbirweb/mcp-db-read-only
|
|
320
|
+
# then open http://127.0.0.1:4800/
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
| Variable | Default | Effect |
|
|
324
|
+
| --- | --- | --- |
|
|
325
|
+
| `DB_LOG_PORT` | none | Serve the live viewer on this port. Needs `DB_LOG_DIR`, `DB_LOG` or `DB_LOG_FILE` as well; alone it only prints a warning |
|
|
326
|
+
| `DB_LOG_HISTORY` | `500` | Without `DB_LOG_DIR`, how many recent entries the viewer keeps in memory. `0` keeps none |
|
|
327
|
+
|
|
328
|
+
The page lists calls newest first, **20 per page by default**, with page controls and a choice of 10, 20, 30 or 50 per page (remembered in your browser). Each call is a card (tool, status, duration, connection, and which client and process made it) that expands to its input, statements and output, every block with its own copy icon. Filters (text, tool, failures only) apply across everything logged, not just the page shown. Page 1 updates itself as calls arrive; on any other page a "new entries" button appears instead, so the page you are reading does not shift. It follows your system's light or dark theme and loads nothing from the internet.
|
|
329
|
+
|
|
330
|
+
With `DB_LOG_DIR` the viewer reads the folder, so it shows everything ever saved there, across restarts, **including calls made by other copies of the server** sharing the folder (Claude Desktop runs several). Without it, it shows this copy's last `DB_LOG_HISTORY` calls from memory.
|
|
331
|
+
|
|
332
|
+
> **The viewer has no access control and listens on every network interface (`0.0.0.0`).** Anyone who can reach the port, including other devices on the same network, can read every query and every result in the log. Use it on a network you trust, or block the port at your firewall. The server prints a reminder of this when the viewer starts.
|
|
333
|
+
|
|
334
|
+
With no `DB_LOG_PORT`, nothing listens on any port.
|
|
335
|
+
|
|
336
|
+
The viewer takes its port on the **first tool call**, not at startup. MCP clients such as Claude Desktop start one copy of the server per chat surface, and most copies are never used; binding lazily means the copy your chat is using gets the port, and idle copies hold none (they open no database connections either until used).
|
|
337
|
+
|
|
338
|
+
If the port is taken when a call arrives (another chat is already using the viewer, say), the call still works, and its result carries one extra line saying which process holds the port: free it, or set `DB_LOG_PORT` to another one. Every later call retries, so once the port is free the viewer comes up on the next call and says so. `current_connection` always shows the viewer's state.
|
|
339
|
+
|
|
340
|
+
In Docker, publish the port as well: `-p 4800:4800 -e DB_LOG=true -e DB_LOG_PORT=4800`. Publishing it as `-p 127.0.0.1:4800:4800` keeps it reachable from this machine only.
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
267
344
|
## Security
|
|
268
345
|
|
|
269
346
|
Every engine is kept read-only by **two independent layers**, so a hole in one is not automatically a write. The first layer runs before any connection is used; the second is enforced by the database server itself wherever the engine offers a way, and structurally where it does not.
|
|
@@ -318,7 +395,7 @@ Other limits worth knowing:
|
|
|
318
395
|
|
|
319
396
|
**Parallel tool calls.** The active connection is a single piece of process state. If a client issues several tool calls in one batch they are handled concurrently, so a `use_database` batched alongside a query is not guaranteed to land first. When a read must be pinned to a particular database, pass the per-call `database` argument instead.
|
|
320
397
|
|
|
321
|
-
**Shutdown.** The server exits on `SIGINT`/`SIGTERM`, not when stdin closes. Open sockets keep the event loop alive, and stdin reaching EOF only means no further requests were buffered.
|
|
398
|
+
**Shutdown.** The server exits on `SIGINT`/`SIGTERM`, not when stdin closes. The live viewer, if running, closes its port with it. Open sockets keep the event loop alive, and stdin reaching EOF only means no further requests were buffered.
|
|
322
399
|
|
|
323
400
|
---
|
|
324
401
|
|
|
@@ -16,6 +16,17 @@ import { MySqlDriver } from "./drivers/sql/MySqlDriver.js";
|
|
|
16
16
|
import { PostgresDriver } from "./drivers/sql/PostgresDriver.js";
|
|
17
17
|
import { SqliteDriver } from "./drivers/sql/SqliteDriver.js";
|
|
18
18
|
import { RowFormatter } from "./formatting/RowFormatter.js";
|
|
19
|
+
import { CallLogger } from "./logging/CallLogger.js";
|
|
20
|
+
import { TextLogChannel } from "./logging/LogChannel.js";
|
|
21
|
+
import { LiveLogViewer } from "./logging/viewer/LiveLogViewer.js";
|
|
22
|
+
import { LiveViewerObserver } from "./logging/viewer/LiveViewerObserver.js";
|
|
23
|
+
import { FolderLogChannel } from "./logging/store/FolderLogChannel.js";
|
|
24
|
+
import { FolderLogStore } from "./logging/store/FolderLogStore.js";
|
|
25
|
+
import { MemoryLogStore } from "./logging/store/MemoryLogStore.js";
|
|
26
|
+
import { JsonLogFormatter, PrettyLogFormatter } from "./logging/LogFormatter.js";
|
|
27
|
+
import { FileSink, StderrSink } from "./logging/LogSink.js";
|
|
28
|
+
import { SilentTracer } from "./logging/StatementTracer.js";
|
|
29
|
+
import { SilentObserver } from "./logging/ToolCallObserver.js";
|
|
19
30
|
import { McpDbServer } from "./server/McpDbServer.js";
|
|
20
31
|
import { DescribeTableTool } from "./tools/browse/DescribeTableTool.js";
|
|
21
32
|
import { GetForeignKeysTool } from "./tools/browse/GetForeignKeysTool.js";
|
|
@@ -63,6 +74,11 @@ export class ApplicationFactory {
|
|
|
63
74
|
* total open connections at MAX_DRIVERS * CONNECTION_LIMIT.
|
|
64
75
|
*/
|
|
65
76
|
static MAX_DRIVERS = 8;
|
|
77
|
+
/**
|
|
78
|
+
* Set once built, for the call log's client name: the logger has to exist
|
|
79
|
+
* before the server that will learn the name at handshake.
|
|
80
|
+
*/
|
|
81
|
+
server = null;
|
|
66
82
|
constructor(configLoader = new EnvironmentConfigLoader(), logger = (message) => console.error(`[mcp-db-ro] ${message}`), versionLoader = new PackageVersionLoader()) {
|
|
67
83
|
this.configLoader = configLoader;
|
|
68
84
|
this.logger = logger;
|
|
@@ -86,31 +102,112 @@ export class ApplicationFactory {
|
|
|
86
102
|
connectTimeoutMs: config.connectTimeoutMs,
|
|
87
103
|
queryTimeoutMs: config.queryTimeoutMs,
|
|
88
104
|
};
|
|
89
|
-
const
|
|
105
|
+
const callLog = this.createCallLog(config.logging, registry);
|
|
106
|
+
const cache = new DriverCache(this.createDriverRegistry(tuning, callLog.tracer), ApplicationFactory.MAX_DRIVERS);
|
|
90
107
|
const connections = new ConnectionManager(registry, cache);
|
|
91
108
|
const drivers = new DriverProvider(registry, cache, QUERY_TOOLS);
|
|
92
|
-
const tools = this.createTools(connections, drivers);
|
|
93
|
-
|
|
109
|
+
const tools = this.createTools(connections, drivers, callLog.viewerStatus);
|
|
110
|
+
const server = new McpDbServer(tools, cache, this.logger, callLog.observer, callLog.services, this.versionLoader.load());
|
|
111
|
+
this.server = server;
|
|
112
|
+
return server;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The call log when DB_LOG or DB_LOG_FILE asks for it, silent stand-ins
|
|
116
|
+
* otherwise. One CallLogger plays both roles, observer for the tools and
|
|
117
|
+
* tracer for the drivers, which is what lets it nest each driver statement
|
|
118
|
+
* under the call that caused it.
|
|
119
|
+
*/
|
|
120
|
+
createCallLog(settings, registry) {
|
|
121
|
+
if (!settings.enabled) {
|
|
122
|
+
return { observer: new SilentObserver(), tracer: new SilentTracer(), services: [], viewerStatus: () => null };
|
|
123
|
+
}
|
|
124
|
+
const channels = [];
|
|
125
|
+
const services = [];
|
|
126
|
+
let viewer = null;
|
|
127
|
+
const outputs = [];
|
|
128
|
+
const sink = settings.file ? new FileSink(settings.file) : new StderrSink();
|
|
129
|
+
if (settings.text) {
|
|
130
|
+
const formatter = settings.format === "json" ? new JsonLogFormatter() : new PrettyLogFormatter();
|
|
131
|
+
channels.push(new TextLogChannel(sink, formatter));
|
|
132
|
+
outputs.push(`${settings.format} text to ${sink.description}`);
|
|
133
|
+
}
|
|
134
|
+
// The viewer reads the folder when there is one, which also shows the
|
|
135
|
+
// calls of every other copy of the server saving there, and otherwise
|
|
136
|
+
// keeps this process's recent entries in memory.
|
|
137
|
+
let store;
|
|
138
|
+
if (settings.directory) {
|
|
139
|
+
const folderStore = new FolderLogStore(settings.directory);
|
|
140
|
+
store = folderStore;
|
|
141
|
+
channels.push(new FolderLogChannel(settings.directory, (entry) => folderStore.noteWritten(entry)));
|
|
142
|
+
outputs.push(`one JSON file per entry in ${settings.directory}`);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const memoryStore = new MemoryLogStore(settings.viewerHistory);
|
|
146
|
+
store = memoryStore;
|
|
147
|
+
if (settings.viewerPort !== null) {
|
|
148
|
+
channels.push(memoryStore);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (settings.viewerPort !== null) {
|
|
152
|
+
// Always all interfaces, as configured: reachable from other machines
|
|
153
|
+
// and from a Docker host without extra settings. The viewer announces
|
|
154
|
+
// that it has no access control when it starts, which is on the first
|
|
155
|
+
// tool call rather than here.
|
|
156
|
+
viewer = new LiveLogViewer("0.0.0.0", settings.viewerPort, store, this.logger);
|
|
157
|
+
services.push(viewer);
|
|
158
|
+
}
|
|
159
|
+
const logger = new CallLogger(channels, () => {
|
|
160
|
+
const target = registry.getActiveTarget();
|
|
161
|
+
return target
|
|
162
|
+
? `${registry.getActiveName()} (${EngineCatalog.label(target.engine)}) ${target.describe()}`
|
|
163
|
+
: null;
|
|
164
|
+
}, this.logger, () => this.clientName());
|
|
165
|
+
// Said once at startup, because with logging on every query and every
|
|
166
|
+
// result is being written somewhere, and the operator should know where.
|
|
167
|
+
this.logger(`call logging on: every tool call, its statements and its full output are written as ${outputs.join(" and ")}`);
|
|
168
|
+
const fallback = outputs.length > 0 ? outputs.join(" and ") : "nowhere else";
|
|
169
|
+
const observer = viewer ? new LiveViewerObserver(logger, viewer, fallback, this.logger) : logger;
|
|
170
|
+
return {
|
|
171
|
+
observer,
|
|
172
|
+
tracer: logger,
|
|
173
|
+
services,
|
|
174
|
+
viewerStatus: () => (viewer ? this.describeViewer(viewer) : null),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
clientName() {
|
|
178
|
+
return this.server?.clientName() ?? null;
|
|
179
|
+
}
|
|
180
|
+
/** One line for current_connection. */
|
|
181
|
+
describeViewer(viewer) {
|
|
182
|
+
const status = viewer.status;
|
|
183
|
+
switch (status.state) {
|
|
184
|
+
case "running":
|
|
185
|
+
return `running at ${status.url}`;
|
|
186
|
+
case "unavailable":
|
|
187
|
+
return `unavailable, ${status.reason} (free it or set DB_LOG_PORT to another port)`;
|
|
188
|
+
case "idle":
|
|
189
|
+
return "starts on the first tool call";
|
|
190
|
+
}
|
|
94
191
|
}
|
|
95
192
|
/** One factory per engine. None of them does I/O; drivers connect on first use. */
|
|
96
|
-
createDriverRegistry(tuning) {
|
|
193
|
+
createDriverRegistry(tuning, tracer) {
|
|
97
194
|
return new DriverRegistry()
|
|
98
|
-
.register("mysql", (target) => new MySqlDriver(target, tuning, this.logger))
|
|
99
|
-
.register("postgres", (target) => new PostgresDriver(target, tuning))
|
|
100
|
-
.register("sqlite", (target) => new SqliteDriver(target, tuning))
|
|
101
|
-
.register("mssql", (target) => new MsSqlDriver(target, tuning))
|
|
102
|
-
.register("clickhouse", (target) => new ClickHouseDriver(target, tuning))
|
|
103
|
-
.register("mongodb", (target) => new MongoDriver(target, tuning))
|
|
104
|
-
.register("redis", (target) => new RedisDriver(target, tuning, this.logger))
|
|
105
|
-
.register("elasticsearch", (target) => new ElasticsearchDriver(target, tuning));
|
|
195
|
+
.register("mysql", (target) => new MySqlDriver(target, tuning, tracer, this.logger))
|
|
196
|
+
.register("postgres", (target) => new PostgresDriver(target, tuning, tracer))
|
|
197
|
+
.register("sqlite", (target) => new SqliteDriver(target, tuning, tracer))
|
|
198
|
+
.register("mssql", (target) => new MsSqlDriver(target, tuning, tracer))
|
|
199
|
+
.register("clickhouse", (target) => new ClickHouseDriver(target, tuning, tracer))
|
|
200
|
+
.register("mongodb", (target) => new MongoDriver(target, tuning, tracer))
|
|
201
|
+
.register("redis", (target) => new RedisDriver(target, tuning, tracer, this.logger))
|
|
202
|
+
.register("elasticsearch", (target) => new ElasticsearchDriver(target, tuning, tracer));
|
|
106
203
|
}
|
|
107
|
-
createTools(connections, drivers) {
|
|
204
|
+
createTools(connections, drivers, viewerStatus) {
|
|
108
205
|
const names = new NamePolicyRegistry();
|
|
109
206
|
const targetFactory = new ConnectionTargetFactory();
|
|
110
207
|
const rows = new RowFormatter();
|
|
111
208
|
const mongoGuard = new MongoOperatorGuard();
|
|
112
209
|
const tools = [
|
|
113
|
-
new CurrentConnectionTool(connections),
|
|
210
|
+
new CurrentConnectionTool(connections, viewerStatus),
|
|
114
211
|
new ListConnectionsTool(connections),
|
|
115
212
|
new ListDatabasesTool(drivers),
|
|
116
213
|
new UseDatabaseTool(connections, names),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
1
2
|
import { ConnectionProfile } from "../domain/ConnectionProfile.js";
|
|
2
3
|
import { ConnectionTargetFactory } from "../connections/ConnectionTargetFactory.js";
|
|
3
4
|
/**
|
|
@@ -30,6 +31,7 @@ export class EnvironmentConfigLoader {
|
|
|
30
31
|
targetFactory;
|
|
31
32
|
static DEFAULT_QUERY_TIMEOUT_MS = 30000;
|
|
32
33
|
static DEFAULT_CONNECT_TIMEOUT_MS = 10000;
|
|
34
|
+
static DEFAULT_VIEWER_HISTORY = 500;
|
|
33
35
|
constructor(env = process.env, targetFactory = new ConnectionTargetFactory()) {
|
|
34
36
|
this.env = env;
|
|
35
37
|
this.targetFactory = targetFactory;
|
|
@@ -46,9 +48,65 @@ export class EnvironmentConfigLoader {
|
|
|
46
48
|
defaultProfileName: this.env.DB_DEFAULT_PROFILE || this.env.MYSQL_DEFAULT_PROFILE || null,
|
|
47
49
|
queryTimeoutMs: this.readTimeout(this.env.DB_QUERY_TIMEOUT_MS ?? this.env.MYSQL_QUERY_TIMEOUT_MS, EnvironmentConfigLoader.DEFAULT_QUERY_TIMEOUT_MS),
|
|
48
50
|
connectTimeoutMs: this.readTimeout(this.env.DB_CONNECT_TIMEOUT_MS ?? this.env.MYSQL_CONNECT_TIMEOUT_MS, EnvironmentConfigLoader.DEFAULT_CONNECT_TIMEOUT_MS),
|
|
51
|
+
logging: this.readLogging(warnings),
|
|
49
52
|
warnings,
|
|
50
53
|
};
|
|
51
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* DB_LOG=true logs text to stderr; DB_LOG_FILE=/path logs text to that
|
|
57
|
+
* file. DB_LOG_DIR=/folder saves every entry as its own JSON file there.
|
|
58
|
+
* Each of the three turns logging on by itself, and they combine.
|
|
59
|
+
* DB_LOG_FORMAT picks pretty (the default) or json for the text output.
|
|
60
|
+
*
|
|
61
|
+
* Relative paths are made absolute here, against the directory the server
|
|
62
|
+
* was started in, so the startup message names the real location.
|
|
63
|
+
*/
|
|
64
|
+
readLogging(warnings) {
|
|
65
|
+
const file = this.env.DB_LOG_FILE ? resolve(this.env.DB_LOG_FILE) : null;
|
|
66
|
+
const directory = this.env.DB_LOG_DIR ? resolve(this.env.DB_LOG_DIR) : null;
|
|
67
|
+
const stderr = ["true", "1", "yes", "on"].includes((this.env.DB_LOG ?? "").toLowerCase());
|
|
68
|
+
const text = stderr || file !== null;
|
|
69
|
+
const enabled = text || directory !== null;
|
|
70
|
+
const requested = (this.env.DB_LOG_FORMAT ?? "").toLowerCase();
|
|
71
|
+
let format = "pretty";
|
|
72
|
+
if (requested === "json") {
|
|
73
|
+
format = "json";
|
|
74
|
+
}
|
|
75
|
+
else if (requested && requested !== "pretty") {
|
|
76
|
+
warnings.push(`DB_LOG_FORMAT="${this.env.DB_LOG_FORMAT}" is not pretty or json, using pretty.`);
|
|
77
|
+
}
|
|
78
|
+
return { enabled, text, file, directory, format, ...this.readViewer(enabled, warnings) };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* DB_LOG_PORT starts the live browser viewer, but only while logging is on:
|
|
82
|
+
* the viewer shows the call log, so without one it has nothing to show, and
|
|
83
|
+
* it says so rather than silently doing nothing.
|
|
84
|
+
*/
|
|
85
|
+
readViewer(loggingEnabled, warnings) {
|
|
86
|
+
const history = this.readCount(this.env.DB_LOG_HISTORY, EnvironmentConfigLoader.DEFAULT_VIEWER_HISTORY);
|
|
87
|
+
const raw = this.env.DB_LOG_PORT;
|
|
88
|
+
if (!raw) {
|
|
89
|
+
return { viewerPort: null, viewerHistory: history };
|
|
90
|
+
}
|
|
91
|
+
const port = Number(raw);
|
|
92
|
+
if (!/^\d+$/.test(raw) || port < 1 || port > 65535) {
|
|
93
|
+
warnings.push(`DB_LOG_PORT="${raw}" is not a port from 1 to 65535, so the live viewer is off.`);
|
|
94
|
+
return { viewerPort: null, viewerHistory: history };
|
|
95
|
+
}
|
|
96
|
+
if (!loggingEnabled) {
|
|
97
|
+
warnings.push("DB_LOG_PORT is set but call logging is off. Set DB_LOG_DIR, DB_LOG=true or DB_LOG_FILE to use the live viewer.");
|
|
98
|
+
return { viewerPort: null, viewerHistory: history };
|
|
99
|
+
}
|
|
100
|
+
return { viewerPort: port, viewerHistory: history };
|
|
101
|
+
}
|
|
102
|
+
/** A non-negative integer, or the fallback. 0 is allowed and means none. */
|
|
103
|
+
readCount(value, fallback) {
|
|
104
|
+
if (value === undefined || value === "") {
|
|
105
|
+
return fallback;
|
|
106
|
+
}
|
|
107
|
+
const parsed = Number(value);
|
|
108
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
109
|
+
}
|
|
52
110
|
/**
|
|
53
111
|
* DB_PROFILES: `{"name": "url"}` or `{"name": {"url": "...", "password": "..."}}`.
|
|
54
112
|
*
|
|
@@ -7,11 +7,16 @@ import { UnsupportedOperationError } from "../errors/UnsupportedOperationError.j
|
|
|
7
7
|
* The two optional capabilities, indexes and foreign keys, default to a
|
|
8
8
|
* refusal here, so an engine without them needs no code to say so, and an
|
|
9
9
|
* engine that has them overrides the one method.
|
|
10
|
+
*
|
|
11
|
+
* Every statement a driver sends goes through `traced`, which reports it to
|
|
12
|
+
* the call log when logging is on and is a plain call when it is off.
|
|
10
13
|
*/
|
|
11
14
|
export class BaseDriver {
|
|
12
15
|
target;
|
|
13
|
-
|
|
16
|
+
tracer;
|
|
17
|
+
constructor(target, tracer) {
|
|
14
18
|
this.target = target;
|
|
19
|
+
this.tracer = tracer;
|
|
15
20
|
}
|
|
16
21
|
async listIndexes(_name) {
|
|
17
22
|
throw new UnsupportedOperationError(this.label, "indexes to list");
|
|
@@ -19,6 +24,10 @@ export class BaseDriver {
|
|
|
19
24
|
async listForeignKeys(_name) {
|
|
20
25
|
throw new UnsupportedOperationError(this.label, "foreign keys");
|
|
21
26
|
}
|
|
27
|
+
/** Run one statement through the tracer, labelled with this engine. */
|
|
28
|
+
traced(text, params, run, describe) {
|
|
29
|
+
return this.tracer.trace(this.label, { text, params }, run, describe);
|
|
30
|
+
}
|
|
22
31
|
get label() {
|
|
23
32
|
return EngineCatalog.label(this.target.engine);
|
|
24
33
|
}
|
|
@@ -25,8 +25,8 @@ export class MongoDriver extends BaseDriver {
|
|
|
25
25
|
static SYSTEM_DATABASES = new Set(["admin", "local", "config"]);
|
|
26
26
|
static SCHEMA_SAMPLE_SIZE = 100;
|
|
27
27
|
client;
|
|
28
|
-
constructor(target, tuning, stages = new MongoStageAllowlist(), sampler = new MongoSchemaSampler()) {
|
|
29
|
-
super(target);
|
|
28
|
+
constructor(target, tuning, tracer, stages = new MongoStageAllowlist(), sampler = new MongoSchemaSampler()) {
|
|
29
|
+
super(target, tracer);
|
|
30
30
|
this.tuning = tuning;
|
|
31
31
|
this.stages = stages;
|
|
32
32
|
this.sampler = sampler;
|
|
@@ -34,7 +34,8 @@ export class MongoDriver extends BaseDriver {
|
|
|
34
34
|
}
|
|
35
35
|
async verify() {
|
|
36
36
|
const { client } = await this.client.get();
|
|
37
|
-
|
|
37
|
+
const database = this.target.database || "admin";
|
|
38
|
+
await this.traced(`${database}.ping`, undefined, () => client.db(database).command({ ping: 1 }), () => "ok");
|
|
38
39
|
}
|
|
39
40
|
close() {
|
|
40
41
|
return this.client.close();
|
|
@@ -45,7 +46,7 @@ export class MongoDriver extends BaseDriver {
|
|
|
45
46
|
*/
|
|
46
47
|
async listDatabases() {
|
|
47
48
|
const { client } = await this.client.get();
|
|
48
|
-
const result = await client.db("admin").admin().listDatabases({ nameOnly: true, authorizedDatabases: true });
|
|
49
|
+
const result = await this.traced("admin.listDatabases", { nameOnly: true, authorizedDatabases: true }, () => client.db("admin").admin().listDatabases({ nameOnly: true, authorizedDatabases: true }), (answer) => `${answer.databases.length} databases`);
|
|
49
50
|
return result.databases.map((entry) => ({
|
|
50
51
|
name: entry.name,
|
|
51
52
|
system: MongoDriver.SYSTEM_DATABASES.has(entry.name),
|
|
@@ -53,22 +54,18 @@ export class MongoDriver extends BaseDriver {
|
|
|
53
54
|
}
|
|
54
55
|
async listObjects(pattern, limit) {
|
|
55
56
|
const database = await this.database();
|
|
56
|
-
const collections = await database
|
|
57
|
-
.listCollections({}, { nameOnly: true, authorizedCollections: true })
|
|
58
|
-
.toArray();
|
|
57
|
+
const collections = await this.traced(`${database.databaseName}.listCollections`, undefined, () => database.listCollections({}, { nameOnly: true, authorizedCollections: true }).toArray());
|
|
59
58
|
return new GlobPattern(pattern).apply(collections.map((entry) => entry.name).sort(), limit);
|
|
60
59
|
}
|
|
61
60
|
/** The inferred shape of a sample, plus any JSON Schema validator the collection declares. */
|
|
62
61
|
async describeObject(name) {
|
|
63
62
|
const database = await this.database();
|
|
64
|
-
const [info] = await database.listCollections({ name }).toArray();
|
|
63
|
+
const [info] = await this.traced(`${database.databaseName}.listCollections`, { name }, () => database.listCollections({ name }).toArray());
|
|
65
64
|
if (!info) {
|
|
66
65
|
throw new ObjectNotFoundError(this.objectNoun, name);
|
|
67
66
|
}
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
.aggregate([{ $sample: { size: MongoDriver.SCHEMA_SAMPLE_SIZE } }], { maxTimeMS: this.tuning.queryTimeoutMs })
|
|
71
|
-
.toArray();
|
|
67
|
+
const sampling = [{ $sample: { size: MongoDriver.SCHEMA_SAMPLE_SIZE } }];
|
|
68
|
+
const documents = await this.traced(`${database.databaseName}.${name}.aggregate`, sampling, () => database.collection(name).aggregate(sampling, { maxTimeMS: this.tuning.queryTimeoutMs }).toArray());
|
|
72
69
|
return {
|
|
73
70
|
collection: name,
|
|
74
71
|
sampled_documents: documents.length,
|
|
@@ -79,7 +76,7 @@ export class MongoDriver extends BaseDriver {
|
|
|
79
76
|
async listIndexes(name) {
|
|
80
77
|
await this.assertCollection(name);
|
|
81
78
|
const database = await this.database();
|
|
82
|
-
return database.collection(name).indexes();
|
|
79
|
+
return this.traced(`${database.databaseName}.${name}.indexes`, undefined, () => database.collection(name).indexes());
|
|
83
80
|
}
|
|
84
81
|
async sample(name, limit) {
|
|
85
82
|
await this.assertCollection(name);
|
|
@@ -95,7 +92,8 @@ export class MongoDriver extends BaseDriver {
|
|
|
95
92
|
skip: request.skip,
|
|
96
93
|
maxTimeMS: this.tuning.queryTimeoutMs,
|
|
97
94
|
});
|
|
98
|
-
|
|
95
|
+
const documents = await this.traced(`${database.databaseName}.${collection}.find`, request, () => cursor.toArray());
|
|
96
|
+
return this.toJson(bson, documents);
|
|
99
97
|
}
|
|
100
98
|
/**
|
|
101
99
|
* Reads at most `limit + 1` documents and closes the cursor, so a pipeline
|
|
@@ -112,32 +110,31 @@ export class MongoDriver extends BaseDriver {
|
|
|
112
110
|
batchSize: limit + 1,
|
|
113
111
|
});
|
|
114
112
|
const rows = [];
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
113
|
+
await this.traced(`${database.databaseName}.${collection}.aggregate`, pipeline, async () => {
|
|
114
|
+
try {
|
|
115
|
+
for await (const document of cursor) {
|
|
116
|
+
rows.push(document);
|
|
117
|
+
if (rows.length > limit) {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
finally {
|
|
123
|
+
await cursor.close();
|
|
124
|
+
}
|
|
125
|
+
return rows;
|
|
126
|
+
});
|
|
126
127
|
return { rows: this.toJson(bson, rows.slice(0, limit)), truncated: rows.length > limit };
|
|
127
128
|
}
|
|
128
129
|
async count(collection, filter) {
|
|
129
130
|
const { bson } = await this.client.get();
|
|
130
131
|
const database = await this.database();
|
|
131
|
-
return database
|
|
132
|
-
.collection(collection)
|
|
133
|
-
.countDocuments(this.fromJson(bson, filter), { maxTimeMS: this.tuning.queryTimeoutMs });
|
|
132
|
+
return this.traced(`${database.databaseName}.${collection}.countDocuments`, filter, () => database.collection(collection).countDocuments(this.fromJson(bson, filter), { maxTimeMS: this.tuning.queryTimeoutMs }));
|
|
134
133
|
}
|
|
135
134
|
async distinct(collection, field, filter) {
|
|
136
135
|
const { bson } = await this.client.get();
|
|
137
136
|
const database = await this.database();
|
|
138
|
-
const values = await database
|
|
139
|
-
.collection(collection)
|
|
140
|
-
.distinct(field, this.fromJson(bson, filter), { maxTimeMS: this.tuning.queryTimeoutMs });
|
|
137
|
+
const values = await this.traced(`${database.databaseName}.${collection}.distinct`, { field, filter }, () => database.collection(collection).distinct(field, this.fromJson(bson, filter), { maxTimeMS: this.tuning.queryTimeoutMs }));
|
|
141
138
|
return this.toJson(bson, values);
|
|
142
139
|
}
|
|
143
140
|
async database() {
|
|
@@ -146,7 +143,7 @@ export class MongoDriver extends BaseDriver {
|
|
|
146
143
|
}
|
|
147
144
|
async assertCollection(name) {
|
|
148
145
|
const database = await this.database();
|
|
149
|
-
const found = await database.listCollections({ name }, { nameOnly: true }).toArray();
|
|
146
|
+
const found = await this.traced(`${database.databaseName}.listCollections`, { name }, () => database.listCollections({ name }, { nameOnly: true }).toArray());
|
|
150
147
|
if (found.length === 0) {
|
|
151
148
|
throw new ObjectNotFoundError(this.objectNoun, name);
|
|
152
149
|
}
|