datanotes 3.0.0 → 3.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.
Files changed (73) 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 +563 -60
  5. package/dist/api/index.d.cts +3 -2
  6. package/dist/api/index.d.ts +3 -2
  7. package/dist/api/index.js +563 -60
  8. package/dist/api/mcp.cjs +181 -11
  9. package/dist/api/mcp.js +181 -11
  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 +9934 -2656
  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/add_column.md +1 -0
  22. package/dist/docs/api/operations/add_constraint.md +2 -0
  23. package/dist/docs/api/operations/alter_column.md +1 -1
  24. package/dist/docs/api/operations/alter_table.md +3 -2
  25. package/dist/docs/api/operations/check_table.md +1 -1
  26. package/dist/docs/api/operations/create_table.md +3 -1
  27. package/dist/docs/api/operations/describe_table.md +1 -1
  28. package/dist/docs/api/operations/graph_neighbors.md +4 -2
  29. package/dist/docs/api/operations/graph_path.md +3 -3
  30. package/dist/docs/api/operations/graph_traverse.md +2 -2
  31. package/dist/docs/api/operations/insert_row.md +2 -2
  32. package/dist/docs/api/operations/record_tool_call.md +1 -0
  33. package/dist/docs/api/operations/tags/queries.md +2 -2
  34. package/dist/docs/api/operations/tags/rows-dml.md +1 -1
  35. package/dist/docs/api.md +4 -4
  36. package/dist/docs/develop/concurrency.md +36 -1
  37. package/dist/docs/develop/http-api.md +11 -1
  38. package/dist/docs/develop/telemetry.md +224 -25
  39. package/dist/docs/guides/data-quality/check-constraints.md +14 -13
  40. package/dist/docs/guides/data-quality/hand-edits.md +10 -10
  41. package/dist/docs/guides/database-objects/bases.md +11 -11
  42. package/dist/docs/guides/database-objects/catalogs-and-schemas.md +16 -10
  43. package/dist/docs/guides/database-objects/columns.md +38 -38
  44. package/dist/docs/guides/database-objects/relations.md +173 -36
  45. package/dist/docs/guides/database-objects/tables.md +90 -109
  46. package/dist/docs/guides/database-objects/views.md +44 -44
  47. package/dist/docs/guides/history/back-up.md +12 -4
  48. package/dist/docs/guides/history/change-data-feed.md +34 -34
  49. package/dist/docs/guides/history/journal-and-undo.md +12 -12
  50. package/dist/docs/guides/history/restore.md +20 -20
  51. package/dist/docs/guides/history/table-history.md +31 -31
  52. package/dist/docs/guides/history/vacuum.md +1 -10
  53. package/dist/docs/guides/read-and-write/automatic-links.md +15 -15
  54. package/dist/docs/guides/read-and-write/import-notes.md +21 -21
  55. package/dist/docs/guides/read-and-write/joins-and-aggregates.md +7 -7
  56. package/dist/docs/guides/read-and-write/read-and-filter.md +10 -10
  57. package/dist/docs/guides/read-and-write/write-rows.md +10 -10
  58. package/dist/docs/guides/search/link-graph.md +95 -0
  59. package/dist/docs/guides/search/text.md +1 -1
  60. package/dist/docs/guides/security/who-did-what.md +2 -0
  61. package/dist/docs/reference/cli.md +10 -3
  62. package/dist/docs/reference/compatibility.md +33 -13
  63. package/dist/docs/reference/errors.md +1 -1
  64. package/dist/docs/reference/operations/auxiliary.md +2 -2
  65. package/dist/docs/reference/operations/queries.md +3 -3
  66. package/dist/docs/reference/operations/rows.md +1 -1
  67. package/dist/docs/reference/operations/schema.md +15 -11
  68. package/dist/docs/reference/operations.md +39 -39
  69. package/dist/docs/reference/settings.md +37 -25
  70. package/dist/docs/release-notes.md +145 -0
  71. package/dist/docs/start/how-it-works.md +35 -3
  72. package/dist/docs/start/quick-start.md +2 -0
  73. package/package.json +3 -2
