@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.
Files changed (45) hide show
  1. package/dist/index.d.ts +18 -367
  2. package/dist/index.js +268 -287
  3. package/dist/otel/index.d.ts +21 -0
  4. package/dist/otel/index.js +86 -0
  5. package/dist/types.d.ts +361 -0
  6. package/package.json +34 -5
  7. package/skills/sp00ky-core/SKILL.md +258 -0
  8. package/skills/sp00ky-core/references/auth.md +98 -0
  9. package/skills/sp00ky-core/references/config.md +76 -0
  10. package/src/events/events.test.ts +2 -1
  11. package/src/index.ts +1 -1
  12. package/src/modules/auth/events/index.ts +2 -1
  13. package/src/modules/auth/index.ts +17 -20
  14. package/src/modules/cache/index.ts +23 -23
  15. package/src/modules/cache/types.ts +2 -2
  16. package/src/modules/data/index.ts +61 -43
  17. package/src/modules/devtools/index.ts +23 -20
  18. package/src/modules/sync/engine.ts +31 -21
  19. package/src/modules/sync/events/index.ts +3 -2
  20. package/src/modules/sync/queue/queue-down.ts +5 -4
  21. package/src/modules/sync/queue/queue-up.ts +14 -13
  22. package/src/modules/sync/scheduler.ts +2 -2
  23. package/src/modules/sync/sync.ts +47 -35
  24. package/src/modules/sync/utils.test.ts +2 -2
  25. package/src/modules/sync/utils.ts +15 -17
  26. package/src/otel/index.ts +127 -0
  27. package/src/services/database/database.ts +11 -11
  28. package/src/services/database/events/index.ts +2 -1
  29. package/src/services/database/local-migrator.ts +21 -21
  30. package/src/services/database/local.ts +16 -15
  31. package/src/services/database/remote.ts +13 -13
  32. package/src/services/logger/index.ts +6 -101
  33. package/src/services/persistence/localstorage.ts +2 -2
  34. package/src/services/persistence/resilient.ts +34 -0
  35. package/src/services/persistence/surrealdb.ts +9 -9
  36. package/src/services/stream-processor/index.ts +32 -31
  37. package/src/services/stream-processor/stream-processor.test.ts +1 -1
  38. package/src/services/stream-processor/wasm-types.ts +2 -2
  39. package/src/{spooky.ts → sp00ky.ts} +40 -38
  40. package/src/types.ts +18 -11
  41. package/src/utils/index.ts +10 -10
  42. package/src/utils/parser.ts +2 -1
  43. package/src/utils/surql.ts +15 -15
  44. package/src/utils/withRetry.test.ts +1 -1
  45. 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 { createLogger, Logger } from '../logger/index';
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
- DatabaseQueryEventPayload,
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: 'spooky-client::Database::query' },
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: 'spooky-client::Database::query' },
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: 'spooky-client::Database::query' },
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: 'spooky-client::Database::close' }, 'Closing database connection');
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 { createEventSystem, EventDefinition, EventSystem } from '../../../events/index';
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 { Surreal } from 'surrealdb';
2
- import { Logger, createLogger } from '../logger/index';
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: 'spooky-client::LocalMigrator::provision' },
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 _spooky_stream_processor_state SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
47
- DEFINE TABLE IF NOT EXISTS _spooky_query SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
48
- DEFINE TABLE IF NOT EXISTS _spooky_schema SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
49
- DEFINE TABLE IF NOT EXISTS _spooky_pending_mutations SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
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: 'spooky-client::LocalMigrator::provision' },
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: 'spooky-client::LocalMigrator::provision' },
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: 'spooky-client::LocalMigrator::provision' },
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: 'spooky-client::LocalMigrator::provision' },
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 _spooky_schema ORDER BY created_at DESC LIMIT 1;`
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 (error) {
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 _spooky_temp;`);
105
- } catch (e) {
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 _spooky_temp;
111
+ USE DB _00_temp;
112
112
  REMOVE DATABASE ${database};
113
113
  `);
114
- } catch (e) {
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 _spooky_schema SET hash = $hash, created_at = time::now() WHERE hash = $hash;`,
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 { applyDiagnostics, DateTime, Diagnostic, RecordId, Surreal } from 'surrealdb';
1
+ import type { Diagnostic} from 'surrealdb';
2
+ import { applyDiagnostics, DateTime, RecordId, Surreal } from 'surrealdb';
2
3
  import { createWasmWorkerEngines } from '@surrealdb/wasm';
3
- import { SpookyConfig } from '../../types';
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, parseRecordIdString, surql } from '../../utils/index';
8
+ import { encodeRecordId } from '../../utils/index';
8
9
 
9
10
  export class LocalDatabaseService extends AbstractDatabaseService {
10
- private config: SpookyConfig<any>['database'];
11
+ private config: Sp00kyConfig<any>['database'];
11
12
  protected eventType = DatabaseEventTypes.LocalQuery;
12
13
 
13
- constructor(config: SpookyConfig<any>['database'], logger: Logger) {
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: 'spooky-client::LocalDatabaseService::diagnostics',
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(): SpookyConfig<any>['database'] {
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: 'spooky-client::LocalDatabaseService::connect' },
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://spooky';
68
+ const storeUrl = store === 'memory' ? 'mem://' : 'indxdb://sp00ky';
68
69
  this.logger.debug(
69
- { storeUrl, Category: 'spooky-client::LocalDatabaseService::connect' },
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: 'spooky-client::LocalDatabaseService::connect' },
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: 'spooky-client::LocalDatabaseService::connect' },
84
+ { Category: 'sp00ky-client::LocalDatabaseService::connect' },
84
85
  '[LocalDatabaseService] client.use returned'
85
86
  );
86
87
 
87
88
  this.logger.info(
88
- { Category: 'spooky-client::LocalDatabaseService::connect' },
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: 'spooky-client::LocalDatabaseService::connect' },
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 { SpookyConfig } from '../../types';
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: SpookyConfig<any>['database'];
14
+ private config: Sp00kyConfig<any>['database'];
15
15
  protected eventType = DatabaseEventTypes.RemoteQuery;
16
16
 
17
- constructor(config: SpookyConfig<any>['database'], logger: Logger) {
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: 'spooky-client::RemoteDatabaseService::diagnostics',
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(): SpookyConfig<any>['database'] {
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: 'spooky-client::RemoteDatabaseService::connect',
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: 'spooky-client::RemoteDatabaseService::connect' },
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: 'spooky-client::RemoteDatabaseService::connect' },
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: 'spooky-client::RemoteDatabaseService::connect' },
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: 'spooky-client::RemoteDatabaseService::connect' },
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 { LoggerProvider, BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
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
- // Map pino levels to OTEL severity numbers
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 (otelEndpoint) {
42
- // Initialize OTEL LoggerProvider
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
+ }