datanotes 3.0.0 → 3.1.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.
Files changed (60) hide show
  1. package/README.md +1 -5
  2. package/dist/api/client.d.cts +37 -4
  3. package/dist/api/client.d.ts +37 -4
  4. package/dist/api/index.cjs +388 -52
  5. package/dist/api/index.d.cts +3 -2
  6. package/dist/api/index.d.ts +3 -2
  7. package/dist/api/index.js +388 -52
  8. package/dist/api/mcp.cjs +6 -3
  9. package/dist/api/mcp.js +6 -3
  10. package/dist/api/otlp.d.cts +63 -15
  11. package/dist/api/otlp.d.ts +63 -15
  12. package/dist/api/redact.d.cts +25 -0
  13. package/dist/api/redact.d.ts +25 -0
  14. package/dist/api/tokens.d.cts +5 -0
  15. package/dist/api/tokens.d.ts +5 -0
  16. package/dist/api/tracing.d.cts +107 -0
  17. package/dist/api/tracing.d.ts +107 -0
  18. package/dist/datanotes.cjs +9380 -2554
  19. package/dist/docs/administration/limits.md +96 -12
  20. package/dist/docs/administration/run-the-service.md +18 -0
  21. package/dist/docs/api/operations/graph_neighbors.md +4 -2
  22. package/dist/docs/api/operations/graph_path.md +3 -3
  23. package/dist/docs/api/operations/graph_traverse.md +2 -2
  24. package/dist/docs/api/operations/record_tool_call.md +1 -0
  25. package/dist/docs/api/operations/tags/queries.md +2 -2
  26. package/dist/docs/api.md +3 -3
  27. package/dist/docs/develop/concurrency.md +36 -1
  28. package/dist/docs/develop/http-api.md +11 -1
  29. package/dist/docs/develop/telemetry.md +224 -25
  30. package/dist/docs/guides/data-quality/check-constraints.md +10 -10
  31. package/dist/docs/guides/data-quality/hand-edits.md +8 -8
  32. package/dist/docs/guides/database-objects/bases.md +11 -11
  33. package/dist/docs/guides/database-objects/catalogs-and-schemas.md +10 -10
  34. package/dist/docs/guides/database-objects/columns.md +38 -38
  35. package/dist/docs/guides/database-objects/relations.md +27 -27
  36. package/dist/docs/guides/database-objects/tables.md +37 -101
  37. package/dist/docs/guides/database-objects/views.md +44 -44
  38. package/dist/docs/guides/history/back-up.md +12 -4
  39. package/dist/docs/guides/history/change-data-feed.md +34 -34
  40. package/dist/docs/guides/history/journal-and-undo.md +12 -12
  41. package/dist/docs/guides/history/restore.md +20 -20
  42. package/dist/docs/guides/history/table-history.md +31 -31
  43. package/dist/docs/guides/history/vacuum.md +1 -10
  44. package/dist/docs/guides/read-and-write/automatic-links.md +15 -15
  45. package/dist/docs/guides/read-and-write/import-notes.md +20 -20
  46. package/dist/docs/guides/read-and-write/joins-and-aggregates.md +7 -7
  47. package/dist/docs/guides/read-and-write/read-and-filter.md +10 -10
  48. package/dist/docs/guides/read-and-write/write-rows.md +10 -10
  49. package/dist/docs/guides/search/link-graph.md +95 -0
  50. package/dist/docs/guides/search/text.md +1 -1
  51. package/dist/docs/guides/security/who-did-what.md +2 -0
  52. package/dist/docs/reference/cli.md +10 -3
  53. package/dist/docs/reference/compatibility.md +33 -13
  54. package/dist/docs/reference/operations/queries.md +3 -3
  55. package/dist/docs/reference/operations.md +11 -11
  56. package/dist/docs/reference/settings.md +37 -25
  57. package/dist/docs/release-notes.md +121 -0
  58. package/dist/docs/start/how-it-works.md +35 -3
  59. package/dist/docs/start/quick-start.md +2 -0
  60. package/package.json +3 -2
@@ -6,21 +6,86 @@ The engine keeps every row in a Markdown note, so its speed depends on the numbe
6
6
 
7
7
  ## Measured times
8
8
 
9
- We measured the engine on databases of 10,000 and 50,000 notes. On Windows 11, Microsoft Defender scanned the files. On Linux, the engine ran in a container with the notes on a Linux volume.
9
+ We measured Datanotes 3.1.0 on synthetic databases of 10,000 and 50,000 notes, made with `bench/scale/`. One machine ran every test:
10
10
 
11
- | Measure | 10,000 notes | 50,000 notes |
12
- | --------------------------------------- | ------------ | ------------ |
13
- | Start, Windows 11 with Defender | 15 to 18 s | 63 to 115 s |
14
- | Start, Linux container | 4 to 5 s | 22 s |
15
- | `select_rows` with `where` | 30 to 35 ms | 178 ms |
16
- | `insert_row` | 91 to 128 ms | 414 ms |
17
- | Memory (resident set size) after writes | Not measured | Up to 2 GB |
11
+ * Windows 11 Pro on an Intel Core i7-13700HX, with 32 GB of memory and an NVMe SSD
12
+ * Node.js 24.21
13
+ * Microsoft Defender scanning every file, with no exclusion
14
+ * Other programs running at the same time
18
15
 