@@ -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.
@@ -6,7 +6,7 @@ A CHECK constraint is a rule written as a `where` filter, such as “a watched f
6
6
 
7
7
  ## Enforce rules with CHECK constraints
8
8
 
9
- `add_constraint` adds a CHECK constraint, a rule every row must satisfy, written as a `where` filter. The engine refuses it while existing rows break the rule and lists them; after that, it enforces the rule on every write. A condition on a column the row leaves empty passes, as in Structured Query Language (SQL). Inside `or`, `and` and `not`, the engine tests an empty value as it is, so a watched film without a `rating` breaks `rated_when_watched`.
9
+ `add_constraint` adds a CHECK constraint, a rule every row must satisfy, written as a `where` filter. The engine refuses it while existing rows break the rule and lists them; after that, it enforces the rule on every write. A condition on a column the row leaves empty passes, as in Structured Query Language (SQL). Inside `or`, `and` and `not`, the engine tests an empty value as it is, so a watched film without a `rating` breaks `rated_when_watched`. `hint` says what to do when a row breaks the rule. The refusal puts it first, and `describe_table` lists it in `constraint_hints`.
10
10
 
11
11
  **`add_constraint`**
12
12
 
@@ -14,7 +14,8 @@ A CHECK constraint is a rule written as a `where` filter, such as “a watched f
14
14
  {
15
15
  "table": "film",
16
16
  "name": "rated_when_watched",
17
- "check": { "or": [{ "status": { "ne": "watched" } }, { "rating": { "exists": true } }] }
17
+ "check": { "or": [{ "status": { "ne": "watched" } }, { "rating": { "exists": true } }] },
18
+ "hint": "A watched film needs a rating from 1 to 5: ask me for it."
18
19
  }
19
20
  ```
20
21
 
@@ -28,7 +29,7 @@ Answer:
28
29
  "commits": [
29
30
  { "table": "diary.classics.film", "version": 8, "operation": "ADD CONSTRAINT" }
30
31
  ],
31
- "migration_id": "20261012T072004-add_constraint-add-constraint-rated_when_watc"
32
+ "migration_id": "20261019T040655-add_constraint-add-constraint-rated_when_watc"
32
33
  }
33
34
  ```
34
35
 
@@ -57,7 +58,7 @@ Refused:
57
58
  ```text
58
59
  insert_row refused — nothing was written:
59
60
  invalid datanotes/diary/classics/film/His Girl Friday.md: rated_when_watched violates CHECK rated_when_watched: {"or":[{"status":{"ne":"watched"}},{"rating":{"exists":true}}]}
60
- Hint: The row breaks a CHECK constraint of the table (named in the message, with its rule): change the values so the rule holds. describe_table lists the table's constraints. Then call insert_row again (dry_run: true checks without writing).
61
+ Hint: A watched film needs a rating from 1 to 5: ask me for it. The row breaks a CHECK constraint of the table (named in the message, with its rule): change the values so the rule holds. describe_table lists the table's constraints. Then call insert_row again (dry_run: true checks without writing).
61
62
  Docs: https://datanotes.dev/guides/data-quality/check-constraints.md#enforce-rules-with-check-constraints (offline: read_docs("guides/data-quality/check-constraints#enforce-rules-with-check-constraints"))
62
63
  ```
63
64
 
@@ -85,7 +86,7 @@ Answer:
85
86
  "files_created": 1,
86
87
  "planned_creates": ["datanotes/diary/classics/film/His Girl Friday.md"],
87
88
  "commits": [{ "table": "diary.classics.film", "version": 10, "operation": "WRITE" }],
88
- "migration_id": "20261012T072004-insert_row-insert-into-film"
89
+ "migration_id": "20261019T040655-insert_row-insert-into-film-2"
89
90
  }
90
91
  ```
91
92
 
@@ -107,7 +108,7 @@ Answer:
107
108
  "commits": [
108
109
  { "table": "diary.classics.film", "version": 11, "operation": "DROP CONSTRAINT" }
109
110
  ],
110
- "migration_id": "20261012T072004-drop_constraint-drop-constraint-at_most_three_"
111
+ "migration_id": "20261019T040655-drop_constraint-drop-constraint-at_most_three_"
111
112
  }
112
113
  ```
