@lv-agent/logdb-client 0.3.0 → 0.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/README.md +49 -6
- package/dist/client.d.ts +11 -5
- package/dist/client.js +41 -7
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +61 -0
- package/package.json +1 -1
- package/proto/logdbd.proto +102 -0
package/README.md
CHANGED
|
@@ -34,11 +34,6 @@ for await (const rec of stream) {
|
|
|
34
34
|
await client.commitOffset('my-app', 'main', 'workers', 'w1', rec.seq);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// SQL query against cache
|
|
38
|
-
const rows = await client.query('my-app', 'main',
|
|
39
|
-
"SELECT seq, event_type FROM records WHERE event_type = 'llm.call' ORDER BY seq DESC LIMIT 10");
|
|
40
|
-
for (const row of rows) console.log(JSON.parse(row));
|
|
41
|
-
|
|
42
37
|
// Subscribe to event types in real-time
|
|
43
38
|
const sub = client.subscribe('my-app', 'main',
|
|
44
39
|
['tool.call', 'llm.call'], 'sandbox-processors', 'worker-1');
|
|
@@ -55,8 +50,8 @@ sub.on('data', (rec) => {
|
|
|
55
50
|
| `append(ns, stream, eventType, content)` | Write a record |
|
|
56
51
|
| `read(ns, stream, seq)` | Point read |
|
|
57
52
|
| `scanAll(ns, stream, fromSeq)` | Scan all records |
|
|
53
|
+
| `query(req)` | Structured query → discriminated `QueryResponse` (see Query) |
|
|
58
54
|
| `tail(ns, stream, opts)` | Live subscription (async iterable) |
|
|
59
|
-
| `query(ns, stream, sql)` | SQL SELECT against query cache |
|
|
60
55
|
| `subscribe(ns, stream, eventTypes, group, id)` | Event-type push subscription |
|
|
61
56
|
| `listNamespaces()` | List all namespaces |
|
|
62
57
|
| `listStreams(ns)` | List streams in a namespace |
|
|
@@ -66,6 +61,54 @@ sub.on('data', (rec) => {
|
|
|
66
61
|
| `deleteStream(ns, stream)` | Delete all records in a stream (admin) |
|
|
67
62
|
| `commitOffset(...)` / `committedOffset(...)` | Consumer group offset management |
|
|
68
63
|
|
|
64
|
+
## Query
|
|
65
|
+
|
|
66
|
+
logdbd's `Query` RPC is a native structured-filter engine that reads the log
|
|
67
|
+
segment directly at the committed cursor (no SQL, no SQLite cache, no Indexer).
|
|
68
|
+
Build a `QueryRequest` with predicates + a result shape; the response is a
|
|
69
|
+
discriminated union keyed on `kind`.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// Count records of a given type
|
|
73
|
+
const { count } = await client.query({
|
|
74
|
+
namespace: 'my-app', stream: 'main',
|
|
75
|
+
eventTypes: ['tool.call'],
|
|
76
|
+
result: 'COUNT',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Filter by metadata + seq range, return records (newest first, top 10)
|
|
80
|
+
const { records } = await client.query({
|
|
81
|
+
namespace: 'my-app', stream: 'main',
|
|
82
|
+
fromSeq: 100, toSeq: 200,
|
|
83
|
+
metadata: [{ key: 'turn_id', value: '7' }],
|
|
84
|
+
result: 'RECORDS',
|
|
85
|
+
descending: true, limit: 10,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Max of a metadata field (numeric; the engine parses string values as u64)
|
|
89
|
+
const { max } = await client.query({
|
|
90
|
+
namespace: 'my-app', stream: 'main',
|
|
91
|
+
eventTypes: ['turn_started'],
|
|
92
|
+
aggregateField: 'turn_id',
|
|
93
|
+
result: 'MAX',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Anti-join: turn_started with no matching turn terminal
|
|
97
|
+
const { records: incomplete } = await client.query({
|
|
98
|
+
namespace: 'my-app', stream: 'main',
|
|
99
|
+
eventTypes: ['turn_started'],
|
|
100
|
+
absent: {
|
|
101
|
+
peerEventTypes: ['turn_completed', 'turn_failed', 'turn_canceled', 'turn_blocked'],
|
|
102
|
+
joinKey: 'turn_id',
|
|
103
|
+
},
|
|
104
|
+
result: 'RECORDS',
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Predicates (all AND-combined): `eventTypes`, `fromSeq`/`toSeq` (inclusive),
|
|
109
|
+
`metadata` (`key` = `value`), `absent` (anti-join). Result shapes: `RECORDS`,
|
|
110
|
+
`COUNT`, `EXISTS`, `COUNT_DISTINCT`, `MIN`, `MAX`, `DISTINCT_VALUES`.
|
|
111
|
+
|
|
69
112
|
## TLS / mTLS
|
|
70
113
|
|
|
71
114
|
```typescript
|
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as grpc from '@grpc/grpc-js';
|
|
2
2
|
import { TailStream } from './tail-stream';
|
|
3
|
-
import type { AppendResult, BatchAppendResult, NamespaceInfo, LogRecord, StreamInfo, StatusResult, Watermark, VerifyResult, TailOptions } from './types';
|
|
3
|
+
import type { AppendResult, BatchAppendResult, NamespaceInfo, LogRecord, QueryRequest, QueryResponse, StreamInfo, StatusResult, Watermark, VerifyResult, TailOptions } from './types';
|
|
4
4
|
export interface ClientOptions {
|
|
5
5
|
/** TLS certificate authority (PEM). */
|
|
6
6
|
tlsCa?: Buffer;
|
|
@@ -57,12 +57,18 @@ export declare class Client {
|
|
|
57
57
|
subscribe(namespace: string, stream: string, eventTypes: string[], consumerGroup: string, consumerId: string): grpc.ClientReadableStream<any>;
|
|
58
58
|
/** Subscribe to new records via Tail. */
|
|
59
59
|
tail(namespace: string, stream: string, opts?: TailOptions): TailStream;
|
|
60
|
-
/** Execute a
|
|
60
|
+
/** Execute a structured query against a stream.
|
|
61
61
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
62
|
+
* Reads the log segment directly at the committed cursor (no SQL, no
|
|
63
|
+
* SQLite cache — cr-027). Build a `QueryRequest` with predicates + a result
|
|
64
|
+
* shape; the returned `QueryResponse` is a discriminated union keyed on
|
|
65
|
+
* `kind` (e.g. `{ kind: 'count', count: 3 }`).
|
|
66
|
+
*
|
|
67
|
+
* `fromSeq`/`toSeq` are an inclusive closed interval; `result` defaults to
|
|
68
|
+
* RECORDS; `limit = 0` means unlimited; aggregations skip records lacking
|
|
69
|
+
* `aggregateField`.
|
|
64
70
|
*/
|
|
65
|
-
query(
|
|
71
|
+
query(req: QueryRequest): Promise<QueryResponse>;
|
|
66
72
|
/** Get the watermark for a namespace/stream. */
|
|
67
73
|
watermark(namespace: string, stream: string): Promise<Watermark>;
|
|
68
74
|
/** List all namespaces. */
|
package/dist/client.js
CHANGED
|
@@ -43,8 +43,12 @@ function loadProto() {
|
|
|
43
43
|
if (protoDef)
|
|
44
44
|
return protoDef;
|
|
45
45
|
const protoPath = path.join(__dirname, '..', 'proto', 'logdbd.proto');
|
|
46
|
+
// keepCase: false — field names are camelCased (eventType, fromSeq, …) to
|
|
47
|
+
// match the rest of this SDK. With true they'd be snake_case and every
|
|
48
|
+
// multi-word request field would be silently dropped / response field read
|
|
49
|
+
// as undefined (empirically confirmed against a live server).
|
|
46
50
|
const pkgDef = protoLoader.loadSync(protoPath, {
|
|
47
|
-
keepCase:
|
|
51
|
+
keepCase: false,
|
|
48
52
|
longs: String,
|
|
49
53
|
enums: String,
|
|
50
54
|
defaults: true,
|
|
@@ -191,17 +195,23 @@ class Client {
|
|
|
191
195
|
return new tail_stream_1.TailStream(call, convertRecord);
|
|
192
196
|
}
|
|
193
197
|
// ── Query ──────────────────────────────────────────────────────────
|
|
194
|
-
/** Execute a
|
|
198
|
+
/** Execute a structured query against a stream.
|
|
195
199
|
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
200
|
+
* Reads the log segment directly at the committed cursor (no SQL, no
|
|
201
|
+
* SQLite cache — cr-027). Build a `QueryRequest` with predicates + a result
|
|
202
|
+
* shape; the returned `QueryResponse` is a discriminated union keyed on
|
|
203
|
+
* `kind` (e.g. `{ kind: 'count', count: 3 }`).
|
|
204
|
+
*
|
|
205
|
+
* `fromSeq`/`toSeq` are an inclusive closed interval; `result` defaults to
|
|
206
|
+
* RECORDS; `limit = 0` means unlimited; aggregations skip records lacking
|
|
207
|
+
* `aggregateField`.
|
|
198
208
|
*/
|
|
199
|
-
query(
|
|
209
|
+
query(req) {
|
|
200
210
|
return new Promise((resolve, reject) => {
|
|
201
|
-
this.client.Query(
|
|
211
|
+
this.client.Query(req, (err, resp) => {
|
|
202
212
|
if (err)
|
|
203
213
|
return reject(err);
|
|
204
|
-
resolve(resp
|
|
214
|
+
resolve(parseQueryResponse(resp));
|
|
205
215
|
});
|
|
206
216
|
});
|
|
207
217
|
}
|
|
@@ -342,3 +352,27 @@ function convertRecord(r) {
|
|
|
342
352
|
content: Buffer.isBuffer(r.content) ? r.content : Buffer.from(r.content || ''),
|
|
343
353
|
};
|
|
344
354
|
}
|
|
355
|
+
/** Resolve the QueryResponse `result` oneof (set by proto-loader because
|
|
356
|
+
* `oneofs: true`) into the typed discriminated union. */
|
|
357
|
+
function parseQueryResponse(resp) {
|
|
358
|
+
switch (resp.result) {
|
|
359
|
+
case 'count':
|
|
360
|
+
return { kind: 'count', count: Number(resp.count) };
|
|
361
|
+
case 'exists':
|
|
362
|
+
return { kind: 'exists', exists: !!resp.exists };
|
|
363
|
+
case 'countDistinct':
|
|
364
|
+
return { kind: 'countDistinct', countDistinct: Number(resp.countDistinct) };
|
|
365
|
+
case 'min':
|
|
366
|
+
return { kind: 'min', min: Number(resp.min) };
|
|
367
|
+
case 'max':
|
|
368
|
+
return { kind: 'max', max: Number(resp.max) };
|
|
369
|
+
case 'distinctValues':
|
|
370
|
+
return { kind: 'distinctValues', values: resp.distinctValues?.values || [] };
|
|
371
|
+
case 'records':
|
|
372
|
+
default:
|
|
373
|
+
return {
|
|
374
|
+
kind: 'records',
|
|
375
|
+
records: (resp.records?.records || []).map(convertRecord),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -20,4 +20,4 @@
|
|
|
20
20
|
*/
|
|
21
21
|
export { Client, ClientOptions } from './client';
|
|
22
22
|
export { TailStream } from './tail-stream';
|
|
23
|
-
export { LogRecord, AppendResult, Watermark, VerifyResult, TailOptions } from './types';
|
|
23
|
+
export { LogRecord, AppendResult, Watermark, VerifyResult, TailOptions, QueryRequest, QueryResponse, QueryResultKind, MetadataFilter, AbsentMatch, } from './types';
|
package/dist/types.d.ts
CHANGED
|
@@ -72,3 +72,64 @@ export interface TailOptions {
|
|
|
72
72
|
consumerGroup?: string;
|
|
73
73
|
consumerId?: string;
|
|
74
74
|
}
|
|
75
|
+
/** Result-shape selector for a structured query. */
|
|
76
|
+
export type QueryResultKind = 'RECORDS' | 'COUNT' | 'COUNT_DISTINCT' | 'MIN' | 'MAX' | 'EXISTS' | 'DISTINCT_VALUES';
|
|
77
|
+
/** A metadata equality predicate (key = value). */
|
|
78
|
+
export interface MetadataFilter {
|
|
79
|
+
key: string;
|
|
80
|
+
value: string;
|
|
81
|
+
}
|
|
82
|
+
/** Anti-join (NOT EXISTS): drop records whose `joinKey` value also appears on a
|
|
83
|
+
* record matching `peerEventTypes`. */
|
|
84
|
+
export interface AbsentMatch {
|
|
85
|
+
peerEventTypes: string[];
|
|
86
|
+
joinKey: string;
|
|
87
|
+
}
|
|
88
|
+
/** Structured query request — predicates (all AND-combined) + result shape.
|
|
89
|
+
* The server reads the log segment directly at the committed cursor. */
|
|
90
|
+
export interface QueryRequest {
|
|
91
|
+
namespace: string;
|
|
92
|
+
stream: string;
|
|
93
|
+
/** IN filter on event_type (empty/absent = all). */
|
|
94
|
+
eventTypes?: string[];
|
|
95
|
+
/** seq >= (inclusive). */
|
|
96
|
+
fromSeq?: number;
|
|
97
|
+
/** seq <= (inclusive). */
|
|
98
|
+
toSeq?: number;
|
|
99
|
+
/** Equality on metadata fields. */
|
|
100
|
+
metadata?: MetadataFilter[];
|
|
101
|
+
/** Result shape (defaults to RECORDS). */
|
|
102
|
+
result?: QueryResultKind;
|
|
103
|
+
/** COUNT_DISTINCT/MIN/MAX/DISTINCT_VALUES target (empty = seq). */
|
|
104
|
+
aggregateField?: string;
|
|
105
|
+
/** Anti-join. */
|
|
106
|
+
absent?: AbsentMatch;
|
|
107
|
+
/** 0 = unlimited. Ignored for aggregations. */
|
|
108
|
+
limit?: number;
|
|
109
|
+
/** Order records/values by seq descending. */
|
|
110
|
+
descending?: boolean;
|
|
111
|
+
}
|
|
112
|
+
/** Typed query response — the server's `result` oneof, resolved to a
|
|
113
|
+
* discriminated union keyed on `kind`. */
|
|
114
|
+
export type QueryResponse = {
|
|
115
|
+
kind: 'records';
|
|
116
|
+
records: LogRecord[];
|
|
117
|
+
} | {
|
|
118
|
+
kind: 'count';
|
|
119
|
+
count: number;
|
|
120
|
+
} | {
|
|
121
|
+
kind: 'exists';
|
|
122
|
+
exists: boolean;
|
|
123
|
+
} | {
|
|
124
|
+
kind: 'countDistinct';
|
|
125
|
+
countDistinct: number;
|
|
126
|
+
} | {
|
|
127
|
+
kind: 'min';
|
|
128
|
+
min: number;
|
|
129
|
+
} | {
|
|
130
|
+
kind: 'max';
|
|
131
|
+
max: number;
|
|
132
|
+
} | {
|
|
133
|
+
kind: 'distinctValues';
|
|
134
|
+
values: string[];
|
|
135
|
+
};
|
package/package.json
CHANGED
package/proto/logdbd.proto
CHANGED
|
@@ -19,6 +19,16 @@ service LogDbService {
|
|
|
19
19
|
// Catalog — namespace & stream management
|
|
20
20
|
rpc ListNamespaces(ListNamespacesRequest) returns (ListNamespacesResponse);
|
|
21
21
|
rpc ListStreams(ListStreamsRequest) returns (ListStreamsResponse);
|
|
22
|
+
|
|
23
|
+
// Management API — stream lifecycle + quota
|
|
24
|
+
rpc CreateStream(CreateStreamRequest) returns (CreateStreamResponse);
|
|
25
|
+
rpc DeleteStream(DeleteStreamRequest) returns (DeleteStreamResponse);
|
|
26
|
+
|
|
27
|
+
// Query — native structured filter against the log segment
|
|
28
|
+
rpc Query(QueryRequest) returns (QueryResponse);
|
|
29
|
+
|
|
30
|
+
// Subscribe — real-time event-type push with consumer-group offset tracking
|
|
31
|
+
rpc Subscribe(SubscribeRequest) returns (stream Record);
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
// ─── Record (full logdbd record) ─────────────────────────────────────────
|
|
@@ -260,3 +270,95 @@ message StreamInfo {
|
|
|
260
270
|
message ListStreamsResponse {
|
|
261
271
|
repeated StreamInfo streams = 1;
|
|
262
272
|
}
|
|
273
|
+
|
|
274
|
+
// ─── Query (native structured filter engine) ──────────────────────────────
|
|
275
|
+
// Reads the log segment directly at the committed cursor (no SQLite cache).
|
|
276
|
+
// See veps/cr-027-native-query-engine.md ("读边界").
|
|
277
|
+
message QueryRequest {
|
|
278
|
+
string namespace = 1;
|
|
279
|
+
string stream = 2;
|
|
280
|
+
// Predicates (all AND-combined).
|
|
281
|
+
repeated string event_types = 3; // IN filter on event_type (empty = all).
|
|
282
|
+
optional uint64 from_seq = 4; // seq >= (inclusive). Absent = no lower bound.
|
|
283
|
+
optional uint64 to_seq = 5; // seq <= (inclusive). Absent = no upper bound.
|
|
284
|
+
repeated MetadataFilter metadata = 6; // Equality on metadata fields.
|
|
285
|
+
// Result shape.
|
|
286
|
+
QueryResult result = 7; // Defaults to RECORDS (=0).
|
|
287
|
+
string aggregate_field = 8; // COUNT_DISTINCT/MIN/MAX/DISTINCT_VALUES target. "" = use seq.
|
|
288
|
+
// Anti-join (NOT EXISTS): drop filtered records whose join_key value also
|
|
289
|
+
// appears on a record matching peer_event_types.
|
|
290
|
+
AbsentMatch absent = 9;
|
|
291
|
+
// Paging / order.
|
|
292
|
+
int32 limit = 10; // 0 = unlimited. Ignored for aggregations.
|
|
293
|
+
bool descending = 11; // Order records/values by seq descending.
|
|
294
|
+
}
|
|
295
|
+
message MetadataFilter {
|
|
296
|
+
string key = 1;
|
|
297
|
+
string value = 2;
|
|
298
|
+
}
|
|
299
|
+
message AbsentMatch {
|
|
300
|
+
repeated string peer_event_types = 1; // Event types that "complete" a group (e.g. turn_completed).
|
|
301
|
+
string join_key = 2; // Metadata field joining candidates to peers (e.g. turn_id).
|
|
302
|
+
}
|
|
303
|
+
enum QueryResult {
|
|
304
|
+
RECORDS = 0;
|
|
305
|
+
COUNT = 1;
|
|
306
|
+
COUNT_DISTINCT = 2;
|
|
307
|
+
MIN = 3;
|
|
308
|
+
MAX = 4;
|
|
309
|
+
EXISTS = 5;
|
|
310
|
+
DISTINCT_VALUES = 6;
|
|
311
|
+
}
|
|
312
|
+
message QueryResponse {
|
|
313
|
+
oneof result {
|
|
314
|
+
RecordsResult records = 1;
|
|
315
|
+
uint64 count = 2;
|
|
316
|
+
bool exists = 3;
|
|
317
|
+
uint64 count_distinct = 4;
|
|
318
|
+
uint64 min = 5;
|
|
319
|
+
uint64 max = 6;
|
|
320
|
+
DistinctValuesResult distinct_values = 7;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
message RecordsResult {
|
|
324
|
+
repeated Record records = 1;
|
|
325
|
+
}
|
|
326
|
+
message DistinctValuesResult {
|
|
327
|
+
repeated string values = 1;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ─── Subscribe (event-type push) ──────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
message SubscribeRequest {
|
|
333
|
+
string namespace = 1;
|
|
334
|
+
string stream = 2;
|
|
335
|
+
repeated string event_types = 3; // subscribe to these event types
|
|
336
|
+
string consumer_group = 4; // for offset tracking
|
|
337
|
+
string consumer_id = 5; // unique consumer within group
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ─── Management API ──────────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
message CreateStreamRequest {
|
|
343
|
+
string namespace = 1;
|
|
344
|
+
string stream = 2;
|
|
345
|
+
// Optional quota settings
|
|
346
|
+
uint64 max_records = 3; // 0 = unlimited
|
|
347
|
+
uint64 max_bytes = 4; // 0 = unlimited
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
message CreateStreamResponse {
|
|
351
|
+
uint32 namespace_id = 1;
|
|
352
|
+
uint64 stream_id = 2;
|
|
353
|
+
bool created = 3; // false if already existed
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
message DeleteStreamRequest {
|
|
357
|
+
string namespace = 1;
|
|
358
|
+
string stream = 2;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
message DeleteStreamResponse {
|
|
362
|
+
bool deleted = 1;
|
|
363
|
+
uint64 deleted_count = 2; // number of records marked deleted
|
|
364
|
+
}
|