19
- These numbers set the size the engine handles well:
16
+ The times are medians. The first start reads every note and builds the [vault index](#start-faster-with-the-vault-index). A restart reads only the notes that changed.
20
17
 
21
- * Up to about 10,000 notes, it starts within 20 seconds and answers within a few hundred milliseconds
22
- * From 10,000 to 20,000 notes, start it before your agents and keep it running
23
- * Above about 20,000 notes, run it as a service on Linux, and give it 2 GB of memory
18
+ | Measure | 10,000 notes | 50,000 notes |
19
+ | ----------------------------------------------------- | ------------ | ------------ |
20
+ | First start | 2.7 s | 15 s |
21
+ | Restart | 0.7 s | 2.0 to 2.3 s |
22
+ | First `select_rows` after a restart | 0.15 s | 1.1 to 2 s |
23
+ | Background work after a restart | 0.6 s | 4 to 6 s |
24
+ | Memory (resident set size) after a restart | 130 MB | 330 MB |
25
+ | Memory (resident set size) after a series of searches | 150 MB | 460 MB |
26
+ | Vault index on disk | 21 MB | 104 MB |
27
+
28
+ We measured the memory once the engine was idle, after a garbage collection, with the read cache off.
29
+
30
+ At 50,000 notes, the operations took these times:
31
+
32
+ | Operation | Median time |
33
+ | ---------------------------------------------- | ----------- |
34
+ | `insert_row` | 37 ms |
35
+ | `update_row` | 47 ms |
36
+ | `select_rows` on a table of 117 rows | 2.8 ms |
37
+ | `select_rows` of 5 notes by path | 0.3 ms |
38
+ | `describe_database` | 24 ms |
39
+ | `graph_neighbors` right after a write | 2.5 ms |
40
+ | `search_notes`, two words found in 1,213 notes | 14 ms |
41
+ | `search_notes`, a person’s name | 7 ms |
42
+ | `search_notes`, a word found in 15,905 notes | 140 ms |
43
+
44
+ The first `select_rows` waits until the engine has compared the vault with its index. These numbers set the size the engine handles well:
45
+
46
+ * Up to about 10,000 notes, it starts within 3 seconds and answers within a few hundred milliseconds
47
+ * From 10,000 to 50,000 notes, start it before your agents and keep it running
48
+ * Above about 20,000 notes, run it as a service, and give it 2 GB of memory
49
+
50
+ ## Start faster with the vault index
51
+
52
+ The engine keeps a vault index in its store, in `index/vault.sqlite`. It records what the engine read from each note: its properties, tags, links and text. It also indexes the words of each note, so `search_notes` reads only the notes that can match. It keeps each text compressed. At the next start, the engine reads only the notes that changed since.
53
+
54
+ * A note counts as unchanged while its size, its modification time and its change time stay the same.
55
+ * The change time moves with every write, also one that restores the old modification time, as `cp -p` and `rsync -t` do.
56
+ * A note written within 2 seconds of being recorded is read again. Some file systems keep times to the second only.
57
+ * A note you moved while the engine was stopped is found by its file id, or by its content.
58
+
59
+ The index is a cache, never the source of truth. Keep these points in mind:
60
+
61
+ * You can delete it at any time. The next start builds it again.
62
+ * The store backup leaves it out, and so does Time Machine on macOS.
63
+ * It needs Node.js 22.13 or later. On an older Node.js, every start reads every note.
64
+ * After each start, the engine checks the index in the background, one table at a time.
65
+ * When SQLite or that check finds damage, the engine stops using the index until the next start rebuilds it.
66
+ * A query with only one- or two-letter words reads every note.
67
+
68
+ To turn it off, set `vaultIndexEnabled` to `false` in the [engine’s settings](https://datanotes.dev/administration/configuration/). The engine then deletes the index, and every start reads every note.
69
+
70
+ ## Run a large database on Windows
71
+
72
+ Microsoft Defender scans every file the engine opens. On a large vault, these scans take most of the start. The engine marks its store as not content indexed, so Windows Search skips it. It never changes your antivirus settings.
73
+
74
+ To speed up the engine, choose one of these options:
75
+
76
+ * Exclude the store folder from real-time scanning. Run this in a PowerShell window opened as administrator:
77
+
78
+ ```powershell
79
+ Add-MpPreference -ExclusionPath "$env:LOCALAPPDATA\Datanotes"
80
+ ```
81
+
82
+ * Exclude the vault folder too, but only when its notes come from sources you trust.
83
+
84
+ * Put the vault and the data home on a Dev Drive. Its performance mode defers the scans.
85
+
86
+ Never exclude the `node.exe` process: that exempts every Node.js program on the machine.
87
+
88
+ Keep the store out of OneDrive, Dropbox and iCloud folders. Sync clients and scanners lock files, and the engine then retries its renames. On macOS, the engine writes a `.metadata_never_index` file in its store, so Spotlight skips it. It also keeps the vault index out of Time Machine, since the engine can rebuild it. Time Machine still backs up the rest of the store.
24
89
 
25
90
  ## Start the engine before the agent
26
91
 
@@ -67,3 +132,22 @@ volumes:
67
132
  ```
68
133
 
69
134
  On a Linux host, a bind mount delivers file events: set `DATANOTES_WATCH=events` there too. [Run in Docker](https://datanotes.dev/administration/docker/) explains the container.
135
+
136
+ ## Check the engine’s indexes
137
+
138
+ The engine keeps indexes of the vault in memory, so a read does not go through every note. It learns each change from the vault’s file events: its own writes, your edits, renames and deletes. It never trusts them over the notes, which stay the source of truth.
139
+
140
+ When an answer looks wrong after many changes outside the engine, check the indexes. Start the engine with `DATANOTES_INDEX_CHECK=1`:
141
+
142
+ ```bash
143
+ DATANOTES_INDEX_CHECK=1 \
144
+ npx -y datanotes serve --database your_database_folder
145
+ ```
146
+
147
+ After every operation, the engine then compares its indexes with a full read of the vault:
148
+
149
+ * A difference shows as an error in the engine’s log, with the notes it concerns.
150
+ * The engine then reads the vault again, so the next operations answer right.
151
+ * Each comparison reads every note, so keep the check for debugging, never in daily use.
152
+
153
+ Every 10 minutes, once no write ran for a minute, the engine also compares the transaction log’s view of the rows with the vault. A difference shows as a warning, and the engine logs the changes it missed.
@@ -22,6 +22,24 @@ The first start creates the engine’s store and its settings file, `settings.js
22
22
  npx -y datanotes store path --database your_database_folder
23
23
  ```
24
24
 
25
+ Add `--json` to get the answer as an object:
26
+
27
+ | Field | Meaning |
28
+ | ----------------- | ----------------------------------------------------------------------------- |
29
+ | `vault` | The folder you gave, as an absolute path |
30
+ | `store` | The store folder |
31
+ | `database_id` | The database’s id |
32
+ | `database_folder` | The database folder, relative to the folder you gave; empty with `--database` |
33
+ | `database_path` | The database folder on disk, with `metastore.yaml` at its root |
34
+
35
+ Every field but `vault` is `null` when the engine cannot tell where the store would be. That happens only when all three hold:
36
+
37
+ * You pass no `--store`, and `DATANOTES_STORE` is not set.
38
+ * This machine never ran the engine on the folder.
39
+ * `metastore.yaml` has no id yet.
40
+
41
+ With `--store`, only `database_id` can be `null`: neither the store nor `metastore.yaml` records the id.
42
+
25
43
  To run the engine in a container, follow [Run in Docker](https://datanotes.dev/administration/docker/).
26
44
 
27
45
  ## Where the engine keeps its store
@@ -5,8 +5,10 @@
5
5
  Direct neighbours of notes: what they link to (`out`) and what links to them (`in`,
6
6
  backlinks), with counts per edge label. `direction`: out, in or both (default).
7
7
 
8
- `from_nodes` selects the notes: `{"paths": [...]}` and/or `{"table": ..., "where": {...}}`.
9
- Edges are labelled with their link column (`people`, `event`, …) or `link` for other links.
8
+ `from_nodes` selects the notes: `{"paths": [...]}`, `{"table": ..., "where": {...}}` and/or
9
+ `{"tables": [...]}`: the rows of several tables, by name or pattern (`["*"]` every table, `["personal.*"]` a
10
+ catalog's). The whole vault's graph in one call: `{"tables": ["*"]}` with direction `out` (at most 5000
11
+ start notes). Edges are labelled with their link column (`people`, `event`, …) or `link` for other links.
10
12
 
11
13
  ## Request body
12
14
 
@@ -1,9 +1,9 @@
1
- # Shortest chain of links between two sets of notes (each `{"paths": [...]}` and/or
1
+ # Shortest chain of links between two sets of notes (each `{"paths": [...]}`, `{"table", "where"}`
2
2
 
3
3
  `POST /datanotes/operations/graph_path/` (operation `graph_path`)
4
4
 
5
- Shortest chain of links between two sets of notes (each `{"paths": [...]}` and/or
6
- `{"table", "where"}`): `found`, `length` and `path`: the steps (`from`, `label`, `direction`, `to`).
5
+ Shortest chain of links between two sets of notes (each `{"paths": [...]}`, `{"table", "where"}`
6
+ and/or `{"tables": [...]}`): `found`, `length` and `path`: the steps (`from`, `label`, `direction`, `to`).
7
7
  `direction` both by default; `max_depth` default 6 (max 10).
8
8
 
9
9
  ## Request body
@@ -1,8 +1,8 @@
1
- # Breadth-first walk over links from `from_nodes` (`{"paths": [...]}` and/or `{"table", "where"}`).
1
+ # Breadth-first walk over links from `from_nodes` (`{"paths": [...]}`, `{"table", "where"}` and/or `{"tables": [...]}`).
2
2
 
3
3
  `POST /datanotes/operations/graph_traverse/` (operation `graph_traverse`)
4
4
 
5
- Breadth-first walk over links from `from_nodes` (`{"paths": [...]}` and/or `{"table", "where"}`).
5
+ Breadth-first walk over links from `from_nodes` (`{"paths": [...]}`, `{"table", "where"}` and/or `{"tables": [...]}`).
6
6
 
7
7
  - `direction`: out (default, follow links), in (follow backlinks) or both.
8
8
  - `edges`: edge labels to follow (link columns such as `people`, `event`, or `link`).
@@ -23,6 +23,7 @@
23
23
  | `session` | `string` | no | | |
24
24
  | `result_chars` | `number` | no | | |
25
25
  | `result_tokens` | `number` | no | | |
26
+ | `summary` | `object` | no | | The result's summary, when `value` is sent already cut: the engine records it (the answer says `takes_summary`). |
26
27
 
27
28
  ## Responses
28
29
 
@@ -11,5 +11,5 @@ Operations that read rows and notes: select_rows, link targets, text and semanti
11
11
  - [The rows a link column may point to, for one row: what an agent or a picker offers before writing](https://datanotes.dev/api/operations/list_link_targets/): `POST /datanotes/operations/list_link_targets/`
12
12
  - [Find notes by meaning, not exact words: `{query, results: [{path, score, heading, snippet,](https://datanotes.dev/api/operations/semantic_search/): `POST /datanotes/operations/semantic_search/`
13
13
  - [Direct neighbours of notes: what they link to (`out`) and what links to them (`in`,](https://datanotes.dev/api/operations/graph_neighbors/): `POST /datanotes/operations/graph_neighbors/`
14
- - [Breadth-first walk over links from `from_nodes` (`{"paths": [...]}` and/or `{"table", "where"}`).](https://datanotes.dev/api/operations/graph_traverse/): `POST /datanotes/operations/graph_traverse/`
15
- - [Shortest chain of links between two sets of notes (each `{"paths": [...]}` and/or](https://datanotes.dev/api/operations/graph_path/): `POST /datanotes/operations/graph_path/`
14
+ - [Breadth-first walk over links from `from_nodes` (`{"paths": [...]}`, `{"table", "where"}` and/or `{"tables": [...]}`).](https://datanotes.dev/api/operations/graph_traverse/): `POST /datanotes/operations/graph_traverse/`
15
+ - [Shortest chain of links between two sets of notes (each `{"paths": [...]}`, `{"table", "where"}`](https://datanotes.dev/api/operations/graph_path/): `POST /datanotes/operations/graph_path/`
package/dist/docs/api.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Datanotes HTTP API
2
2
 
3
- > OpenAPI 3.1.0 description of the Datanotes 3.0.0 HTTP API: https://datanotes.dev/openapi.json
3
+ > OpenAPI 3.1.0 description of the Datanotes 3.1.0 HTTP API: https://datanotes.dev/openapi.json
4
4
 
5
5
  Database of Markdown notes: tables of notes, rows, queries, history, a change feed and the vault's notes.
6
6
 
@@ -69,8 +69,8 @@ Operations that read rows and notes: select_rows, link targets, text and semanti
69
69
  - [The rows a link column may point to, for one row: what an agent or a picker offers before writing](https://datanotes.dev/api/operations/list_link_targets/): `POST /datanotes/operations/list_link_targets/`
70
70
  - [Find notes by meaning, not exact words: `{query, results: [{path, score, heading, snippet,](https://datanotes.dev/api/operations/semantic_search/): `POST /datanotes/operations/semantic_search/`
71
71
  - [Direct neighbours of notes: what they link to (`out`) and what links to them (`in`,](https://datanotes.dev/api/operations/graph_neighbors/): `POST /datanotes/operations/graph_neighbors/`
72
- - [Breadth-first walk over links from `from_nodes` (`{"paths": [...]}` and/or `{"table", "where"}`).](https://datanotes.dev/api/operations/graph_traverse/): `POST /datanotes/operations/graph_traverse/`
73
- - [Shortest chain of links between two sets of notes (each `{"paths": [...]}` and/or](https://datanotes.dev/api/operations/graph_path/): `POST /datanotes/operations/graph_path/`
72
+ - [Breadth-first walk over links from `from_nodes` (`{"paths": [...]}`, `{"table", "where"}` and/or `{"tables": [...]}`).](https://datanotes.dev/api/operations/graph_traverse/): `POST /datanotes/operations/graph_traverse/`
73
+ - [Shortest chain of links between two sets of notes (each `{"paths": [...]}`, `{"table", "where"}`](https://datanotes.dev/api/operations/graph_path/): `POST /datanotes/operations/graph_path/`
74
74
 
75
75
  ### History
76
76
 
@@ -24,7 +24,42 @@ Every client goes through that engine:
24
24
 
25
25
  ## Writes run one at a time
26
26
 
27
- The engine applies writes one at a time, under the lock of its transaction log. Reads don’t wait for that lock: they run in parallel, with each other and with writes.
27
+ The engine runs writes one at a time, in the order they arrive, under the lock of its transaction log. Reads run in parallel with each other.
28
+
29
+ A write that still runs after a minute lets the next write start, so one stuck write can’t hold up the rest. The engine logs a warning that names the operation.
30
+
31
+ ## Reads wait for writes
32
+
33
+ The engine runs on one thread. Under a stream of reads, a write would share that thread with every read that arrives while it runs, and take several times longer. So writes go first:
34
+
35
+ * **A write runs or waits**: a read that arrives waits until no write is left.
36
+ * **A read already under way**: it finishes normally. The write doesn’t wait for it.
37
+ * **A read waits at most `writePriorityMaxWaitMs`**: 100 ms by default. It also starts once 8 writes started after it came. A burst of writes never starves the reads.
38
+ * **A long poll of the change feed**: `list_changes` with `wait_ms` never waits, and never holds a write.
39
+
40
+ What each operation does is unchanged: a read sees every write that ended before it started. Set `writePriority` to `false` in the [settings](https://datanotes.dev/reference/settings/) to let reads and writes start in the order they arrive. The span `engine.schedule.wait` shows the wait in [traces](https://datanotes.dev/develop/telemetry/#the-spans-the-engine-sends).
41
+
42
+ ## Repeated reads come from memory
43
+
44
+ A read you send again with the same arguments, with the same token, often has the same answer. The engine keeps the answers of recent reads in memory, and sends the kept answer while nothing it read has changed:
45
+
46
+ * **A write**: a read of the tables it changed is computed again. Reads of other tables keep their answers.
47
+ * **Undo, a hand edit on disk, a sync tool**: they count as writes. The engine sees them before it answers.
48
+ * **A schema change**: every answer is computed again.
49
+ * **A view**: it depends on the tables its query reads.
50
+
51
+ Some reads depend on less than a table, or on more:
52
+
53
+ * **`read_note`**: it depends on its note. Deleting or renaming a folder also counts.
54
+ * **`list_folder`**: it depends on the notes and folders created, deleted or renamed in its folder. Deleting or renaming any folder also counts.
55
+ * **`list_tables`**: it depends on the schema only. A write to a table keeps its answer.
56
+ * **`describe_database`**: it depends on every table, and on the notes whose properties can’t be read.
57
+ * **A filter through a link column**: a key such as `project.status` follows the link column `project`. The read also depends on the tables the link leads to.
58
+ * **Reads that follow links or search the text**: `expand`, `within`, the graph operations and `search_notes` depend on the whole vault.
59
+
60
+ Reads that compare with the current time (`{now}`) are never kept. A read with `{today}` is kept 15 minutes at most. Reads of history, of the change feed and of `system.information_schema` are never kept either.
61
+
62
+ `readCacheMaxBytes` sets the memory for the answers: 33554432 bytes (32 MiB) by default. The least recently used answers go first. Set it to `0` to turn the cache off.
28
63
 
29
64
  The engine checks each write against the database as the writes before it left it. A write is checked first, then applied. If another write is applied while the engine checks yours, the engine checks yours again on the new state. It tries up to 5 times before it gives up.
30
65
 
@@ -32,7 +32,7 @@ An operation’s body takes the same arguments as the agent’s tool of the same
32
32
  * **[`list_folder`](https://datanotes.dev/reference/operations/queries/#list_folder)**: the notes and folders in a `folder`
33
33
  * **[`search_notes`](https://datanotes.dev/reference/operations/queries/#search_notes)**: the notes that contain a `query`
34
34
 
35
- Every route and field keeps working in every 3.x version: see [Compatibility](https://datanotes.dev/reference/compatibility/).
35
+ Routes and fields follow semantic versioning: see [Compatibility](https://datanotes.dev/reference/compatibility/).
36
36
 
37
37
  ## Record who makes each write
38
38
 
@@ -72,6 +72,16 @@ Several clients can write at the same time. [Concurrent writes](https://datanote
72
72
 
73
73
  The engine binds to `127.0.0.1` unless you start it with `--host`. It answers no Cross-Origin Resource Sharing (CORS) preflight, so web pages can’t call it. To reach it from another device, follow the [security model](https://datanotes.dev/guides/security/security-model/).
74
74
 
75
+ ## Get large answers compressed
76
+
77
+ A client on another machine gets answers of 16 KB or more compressed with gzip. It asks for that with the `Accept-Encoding: gzip` header, as most HTTP clients do. A client on the engine’s machine gets them as they are: compressing would cost the engine more time than it saves.
78
+
79
+ The `httpCompression` [setting](https://datanotes.dev/reference/settings/) changes this:
80
+
81
+ * **`remote`**: clients on other machines only (the default).
82
+ * **`always`**: this machine’s clients too.
83
+ * **`off`**: no compression.
84
+
75
85
  ## Generate a client or explore the routes
76
86
 
77
87
  The [HTTP API reference](https://datanotes.dev/api/) documents every route, generated from the same OpenAPI description, [openapi.json](https://datanotes.dev/openapi.json). From JavaScript, use the [JavaScript client](https://datanotes.dev/develop/client/). Agents use the [Model Context Protocol (MCP) server](https://datanotes.dev/start/connect-agents/).
@@ -1,77 +1,276 @@
1
- # Send logs to your observability backend
1
+ # Send logs and traces to your observability backend
2
2
 
3
- > Send the logs of the engine, the MCP server and the apps to any OpenTelemetry backend.
3
+ > Send the logs and traces of the engine, the MCP server and the apps to any OpenTelemetry backend, and find slow operations.
4
4
 
5
- Every part of Datanotes sends its logs with OpenTelemetry, the vendor-neutral standard, so any backend with an OpenTelemetry Protocol (OTLP) endpoint can receive them. This page shows how to point the engine, the Model Context Protocol (MCP) server and the apps at your backend. It also shows how to follow one agent call across all three. Datanotes has no vendor-specific code.
5
+ Every part of Datanotes sends logs and traces with OpenTelemetry, the vendor-neutral standard. Any backend with an OpenTelemetry Protocol (OTLP) endpoint can receive them. This page shows you how to:
6
+
7
+ * Point the engine, the Model Context Protocol (MCP) server and the apps at your backend
8
+ * Read the spans the engine sends, and what each one measures
9
+ * Follow one agent call across the MCP server and the engine
10
+ * Find the operations that are slow, and why
11
+
12
+ Datanotes has no vendor-specific code.
6
13
 
7
14
  ## What Datanotes sends and where
8
15
 
9
- Datanotes sends logs only, over HTTP, to `your_endpoint/v1/logs`. It encodes them as `http/protobuf`, the OpenTelemetry default, or as `http/json`. Any OpenTelemetry Collector receives them, and so do these backends, among others:
16
+ Datanotes sends two signals over HTTP:
17
+
18
+ * **Logs** to `your_endpoint/v1/logs`
19
+ * **Traces** to `your_endpoint/v1/traces`
20
+
21
+ It encodes them as `http/protobuf`, the OpenTelemetry default, or as `http/json`. Any OpenTelemetry Collector receives them, and so do these backends, among others:
10
22
 
11
23
  * SigNoz
12
- * Grafana: Alloy, Loki or Grafana Cloud
24
+ * Grafana: Alloy, Tempo, Loki or Grafana Cloud
13
25
  * Elastic, Datadog, Honeycomb, New Relic and Dynatrace
14
26
 
15
- Datanotes sends no traces or metrics. For a backend that accepts only gRPC, put an OpenTelemetry Collector in front of it. When the collector doesn’t answer, Datanotes counts the records as lost and keeps working.
27
+ Datanotes sends no metrics: your backend computes rates and latency percentiles from the spans. For a backend that accepts only gRPC, put an OpenTelemetry Collector in front of it. When the collector doesn’t answer, Datanotes counts the records and spans as lost and keeps working.
16
28
 
17
29
  ## Configure the export with environment variables
18
30
 
19
31
  The engine, the MCP server and the apps all read the standard OpenTelemetry variables:
20
32
 
21
- | Variable | Meaning |
22
- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
23
- | `OTEL_EXPORTER_OTLP_ENDPOINT` | The collector’s base address, for example `http://127.0.0.1:4318` |
24
- | `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | Or the full `…/v1/logs` address |
25
- | `OTEL_EXPORTER_OTLP_HEADERS` | `key=value,key2=value2`, with URL-encoded values: the API key or `Authorization` your backend asks for |
26
- | `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/protobuf` (default) or `http/json` |
27
- | `OTEL_SERVICE_NAME` | The service name; defaults to `datanotes`, `datanotes-mcp` (for `datanotes mcp`) or the app’s name |
28
- | `OTEL_RESOURCE_ATTRIBUTES` | Extra resource attributes, for example `deployment.environment=home` |
29
- | `OTEL_SDK_DISABLED=true` or `OTEL_LOGS_EXPORTER=none` | Turns the export off |
33
+ | Variable | Meaning |
34
+ | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
35
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | The collector’s base address, for example `http://127.0.0.1:4318` |
36
+ | `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | Or the full `…/v1/logs` address |
37
+ | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | Or the full `…/v1/traces` address |
38
+ | `OTEL_EXPORTER_OTLP_HEADERS` | `key=value,key2=value2`, with URL-encoded values: the API key or `Authorization` your backend asks for |
39
+ | `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/protobuf` (default) or `http/json` |
40
+ | `OTEL_SERVICE_NAME` | The service name; defaults to `datanotes`, `datanotes-mcp` (for `datanotes mcp`) or the app’s name |
41
+ | `OTEL_RESOURCE_ATTRIBUTES` | Extra resource attributes, for example `team=home` |
42
+ | `OTEL_TRACES_SAMPLER` and `OTEL_TRACES_SAMPLER_ARG` | `traceidratio` with a share from 0 to 1, `always_on` or `always_off`, plain or with the `parentbased_` prefix |
43
+ | `OTEL_SDK_DISABLED=true` | Turns all the export off |
44
+ | `OTEL_LOGS_EXPORTER=none` or `OTEL_TRACES_EXPORTER=none` | Turns off the logs or the traces only |
30
45
 
31
- Datanotes also reads the `…_LOGS_…` variants of the headers and protocol variables. Without an endpoint, it sends nothing.
46
+ Datanotes also reads the `…_LOGS_…` and `…_TRACES_…` variants of the headers and protocol variables. Without an endpoint, it sends nothing.
32
47
 
33
48
  ## Configure the engine in its settings file
34
49
 
35
- Instead of the variables, you can configure the engine in its settings file, `settings.json` in its store (`datanotes store path` shows where). A variable you set overrides the matching setting:
50
+ Instead of the variables, you can configure the engine in its settings file, `settings.json` in its store. `datanotes store path` shows where. A variable you set overrides the matching setting:
36
51
 
37
52
  ```json
38
53
  {
39
54
  "telemetryEnabled": true,
40
55
  "telemetryEndpoint": "http://127.0.0.1:4318",
41
56
  "telemetryServiceName": "datanotes",
57
+ "telemetryEnvironment": "home",
42
58
  "telemetryProtocol": "http/protobuf",
43
- "telemetryHeaders": {}
59
+ "telemetryHeaders": {},
60
+ "telemetryTraces": true,
61
+ "telemetryTraceSampleRatio": 1,
62
+ "telemetryMaxSpansPerTrace": 1000,
63
+ "telemetrySlowSpanMs": 0
44
64
  }
45
65
  ```
46
66
 
47
- `telemetryHeaders` holds what your backend asks for, such as an API key or `Authorization: Bearer …`. The engine keeps it in the settings file. To keep a key out of that file, set `OTEL_EXPORTER_OTLP_HEADERS` instead.
67
+ Each setting does one thing:
68
+
69
+ * **`telemetryEnabled`** turns the export of logs and traces on.
70
+ * **`telemetryEnvironment`** names the environment, such as `dev` or `home`. The engine sends it as `deployment.environment.name` and as the older `deployment.environment`, which some backends group by.
71
+ * **`telemetryHeaders`** holds what your backend asks for, such as an API key. To keep a key out of the file, set `OTEL_EXPORTER_OTLP_HEADERS` instead.
72
+ * **`telemetryTraces`** sends the traces too. It is on by default: turning telemetry on gives you both signals.
73
+ * **`telemetryTraceSampleRatio`** keeps that share of new traces, from 0 to 1. The default, 1, keeps every trace. A trace that a caller started follows the caller’s choice.
74
+ * **`telemetryMaxSpansPerTrace`** caps the spans of one trace. The engine drops the rest and counts them in `datanotes.spans_dropped` on the trace’s first span.
75
+ * **`telemetrySlowSpanMs`** logs a request, an operation or a background job as a WARN when it lasts at least that many milliseconds. 0 turns it off.
76
+
77
+ The engine reads the settings again while it runs, so a change applies within a few seconds. With traces off, tracing costs the engine one check per instrumented call.
48
78
 
49
79
  ## What the engine logs
50
80
 
51
81
  The engine logs these events:
52
82
 
53
83
  * Every HTTP request and operation, with the route, duration, outcome and actor
54
- * The tool calls MCP servers report, with the tool, duration, result size in characters and tokens, client, model and session
84
+ * The tool calls MCP servers report, with the tool, duration, result size and client
55
85
  * Refused requests
56
86
  * The semantic index and git sync
57
87
  * Errors
58
88
 
89
+ A record made during a traced request names its trace. The OTLP record carries the trace and span IDs, so your backend links the log to the trace. The log files carry them as `trace_id` and `span_id`. While a request’s trace is sent, its INFO records become DEBUG records: the span already says what they say.
90
+
59
91
  Whatever the collector does, the engine also writes its records to files in `logs/`, in its store outside the vault. The `fileLogEnabled` setting turns these files on or off, and `logRetentionDays` sets how long the engine keeps them.
60
92
 
93
+ ## The spans the engine sends
94
+
95
+ A trace is a tree of spans. Each span measures one piece of work and names its parent. A request to the engine gives this tree:
96
+
97
+ ```text
98
+ POST /datanotes/operations/insert_row/ SERVER: the HTTP request
99
+ └── engine.op insert_row the operation
100
+ ├── engine.schedule.wait a read waits for writes (only then)
101
+ ├── engine.settle wait for the vault to settle
102
+ └── engine.insert_row the engine's work
103
+ ├── engine.autolink links found in the text
104
+ ├── engine.checks CHECKs and link rules
105
+ ├── engine.lock.acquire wait for the write lock
106
+ └── engine.lock.hold work under the lock
107
+ ├── engine.transaction_log hand edits logged first
108
+ ├── engine.apply files written
109
+ │ └── engine.journal.append the undo entry
110
+ └── engine.transaction_log the operation's commit
111
+ ```
112
+
113
+ A read gives a similar tree under `engine.query.execute`:
114
+
115
+ * **`engine.query.view`** runs the stored query of a view.
116
+ * **`engine.query.plan`** resolves dates and `within` and compiles the filter.
117
+ * **`engine.query.scan`** reads every row of the table. Its `datanotes.query.plan` is `full_scan`: Datanotes has no indexes, and a file cache spares the reads of unchanged notes.
118
+ * **`engine.query.filter`**, **`engine.query.join`**, **`engine.query.group`**, **`engine.query.sort`** and **`engine.query.project`** follow, as the query needs them.
119
+
120
+ Other spans cover the rest of the engine’s time:
121
+
122
+ * **`engine.metastore.parse`** when `metastore.yaml` changed and the engine parses it again
123
+
124
+ * **`engine.history.read`** for a read as of a version or a time
125
+
126
+ * **`engine.graph.build`** when a graph operation rebuilds the link graph after a change. Calls that need it at the same time wait for that one build and count it in `datanotes.graph.build_waits`.
127
+
128
+ * **`engine.change_feed.record`** for the change feed’s events of an operation
129
+
130
+ * **`POST`** (CLIENT) for each webhook delivery, with `url.full`, `server.address` and `http.response.status_code`
131
+
132
+ * **`job <name>`** for background work. The jobs are:
133
+
134
+ * `job hand_edits_log`
135
+ * `job adopt_note`
136
+ * `job schema_reload`
137
+ * `job semantic_index`
138
+ * `job store_maintenance`
139
+ * `job change_feed_prune`
140
+ * `job change_feed_hand_edits`
141
+ * `job transaction_log_warmup`
142
+ * `job auto_recompute`
143
+ * `job inverse_links`
144
+ * `job datanotes_start`, the engine’s start
145
+
146
+ Background work is always a trace of its own, even when a request saw the change behind it. After a start, `job transaction_log_warmup` reads each table’s transaction log once, one table at a time, so a write waits for one table at most.
147
+
148
+ ### Attributes of the operation span
149
+
150
+ The `engine.op <operation>` span follows the conventions of database spans. Most backends list these spans as database calls:
151
+
152
+ | Attribute | Meaning |
153
+ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
154
+ | `db.system.name`, `db.system` | `datanotes` |
155
+ | `db.operation.name` | The operation, such as `select_rows` |
156
+ | `db.collection.name` | The table or view the call names |
157
+ | `db.namespace` | The catalog and schema, when the call gives a full name |
158
+ | `db.query.summary` | The operation and its table, such as `select_rows task` |
159
+ | `db.response.returned_rows` | The rows the call returned |
160
+ | `datanotes.rows_scanned` | The rows the engine read to answer |
161
+ | `datanotes.notes_changed`, `datanotes.files_created` | What a write changed |
162
+ | `datanotes.migration_id` | The write’s journal entry, for `undo_migration` |
163
+ | `datanotes.dry_run` | Whether the call was a dry run |
164
+ | `datanotes.long_poll` | `true` for a `list_changes` call that waits for changes |
165
+ | `datanotes.schedule.class` | `read`, `write` or `free`: how [write priority](https://datanotes.dev/develop/concurrency/#reads-wait-for-writes) treats the call |
166
+ | `datanotes.schedule.wait_ms` | How long the call waited for writes before it started |
167
+ | `datanotes.schedule.released` | Why a waiting read started: `writes_done`, `max_wait` or `max_writes` |
168
+ | `datanotes.read_cache` | For a read: `hit` (answered from memory), `miss` or `not_kept` ([read cache](https://datanotes.dev/develop/concurrency/#repeated-reads-come-from-memory)) |
169
+ | `error.type` | Why the call failed or was refused |
170
+
171
+ The SERVER span adds `http.request.method`, `http.route`, `http.response.status_code`, `client.address` and `user_agent.original`. It also names the caller: `datanotes.token.name` holds the token’s name, never the token. `datanotes.client.harness` and `datanotes.client.model` hold what the agent says of itself.
172
+
173
+ ### Calls to the vault
174
+
175
+ Reading a table of thousands of notes means thousands of file reads. The engine doesn’t make a span of each one. It adds them up on the span that made them and on its operation span:
176
+
177
+ * `datanotes.io.read_count`, `datanotes.io.read_chars` and `datanotes.io.read_ms`
178
+ * The same for `write`, `create`, `list`, `exists`, `rename` and the other calls
179
+ * `datanotes.cache.hits` and `datanotes.cache.misses` of the file cache
180
+ * `datanotes.cache.text_reads` and `datanotes.cache.text_source`: note texts the file cache read again, or took from the [vault index](https://datanotes.dev/administration/limits/#start-faster-with-the-vault-index)
181
+ * `datanotes.validate.rows` and `datanotes.validate.ms`, `datanotes.checks.rows` and `datanotes.checks.ms` for the checks run once per row
182
+
183
+ `_chars` counts characters, not bytes: the length of the text as JavaScript counts it, in UTF-16 code units. In English Markdown, characters and bytes are about the same. Accents, other alphabets and emoji take more bytes than characters. For `list`, `_chars` counts the files and folders listed. The calls that move no text count 0.
184
+
185
+ A single call that takes 20 ms or more gets a span of its own, `vault.<call>`, with its `file.path` and `datanotes.io.chars`.
186
+
187
+ ### Errors and refusals
188
+
189
+ The engine tells failures and refusals apart:
190
+
191
+ * **A failure** is a bug, a disk error, a write that failed partway or a 5xx answer. Its span has status ERROR and an `exception` event with the type, message and stack.
192
+ * **A refusal** is a call the caller can fix: bad arguments, an unknown table, a broken CHECK. Its span keeps the status unset and sets `error.type`, such as `violation` or `TableNotFoundError`.
193
+
194
+ An empty answer to a `list_changes` poll sends no trace and no log line: services poll every few seconds.
195
+
196
+ ## Follow one agent call across services
197
+
198
+ One agent call is one trace across the MCP server and the engine:
199
+
200
+ 1. The MCP server makes a SERVER span, `tools/call <tool>`. It continues the client’s trace when the client sends `traceparent` in the call’s `_meta`.
201
+ 2. Each request it sends to the engine is a CLIENT span, child of the tool’s span. The request carries that span as its `traceparent` header.
202
+ 3. The engine’s SERVER span continues that trace, and its operation spans follow.
203
+
204
+ The tool span carries `mcp.method.name`, `gen_ai.operation.name` (`execute_tool`), `gen_ai.tool.name` and `mcp.session.id`. Find the agent call by its tool or session, open the trace, and you see where the time went in both processes.
205
+
206
+ The engine, the MCP server and the apps also log the same `session` and `tool` for each agent call. `session` is the first 16 characters of the MCP session ID.
207
+
208
+ ## Find the slow operations
209
+
210
+ Your backend computes the latency of each span name from the traces. To find where the engine spends its time:
211
+
212
+ 1. List the `engine.op <operation>` spans by duration, or open your backend’s database calls view. Leave out the spans with `datanotes.long_poll`: they wait on purpose.
213
+ 2. Open a slow trace and compare its children. A long `engine.lock.acquire` means the call waited for another write. A long `engine.lock.hold` means the write itself was slow. A long `engine.schedule.wait` means a read waited for writes to end.
214
+ 3. In a read, compare `engine.query.scan` with the rest. A slow scan with many `datanotes.cache.misses` reads many notes from disk. A slow scan with few misses works on a large table: narrow it with `where`, or split the table.
215
+ 4. Look at the `datanotes.io.*_ms` attributes of the operation span. They show how much of its time went to the disk.
216
+ 5. Set `telemetrySlowSpanMs` to log the slow calls as WARN records, and alert on them.
217
+
61
218
  ## Configure the MCP server and the apps
62
219
 
63
220
  The MCP server and the apps read the same variables, and each has its own alternative:
64
221
 
65
- * **`datanotes mcp`**: the `--otlp-endpoint your_endpoint` option
222
+ * **`datanotes mcp`**: the `--otlp-endpoint your_endpoint` option, for its logs and traces
66
223
  * **Apps**: an `otlp` block in the app’s configuration, `{"endpoint": "http://127.0.0.1:4318", "headers": {}, "serviceName": "…"}`
67
224
 
68
- ## Follow one agent call across services
225
+ ## Continue a trace from your own program
226
+
227
+ Your program continues its trace in the engine when it sends the W3C `traceparent` header, and `tracestate` when it has one. `datanotes/client` does it for you with two options of `DatanotesClient`:
228
+
229
+ * **`trace`** is the trace to continue: a `traceparent` value, a context, or a function that returns the current one.
230
+ * **`tracer`** makes a CLIENT span for each request, child of `trace`, and sends that span as the engine’s parent.
231
+
232
+ ```ts
233
+ import {
234
+ DatanotesClient,
235
+ OtlpTraceExporter,
236
+ Tracer,
237
+ otlpTracesFromEnv,
238
+ } from "datanotes/client";
239
+
240
+
241
+ const otlp = otlpTracesFromEnv(process.env, { serviceName: "my-app" });
242
+ const exporter = otlp ? new OtlpTraceExporter(otlp) : null;
243
+ const tracer = new Tracer({ export: (s) => exporter?.export(s) });
244
+
245
+
246
+ const run = tracer.startSpan("import films", { kind: "server" });
247
+ const client = new DatanotesClient({
248
+ url: "http://127.0.0.1:27150",
249
+ token: process.env.DATANOTES_TOKEN ?? "",
250
+ tracer,
251
+ trace: run.context,
252
+ });
253
+ await client.call("insert_row", {
254
+ table: "film",
255
+ values: { title: "Arrival" },
256
+ });
257
+ run.end();
258
+ await exporter?.close();
259
+ ```
260
+
261
+ Without a tracer, pass the incoming header as it is. A call can also name its own trace:
262
+
263
+ ```ts
264
+ await client.call("select_rows", { table: "film" }, undefined, {
265
+ trace: request.headers.traceparent,
266
+ });
267
+ ```
69
268
 
70
- The engine, the MCP server and the apps log the same `session` and `tool` for each agent call. `session` is the first 16 characters of the MCP session ID. Filter by `session` in your backend to see the call in the MCP server, in the app whose tool it reached, and the writes it made in the engine.
269
+ Any HTTP client works too: send `traceparent: 00-<trace id>-<span id>-01` with the request.
71
270
 
72
271
  ## Send logs from your own program
73
272
 
74
- `datanotes/client` exports the exporter that Datanotes uses, so your own programs can log the same way:
273
+ `datanotes/client` exports the log exporter that Datanotes uses, so your own programs can log the same way:
75
274
 
76
275
  ```ts
77
276
  import { OtlpLogExporter, otlpFromEnv } from "datanotes/client";
@@ -83,4 +282,4 @@ log?.log("INFO", "imported films", { count: 8 });
83
282
  await log?.close();
84
283
  ```
85
284
 
86
- `otlpFromEnv` reads the variables above and returns `null` when you set no endpoint or turn the export off; the program then logs nothing. Call `close()` before the program exits to send the remaining records.
285
+ `otlpFromEnv` reads the variables above and returns `null` when you set no endpoint or turn the export off. The program then logs nothing. Pass a span’s context as the fourth argument of `log` to link the record to that span. Call `close()` before the program exits to send the remaining records.