113
114
 
@@ -148,7 +149,7 @@ Answer:
148
149
  "commits": [
149
150
  { "table": "main.public.genre", "version": 4, "operation": "ADD CONSTRAINT" }
150
151
  ],
151
- "migration_id": "20261012T071949-add_constraint-add-constraint-parent_by_level"
152
+ "migration_id": "20261019T040641-add_constraint-add-constraint-parent_by_level"
152
153
  }
153
154
  ```
154
155
 
@@ -227,34 +228,34 @@ Answer:
227
228
  "ok": true,
228
229
  "op": "batch",
229
230
  "dry_run": false,
230
- "migration_id": "20261012T071949-batch-add-the-silent-era-and-slapsti",
231
+ "migration_id": "20261019T040641-batch-add-the-silent-era-and-slapsti",
231
232
  "steps": [
232
233
  {
233
234
  "index": 0,
234
235
  "op": "insert_row",
235
236
  "ok": true,
236
- "migration_id": "20261012T071949-insert_row-insert-into-genre-4",
237
+ "migration_id": "20261019T040641-insert_row-insert-into-genre",
237
238
  "result": {
238
239
  "ok": true,
239
240
  "op": "insert_row",
240
241
  "files_created": 1,
241
242
  "planned_creates": ["datanotes/main/public/genre/Silent cinema.md"],
242
243
  "commits": [{ "table": "main.public.genre", "version": 5, "operation": "WRITE" }],
243
- "migration_id": "20261012T071949-insert_row-insert-into-genre-4"
244
+ "migration_id": "20261019T040641-insert_row-insert-into-genre"
244
245
  }
245
246
  },
246
247
  {
247
248
  "index": 1,
248
249
  "op": "insert_row",
249
250
  "ok": true,
250
- "migration_id": "20261012T071949-insert_row-insert-into-genre-5",
251
+ "migration_id": "20261019T040641-insert_row-insert-into-genre-2",
251
252
  "result": {
252
253
  "ok": true,
253
254
  "op": "insert_row",
254
255
  "files_created": 1,
255
256
  "planned_creates": ["datanotes/main/public/genre/Slapstick comedy.md"],
256
257
  "commits": [{ "table": "main.public.genre", "version": 6, "operation": "WRITE" }],
257
- "migration_id": "20261012T071949-insert_row-insert-into-genre-5"
258
+ "migration_id": "20261019T040641-insert_row-insert-into-genre-2"
258
259
  }
259
260
  }
260
261
  ]
@@ -342,7 +343,7 @@ Answer:
342
343
  }
343
344
  ],
344
345
  "commits": [{ "table": "main.public.genre", "version": 7, "operation": "UPDATE" }],
345
- "migration_id": "20261012T071949-update_row-update-genre"
346
+ "migration_id": "20261019T040641-update_row-update-genre"
346
347
  }
347
348
  ```
348
349
 
@@ -35,7 +35,7 @@ You edit `datanotes/main/public/genre/Silent film.md` by hand, outside the engin
35
35
  ```markdown
36
36
  ---
37
37
  table: main.public.genre
38
- id: 01a12efb-65b7-7feb-9066-1ef0e98cd245
38
+ id: 01a15257-1485-7217-8806-89c6ca196427
39
39
  name: Silent film
40
40
  kind: cinema
41
41
  parent: "[[Cinema]]"
@@ -47,7 +47,7 @@ Read the hand edit’s event. `list_changes` long-polls with `wait_ms` until the
47
47
  **`list_changes`**
48
48
 
49
49
  ```json
50
- { "since": 39, "tables": ["genre"], "wait_ms": 10000 }
50
+ { "since": 45, "tables": ["genre"], "wait_ms": 10000 }
51
51
  ```
52
52
 
53
53
  Answer:
@@ -55,7 +55,7 @@ Answer:
55
55
  ```json
