From 5c21b7108d7e731f152d4d5ae004ec3ae215b864 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Sun, 5 Apr 2026 11:21:49 +0200 Subject: [PATCH 1/4] Fix parameter query results logging. --- packages/service-core/src/sync/BucketChecksumState.ts | 2 +- packages/service-core/test/src/sync/BucketChecksumState.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/service-core/src/sync/BucketChecksumState.ts b/packages/service-core/src/sync/BucketChecksumState.ts index 21ef5a025..70b5e9769 100644 --- a/packages/service-core/src/sync/BucketChecksumState.ts +++ b/packages/service-core/src/sync/BucketChecksumState.ts @@ -517,7 +517,7 @@ export class BucketParameterState { let errorMessage = error.message; const logData: any = { - checkpoint: checkpoint, + checkpoint: checkpoint.base.checkpoint, user_id: this.syncParams.userId, parameter_query_results: update.buckets.length }; diff --git a/packages/service-core/test/src/sync/BucketChecksumState.test.ts b/packages/service-core/test/src/sync/BucketChecksumState.test.ts index eab2f6b38..efcc16868 100644 --- a/packages/service-core/test/src/sync/BucketChecksumState.test.ts +++ b/packages/service-core/test/src/sync/BucketChecksumState.test.ts @@ -985,6 +985,7 @@ bucket_definitions: expect(errorMessages[0]).toContain('tasks: 20'); expect(errorMessages[0]).toContain('comments: 10'); + expect(errorData[0].checkpoint).toEqual(1n); expect(errorData[0].parameter_query_results).toBe(60); expect(errorData[0].parameter_query_results_by_definition).toEqual({ projects: 30, From f384e94442de4d34970da8e48b4bfa26c98b2831 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Sun, 5 Apr 2026 11:28:47 +0200 Subject: [PATCH 2/4] Changeset. --- .changeset/unlucky-dingos-care.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/unlucky-dingos-care.md diff --git a/.changeset/unlucky-dingos-care.md b/.changeset/unlucky-dingos-care.md new file mode 100644 index 000000000..afb7e0a27 --- /dev/null +++ b/.changeset/unlucky-dingos-care.md @@ -0,0 +1,5 @@ +--- +'@powersync/service-core': patch +--- + +Fix PSYNC_S2305 logging when parameter limit is exceeded. From 34d2eb59990473e38c9408d2e9133d17e0a423bc Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Sun, 5 Apr 2026 12:33:55 +0200 Subject: [PATCH 3/4] Filter logs. --- .../src/db/connection/DatabaseClient.ts | 3 + libs/lib-services/package.json | 1 + libs/lib-services/src/logger/Logger.ts | 56 ++++++++++++++++++- .../src/storage/MongoBucketStorage.ts | 4 +- .../implementation/MongoSyncBucketStorage.ts | 13 ++++- .../src/storage/implementation/db.ts | 5 +- .../storage/PostgresBucketStorageFactory.ts | 1 + .../src/storage/PostgresSyncRulesStorage.ts | 2 + pnpm-lock.yaml | 15 +++-- 9 files changed, 86 insertions(+), 14 deletions(-) diff --git a/libs/lib-postgres/src/db/connection/DatabaseClient.ts b/libs/lib-postgres/src/db/connection/DatabaseClient.ts index 639492331..43e208644 100644 --- a/libs/lib-postgres/src/db/connection/DatabaseClient.ts +++ b/libs/lib-postgres/src/db/connection/DatabaseClient.ts @@ -1,4 +1,5 @@ import * as lib_postgres from '@powersync/lib-service-postgres'; +import { DO_NOT_LOG } from '@powersync/lib-services-framework'; import * as pgwire from '@powersync/service-jpgwire'; import { AbstractPostgresConnection, sql } from './AbstractPostgresConnection.js'; import { ConnectionLease, ConnectionSlot, NotificationListener } from './ConnectionSlot.js'; @@ -31,6 +32,8 @@ export const TRANSACTION_CONNECTION_COUNT = 5; * which require being executed on the same connection. */ export class DatabaseClient extends AbstractPostgresConnection { + [DO_NOT_LOG] = true; + closed: boolean; pool: pgwire.PgClient; diff --git a/libs/lib-services/package.json b/libs/lib-services/package.json index 16cae2d32..7b4402160 100644 --- a/libs/lib-services/package.json +++ b/libs/lib-services/package.json @@ -27,6 +27,7 @@ "dotenv": "^16.4.5", "ipaddr.js": "^2.1.0", "lodash": "^4.17.21", + "safe-stable-stringify": "^2.5.0", "ts-codec": "^1.3.0", "uuid": "^11.1.0", "winston": "^3.13.0", diff --git a/libs/lib-services/src/logger/Logger.ts b/libs/lib-services/src/logger/Logger.ts index 3dbd7cc02..989db664c 100644 --- a/libs/lib-services/src/logger/Logger.ts +++ b/libs/lib-services/src/logger/Logger.ts @@ -1,4 +1,7 @@ -import winston from 'winston'; +import { ServiceAssertionError } from '@powersync/service-errors'; + +import jsonStringify from 'safe-stable-stringify'; +import winston, { format } from 'winston'; const prefixFormat = winston.format((info) => { if (info.prefix) { @@ -13,13 +16,60 @@ const prefixFormat = winston.format((info) => { export const DEFAULT_LOG_LEVEL = 'info'; export const DEFAULT_LOG_FORMAT = process.env.NODE_ENV == 'production' ? 'json' : 'text'; +/** + * Set this field on an object to ensure it is never logged. + * This will throw an assertion error if it is logged. + */ +export const DO_NOT_LOG = Symbol('DO_NOT_LOG'); + +/** + * Filter potentially noise or sensitive values from logs. + * + * This throws a hard error if the DO_NOT_LOG Symbol is encountered. + */ +const logFilter = (key: string, value: unknown) => { + if (value != null && typeof value == 'object' && (value as any)[DO_NOT_LOG]) { + throw new ServiceAssertionError(`${Object.getPrototypeOf(value)?.constructor?.name} must not be logged`); + } + return value; +}; + +const MESSAGE = Symbol.for('message'); + +/** + * Like winston.format.simple, but with a custom replacer to filter logs. + */ +const filteredSimple = format((info) => { + const stringifiedRest = jsonStringify( + Object.assign({}, info, { + level: undefined, + message: undefined, + splat: undefined + }), + logFilter + ); + + const padding = (info.padding && info.padding[info.level]) || ''; + if (stringifiedRest !== '{}') { + info[MESSAGE] = `${info.level}:${padding} ${info.message} ${stringifiedRest}`; + } else { + info[MESSAGE] = `${info.level}:${padding} ${info.message}`; + } + + return info; +}); + export namespace LogFormat { export const development = winston.format.combine( prefixFormat(), winston.format.colorize({ level: true }), - winston.format.simple() + filteredSimple() + ); + export const production = winston.format.combine( + prefixFormat(), + winston.format.timestamp(), + winston.format.json({ replacer: logFilter }) ); - export const production = winston.format.combine(prefixFormat(), winston.format.timestamp(), winston.format.json()); } export const logger = winston.createLogger(); diff --git a/modules/module-mongodb-storage/src/storage/MongoBucketStorage.ts b/modules/module-mongodb-storage/src/storage/MongoBucketStorage.ts index 1ba5a52b5..10f0e61e2 100644 --- a/modules/module-mongodb-storage/src/storage/MongoBucketStorage.ts +++ b/modules/module-mongodb-storage/src/storage/MongoBucketStorage.ts @@ -1,6 +1,6 @@ import { GetIntanceOptions, storage } from '@powersync/service-core'; -import { ErrorCode, ServiceError } from '@powersync/lib-services-framework'; +import { DO_NOT_LOG, ErrorCode, ServiceError } from '@powersync/lib-services-framework'; import { v4 as uuid } from 'uuid'; import * as lib_mongo from '@powersync/lib-service-mongodb'; @@ -18,6 +18,8 @@ export interface MongoBucketStorageOptions { } export class MongoBucketStorage extends storage.BucketStorageFactory { + [DO_NOT_LOG] = true; + private readonly client: mongo.MongoClient; private readonly session: mongo.ClientSession; // TODO: This is still Postgres specific and needs to be reworked diff --git a/modules/module-mongodb-storage/src/storage/implementation/MongoSyncBucketStorage.ts b/modules/module-mongodb-storage/src/storage/implementation/MongoSyncBucketStorage.ts index ce3e3f549..67b975376 100644 --- a/modules/module-mongodb-storage/src/storage/implementation/MongoSyncBucketStorage.ts +++ b/modules/module-mongodb-storage/src/storage/implementation/MongoSyncBucketStorage.ts @@ -2,6 +2,7 @@ import * as lib_mongo from '@powersync/lib-service-mongodb'; import { mongo } from '@powersync/lib-service-mongodb'; import { BaseObserver, + DO_NOT_LOG, logger, ReplicationAbortedError, ServiceAssertionError @@ -67,6 +68,8 @@ export class MongoSyncBucketStorage extends BaseObserver implements storage.SyncRulesBucketStorage { + [DO_NOT_LOG] = true; + private readonly db: VersionedPowerSyncMongo; readonly checksums: MongoChecksums; @@ -1043,15 +1046,19 @@ interface InternalCheckpointChanges extends CheckpointChanges { } class MongoReplicationCheckpoint implements ReplicationCheckpoint { + #storage: MongoSyncBucketStorage; + constructor( - private storage: MongoSyncBucketStorage, + storage: MongoSyncBucketStorage, public readonly checkpoint: InternalOpId, public readonly lsn: string | null, public snapshotTime: mongo.Timestamp - ) {} + ) { + this.#storage = storage; + } async getParameterSets(lookups: ScopedParameterLookup[]): Promise { - return this.storage.getParameterSets(this, lookups); + return this.#storage.getParameterSets(this, lookups); } } diff --git a/modules/module-mongodb-storage/src/storage/implementation/db.ts b/modules/module-mongodb-storage/src/storage/implementation/db.ts index 93e8ebd50..de37a8a82 100644 --- a/modules/module-mongodb-storage/src/storage/implementation/db.ts +++ b/modules/module-mongodb-storage/src/storage/implementation/db.ts @@ -2,7 +2,7 @@ import * as lib_mongo from '@powersync/lib-service-mongodb'; import { mongo } from '@powersync/lib-service-mongodb'; import { POWERSYNC_VERSION, storage } from '@powersync/service-core'; -import { ServiceAssertionError } from '@powersync/lib-services-framework'; +import { DO_NOT_LOG, ServiceAssertionError } from '@powersync/lib-services-framework'; import { MongoStorageConfig } from '../../types/types.js'; import { BucketDataDocument, @@ -29,6 +29,8 @@ export interface PowerSyncMongoOptions { } export class PowerSyncMongo { + [DO_NOT_LOG] = true; + readonly current_data: mongo.Collection; readonly v3_current_data: mongo.Collection; readonly bucket_data: mongo.Collection; @@ -206,6 +208,7 @@ export class PowerSyncMongo { export class VersionedPowerSyncMongo { readonly client: mongo.MongoClient; readonly db: mongo.Db; + [DO_NOT_LOG] = true; readonly storageConfig: StorageConfig; #upstream: PowerSyncMongo; diff --git a/modules/module-postgres-storage/src/storage/PostgresBucketStorageFactory.ts b/modules/module-postgres-storage/src/storage/PostgresBucketStorageFactory.ts index 45623b676..c0875ca4a 100644 --- a/modules/module-postgres-storage/src/storage/PostgresBucketStorageFactory.ts +++ b/modules/module-postgres-storage/src/storage/PostgresBucketStorageFactory.ts @@ -18,6 +18,7 @@ export type PostgresBucketStorageOptions = { }; export class PostgresBucketStorageFactory extends storage.BucketStorageFactory { + [framework.DO_NOT_LOG] = true; readonly db: lib_postgres.DatabaseClient; public readonly slot_name_prefix: string; diff --git a/modules/module-postgres-storage/src/storage/PostgresSyncRulesStorage.ts b/modules/module-postgres-storage/src/storage/PostgresSyncRulesStorage.ts index c84284bc1..807aeee55 100644 --- a/modules/module-postgres-storage/src/storage/PostgresSyncRulesStorage.ts +++ b/modules/module-postgres-storage/src/storage/PostgresSyncRulesStorage.ts @@ -50,6 +50,8 @@ export class PostgresSyncRulesStorage extends framework.BaseObserver implements storage.SyncRulesBucketStorage { + [framework.DO_NOT_LOG] = true; + public readonly group_id: number; public readonly sync_rules: storage.PersistedSyncRulesContent; public readonly slot_name: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3930a6358..8f83e1859 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -167,6 +167,9 @@ importers: lodash: specifier: ^4.17.21 version: 4.17.21 + safe-stable-stringify: + specifier: ^2.5.0 + version: 2.5.0 ts-codec: specifier: ^1.3.0 version: 1.3.0 @@ -3435,8 +3438,8 @@ packages: safe-regex2@5.0.0: resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==} - safe-stable-stringify@2.4.3: - resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} safer-buffer@2.1.2: @@ -6243,7 +6246,7 @@ snapshots: '@types/triple-beam': 1.3.5 fecha: 4.2.3 ms: 2.1.3 - safe-stable-stringify: 2.4.3 + safe-stable-stringify: 2.5.0 triple-beam: 1.4.1 long@5.2.3: {} @@ -6564,7 +6567,7 @@ snapshots: process-warning: 5.0.0 quick-format-unescaped: 4.0.4 real-require: 0.2.0 - safe-stable-stringify: 2.4.3 + safe-stable-stringify: 2.5.0 sonic-boom: 4.2.0 thread-stream: 4.0.0 @@ -6813,7 +6816,7 @@ snapshots: dependencies: ret: 0.5.0 - safe-stable-stringify@2.4.3: {} + safe-stable-stringify@2.5.0: {} safer-buffer@2.1.2: {} @@ -7289,7 +7292,7 @@ snapshots: logform: 2.6.1 one-time: 1.0.0 readable-stream: 3.6.2 - safe-stable-stringify: 2.4.3 + safe-stable-stringify: 2.5.0 stack-trace: 0.0.10 triple-beam: 1.4.1 winston-transport: 4.7.1 From 10cdd7d9df3d48e0d937272dade602af5c6a7a22 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Sun, 5 Apr 2026 12:36:46 +0200 Subject: [PATCH 4/4] Update changeset. --- .changeset/unlucky-dingos-care.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.changeset/unlucky-dingos-care.md b/.changeset/unlucky-dingos-care.md index afb7e0a27..71d296ebc 100644 --- a/.changeset/unlucky-dingos-care.md +++ b/.changeset/unlucky-dingos-care.md @@ -1,5 +1,9 @@ --- +'@powersync/service-module-postgres-storage': patch +'@powersync/service-module-mongodb-storage': patch '@powersync/service-core': patch +'@powersync/lib-service-postgres': patch +'@powersync/lib-services-framework': patch --- Fix PSYNC_S2305 logging when parameter limit is exceeded.