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.
- package/README.md +1 -5
- package/dist/api/client.d.cts +37 -4
- package/dist/api/client.d.ts +37 -4
- package/dist/api/index.cjs +563 -60
- package/dist/api/index.d.cts +3 -2
- package/dist/api/index.d.ts +3 -2
- package/dist/api/index.js +563 -60
- package/dist/api/mcp.cjs +181 -11
- package/dist/api/mcp.js +181 -11
- package/dist/api/otlp.d.cts +63 -15
- package/dist/api/otlp.d.ts +63 -15
- package/dist/api/redact.d.cts +25 -0
- package/dist/api/redact.d.ts +25 -0
- package/dist/api/tokens.d.cts +5 -0
- package/dist/api/tokens.d.ts +5 -0
- package/dist/api/tracing.d.cts +107 -0
- package/dist/api/tracing.d.ts +107 -0
- package/dist/datanotes.cjs +9934 -2656
- package/dist/docs/administration/limits.md +96 -12
- package/dist/docs/administration/run-the-service.md +18 -0
- package/dist/docs/api/operations/add_column.md +1 -0
- package/dist/docs/api/operations/add_constraint.md +2 -0
- package/dist/docs/api/operations/alter_column.md +1 -1
- package/dist/docs/api/operations/alter_table.md +3 -2
- package/dist/docs/api/operations/check_table.md +1 -1
- package/dist/docs/api/operations/create_table.md +3 -1
- package/dist/docs/api/operations/describe_table.md +1 -1
- package/dist/docs/api/operations/graph_neighbors.md +4 -2
- package/dist/docs/api/operations/graph_path.md +3 -3
- package/dist/docs/api/operations/graph_traverse.md +2 -2
- package/dist/docs/api/operations/insert_row.md +2 -2
- package/dist/docs/api/operations/record_tool_call.md +1 -0
- package/dist/docs/api/operations/tags/queries.md +2 -2
- package/dist/docs/api/operations/tags/rows-dml.md +1 -1
- package/dist/docs/api.md +4 -4
- package/dist/docs/develop/concurrency.md +36 -1
- package/dist/docs/develop/http-api.md +11 -1
- package/dist/docs/develop/telemetry.md +224 -25
- package/dist/docs/guides/data-quality/check-constraints.md +14 -13
- package/dist/docs/guides/data-quality/hand-edits.md +10 -10
- package/dist/docs/guides/database-objects/bases.md +11 -11
- package/dist/docs/guides/database-objects/catalogs-and-schemas.md +16 -10
- package/dist/docs/guides/database-objects/columns.md +38 -38
- package/dist/docs/guides/database-objects/relations.md +173 -36
- package/dist/docs/guides/database-objects/tables.md +90 -109
- package/dist/docs/guides/database-objects/views.md +44 -44
- package/dist/docs/guides/history/back-up.md +12 -4
- package/dist/docs/guides/history/change-data-feed.md +34 -34
- package/dist/docs/guides/history/journal-and-undo.md +12 -12
- package/dist/docs/guides/history/restore.md +20 -20
- package/dist/docs/guides/history/table-history.md +31 -31
- package/dist/docs/guides/history/vacuum.md +1 -10
- package/dist/docs/guides/read-and-write/automatic-links.md +15 -15
- package/dist/docs/guides/read-and-write/import-notes.md +21 -21
- package/dist/docs/guides/read-and-write/joins-and-aggregates.md +7 -7
- package/dist/docs/guides/read-and-write/read-and-filter.md +10 -10
- package/dist/docs/guides/read-and-write/write-rows.md +10 -10
- package/dist/docs/guides/search/link-graph.md +95 -0
- package/dist/docs/guides/search/text.md +1 -1
- package/dist/docs/guides/security/who-did-what.md +2 -0
- package/dist/docs/reference/cli.md +10 -3
- package/dist/docs/reference/compatibility.md +33 -13
- package/dist/docs/reference/errors.md +1 -1
- package/dist/docs/reference/operations/auxiliary.md +2 -2
- package/dist/docs/reference/operations/queries.md +3 -3
- package/dist/docs/reference/operations/rows.md +1 -1
- package/dist/docs/reference/operations/schema.md +15 -11
- package/dist/docs/reference/operations.md +39 -39
- package/dist/docs/reference/settings.md +37 -25
- package/dist/docs/release-notes.md +145 -0
- package/dist/docs/start/how-it-works.md +35 -3
- package/dist/docs/start/quick-start.md +2 -0
- 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
|
|
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
|
|
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
|
|
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
|
|
22
|
-
|
|
|
23
|
-
| `OTEL_EXPORTER_OTLP_ENDPOINT`
|
|
24
|
-
| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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:
|
|
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":
|
|
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":
|
|
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": "
|
|
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:
|
|
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": "
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
478
|
+
"migration_id": "20261019T040655-set_table_bases-diary-classics-genre_draft-ext"
|
|
479
479
|
}
|
|
480
480
|
```
|