56
56
  [
57
57
  {
58
- "seq": 40,
58
+ "seq": 46,
59
59
  "kind": "update",
60
60
  "path": "datanotes/main/public/genre/Silent film.md",
61
61
  "source": "external",
@@ -152,7 +152,7 @@ Answer:
152
152
  }
153
153
  ],
154
154
  "commits": [{ "table": "main.public.genre", "version": 9, "operation": "UPDATE" }],
155
- "migration_id": "20261012T071951-update_row-update-genre"
155
+ "migration_id": "20261019T040642-update_row-update-genre"
156
156
  }
157
157
  ```
158
158
 
@@ -167,7 +167,7 @@ You edit `datanotes/main/public/watch/2026-10-03T2130.md` by hand, outside the e
167
167
  ```markdown
168
168
  ---
169
169
  table: main.public.watch
170
- id: 01a12efb-5b61-7abf-9cb9-45a1f5a8f839
170
+ id: 01a15257-0c9b-7856-9c12-bebb7eaf82e5
171
171
  film: His Girl Friday
172
172
  occurred_at: 2026-10-03T21:30
173
173
  where: cinema
@@ -226,7 +226,7 @@ Answer:
226
226
  }
227
227
  ],
228
228
  "commits": [{ "table": "main.public.watch", "version": 3, "operation": "RECOMPUTE" }],
229
- "migration_id": "20261012T071948-recompute_columns-recompute-id-day-label-of-watc"
229
+ "migration_id": "20261019T040640-recompute_columns-recompute-id-day-label-of-watc"
230
230
  }
231
231
  ```
232
232
 
@@ -292,7 +292,7 @@ Answer:
292
292
  ```markdown
293
293
  ---
294
294
  table: main.public.watch
295
- id: 01a12efb-c19b-751f-b73a-92c00d579131
295
+ id: 01a15257-70f0-7ede-9186-f790fe037b07
296
296
  occurred_at: 2026-09-25T21:00
297
297
  film: "[[Charade]]"
298
298
  where: home
@@ -311,7 +311,7 @@ You edit `datanotes/main/public/film/Metropolis.md` by hand, outside the engine.
311
311
  ```markdown
312
312
  ---
313
313
  table: main.public.film
314
- id: 01a12efb-81ad-7b8f-aa3d-16ff4a7685be
314
+ id: 01a15257-31bb-7113-b86a-5d6fdca79420
315
315
  year: 1927
316
316
  minutes: 153
317
317
  status: to watch
@@ -384,7 +384,7 @@ You edit `datanotes/main/public/film/Sherlock Jr..md` by hand, outside the engin
384
384
  ```markdown
385
385
  ---
386
386
  table: main.public.film
387
- id: 01a12efb-6c40-791d-b62a-7b324b13333f
387
+ id: 01a15257-1cbb-7591-97e2-5b880ad79d39
388
388
  title: Sherlock Jr.: the projectionist
389
389
  year: 1924
390
390
  status: watched
@@ -471,7 +471,7 @@ You edit `datanotes/main/public/film/Sherlock Jr..md` by hand, outside the engin
471
471
  ```markdown
472
472
  ---
473
473
  table: main.public.film
474
- id: 01a12efb-6c40-791d-b62a-7b324b13333f
474
+ id: 01a15257-1cbb-7591-97e2-5b880ad79d39
475
475
  title: "Sherlock Jr.: the projectionist"
476
476
  year: 1924
477
477
  status: watched
@@ -30,7 +30,7 @@ Answer:
30
30
  "ok": true,
31
31
  "op": "define_base",
32
32
  "planned_texts": ["datanotes/metastore.yaml"],
33
- "migration_id": "20261012T072003-define_base-create-base-entry"
33
+ "migration_id": "20261019T040655-define_base-create-base-entry"
34
34
  }
35
35
  ```
36
36
 
@@ -50,7 +50,7 @@ Answer:
50
50
  "commits": [
51
51
  { "table": "diary.classics.film", "version": 6, "operation": "SET TABLE BASES" }
52
52
  ],
