datanotes 1.2.1 → 1.4.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/SECURITY.md CHANGED
@@ -47,7 +47,10 @@ Nothing leaves the machine unless you turn it on:
47
47
  - **Semantic search** sends note text to the embedding provider you configure (`semanticEnabled`,
48
48
  off by default; provider `semanticProvider` / `semanticBaseUrl`). A local provider (Ollama,
49
49
  LM Studio) keeps it on the machine.
50
- - **Telemetry** sends logs to the OTLP endpoint you configure (`telemetryEnabled`, off by default).
50
+ - **Telemetry** sends logs to the OTLP endpoint you configure (`telemetryEnabled`, off by default):
51
+ operations, routes, note paths, actors, tool names and error messages (which may quote a value),
52
+ not note bodies. `telemetryHeaders` (an API key) is
53
+ stored in the settings file; `OTEL_EXPORTER_OTLP_HEADERS` keeps it out.
51
54
  - **Webhooks** (created by the owner) post change events to the URLs they name.
52
55
  - **Apps' MCP endpoints** (registered by the owner): the engine sends each app's `mcp_token`, and
53
56
  the arguments and caller of every call of its tools, to the app's `mcp_url`, which may be remote.
@@ -26,6 +26,7 @@ __export(index_exports, {
26
26
  DatanotesError: () => DatanotesError,
27
27
  OPERATIONS: () => OPERATIONS,
28
28
  OperationInputError: () => OperationInputError,
29
+ OtlpLogExporter: () => OtlpLogExporter,
29
30
  ROUTE_PREFIX: () => ROUTE_PREFIX,
30
31
  describeBatch: () => describeBatch,
31
32
  describeRefusal: () => describeRefusal,
@@ -33,6 +34,8 @@ __export(index_exports, {
33
34
  fetchTransport: () => fetchTransport,
34
35
  operationRequest: () => operationRequest,
35
36
  operationRoute: () => operationRoute,
37
+ otlpFromEnv: () => otlpFromEnv,
38
+ parseOtlpHeaders: () => parseOtlpHeaders,
36
39
  resultOf: () => resultOf
37
40
  });
38
41
  module.exports = __toCommonJS(index_exports);
@@ -3062,6 +3065,197 @@ function estimateTokens(text) {
3062
3065
  return n;
3063
3066
  }
3064
3067
 
3068
+ // src/api/otlp.ts
3069
+ var SEVERITY_NUMBER = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17 };
3070
+ function attr(key, v) {
3071
+ if (v === void 0 || v === null) return null;
3072
+ if (typeof v === "boolean") return { key, value: { boolValue: v } };
3073
+ if (typeof v === "number") return Number.isInteger(v) ? { key, value: { intValue: String(v) } } : { key, value: { doubleValue: v } };
3074
+ return { key, value: { stringValue: v } };
3075
+ }
3076
+ var attrs = (a) => Object.entries(a).map(([k, v]) => attr(k, v)).filter((x) => x !== null);
3077
+ var fetchSend = async (url, body, headers) => {
3078
+ const res = await fetch(url, { method: "POST", headers, body });
3079
+ if (!res.ok) throw new Error(`OTLP HTTP ${res.status}`);
3080
+ };
3081
+ var Pb = class {
3082
+ constructor() {
3083
+ this.parts = [];
3084
+ }
3085
+ varint(v) {
3086
+ let x = BigInt.asUintN(64, v);
3087
+ const b128 = BigInt(128);
3088
+ const low7 = BigInt(127);
3089
+ const seven = BigInt(7);
3090
+ while (x >= b128) {
3091
+ this.parts.push(Number(x & low7) | 128);
3092
+ x >>= seven;
3093
+ }
3094
+ this.parts.push(Number(x));
3095
+ }
3096
+ tag(field, wire) {
3097
+ this.varint(BigInt(field << 3 | wire));
3098
+ }
3099
+ uint(field, v) {
3100
+ this.tag(field, 0);
3101
+ this.varint(BigInt(v));
3102
+ return this;
3103
+ }
3104
+ fixed64(field, v) {
3105
+ this.tag(field, 1);
3106
+ for (let i = 0; i < 8; i++) this.parts.push(Number(v >> BigInt(8 * i) & BigInt(255)));
3107
+ return this;
3108
+ }
3109
+ double(field, v) {
3110
+ this.tag(field, 1);
3111
+ const b = new DataView(new ArrayBuffer(8));
3112
+ b.setFloat64(0, v, true);
3113
+ for (let i = 0; i < 8; i++) this.parts.push(b.getUint8(i));
3114
+ return this;
3115
+ }
3116
+ bytes(field, data) {
3117
+ this.tag(field, 2);
3118
+ this.varint(BigInt(data.length));
3119
+ for (const x of data) this.parts.push(x);
3120
+ return this;
3121
+ }
3122
+ string(field, s) {
3123
+ return this.bytes(field, new TextEncoder().encode(s));
3124
+ }
3125
+ message(field, m) {
3126
+ return this.bytes(field, m.finish());
3127
+ }
3128
+ finish() {
3129
+ return Uint8Array.from(this.parts);
3130
+ }
3131
+ };
3132
+ function pbValue(v) {
3133
+ if (typeof v === "string") return new Pb().string(1, v);
3134
+ if (typeof v === "boolean") return new Pb().uint(2, v ? 1 : 0);
3135
+ return Number.isInteger(v) ? new Pb().uint(3, BigInt(v)) : new Pb().double(4, v);
3136
+ }
3137
+ function pbAttrs(m, field, a) {
3138
+ for (const [k, v] of Object.entries(a)) {
3139
+ if (v === void 0 || v === null) continue;
3140
+ m.message(field, new Pb().string(1, k).message(2, pbValue(v)));
3141
+ }
3142
+ return m;
3143
+ }
3144
+ var OtlpLogExporter = class {
3145
+ constructor(o) {
3146
+ this.o = o;
3147
+ this.buffer = [];
3148
+ this.inflight = Promise.resolve();
3149
+ this.timer = null;
3150
+ /** Records sent, and records lost because the collector did not take them. */
3151
+ this.sent = 0;
3152
+ this.dropped = 0;
3153
+ const every = o.flushMs ?? 5e3;
3154
+ if (every > 0) {
3155
+ this.timer = setInterval(() => void this.flush(), every);
3156
+ this.timer.unref?.();
3157
+ }
3158
+ }
3159
+ log(severity, body, attributes = {}) {
3160
+ const ms = (this.o.now ?? Date.now)();
3161
+ this.buffer.push({ timeUnixNano: `${Math.round(ms)}000000`, severity, body, attributes });
3162
+ if (this.buffer.length >= (this.o.maxBatch ?? 50)) void this.flush();
3163
+ }
3164
+ /** Send what is buffered. Never rejects. */
3165
+ flush() {
3166
+ if (this.buffer.length === 0) return this.inflight;
3167
+ const batch = this.buffer;
3168
+ this.buffer = [];
3169
+ const resource = { "service.name": this.o.serviceName, "service.version": this.o.serviceVersion, ...this.o.resource };
3170
+ const protobuf = (this.o.protocol ?? "http/protobuf") === "http/protobuf";
3171
+ const body = protobuf ? this.protobuf(resource, batch) : JSON.stringify({
3172
+ resourceLogs: [
3173
+ {
3174
+ resource: { attributes: attrs(resource) },
3175
+ scopeLogs: [
3176
+ {
3177
+ scope: { name: this.o.serviceName },
3178
+ logRecords: batch.map((r) => ({
3179
+ timeUnixNano: r.timeUnixNano,
3180
+ observedTimeUnixNano: r.timeUnixNano,
3181
+ severityNumber: SEVERITY_NUMBER[r.severity],
3182
+ severityText: r.severity,
3183
+ body: { stringValue: r.body },
3184
+ attributes: attrs(r.attributes)
3185
+ }))
3186
+ }
3187
+ ]
3188
+ }
3189
+ ]
3190
+ });
3191
+ const url = `${this.o.endpoint.replace(/\/+$/, "")}/v1/logs`;
3192
+ const send = this.o.send ?? fetchSend;
3193
+ this.inflight = this.inflight.then(
3194
+ () => send(url, body, { "Content-Type": protobuf ? "application/x-protobuf" : "application/json", ...this.o.headers ?? {} }).then(
3195
+ () => void (this.sent += batch.length),
3196
+ () => void (this.dropped += batch.length)
3197
+ )
3198
+ );
3199
+ return this.inflight;
3200
+ }
3201
+ protobuf(resource, batch) {
3202
+ const scope = new Pb().message(1, new Pb().string(1, this.o.serviceName));
3203
+ for (const r of batch) {
3204
+ const t = BigInt(r.timeUnixNano);
3205
+ const rec = new Pb().fixed64(1, t).uint(2, SEVERITY_NUMBER[r.severity]).string(3, r.severity).message(5, new Pb().string(1, r.body));
3206
+ pbAttrs(rec, 6, r.attributes).fixed64(11, t);
3207
+ scope.message(2, rec);
3208
+ }
3209
+ const resourceLogs = new Pb().message(1, pbAttrs(new Pb(), 1, resource)).message(2, scope);
3210
+ return new Pb().message(1, resourceLogs).finish();
3211
+ }
3212
+ /** Stop the timer and send what is left. */
3213
+ async close() {
3214
+ if (this.timer) clearInterval(this.timer);
3215
+ this.timer = null;
3216
+ await this.flush();
3217
+ }
3218
+ };
3219
+ function parseOtlpHeaders(text) {
3220
+ const out = {};
3221
+ for (const part of (text ?? "").split(",")) {
3222
+ const i = part.indexOf("=");
3223
+ if (i <= 0) continue;
3224
+ const key = part.slice(0, i).trim();
3225
+ const value = part.slice(i + 1).trim();
3226
+ if (!key) continue;
3227
+ try {
3228
+ out[key] = decodeURIComponent(value);
3229
+ } catch {
3230
+ out[key] = value;
3231
+ }
3232
+ }
3233
+ return out;
3234
+ }
3235
+ var parseResourceAttributes = parseOtlpHeaders;
3236
+ var truthy = (v) => (v ?? "").trim().toLowerCase() === "true";
3237
+ function otlpFromEnv(env, defaults) {
3238
+ if (truthy(env.OTEL_SDK_DISABLED) || (env.OTEL_LOGS_EXPORTER ?? "").trim().toLowerCase() === "none") return null;
3239
+ const logs = env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT?.trim();
3240
+ const base = env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
3241
+ const endpoint = logs ? logs.replace(/\/v1\/logs\/?$/, "") : base;
3242
+ if (!endpoint) return null;
3243
+ const protocol = otlpProtocol(env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL ?? env.OTEL_EXPORTER_OTLP_PROTOCOL);
3244
+ const resource = parseResourceAttributes(env.OTEL_RESOURCE_ATTRIBUTES);
3245
+ return {
3246
+ endpoint,
3247
+ serviceName: env.OTEL_SERVICE_NAME?.trim() || resource["service.name"] || defaults.serviceName,
3248
+ ...defaults.serviceVersion ? { serviceVersion: defaults.serviceVersion } : {},
3249
+ ...protocol ? { protocol } : {},
3250
+ ...Object.keys(resource).length ? { resource } : {},
3251
+ headers: { ...parseOtlpHeaders(env.OTEL_EXPORTER_OTLP_HEADERS), ...parseOtlpHeaders(env.OTEL_EXPORTER_OTLP_LOGS_HEADERS) }
3252
+ };
3253
+ }
3254
+ function otlpProtocol(v) {
3255
+ const p = (v ?? "").trim().toLowerCase();
3256
+ return p === "http/protobuf" || p === "http/json" ? p : void 0;
3257
+ }
3258
+
3065
3259
  // src/api/client.ts
3066
3260
  var ACTOR_HEADER = "X-Datanotes-Actor";
3067
3261
  var DatanotesError = class extends Error {
@@ -6,4 +6,5 @@ export { OPERATIONS, type OperationDef } from "./catalog";
6
6
  export { operationRequest, operationRoute, CALLABLE_OPERATIONS, OperationInputError, ROUTE_PREFIX, type OperationRequest } from "./requests";
7
7
  export { resultOf, describeRefusal, describeBatch, type OperationResult } from "./results";
8
8
  export { estimateTokens } from "./tokens";
9
+ export { OtlpLogExporter, otlpFromEnv, parseOtlpHeaders, type OtlpOptions, type Severity, type AttrValue } from "./otlp";
9
10
  export { DatanotesClient, DatanotesError, fetchTransport, ACTOR_HEADER, type ActorInfo, type AppTool, type ClientOptions, type HttpRequest, type HttpResponse, type ToolCallRecord, type Transport, } from "./client";
package/dist/api/index.js CHANGED
@@ -3023,6 +3023,197 @@ function estimateTokens(text) {
3023
3023
  return n;
3024
3024
  }
3025
3025
 
3026
+ // src/api/otlp.ts
3027
+ var SEVERITY_NUMBER = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17 };
3028
+ function attr(key, v) {
3029
+ if (v === void 0 || v === null) return null;
3030
+ if (typeof v === "boolean") return { key, value: { boolValue: v } };
3031
+ if (typeof v === "number") return Number.isInteger(v) ? { key, value: { intValue: String(v) } } : { key, value: { doubleValue: v } };
3032
+ return { key, value: { stringValue: v } };
3033
+ }
3034
+ var attrs = (a) => Object.entries(a).map(([k, v]) => attr(k, v)).filter((x) => x !== null);
3035
+ var fetchSend = async (url, body, headers) => {
3036
+ const res = await fetch(url, { method: "POST", headers, body });
3037
+ if (!res.ok) throw new Error(`OTLP HTTP ${res.status}`);
3038
+ };
3039
+ var Pb = class {
3040
+ constructor() {
3041
+ this.parts = [];
3042
+ }
3043
+ varint(v) {
3044
+ let x = BigInt.asUintN(64, v);
3045
+ const b128 = BigInt(128);
3046
+ const low7 = BigInt(127);
3047
+ const seven = BigInt(7);
3048
+ while (x >= b128) {
3049
+ this.parts.push(Number(x & low7) | 128);
3050
+ x >>= seven;
3051
+ }
3052
+ this.parts.push(Number(x));
3053
+ }
3054
+ tag(field, wire) {
3055
+ this.varint(BigInt(field << 3 | wire));
3056
+ }
3057
+ uint(field, v) {
3058
+ this.tag(field, 0);
3059
+ this.varint(BigInt(v));
3060
+ return this;
3061
+ }
3062
+ fixed64(field, v) {
3063
+ this.tag(field, 1);
3064
+ for (let i = 0; i < 8; i++) this.parts.push(Number(v >> BigInt(8 * i) & BigInt(255)));
3065
+ return this;
3066
+ }
3067
+ double(field, v) {
3068
+ this.tag(field, 1);
3069
+ const b = new DataView(new ArrayBuffer(8));
3070
+ b.setFloat64(0, v, true);
3071
+ for (let i = 0; i < 8; i++) this.parts.push(b.getUint8(i));
3072
+ return this;
3073
+ }
3074
+ bytes(field, data) {
3075
+ this.tag(field, 2);
3076
+ this.varint(BigInt(data.length));
3077
+ for (const x of data) this.parts.push(x);
3078
+ return this;
3079
+ }
3080
+ string(field, s) {
3081
+ return this.bytes(field, new TextEncoder().encode(s));
3082
+ }
3083
+ message(field, m) {
3084
+ return this.bytes(field, m.finish());
3085
+ }
3086
+ finish() {
3087
+ return Uint8Array.from(this.parts);
3088
+ }
3089
+ };
3090
+ function pbValue(v) {
3091
+ if (typeof v === "string") return new Pb().string(1, v);
3092
+ if (typeof v === "boolean") return new Pb().uint(2, v ? 1 : 0);
3093
+ return Number.isInteger(v) ? new Pb().uint(3, BigInt(v)) : new Pb().double(4, v);
3094
+ }
3095
+ function pbAttrs(m, field, a) {
3096
+ for (const [k, v] of Object.entries(a)) {
3097
+ if (v === void 0 || v === null) continue;
3098
+ m.message(field, new Pb().string(1, k).message(2, pbValue(v)));
3099
+ }
3100
+ return m;
3101
+ }
3102
+ var OtlpLogExporter = class {
3103
+ constructor(o) {
3104
+ this.o = o;
3105
+ this.buffer = [];
3106
+ this.inflight = Promise.resolve();
3107
+ this.timer = null;
3108
+ /** Records sent, and records lost because the collector did not take them. */
3109
+ this.sent = 0;
3110
+ this.dropped = 0;
3111
+ const every = o.flushMs ?? 5e3;
3112
+ if (every > 0) {
3113
+ this.timer = setInterval(() => void this.flush(), every);
3114
+ this.timer.unref?.();
3115
+ }
3116
+ }
3117
+ log(severity, body, attributes = {}) {
3118
+ const ms = (this.o.now ?? Date.now)();
3119
+ this.buffer.push({ timeUnixNano: `${Math.round(ms)}000000`, severity, body, attributes });
3120
+ if (this.buffer.length >= (this.o.maxBatch ?? 50)) void this.flush();
3121
+ }
3122
+ /** Send what is buffered. Never rejects. */
3123
+ flush() {
3124
+ if (this.buffer.length === 0) return this.inflight;
3125
+ const batch = this.buffer;
3126
+ this.buffer = [];
3127
+ const resource = { "service.name": this.o.serviceName, "service.version": this.o.serviceVersion, ...this.o.resource };
3128
+ const protobuf = (this.o.protocol ?? "http/protobuf") === "http/protobuf";
3129
+ const body = protobuf ? this.protobuf(resource, batch) : JSON.stringify({
3130
+ resourceLogs: [
3131
+ {
3132
+ resource: { attributes: attrs(resource) },
3133
+ scopeLogs: [
3134
+ {
3135
+ scope: { name: this.o.serviceName },
3136
+ logRecords: batch.map((r) => ({
3137
+ timeUnixNano: r.timeUnixNano,
3138
+ observedTimeUnixNano: r.timeUnixNano,
3139
+ severityNumber: SEVERITY_NUMBER[r.severity],
3140
+ severityText: r.severity,
3141
+ body: { stringValue: r.body },
3142
+ attributes: attrs(r.attributes)
3143
+ }))
3144
+ }
3145
+ ]
3146
+ }
3147
+ ]
3148
+ });
3149
+ const url = `${this.o.endpoint.replace(/\/+$/, "")}/v1/logs`;
3150
+ const send = this.o.send ?? fetchSend;
3151
+ this.inflight = this.inflight.then(
3152
+ () => send(url, body, { "Content-Type": protobuf ? "application/x-protobuf" : "application/json", ...this.o.headers ?? {} }).then(
3153
+ () => void (this.sent += batch.length),
3154
+ () => void (this.dropped += batch.length)
3155
+ )
3156
+ );
3157
+ return this.inflight;
3158
+ }
3159
+ protobuf(resource, batch) {
3160
+ const scope = new Pb().message(1, new Pb().string(1, this.o.serviceName));
3161
+ for (const r of batch) {
3162
+ const t = BigInt(r.timeUnixNano);
3163
+ const rec = new Pb().fixed64(1, t).uint(2, SEVERITY_NUMBER[r.severity]).string(3, r.severity).message(5, new Pb().string(1, r.body));
3164
+ pbAttrs(rec, 6, r.attributes).fixed64(11, t);
3165
+ scope.message(2, rec);
3166
+ }
3167
+ const resourceLogs = new Pb().message(1, pbAttrs(new Pb(), 1, resource)).message(2, scope);
3168
+ return new Pb().message(1, resourceLogs).finish();
3169
+ }
3170
+ /** Stop the timer and send what is left. */
3171
+ async close() {
3172
+ if (this.timer) clearInterval(this.timer);
3173
+ this.timer = null;
3174
+ await this.flush();
3175
+ }
3176
+ };
3177
+ function parseOtlpHeaders(text) {
3178
+ const out = {};
3179
+ for (const part of (text ?? "").split(",")) {
3180
+ const i = part.indexOf("=");
3181
+ if (i <= 0) continue;
3182
+ const key = part.slice(0, i).trim();
3183
+ const value = part.slice(i + 1).trim();
3184
+ if (!key) continue;
3185
+ try {
3186
+ out[key] = decodeURIComponent(value);
3187
+ } catch {
3188
+ out[key] = value;
3189
+ }
3190
+ }
3191
+ return out;
3192
+ }
3193
+ var parseResourceAttributes = parseOtlpHeaders;
3194
+ var truthy = (v) => (v ?? "").trim().toLowerCase() === "true";
3195
+ function otlpFromEnv(env, defaults) {
3196
+ if (truthy(env.OTEL_SDK_DISABLED) || (env.OTEL_LOGS_EXPORTER ?? "").trim().toLowerCase() === "none") return null;
3197
+ const logs = env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT?.trim();
3198
+ const base = env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
3199
+ const endpoint = logs ? logs.replace(/\/v1\/logs\/?$/, "") : base;
3200
+ if (!endpoint) return null;
3201
+ const protocol = otlpProtocol(env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL ?? env.OTEL_EXPORTER_OTLP_PROTOCOL);
3202
+ const resource = parseResourceAttributes(env.OTEL_RESOURCE_ATTRIBUTES);
3203
+ return {
3204
+ endpoint,
3205
+ serviceName: env.OTEL_SERVICE_NAME?.trim() || resource["service.name"] || defaults.serviceName,
3206
+ ...defaults.serviceVersion ? { serviceVersion: defaults.serviceVersion } : {},
3207
+ ...protocol ? { protocol } : {},
3208
+ ...Object.keys(resource).length ? { resource } : {},
3209
+ headers: { ...parseOtlpHeaders(env.OTEL_EXPORTER_OTLP_HEADERS), ...parseOtlpHeaders(env.OTEL_EXPORTER_OTLP_LOGS_HEADERS) }
3210
+ };
3211
+ }
3212
+ function otlpProtocol(v) {
3213
+ const p = (v ?? "").trim().toLowerCase();
3214
+ return p === "http/protobuf" || p === "http/json" ? p : void 0;
3215
+ }
3216
+
3026
3217
  // src/api/client.ts
3027
3218
  var ACTOR_HEADER = "X-Datanotes-Actor";
3028
3219
  var DatanotesError = class extends Error {
@@ -3130,6 +3321,7 @@ export {
3130
3321
  DatanotesError,
3131
3322
  OPERATIONS,
3132
3323
  OperationInputError,
3324
+ OtlpLogExporter,
3133
3325
  ROUTE_PREFIX,
3134
3326
  describeBatch,
3135
3327
  describeRefusal,
@@ -3137,5 +3329,7 @@ export {
3137
3329
  fetchTransport,
3138
3330
  operationRequest,
3139
3331
  operationRoute,
3332
+ otlpFromEnv,
3333
+ parseOtlpHeaders,
3140
3334
  resultOf
3141
3335
  };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * OpenTelemetry log export over OTLP/HTTP, vendor-neutral: records are buffered and POSTed to
3
+ * `<endpoint>/v1/logs` as `http/protobuf` (the OTLP default) or `http/json`, which every
4
+ * OpenTelemetry collector and OTLP backend reads. No dependency, no SDK. Export failures are
5
+ * counted, never thrown: logging must not break the program. (OTLP over gRPC is not supported:
6
+ * use the collector's HTTP port, 4318 by default.)
7
+ *
8
+ * Configure it in code, or from the standard OpenTelemetry variables with `otlpFromEnv`.
9
+ */
10
+ export type Severity = "DEBUG" | "INFO" | "WARN" | "ERROR";
11
+ export type AttrValue = string | number | boolean | null | undefined;
12
+ export type OtlpProtocol = "http/protobuf" | "http/json";
13
+ export interface OtlpOptions {
14
+ /** Collector base URL, e.g. `http://127.0.0.1:4318` (`/v1/logs` is added). */
15
+ endpoint: string;
16
+ /** `service.name` of the records. */
17
+ serviceName: string;
18
+ serviceVersion?: string;
19
+ /** `http/protobuf` (default) or `http/json`. */
20
+ protocol?: OtlpProtocol;
21
+ /** Extra headers, e.g. an API key or `Authorization`, as the backend asks. */
22
+ headers?: Record<string, string>;
23
+ /** More resource attributes (e.g. `host.name`). */
24
+ resource?: Record<string, AttrValue>;
25
+ /** Send at most this many records per request (default 50); a full buffer is sent at once. */
26
+ maxBatch?: number;
27
+ /** Send the buffer every this many ms (default 5000; 0: only on `flush`). */
28
+ flushMs?: number;
29
+ /** POST a body; resolves on 2xx (default: fetch). `headers` include the Content-Type. */
30
+ send?: (url: string, body: string | Uint8Array, headers: Record<string, string>) => Promise<void>;
31
+ now?: () => number;
32
+ }
33
+ export declare class OtlpLogExporter {
34
+ private readonly o;
35
+ private buffer;
36
+ private inflight;
37
+ private timer;
38
+ /** Records sent, and records lost because the collector did not take them. */
39
+ sent: number;
40
+ dropped: number;
41
+ constructor(o: OtlpOptions);
42
+ log(severity: Severity, body: string, attributes?: Record<string, AttrValue>): void;
43
+ /** Send what is buffered. Never rejects. */
44
+ flush(): Promise<void>;
45
+ private protobuf;
46
+ /** Stop the timer and send what is left. */
47
+ close(): Promise<void>;
48
+ }
49
+ /** `k1=v1,k2=v2` (values URL-encoded), the format of `OTEL_EXPORTER_OTLP_HEADERS`. */
50
+ export declare function parseOtlpHeaders(text: string | undefined): Record<string, string>;
51
+ /** `k1=v1,k2=v2` resource attributes, the format of `OTEL_RESOURCE_ATTRIBUTES`. */
52
+ export declare const parseResourceAttributes: typeof parseOtlpHeaders;
53
+ /**
54
+ * Exporter options from the standard OpenTelemetry variables, or null when export is off (no
55
+ * endpoint, `OTEL_SDK_DISABLED=true` or `OTEL_LOGS_EXPORTER=none`):
56
+ * `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` (full URL of /v1/logs) or `OTEL_EXPORTER_OTLP_ENDPOINT`;
57
+ * `OTEL_EXPORTER_OTLP_[LOGS_]HEADERS`; `OTEL_EXPORTER_OTLP_[LOGS_]PROTOCOL` (`http/protobuf`,
58
+ * `http/json`); `OTEL_SERVICE_NAME`; `OTEL_RESOURCE_ATTRIBUTES`.
59
+ */
60
+ export declare function otlpFromEnv(env: Record<string, string | undefined>, defaults: {
61
+ serviceName: string;
62
+ serviceVersion?: string;
63
+ }): OtlpOptions | null;
64
+ /** `http/protobuf` or `http/json` from a setting or variable; undefined for anything else. */
65
+ export declare function otlpProtocol(v: string | undefined): OtlpProtocol | undefined;
@@ -7425,6 +7425,8 @@ var DEFAULT_SETTINGS = {
7425
7425
  telemetryEnabled: false,
7426
7426
  telemetryEndpoint: "http://127.0.0.1:4318",
7427
7427
  telemetryServiceName: "datanotes",
7428
+ telemetryHeaders: {},
7429
+ telemetryProtocol: "http/protobuf",
7428
7430
  autoRecompute: true,
7429
7431
  viewNotes: true,
7430
7432
  semanticEnabled: false,
@@ -7450,6 +7452,8 @@ var KEYS = [
7450
7452
  "telemetryEnabled",
7451
7453
  "telemetryEndpoint",
7452
7454
  "telemetryServiceName",
7455
+ "telemetryHeaders",
7456
+ "telemetryProtocol",
7453
7457
  "autoRecompute",
7454
7458
  "viewNotes",
7455
7459
  "semanticEnabled",
@@ -7500,6 +7504,18 @@ function mergeWithDefaults(loaded) {
7500
7504
  }
7501
7505
  break;
7502
7506
  }
7507
+ case "telemetryProtocol": {
7508
+ if (raw === "http/protobuf" || raw === "http/json") out.telemetryProtocol = raw;
7509
+ break;
7510
+ }
7511
+ case "telemetryHeaders": {
7512
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
7513
+ out.telemetryHeaders = Object.fromEntries(
7514
+ Object.entries(raw).filter(([k, v]) => /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/.test(k) && typeof v === "string")
7515
+ );
7516
+ }
7517
+ break;
7518
+ }
7503
7519
  case "telemetryServiceName": {
7504
7520
  if (typeof raw === "string" && raw.trim().length > 0) {
7505
7521
  out.telemetryServiceName = raw.trim();
@@ -7634,89 +7650,12 @@ async function storeRename(app, from, to) {
7634
7650
  }
7635
7651
 
7636
7652
  // src/core/telemetry.ts
7637
- var SEVERITY_NUMBER = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17 };
7638
7653
  var NOOP_TELEMETRY = {
7639
7654
  log: () => {
7640
7655
  },
7641
7656
  flush: async () => {
7642
7657
  }
7643
7658
  };
7644
- function otlpAttr(key, v) {
7645
- if (v === void 0 || v === null) return null;
7646
- if (typeof v === "boolean") return { key, value: { boolValue: v } };
7647
- if (typeof v === "number") {
7648
- return Number.isInteger(v) ? { key, value: { intValue: String(v) } } : { key, value: { doubleValue: v } };
7649
- }
7650
- return { key, value: { stringValue: v } };
7651
- }
7652
- function attrs(a) {
7653
- return Object.entries(a).map(([k, v]) => otlpAttr(k, v)).filter((x) => x != null);
7654
- }
7655
- function otlpLogsPayload(serviceName, records, resource = {}) {
7656
- return JSON.stringify({
7657
- resourceLogs: [
7658
- {
7659
- resource: { attributes: attrs({ "service.name": serviceName, ...resource }) },
7660
- scopeLogs: [
7661
- {
7662
- scope: { name: "obsidian-datanotes" },
7663
- logRecords: records.map((r) => ({
7664
- timeUnixNano: r.timeUnixNano,
7665
- observedTimeUnixNano: r.timeUnixNano,
7666
- severityNumber: SEVERITY_NUMBER[r.severity],
7667
- severityText: r.severity,
7668
- body: { stringValue: r.body },
7669
- attributes: attrs(r.attributes)
7670
- }))
7671
- }
7672
- ]
7673
- }
7674
- ]
7675
- });
7676
- }
7677
- var OtlpLogger = class {
7678
- constructor(opts) {
7679
- this.opts = opts;
7680
- this.buffer = [];
7681
- this.inflight = Promise.resolve();
7682
- this.dropped = 0;
7683
- this.sent = 0;
7684
- }
7685
- log(severity, body, attributes = {}) {
7686
- if (!this.opts.getSettings().telemetryEnabled) return;
7687
- const ms = (this.opts.now ?? Date.now)();
7688
- this.buffer.push({
7689
- timeUnixNano: `${Math.round(ms)}000000`,
7690
- severity,
7691
- body,
7692
- attributes
7693
- });
7694
- if (this.buffer.length >= (this.opts.maxBatch ?? 50)) void this.flush();
7695
- }
7696
- /** Send buffered records. Never rejects. */
7697
- flush() {
7698
- const settings = this.opts.getSettings();
7699
- if (!settings.telemetryEnabled || this.buffer.length === 0) {
7700
- this.buffer = [];
7701
- return this.inflight;
7702
- }
7703
- const batch = this.buffer;
7704
- this.buffer = [];
7705
- const url = `${settings.telemetryEndpoint.replace(/\/+$/, "")}/v1/logs`;
7706
- const body = otlpLogsPayload(settings.telemetryServiceName, batch, this.opts.resource);
7707
- this.inflight = this.inflight.then(
7708
- () => this.opts.transport(url, body).then(
7709
- () => {
7710
- this.sent += batch.length;
7711
- },
7712
- () => {
7713
- this.dropped += batch.length;
7714
- }
7715
- )
7716
- );
7717
- return this.inflight;
7718
- }
7719
- };
7720
7659
  var current = NOOP_TELEMETRY;
7721
7660
  function setTelemetry(t) {
7722
7661
  current = t;
@@ -15250,6 +15189,246 @@ var SemanticIndex = class {
15250
15189
  }
15251
15190
  };
15252
15191
 
15192
+ // src/api/otlp.ts
15193
+ var SEVERITY_NUMBER = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17 };
15194
+ function attr(key, v) {
15195
+ if (v === void 0 || v === null) return null;
15196
+ if (typeof v === "boolean") return { key, value: { boolValue: v } };
15197
+ if (typeof v === "number") return Number.isInteger(v) ? { key, value: { intValue: String(v) } } : { key, value: { doubleValue: v } };
15198
+ return { key, value: { stringValue: v } };
15199
+ }
15200
+ var attrs = (a) => Object.entries(a).map(([k, v]) => attr(k, v)).filter((x) => x !== null);
15201
+ var fetchSend = async (url, body, headers) => {
15202
+ const res = await fetch(url, { method: "POST", headers, body });
15203
+ if (!res.ok) throw new Error(`OTLP HTTP ${res.status}`);
15204
+ };
15205
+ var Pb = class {
15206
+ constructor() {
15207
+ this.parts = [];
15208
+ }
15209
+ varint(v) {
15210
+ let x = BigInt.asUintN(64, v);
15211
+ const b128 = BigInt(128);
15212
+ const low7 = BigInt(127);
15213
+ const seven = BigInt(7);
15214
+ while (x >= b128) {
15215
+ this.parts.push(Number(x & low7) | 128);
15216
+ x >>= seven;
15217
+ }
15218
+ this.parts.push(Number(x));
15219
+ }
15220
+ tag(field, wire) {
15221
+ this.varint(BigInt(field << 3 | wire));
15222
+ }
15223
+ uint(field, v) {
15224
+ this.tag(field, 0);
15225
+ this.varint(BigInt(v));
15226
+ return this;
15227
+ }
15228
+ fixed64(field, v) {
15229
+ this.tag(field, 1);
15230
+ for (let i = 0; i < 8; i++) this.parts.push(Number(v >> BigInt(8 * i) & BigInt(255)));
15231
+ return this;
15232
+ }
15233
+ double(field, v) {
15234
+ this.tag(field, 1);
15235
+ const b = new DataView(new ArrayBuffer(8));
15236
+ b.setFloat64(0, v, true);
15237
+ for (let i = 0; i < 8; i++) this.parts.push(b.getUint8(i));
15238
+ return this;
15239
+ }
15240
+ bytes(field, data) {
15241
+ this.tag(field, 2);
15242
+ this.varint(BigInt(data.length));
15243
+ for (const x of data) this.parts.push(x);
15244
+ return this;
15245
+ }
15246
+ string(field, s) {
15247
+ return this.bytes(field, new TextEncoder().encode(s));
15248
+ }
15249
+ message(field, m) {
15250
+ return this.bytes(field, m.finish());
15251
+ }
15252
+ finish() {
15253
+ return Uint8Array.from(this.parts);
15254
+ }
15255
+ };
15256
+ function pbValue(v) {
15257
+ if (typeof v === "string") return new Pb().string(1, v);
15258
+ if (typeof v === "boolean") return new Pb().uint(2, v ? 1 : 0);
15259
+ return Number.isInteger(v) ? new Pb().uint(3, BigInt(v)) : new Pb().double(4, v);
15260
+ }
15261
+ function pbAttrs(m, field, a) {
15262
+ for (const [k, v] of Object.entries(a)) {
15263
+ if (v === void 0 || v === null) continue;
15264
+ m.message(field, new Pb().string(1, k).message(2, pbValue(v)));
15265
+ }
15266
+ return m;
15267
+ }
15268
+ var OtlpLogExporter = class {
15269
+ constructor(o) {
15270
+ this.o = o;
15271
+ this.buffer = [];
15272
+ this.inflight = Promise.resolve();
15273
+ this.timer = null;
15274
+ /** Records sent, and records lost because the collector did not take them. */
15275
+ this.sent = 0;
15276
+ this.dropped = 0;
15277
+ const every = o.flushMs ?? 5e3;
15278
+ if (every > 0) {
15279
+ this.timer = setInterval(() => void this.flush(), every);
15280
+ this.timer.unref?.();
15281
+ }
15282
+ }
15283
+ log(severity, body, attributes = {}) {
15284
+ const ms = (this.o.now ?? Date.now)();
15285
+ this.buffer.push({ timeUnixNano: `${Math.round(ms)}000000`, severity, body, attributes });
15286
+ if (this.buffer.length >= (this.o.maxBatch ?? 50)) void this.flush();
15287
+ }
15288
+ /** Send what is buffered. Never rejects. */
15289
+ flush() {
15290
+ if (this.buffer.length === 0) return this.inflight;
15291
+ const batch = this.buffer;
15292
+ this.buffer = [];
15293
+ const resource = { "service.name": this.o.serviceName, "service.version": this.o.serviceVersion, ...this.o.resource };
15294
+ const protobuf = (this.o.protocol ?? "http/protobuf") === "http/protobuf";
15295
+ const body = protobuf ? this.protobuf(resource, batch) : JSON.stringify({
15296
+ resourceLogs: [
15297
+ {
15298
+ resource: { attributes: attrs(resource) },
15299
+ scopeLogs: [
15300
+ {
15301
+ scope: { name: this.o.serviceName },
15302
+ logRecords: batch.map((r) => ({
15303
+ timeUnixNano: r.timeUnixNano,
15304
+ observedTimeUnixNano: r.timeUnixNano,
15305
+ severityNumber: SEVERITY_NUMBER[r.severity],
15306
+ severityText: r.severity,
15307
+ body: { stringValue: r.body },
15308
+ attributes: attrs(r.attributes)
15309
+ }))
15310
+ }
15311
+ ]
15312
+ }
15313
+ ]
15314
+ });
15315
+ const url = `${this.o.endpoint.replace(/\/+$/, "")}/v1/logs`;
15316
+ const send2 = this.o.send ?? fetchSend;
15317
+ this.inflight = this.inflight.then(
15318
+ () => send2(url, body, { "Content-Type": protobuf ? "application/x-protobuf" : "application/json", ...this.o.headers ?? {} }).then(
15319
+ () => void (this.sent += batch.length),
15320
+ () => void (this.dropped += batch.length)
15321
+ )
15322
+ );
15323
+ return this.inflight;
15324
+ }
15325
+ protobuf(resource, batch) {
15326
+ const scope = new Pb().message(1, new Pb().string(1, this.o.serviceName));
15327
+ for (const r of batch) {
15328
+ const t = BigInt(r.timeUnixNano);
15329
+ const rec = new Pb().fixed64(1, t).uint(2, SEVERITY_NUMBER[r.severity]).string(3, r.severity).message(5, new Pb().string(1, r.body));
15330
+ pbAttrs(rec, 6, r.attributes).fixed64(11, t);
15331
+ scope.message(2, rec);
15332
+ }
15333
+ const resourceLogs = new Pb().message(1, pbAttrs(new Pb(), 1, resource)).message(2, scope);
15334
+ return new Pb().message(1, resourceLogs).finish();
15335
+ }
15336
+ /** Stop the timer and send what is left. */
15337
+ async close() {
15338
+ if (this.timer) clearInterval(this.timer);
15339
+ this.timer = null;
15340
+ await this.flush();
15341
+ }
15342
+ };
15343
+ function parseOtlpHeaders(text2) {
15344
+ const out = {};
15345
+ for (const part of (text2 ?? "").split(",")) {
15346
+ const i = part.indexOf("=");
15347
+ if (i <= 0) continue;
15348
+ const key = part.slice(0, i).trim();
15349
+ const value = part.slice(i + 1).trim();
15350
+ if (!key) continue;
15351
+ try {
15352
+ out[key] = decodeURIComponent(value);
15353
+ } catch {
15354
+ out[key] = value;
15355
+ }
15356
+ }
15357
+ return out;
15358
+ }
15359
+ var parseResourceAttributes = parseOtlpHeaders;
15360
+ var truthy = (v) => (v ?? "").trim().toLowerCase() === "true";
15361
+ function otlpFromEnv(env, defaults) {
15362
+ if (truthy(env.OTEL_SDK_DISABLED) || (env.OTEL_LOGS_EXPORTER ?? "").trim().toLowerCase() === "none") return null;
15363
+ const logs = env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT?.trim();
15364
+ const base = env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
15365
+ const endpoint = logs ? logs.replace(/\/v1\/logs\/?$/, "") : base;
15366
+ if (!endpoint) return null;
15367
+ const protocol = otlpProtocol(env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL ?? env.OTEL_EXPORTER_OTLP_PROTOCOL);
15368
+ const resource = parseResourceAttributes(env.OTEL_RESOURCE_ATTRIBUTES);
15369
+ return {
15370
+ endpoint,
15371
+ serviceName: env.OTEL_SERVICE_NAME?.trim() || resource["service.name"] || defaults.serviceName,
15372
+ ...defaults.serviceVersion ? { serviceVersion: defaults.serviceVersion } : {},
15373
+ ...protocol ? { protocol } : {},
15374
+ ...Object.keys(resource).length ? { resource } : {},
15375
+ headers: { ...parseOtlpHeaders(env.OTEL_EXPORTER_OTLP_HEADERS), ...parseOtlpHeaders(env.OTEL_EXPORTER_OTLP_LOGS_HEADERS) }
15376
+ };
15377
+ }
15378
+ function otlpProtocol(v) {
15379
+ const p = (v ?? "").trim().toLowerCase();
15380
+ return p === "http/protobuf" || p === "http/json" ? p : void 0;
15381
+ }
15382
+
15383
+ // src/server/otlp-sink.ts
15384
+ function resolveOtlp(s, env, base) {
15385
+ const fromEnv = otlpFromEnv(env, { serviceName: s.telemetryServiceName, serviceVersion: base.serviceVersion });
15386
+ const disabled = (env.OTEL_SDK_DISABLED ?? "").trim().toLowerCase() === "true" || (env.OTEL_LOGS_EXPORTER ?? "").trim().toLowerCase() === "none";
15387
+ if (disabled || !fromEnv && !s.telemetryEnabled) return null;
15388
+ const resource = { ...base.resource, ...parseResourceAttributes(env.OTEL_RESOURCE_ATTRIBUTES) };
15389
+ const envProtocol = otlpProtocol(env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL ?? env.OTEL_EXPORTER_OTLP_PROTOCOL);
15390
+ return {
15391
+ endpoint: fromEnv?.endpoint ?? s.telemetryEndpoint,
15392
+ serviceName: env.OTEL_SERVICE_NAME?.trim() || s.telemetryServiceName,
15393
+ serviceVersion: base.serviceVersion,
15394
+ protocol: envProtocol ?? s.telemetryProtocol ?? "http/protobuf",
15395
+ headers: { ...s.telemetryHeaders ?? {}, ...fromEnv?.headers ?? {} },
15396
+ resource
15397
+ };
15398
+ }
15399
+ var OtlpSink = class {
15400
+ constructor(options, send2) {
15401
+ this.options = options;
15402
+ this.send = send2;
15403
+ this.exporter = null;
15404
+ this.key = "";
15405
+ }
15406
+ /** The exporter for the current options (a new one when they changed), or null. */
15407
+ current() {
15408
+ const o = this.options();
15409
+ const key = o ? JSON.stringify(o) : "";
15410
+ if (key !== this.key) {
15411
+ const old = this.exporter;
15412
+ void old?.close();
15413
+ this.exporter = o ? new OtlpLogExporter({ ...o, flushMs: 0, ...this.send ? { send: this.send } : {} }) : null;
15414
+ this.key = key;
15415
+ }
15416
+ return this.exporter;
15417
+ }
15418
+ log(severity, body, attributes) {
15419
+ this.current()?.log(severity, body, attributes);
15420
+ }
15421
+ async flush() {
15422
+ await this.current()?.flush();
15423
+ }
15424
+ get sent() {
15425
+ return this.exporter?.sent ?? 0;
15426
+ }
15427
+ get dropped() {
15428
+ return this.exporter?.dropped ?? 0;
15429
+ }
15430
+ };
15431
+
15253
15432
  // src/core/view-block.ts
15254
15433
  var import_yaml3 = __toESM(require_dist(), 1);
15255
15434
  var BLOCK_KEYS = /* @__PURE__ */ new Set(["view", "table", "columns", "where", "order_by", "limit", "join", "group_by", "aggregates", "expand", "labels", "row_link", "chart", "with_table"]);
@@ -20757,14 +20936,10 @@ var EngineRuntime = class {
20757
20936
  session: call.session ?? ""
20758
20937
  });
20759
20938
  }
20760
- /** Logs to the OTLP collector (when enabled) and to `.datanotes/logs/`. */
20939
+ /** Logs to an OpenTelemetry collector (when configured) and to `.datanotes/logs/`. */
20761
20940
  startTelemetry() {
20762
20941
  const s = this.o.getSettings;
20763
- const logger = new OtlpLogger({
20764
- getSettings: s,
20765
- transport: async (url, body) => this.o.postLogs(url, body),
20766
- resource: { "service.version": this.o.version, "host.name": this.o.hostName }
20767
- });
20942
+ const logger = new OtlpSink(() => resolveOtlp(s(), this.o.env ?? {}, { serviceVersion: this.o.version, resource: { "service.instance.id": this.o.hostName } }), this.o.otlpSend);
20768
20943
  const source = this.o.hostName;
20769
20944
  const fileLog = new FileLogger({ enabled: () => s().fileLogEnabled, write: (day, lines) => appendLog(this.o.app, s(), day, lines, source) });
20770
20945
  const tee = new TeeTelemetry([logger, fileLog]);
@@ -22069,7 +22244,7 @@ var NodeWatcher = class {
22069
22244
  };
22070
22245
 
22071
22246
  // src/node/service.ts
22072
- var VERSION = true ? "1.2.1" : "dev";
22247
+ var VERSION = true ? "1.4.0" : "dev";
22073
22248
  function parseArgs(argv, env = process.env) {
22074
22249
  const get = (name) => {
22075
22250
  const i = argv.indexOf(`--${name}`);
@@ -22137,10 +22312,7 @@ async function startService(o, env = process.env) {
22137
22312
  const res = await fetch(req.url, { method: req.method, headers: req.headers, body: req.body });
22138
22313
  return { status: res.status, text: await res.text() };
22139
22314
  },
22140
- postLogs: async (url, body) => {
22141
- const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body });
22142
- if (!res.ok) throw new Error(`OTLP HTTP ${res.status}`);
22143
- },
22315
+ env,
22144
22316
  embeddingKey: () => env.DATANOTES_EMBEDDING_KEY || keyFromFile(),
22145
22317
  version: VERSION,
22146
22318
  hostName: "service",
@@ -22247,7 +22419,7 @@ Docs: https://datanotes.dev`;
22247
22419
  }
22248
22420
 
22249
22421
  // src/node/cli.ts
22250
- var VERSION2 = true ? "1.2.1" : "dev";
22422
+ var VERSION2 = true ? "1.4.0" : "dev";
22251
22423
  async function main2(argv = process.argv.slice(2)) {
22252
22424
  const [command, ...rest] = argv;
22253
22425
  if (command === "--version" || command === "-v" || command === "version") return void process.stdout.write(`${VERSION2}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datanotes",
3
- "version": "1.2.1",
3
+ "version": "1.4.0",
4
4
  "description": "The engine of your second brain: Markdown notes as database tables, for you and your agents.",
5
5
  "keywords": [
6
6
  "markdown",