@spooky-sync/core 0.0.1-canary.4 → 0.0.1-canary.41
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/dist/index.d.ts +18 -367
- package/dist/index.js +268 -287
- package/dist/otel/index.d.ts +21 -0
- package/dist/otel/index.js +86 -0
- package/dist/types.d.ts +361 -0
- package/package.json +34 -5
- package/skills/sp00ky-core/SKILL.md +258 -0
- package/skills/sp00ky-core/references/auth.md +98 -0
- package/skills/sp00ky-core/references/config.md +76 -0
- package/src/events/events.test.ts +2 -1
- package/src/index.ts +1 -1
- package/src/modules/auth/events/index.ts +2 -1
- package/src/modules/auth/index.ts +17 -20
- package/src/modules/cache/index.ts +23 -23
- package/src/modules/cache/types.ts +2 -2
- package/src/modules/data/index.ts +61 -43
- package/src/modules/devtools/index.ts +23 -20
- package/src/modules/sync/engine.ts +31 -21
- package/src/modules/sync/events/index.ts +3 -2
- package/src/modules/sync/queue/queue-down.ts +5 -4
- package/src/modules/sync/queue/queue-up.ts +14 -13
- package/src/modules/sync/scheduler.ts +2 -2
- package/src/modules/sync/sync.ts +47 -35
- package/src/modules/sync/utils.test.ts +2 -2
- package/src/modules/sync/utils.ts +15 -17
- package/src/otel/index.ts +127 -0
- package/src/services/database/database.ts +11 -11
- package/src/services/database/events/index.ts +2 -1
- package/src/services/database/local-migrator.ts +21 -21
- package/src/services/database/local.ts +16 -15
- package/src/services/database/remote.ts +13 -13
- package/src/services/logger/index.ts +6 -101
- package/src/services/persistence/localstorage.ts +2 -2
- package/src/services/persistence/resilient.ts +34 -0
- package/src/services/persistence/surrealdb.ts +9 -9
- package/src/services/stream-processor/index.ts +32 -31
- package/src/services/stream-processor/stream-processor.test.ts +1 -1
- package/src/services/stream-processor/wasm-types.ts +2 -2
- package/src/{spooky.ts → sp00ky.ts} +40 -38
- package/src/types.ts +18 -11
- package/src/utils/index.ts +10 -10
- package/src/utils/parser.ts +2 -1
- package/src/utils/surql.ts +15 -15
- package/src/utils/withRetry.test.ts +1 -1
- package/tsdown.config.ts +1 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Level } from 'pino';
|
|
2
|
+
import type { PinoTransmit } from '../types';
|
|
3
|
+
|
|
4
|
+
// Map pino levels to OTEL severity numbers
|
|
5
|
+
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#severity-fields
|
|
6
|
+
function mapLevelToSeverityNumber(level: string): number {
|
|
7
|
+
switch (level) {
|
|
8
|
+
case 'trace':
|
|
9
|
+
return 1;
|
|
10
|
+
case 'debug':
|
|
11
|
+
return 5;
|
|
12
|
+
case 'info':
|
|
13
|
+
return 9;
|
|
14
|
+
case 'warn':
|
|
15
|
+
return 13;
|
|
16
|
+
case 'error':
|
|
17
|
+
return 17;
|
|
18
|
+
case 'fatal':
|
|
19
|
+
return 21;
|
|
20
|
+
default:
|
|
21
|
+
return 9;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function loadOtelModules(otelEndpoint: string) {
|
|
26
|
+
const [{ LoggerProvider, BatchLogRecordProcessor }, { OTLPLogExporter }, { resourceFromAttributes }, { ATTR_SERVICE_NAME }] =
|
|
27
|
+
await Promise.all([
|
|
28
|
+
import('@opentelemetry/sdk-logs'),
|
|
29
|
+
import('@opentelemetry/exporter-logs-otlp-proto'),
|
|
30
|
+
import('@opentelemetry/resources'),
|
|
31
|
+
import('@opentelemetry/semantic-conventions'),
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const resource = resourceFromAttributes({
|
|
35
|
+
[ATTR_SERVICE_NAME]: 'sp00ky-client',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const exporter = new OTLPLogExporter({
|
|
39
|
+
url: otelEndpoint,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const loggerProvider = new LoggerProvider({
|
|
43
|
+
resource,
|
|
44
|
+
processors: [new BatchLogRecordProcessor(exporter)],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const otelLoggerCache: Record<string, ReturnType<typeof loggerProvider.getLogger>> = {};
|
|
48
|
+
|
|
49
|
+
return (category: string) => {
|
|
50
|
+
if (!otelLoggerCache[category]) {
|
|
51
|
+
otelLoggerCache[category] = loggerProvider.getLogger(category);
|
|
52
|
+
}
|
|
53
|
+
return otelLoggerCache[category];
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Creates a pino browser transmit object that forwards logs to an OpenTelemetry collector.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* import { createOtelTransmit } from '@spooky-sync/core/otel';
|
|
63
|
+
*
|
|
64
|
+
* new Sp00kyClient({
|
|
65
|
+
* // ...
|
|
66
|
+
* otelTransmit: createOtelTransmit('http://localhost:4318/v1/logs'),
|
|
67
|
+
* });
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export function createOtelTransmit(endpoint: string, level: Level = 'info'): PinoTransmit {
|
|
71
|
+
// Start loading OTel modules eagerly (don't await — we're synchronous)
|
|
72
|
+
const otelReady = loadOtelModules(endpoint);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
level: level,
|
|
76
|
+
send: (levelLabel: string, logEvent: any) => {
|
|
77
|
+
// oxlint-disable-next-line promise/always-return
|
|
78
|
+
otelReady.then((getOtelLogger) => {
|
|
79
|
+
try {
|
|
80
|
+
const messages = [...logEvent.messages];
|
|
81
|
+
const severityNumber = mapLevelToSeverityNumber(levelLabel);
|
|
82
|
+
|
|
83
|
+
// Construct the message body
|
|
84
|
+
let body = '';
|
|
85
|
+
const lastMsg = messages.pop();
|
|
86
|
+
|
|
87
|
+
if (typeof lastMsg === 'string') {
|
|
88
|
+
body = lastMsg;
|
|
89
|
+
} else if (lastMsg) {
|
|
90
|
+
body = JSON.stringify(lastMsg);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let category = 'sp00ky-client::unknown';
|
|
94
|
+
|
|
95
|
+
const attributes = {};
|
|
96
|
+
for (const msg of messages) {
|
|
97
|
+
if (typeof msg === 'object') {
|
|
98
|
+
if (msg.Category) {
|
|
99
|
+
category = msg.Category;
|
|
100
|
+
delete msg.Category;
|
|
101
|
+
}
|
|
102
|
+
Object.assign(attributes, msg);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Emit to OTEL SDK
|
|
107
|
+
getOtelLogger(category).emit({
|
|
108
|
+
severityNumber: severityNumber,
|
|
109
|
+
severityText: levelLabel.toUpperCase(),
|
|
110
|
+
body: body,
|
|
111
|
+
attributes: {
|
|
112
|
+
...logEvent.bindings[0],
|
|
113
|
+
...attributes,
|
|
114
|
+
},
|
|
115
|
+
timestamp: new Date(logEvent.ts),
|
|
116
|
+
});
|
|
117
|
+
} catch (e) {
|
|
118
|
+
// oxlint-disable-next-line no-console
|
|
119
|
+
console.warn('Failed to transmit log to OTEL endpoint', e);
|
|
120
|
+
}
|
|
121
|
+
}).catch((e) => {
|
|
122
|
+
// oxlint-disable-next-line no-console
|
|
123
|
+
console.warn('Failed to load OpenTelemetry modules', e);
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { Surreal, SurrealTransaction } from 'surrealdb';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import type { Surreal, SurrealTransaction } from 'surrealdb';
|
|
2
|
+
import type { Logger } from '../logger/index';
|
|
3
|
+
import type {
|
|
4
4
|
DatabaseEventSystem,
|
|
5
|
-
DatabaseEventTypes
|
|
6
|
-
|
|
7
|
-
} from './events/index';
|
|
8
|
-
import { SealedQuery } from '../../utils/surql';
|
|
5
|
+
DatabaseEventTypes} from './events/index';
|
|
6
|
+
import type { SealedQuery } from '../../utils/surql';
|
|
9
7
|
|
|
10
8
|
export abstract class AbstractDatabaseService {
|
|
11
9
|
protected client: Surreal;
|
|
@@ -43,11 +41,12 @@ export abstract class AbstractDatabaseService {
|
|
|
43
41
|
async query<T extends unknown[]>(query: string, vars?: Record<string, unknown>): Promise<T> {
|
|
44
42
|
return new Promise((resolve, reject) => {
|
|
45
43
|
this.queryQueue = this.queryQueue
|
|
44
|
+
// oxlint-disable-next-line promise/always-return
|
|
46
45
|
.then(async () => {
|
|
47
46
|
const startTime = performance.now();
|
|
48
47
|
try {
|
|
49
48
|
this.logger.debug(
|
|
50
|
-
{ query, vars, Category: '
|
|
49
|
+
{ query, vars, Category: 'sp00ky-client::Database::query' },
|
|
51
50
|
'Executing query'
|
|
52
51
|
);
|
|
53
52
|
const pending = this.client.query(query, vars);
|
|
@@ -67,7 +66,7 @@ export abstract class AbstractDatabaseService {
|
|
|
67
66
|
|
|
68
67
|
resolve(result);
|
|
69
68
|
this.logger.trace(
|
|
70
|
-
{ query, result, Category: '
|
|
69
|
+
{ query, result, Category: 'sp00ky-client::Database::query' },
|
|
71
70
|
'Query executed successfully'
|
|
72
71
|
);
|
|
73
72
|
} catch (err) {
|
|
@@ -84,9 +83,10 @@ export abstract class AbstractDatabaseService {
|
|
|
84
83
|
});
|
|
85
84
|
|
|
86
85
|
this.logger.error(
|
|
87
|
-
{ query, vars, err, Category: '
|
|
86
|
+
{ query, vars, err, Category: 'sp00ky-client::Database::query' },
|
|
88
87
|
'Query execution failed'
|
|
89
88
|
);
|
|
89
|
+
// oxlint-disable-next-line no-multiple-resolved -- resolve/reject are in try/catch, mutually exclusive
|
|
90
90
|
reject(err);
|
|
91
91
|
}
|
|
92
92
|
})
|
|
@@ -102,7 +102,7 @@ export abstract class AbstractDatabaseService {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
async close(): Promise<void> {
|
|
105
|
-
this.logger.info({ Category: '
|
|
105
|
+
this.logger.info({ Category: 'sp00ky-client::Database::close' }, 'Closing database connection');
|
|
106
106
|
await this.client.close();
|
|
107
107
|
}
|
|
108
108
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { EventDefinition, EventSystem } from '../../../events/index';
|
|
2
|
+
import { createEventSystem } from '../../../events/index';
|
|
2
3
|
|
|
3
4
|
export const DatabaseEventTypes = {
|
|
4
5
|
LocalQuery: 'DATABASE_LOCAL_QUERY',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
3
|
-
import { LocalDatabaseService } from './local';
|
|
1
|
+
import type { Logger} from '../logger/index';
|
|
2
|
+
import { createLogger } from '../logger/index';
|
|
3
|
+
import type { LocalDatabaseService } from './local';
|
|
4
4
|
|
|
5
5
|
export interface SchemaRecord {
|
|
6
6
|
hash: string;
|
|
@@ -23,8 +23,8 @@ export class LocalMigrator {
|
|
|
23
23
|
logger: Logger
|
|
24
24
|
) {
|
|
25
25
|
this.logger = logger.child({ service: 'LocalMigrator' });
|
|
26
|
-
logger?.child({ service: 'LocalMigrator' }) ??
|
|
27
|
-
createLogger('info').child({ service: 'LocalMigrator' });
|
|
26
|
+
void (logger?.child({ service: 'LocalMigrator' }) ??
|
|
27
|
+
createLogger('info').child({ service: 'LocalMigrator' }));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
async provision(schemaSurql: string): Promise<void> {
|
|
@@ -34,7 +34,7 @@ export class LocalMigrator {
|
|
|
34
34
|
|
|
35
35
|
if (await this.isSchemaUpToDate(hash)) {
|
|
36
36
|
this.logger.info(
|
|
37
|
-
{ Category: '
|
|
37
|
+
{ Category: 'sp00ky-client::LocalMigrator::provision' },
|
|
38
38
|
'[Provisioning] Schema is up to date, skipping migration'
|
|
39
39
|
);
|
|
40
40
|
return;
|
|
@@ -43,10 +43,10 @@ export class LocalMigrator {
|
|
|
43
43
|
await this.recreateDatabase(database);
|
|
44
44
|
|
|
45
45
|
const systemSchema = `
|
|
46
|
-
DEFINE TABLE IF NOT EXISTS
|
|
47
|
-
DEFINE TABLE IF NOT EXISTS
|
|
48
|
-
DEFINE TABLE IF NOT EXISTS
|
|
49
|
-
DEFINE TABLE IF NOT EXISTS
|
|
46
|
+
DEFINE TABLE IF NOT EXISTS _00_stream_processor_state SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
|
|
47
|
+
DEFINE TABLE IF NOT EXISTS _00_query SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
|
|
48
|
+
DEFINE TABLE IF NOT EXISTS _00_schema SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
|
|
49
|
+
DEFINE TABLE IF NOT EXISTS _00_pending_mutations SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
|
|
50
50
|
`;
|
|
51
51
|
const fullSchema = schemaSurql + '\n' + systemSchema;
|
|
52
52
|
|
|
@@ -60,7 +60,7 @@ export class LocalMigrator {
|
|
|
60
60
|
// SKIP INDEXES: WASM engine hangs on DEFINE INDEX (confirmed)
|
|
61
61
|
if (cleanStatement.toUpperCase().startsWith('DEFINE INDEX')) {
|
|
62
62
|
this.logger.warn(
|
|
63
|
-
{ Category: '
|
|
63
|
+
{ Category: 'sp00ky-client::LocalMigrator::provision' },
|
|
64
64
|
`[Provisioning] Skipping index definition (WASM hang avoidance): ${cleanStatement.substring(0, 50)}...`
|
|
65
65
|
);
|
|
66
66
|
continue;
|
|
@@ -68,17 +68,17 @@ export class LocalMigrator {
|
|
|
68
68
|
|
|
69
69
|
try {
|
|
70
70
|
this.logger.info(
|
|
71
|
-
{ Category: '
|
|
71
|
+
{ Category: 'sp00ky-client::LocalMigrator::provision' },
|
|
72
72
|
`[Provisioning] (${i + 1}/${statements.length}) Executing: ${statement.substring(0, 50)}...`
|
|
73
73
|
);
|
|
74
74
|
await this.localDb.query(statement);
|
|
75
75
|
this.logger.info(
|
|
76
|
-
{ Category: '
|
|
76
|
+
{ Category: 'sp00ky-client::LocalMigrator::provision' },
|
|
77
77
|
`[Provisioning] (${i + 1}/${statements.length}) Done`
|
|
78
78
|
);
|
|
79
79
|
} catch (e) {
|
|
80
80
|
this.logger.error(
|
|
81
|
-
{ Category: '
|
|
81
|
+
{ Category: 'sp00ky-client::LocalMigrator::provision' },
|
|
82
82
|
`[Provisioning] (${i + 1}/${statements.length}) Error executing statement: ${statement}`
|
|
83
83
|
);
|
|
84
84
|
throw e;
|
|
@@ -91,27 +91,27 @@ export class LocalMigrator {
|
|
|
91
91
|
private async isSchemaUpToDate(hash: string): Promise<boolean> {
|
|
92
92
|
try {
|
|
93
93
|
const [lastSchemaRecord] = await this.localDb.query<any>(
|
|
94
|
-
`SELECT hash, created_at FROM ONLY
|
|
94
|
+
`SELECT hash, created_at FROM ONLY _00_schema ORDER BY created_at DESC LIMIT 1;`
|
|
95
95
|
);
|
|
96
96
|
return lastSchemaRecord?.hash === hash;
|
|
97
|
-
} catch (
|
|
97
|
+
} catch (_error) {
|
|
98
98
|
return false;
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
private async recreateDatabase(database: string) {
|
|
103
103
|
try {
|
|
104
|
-
await this.localDb.query(`DEFINE DATABASE
|
|
105
|
-
} catch (
|
|
104
|
+
await this.localDb.query(`DEFINE DATABASE _00_temp;`);
|
|
105
|
+
} catch (_e) {
|
|
106
106
|
// Ignore if exists
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
try {
|
|
110
110
|
await this.localDb.query(`
|
|
111
|
-
USE DB
|
|
111
|
+
USE DB _00_temp;
|
|
112
112
|
REMOVE DATABASE ${database};
|
|
113
113
|
`);
|
|
114
|
-
} catch (
|
|
114
|
+
} catch (_e) {
|
|
115
115
|
// Ignore error if database doesn't exist
|
|
116
116
|
}
|
|
117
117
|
|
|
@@ -196,7 +196,7 @@ export class LocalMigrator {
|
|
|
196
196
|
|
|
197
197
|
private async createHashRecord(hash: string) {
|
|
198
198
|
await this.localDb.query(
|
|
199
|
-
`UPSERT
|
|
199
|
+
`UPSERT _00_schema SET hash = $hash, created_at = time::now() WHERE hash = $hash;`,
|
|
200
200
|
{ hash }
|
|
201
201
|
);
|
|
202
202
|
}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Diagnostic} from 'surrealdb';
|
|
2
|
+
import { applyDiagnostics, DateTime, RecordId, Surreal } from 'surrealdb';
|
|
2
3
|
import { createWasmWorkerEngines } from '@surrealdb/wasm';
|
|
3
|
-
import {
|
|
4
|
-
import { Logger } from '../logger/index';
|
|
4
|
+
import type { Sp00kyConfig } from '../../types';
|
|
5
|
+
import type { Logger } from '../logger/index';
|
|
5
6
|
import { AbstractDatabaseService } from './database';
|
|
6
7
|
import { createDatabaseEventSystem, DatabaseEventTypes } from './events/index';
|
|
7
|
-
import { encodeRecordId
|
|
8
|
+
import { encodeRecordId } from '../../utils/index';
|
|
8
9
|
|
|
9
10
|
export class LocalDatabaseService extends AbstractDatabaseService {
|
|
10
|
-
private config:
|
|
11
|
+
private config: Sp00kyConfig<any>['database'];
|
|
11
12
|
protected eventType = DatabaseEventTypes.LocalQuery;
|
|
12
13
|
|
|
13
|
-
constructor(config:
|
|
14
|
+
constructor(config: Sp00kyConfig<any>['database'], logger: Logger) {
|
|
14
15
|
const events = createDatabaseEventSystem();
|
|
15
16
|
super(
|
|
16
17
|
new Surreal({
|
|
@@ -38,7 +39,7 @@ export class LocalDatabaseService extends AbstractDatabaseService {
|
|
|
38
39
|
type,
|
|
39
40
|
phase,
|
|
40
41
|
service: 'surrealdb:local',
|
|
41
|
-
Category: '
|
|
42
|
+
Category: 'sp00ky-client::LocalDatabaseService::diagnostics',
|
|
42
43
|
},
|
|
43
44
|
`Local SurrealDB diagnostics captured ${type}:${phase}`
|
|
44
45
|
);
|
|
@@ -52,26 +53,26 @@ export class LocalDatabaseService extends AbstractDatabaseService {
|
|
|
52
53
|
this.config = config;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
getConfig():
|
|
56
|
+
getConfig(): Sp00kyConfig<any>['database'] {
|
|
56
57
|
return this.config;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
async connect(): Promise<void> {
|
|
60
61
|
const { namespace, database } = this.getConfig();
|
|
61
62
|
this.logger.info(
|
|
62
|
-
{ namespace, database, Category: '
|
|
63
|
+
{ namespace, database, Category: 'sp00ky-client::LocalDatabaseService::connect' },
|
|
63
64
|
'Connecting to local database'
|
|
64
65
|
);
|
|
65
66
|
try {
|
|
66
67
|
const store = this.getConfig().store ?? 'memory';
|
|
67
|
-
const storeUrl = store === 'memory' ? 'mem://' : 'indxdb://
|
|
68
|
+
const storeUrl = store === 'memory' ? 'mem://' : 'indxdb://sp00ky';
|
|
68
69
|
this.logger.debug(
|
|
69
|
-
{ storeUrl, Category: '
|
|
70
|
+
{ storeUrl, Category: 'sp00ky-client::LocalDatabaseService::connect' },
|
|
70
71
|
'[LocalDatabaseService] Calling client.connect'
|
|
71
72
|
);
|
|
72
73
|
await this.client.connect(storeUrl, {});
|
|
73
74
|
this.logger.debug(
|
|
74
|
-
{ namespace, database, Category: '
|
|
75
|
+
{ namespace, database, Category: 'sp00ky-client::LocalDatabaseService::connect' },
|
|
75
76
|
'[LocalDatabaseService] client.connect returned. Calling client.use'
|
|
76
77
|
);
|
|
77
78
|
|
|
@@ -80,17 +81,17 @@ export class LocalDatabaseService extends AbstractDatabaseService {
|
|
|
80
81
|
database,
|
|
81
82
|
});
|
|
82
83
|
this.logger.debug(
|
|
83
|
-
{ Category: '
|
|
84
|
+
{ Category: 'sp00ky-client::LocalDatabaseService::connect' },
|
|
84
85
|
'[LocalDatabaseService] client.use returned'
|
|
85
86
|
);
|
|
86
87
|
|
|
87
88
|
this.logger.info(
|
|
88
|
-
{ Category: '
|
|
89
|
+
{ Category: 'sp00ky-client::LocalDatabaseService::connect' },
|
|
89
90
|
'Connected to local database'
|
|
90
91
|
);
|
|
91
92
|
} catch (err) {
|
|
92
93
|
this.logger.error(
|
|
93
|
-
{ err, Category: '
|
|
94
|
+
{ err, Category: 'sp00ky-client::LocalDatabaseService::connect' },
|
|
94
95
|
'Failed to connect to local database'
|
|
95
96
|
);
|
|
96
97
|
throw err;
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Diagnostic} from 'surrealdb';
|
|
1
3
|
import {
|
|
2
4
|
applyDiagnostics,
|
|
3
5
|
createRemoteEngines,
|
|
4
|
-
Diagnostic,
|
|
5
6
|
Surreal,
|
|
6
|
-
SurrealTransaction,
|
|
7
7
|
} from 'surrealdb';
|
|
8
|
-
import {
|
|
9
|
-
import { Logger } from '../logger/index';
|
|
8
|
+
import type { Sp00kyConfig } from '../../types';
|
|
9
|
+
import type { Logger } from '../logger/index';
|
|
10
10
|
import { AbstractDatabaseService } from './database';
|
|
11
11
|
import { createDatabaseEventSystem, DatabaseEventTypes } from './events/index';
|
|
12
12
|
|
|
13
13
|
export class RemoteDatabaseService extends AbstractDatabaseService {
|
|
14
|
-
private config:
|
|
14
|
+
private config: Sp00kyConfig<any>['database'];
|
|
15
15
|
protected eventType = DatabaseEventTypes.RemoteQuery;
|
|
16
16
|
|
|
17
|
-
constructor(config:
|
|
17
|
+
constructor(config: Sp00kyConfig<any>['database'], logger: Logger) {
|
|
18
18
|
const events = createDatabaseEventSystem();
|
|
19
19
|
super(
|
|
20
20
|
new Surreal({
|
|
@@ -29,7 +29,7 @@ export class RemoteDatabaseService extends AbstractDatabaseService {
|
|
|
29
29
|
type,
|
|
30
30
|
phase,
|
|
31
31
|
service: 'surrealdb:remote',
|
|
32
|
-
Category: '
|
|
32
|
+
Category: 'sp00ky-client::RemoteDatabaseService::diagnostics',
|
|
33
33
|
},
|
|
34
34
|
`Remote SurrealDB diagnostics captured ${type}:${phase}`
|
|
35
35
|
);
|
|
@@ -43,7 +43,7 @@ export class RemoteDatabaseService extends AbstractDatabaseService {
|
|
|
43
43
|
this.config = config;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
getConfig():
|
|
46
|
+
getConfig(): Sp00kyConfig<any>['database'] {
|
|
47
47
|
return this.config;
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -55,7 +55,7 @@ export class RemoteDatabaseService extends AbstractDatabaseService {
|
|
|
55
55
|
endpoint,
|
|
56
56
|
namespace,
|
|
57
57
|
database,
|
|
58
|
-
Category: '
|
|
58
|
+
Category: 'sp00ky-client::RemoteDatabaseService::connect',
|
|
59
59
|
},
|
|
60
60
|
'Connecting to remote database'
|
|
61
61
|
);
|
|
@@ -68,25 +68,25 @@ export class RemoteDatabaseService extends AbstractDatabaseService {
|
|
|
68
68
|
|
|
69
69
|
if (token) {
|
|
70
70
|
this.logger.debug(
|
|
71
|
-
{ Category: '
|
|
71
|
+
{ Category: 'sp00ky-client::RemoteDatabaseService::connect' },
|
|
72
72
|
'Authenticating with token'
|
|
73
73
|
);
|
|
74
74
|
await this.client.authenticate(token);
|
|
75
75
|
}
|
|
76
76
|
this.logger.info(
|
|
77
|
-
{ Category: '
|
|
77
|
+
{ Category: 'sp00ky-client::RemoteDatabaseService::connect' },
|
|
78
78
|
'Connected to remote database'
|
|
79
79
|
);
|
|
80
80
|
} catch (err) {
|
|
81
81
|
this.logger.error(
|
|
82
|
-
{ err, Category: '
|
|
82
|
+
{ err, Category: 'sp00ky-client::RemoteDatabaseService::connect' },
|
|
83
83
|
'Failed to connect to remote database'
|
|
84
84
|
);
|
|
85
85
|
throw err;
|
|
86
86
|
}
|
|
87
87
|
} else {
|
|
88
88
|
this.logger.warn(
|
|
89
|
-
{ Category: '
|
|
89
|
+
{ Category: 'sp00ky-client::RemoteDatabaseService::connect' },
|
|
90
90
|
'No endpoint configured for remote database'
|
|
91
91
|
);
|
|
92
92
|
}
|
|
@@ -1,114 +1,19 @@
|
|
|
1
|
-
import pino, { Level, type Logger as PinoLogger, type LoggerOptions } from 'pino';
|
|
2
|
-
import {
|
|
3
|
-
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-proto';
|
|
4
|
-
import { resourceFromAttributes } from '@opentelemetry/resources';
|
|
5
|
-
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
|
|
6
|
-
import { createContextKey } from '@opentelemetry/api';
|
|
7
|
-
|
|
8
|
-
const CATEGORY_KEY = createContextKey('Category');
|
|
1
|
+
import pino, { type Level, type Logger as PinoLogger, type LoggerOptions } from 'pino';
|
|
2
|
+
import type { PinoTransmit } from '../../types';
|
|
9
3
|
|
|
10
4
|
export type Logger = PinoLogger;
|
|
11
5
|
|
|
12
|
-
|
|
13
|
-
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#severity-fields
|
|
14
|
-
function mapLevelToSeverityNumber(level: string): number {
|
|
15
|
-
switch (level) {
|
|
16
|
-
case 'trace':
|
|
17
|
-
return 1;
|
|
18
|
-
case 'debug':
|
|
19
|
-
return 5;
|
|
20
|
-
case 'info':
|
|
21
|
-
return 9;
|
|
22
|
-
case 'warn':
|
|
23
|
-
return 13;
|
|
24
|
-
case 'error':
|
|
25
|
-
return 17;
|
|
26
|
-
case 'fatal':
|
|
27
|
-
return 21;
|
|
28
|
-
default:
|
|
29
|
-
return 9;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function createLogger(level: Level = 'info', otelEndpoint?: string): Logger {
|
|
6
|
+
export function createLogger(level: Level = 'info', transmit?: PinoTransmit): Logger {
|
|
34
7
|
const browserConfig: LoggerOptions['browser'] = {
|
|
35
8
|
asObject: true,
|
|
36
9
|
write: (o: any) => {
|
|
10
|
+
// oxlint-disable-next-line no-console
|
|
37
11
|
console.log(JSON.stringify(o));
|
|
38
12
|
},
|
|
39
13
|
};
|
|
40
14
|
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
const resource = resourceFromAttributes({
|
|
44
|
-
[ATTR_SERVICE_NAME]: 'spooky-client',
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const exporter = new OTLPLogExporter({
|
|
48
|
-
url: otelEndpoint,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
// Pass processors in constructor as this SDK version requires it
|
|
52
|
-
const loggerProvider = new LoggerProvider({
|
|
53
|
-
resource,
|
|
54
|
-
processors: [new BatchLogRecordProcessor(exporter)],
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const otelLogger: Record<string, ReturnType<typeof loggerProvider.getLogger>> = {};
|
|
58
|
-
|
|
59
|
-
const getOtelLogger = (category: string) => {
|
|
60
|
-
if (!otelLogger[category]) {
|
|
61
|
-
otelLogger[category] = loggerProvider.getLogger(category);
|
|
62
|
-
}
|
|
63
|
-
return otelLogger[category];
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
browserConfig.transmit = {
|
|
67
|
-
level: level,
|
|
68
|
-
send: (levelLabel: string, logEvent: any) => {
|
|
69
|
-
try {
|
|
70
|
-
const messages = [...logEvent.messages];
|
|
71
|
-
const severityNumber = mapLevelToSeverityNumber(levelLabel);
|
|
72
|
-
|
|
73
|
-
// Construct the message body
|
|
74
|
-
let body = '';
|
|
75
|
-
const msg = messages.pop();
|
|
76
|
-
|
|
77
|
-
if (typeof msg === 'string') {
|
|
78
|
-
body = msg;
|
|
79
|
-
} else if (msg) {
|
|
80
|
-
body = JSON.stringify(msg);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
let category = 'spooky-client::unknown';
|
|
84
|
-
|
|
85
|
-
const attributes = {};
|
|
86
|
-
for (const msg of messages) {
|
|
87
|
-
if (typeof msg === 'object') {
|
|
88
|
-
if (msg.Category) {
|
|
89
|
-
category = msg.Category;
|
|
90
|
-
delete msg.Category;
|
|
91
|
-
}
|
|
92
|
-
Object.assign(attributes, msg);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Emit to OTEL SDK
|
|
97
|
-
getOtelLogger(category).emit({
|
|
98
|
-
severityNumber: severityNumber,
|
|
99
|
-
severityText: levelLabel.toUpperCase(),
|
|
100
|
-
body: body,
|
|
101
|
-
attributes: {
|
|
102
|
-
...logEvent.bindings[0],
|
|
103
|
-
...attributes,
|
|
104
|
-
},
|
|
105
|
-
timestamp: new Date(logEvent.ts),
|
|
106
|
-
});
|
|
107
|
-
} catch (e) {
|
|
108
|
-
console.warn('Failed to transmit log to OTEL endpoint', e);
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
};
|
|
15
|
+
if (transmit) {
|
|
16
|
+
browserConfig.transmit = transmit;
|
|
112
17
|
}
|
|
113
18
|
|
|
114
19
|
return pino({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Logger } from 'pino';
|
|
2
|
-
import { PersistenceClient } from '../../types';
|
|
1
|
+
import type { Logger } from 'pino';
|
|
2
|
+
import type { PersistenceClient } from '../../types';
|
|
3
3
|
|
|
4
4
|
export class LocalStoragePersistenceClient implements PersistenceClient {
|
|
5
5
|
private logger: Logger;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Logger } from 'pino';
|
|
2
|
+
import type { PersistenceClient } from '../../types';
|
|
3
|
+
|
|
4
|
+
export class ResilientPersistenceClient implements PersistenceClient {
|
|
5
|
+
private logger: Logger;
|
|
6
|
+
|
|
7
|
+
constructor(
|
|
8
|
+
private inner: PersistenceClient,
|
|
9
|
+
logger: Logger
|
|
10
|
+
) {
|
|
11
|
+
this.logger = logger.child({ service: 'ResilientPersistenceClient' });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
set<T>(key: string, value: T): Promise<void> {
|
|
15
|
+
return this.inner.set(key, value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async get<T>(key: string): Promise<T | null> {
|
|
19
|
+
try {
|
|
20
|
+
return await this.inner.get<T>(key);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
this.logger.warn(
|
|
23
|
+
{ key, error: e, Category: 'sp00ky-client::ResilientPersistenceClient::get' },
|
|
24
|
+
'Persistence read failed, dropping key'
|
|
25
|
+
);
|
|
26
|
+
await this.inner.remove(key).catch(() => {});
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
remove(key: string): Promise<void> {
|
|
32
|
+
return this.inner.remove(key);
|
|
33
|
+
}
|
|
34
|
+
}
|