53
- "migration_id": "20261012T072003-set_table_bases-diary-classics-film-extends-en"
53
+ "migration_id": "20261019T040655-set_table_bases-diary-classics-film-extends-en"
54
54
  }
55
55
  ```
56
56
 
@@ -118,14 +118,14 @@ Answer:
118
118
  "rows": [
119
119
  {
120
120
  "path": "datanotes/diary/classics/actors/Max Schreck.md",
121
- "id": "01a12efb-9be0-7a9c-8083-7efd16947e0f",
121
+ "id": "01a15257-4be9-775a-9d44-7c12bf38b0ed",
122
122
  "added_on": "2026-10-05",
123
123
  "note": "The face of Nosferatu",
124
124
  "_source": "diary.classics.actors"
125
125
  },
126
126
  {
127
127
  "path": "datanotes/diary/classics/film/Nosferatu.md",
128
- "id": "01a12efb-9b66-77e4-b6ce-d40737b6f5ea",
128
+ "id": "01a15257-4b79-7e2f-83db-94f0aef0d4ae",
129
129
  "added_on": "2026-10-05",
130
130
  "note": null,
131
131
  "_source": "diary.classics.film"
@@ -169,7 +169,7 @@ Answer:
169
169
  "commits": [
170
170
  { "table": "diary.classics.actors", "version": 3, "operation": "DEFINE BASE" }
171
171
  ],
172
- "migration_id": "20261012T072003-define_base-alter-base-entry"
172
+ "migration_id": "20261019T040655-define_base-alter-base-entry"
173
173
  }
174
174
  ```
175
175
 
@@ -246,7 +246,7 @@ Answer:
246
246
  "commits": [
247
247
  { "table": "diary.classics.actors", "version": 4, "operation": "DEFINE BASE" }
248
248
  ],
249
- "migration_id": "20261012T072003-define_base-alter-base-entry-2"
249
+ "migration_id": "20261019T040655-define_base-alter-base-entry-2"
250
250
  }
251
251
  ```
252
252
 
@@ -280,7 +280,7 @@ Answer:
280
280
  "ok": true,
281
281
  "op": "define_base",
282
282
  "planned_texts": ["datanotes/metastore.yaml"],
283
- "migration_id": "20261012T072003-define_base-create-base-credited"
283
+ "migration_id": "20261019T040655-define_base-create-base-credited"
284
284
  }
285
285
  ```
286
286
 
@@ -300,7 +300,7 @@ Answer:
300
300
  "commits": [
301
301
  { "table": "diary.classics.actors", "version": 5, "operation": "SET TABLE BASES" }
302
302
  ],
303
- "migration_id": "20261012T072003-set_table_bases-diary-classics-actors-extends--2"
303
+ "migration_id": "20261019T040655-set_table_bases-diary-classics-actors-extends--2"
304
304
  }
305
305
  ```
306
306
 
@@ -329,7 +329,7 @@ Answer:
329
329
  "ok": true,
330
330
  "op": "define_base",
331
331
  "planned_texts": ["datanotes/metastore.yaml"],
332
- "migration_id": "20261012T072003-define_base-create-base-tree"
332
+ "migration_id": "20261019T040655-define_base-create-base-tree"
333
333
  }
334
334
  ```
335
335
 
@@ -369,7 +369,7 @@ Answer:
369
369
  "commits": [
370
370
  { "table": "diary.classics.genre", "version": 1, "operation": "SET TABLE BASES" }
371
371
  ],
372
- "migration_id": "20261012T072003-set_table_bases-diary-classics-genre-extends-t"
372
+ "migration_id": "20261019T040655-set_table_bases-diary-classics-genre-extends-t"
373
373
  }
374
374
  ```
375
375
 
@@ -475,6 +475,6 @@ Answer:
475
475
  "operation": "SET TABLE BASES"
476
476
  }
477
477
  ],
478
- "migration_id": "20261012T072004-set_table_bases-diary-classics-genre_draft-ext"
478
+ "migration_id": "20261019T040655-set_table_bases-diary-classics-genre_draft-ext"
479
479
  }
480
480
  ```