@powersync/service-core 1.16.2 → 1.17.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/CHANGELOG.md +27 -0
- package/dist/routes/configure-fastify.d.ts +5 -0
- package/dist/routes/endpoints/socket-route.js +7 -0
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.d.ts +10 -0
- package/dist/routes/endpoints/sync-stream.js +10 -1
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/storage/ReportStorage.d.ts +5 -0
- package/dist/sync/sync.d.ts +2 -2
- package/dist/sync/sync.js +2 -2
- package/dist/sync/sync.js.map +1 -1
- package/dist/util/param-logging.d.ts +23 -0
- package/dist/util/param-logging.js +40 -0
- package/dist/util/param-logging.js.map +1 -0
- package/dist/util/protocol-types.d.ts +6 -2
- package/dist/util/protocol-types.js +4 -0
- package/dist/util/protocol-types.js.map +1 -1
- package/package.json +5 -5
- package/src/routes/endpoints/socket-route.ts +9 -0
- package/src/routes/endpoints/sync-stream.ts +13 -1
- package/src/storage/ReportStorage.ts +7 -0
- package/src/sync/sync.ts +4 -10
- package/src/util/param-logging.ts +60 -0
- package/src/util/protocol-types.ts +7 -2
- package/test/src/routes/stream.test.ts +94 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for {@link limitParamsForLogging}.
|
|
3
|
+
*/
|
|
4
|
+
export type ParamLoggingFormatOptions = {
|
|
5
|
+
maxKeyCount: number;
|
|
6
|
+
maxStringLength: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Default options for {@link limitParamsForLogging}.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_PARAM_LOGGING_FORMAT_OPTIONS: ParamLoggingFormatOptions = {
|
|
13
|
+
maxKeyCount: 20,
|
|
14
|
+
maxStringLength: 100
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Formats potentially arbitrary parameters for logging.
|
|
19
|
+
* This limits the number of keys and strings to a maximum length.
|
|
20
|
+
* A warning key-value is added if the number of keys exceeds the maximum.
|
|
21
|
+
* String values exceeding the maximum length are truncated.
|
|
22
|
+
* Non-String values are stringified, the maximum length is then applied.
|
|
23
|
+
* @param params - The parameters to format.
|
|
24
|
+
* @param options - The options to use.
|
|
25
|
+
* @default DEFAULT_PARAM_LOGGING_FORMAT_OPTIONS
|
|
26
|
+
* @returns The formatted parameters.
|
|
27
|
+
*/
|
|
28
|
+
export function limitParamsForLogging(
|
|
29
|
+
params: Record<string, any>,
|
|
30
|
+
options: Partial<ParamLoggingFormatOptions> = DEFAULT_PARAM_LOGGING_FORMAT_OPTIONS
|
|
31
|
+
) {
|
|
32
|
+
const {
|
|
33
|
+
maxStringLength = DEFAULT_PARAM_LOGGING_FORMAT_OPTIONS.maxStringLength,
|
|
34
|
+
maxKeyCount = DEFAULT_PARAM_LOGGING_FORMAT_OPTIONS.maxKeyCount
|
|
35
|
+
} = options;
|
|
36
|
+
|
|
37
|
+
function trimString(value: string): string {
|
|
38
|
+
if (value.length > maxStringLength) {
|
|
39
|
+
return value.slice(0, maxStringLength - 3) + '...';
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return Object.fromEntries(
|
|
45
|
+
Object.entries(params).map(([key, value], index) => {
|
|
46
|
+
if (index == maxKeyCount) {
|
|
47
|
+
return ['⚠️', 'Additional parameters omitted'];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (index > maxKeyCount) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (typeof value == 'string') {
|
|
55
|
+
return [key, trimString(value)];
|
|
56
|
+
}
|
|
57
|
+
return [key, trimString(JSON.stringify(value))];
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as t from 'ts-codec';
|
|
2
|
-
import { BucketPriority, SqliteJsonRow } from '@powersync/service-sync-rules';
|
|
3
1
|
import { JsonContainer } from '@powersync/service-jsonbig';
|
|
2
|
+
import { BucketPriority, SqliteJsonRow } from '@powersync/service-sync-rules';
|
|
3
|
+
import * as t from 'ts-codec';
|
|
4
4
|
|
|
5
5
|
export const BucketRequest = t.object({
|
|
6
6
|
name: t.string,
|
|
@@ -81,6 +81,11 @@ export const StreamingSyncRequest = t.object({
|
|
|
81
81
|
*/
|
|
82
82
|
parameters: t.record(t.any).optional(),
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Application metadata to be used in logging.
|
|
86
|
+
*/
|
|
87
|
+
app_metadata: t.record(t.string).optional(),
|
|
88
|
+
|
|
84
89
|
/**
|
|
85
90
|
* Unique client id.
|
|
86
91
|
*/
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { BasicRouterRequest, Context, SyncRulesBucketStorage } from '@/index.js';
|
|
2
|
-
import {
|
|
2
|
+
import { RouterResponse, ServiceError, logger } from '@powersync/lib-services-framework';
|
|
3
3
|
import { SqlSyncRules } from '@powersync/service-sync-rules';
|
|
4
4
|
import { Readable, Writable } from 'stream';
|
|
5
5
|
import { pipeline } from 'stream/promises';
|
|
6
6
|
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import winston from 'winston';
|
|
7
8
|
import { syncStreamed } from '../../../src/routes/endpoints/sync-stream.js';
|
|
9
|
+
import { DEFAULT_PARAM_LOGGING_FORMAT_OPTIONS, limitParamsForLogging } from '../../../src/util/param-logging.js';
|
|
8
10
|
import { mockServiceContext } from './mocks.js';
|
|
9
11
|
|
|
10
12
|
describe('Stream Route', () => {
|
|
@@ -77,6 +79,97 @@ describe('Stream Route', () => {
|
|
|
77
79
|
const r = await drainWithTimeout(stream).catch((error) => error);
|
|
78
80
|
expect(r.message).toContain('Simulated storage error');
|
|
79
81
|
});
|
|
82
|
+
|
|
83
|
+
it('logs the application metadata', async () => {
|
|
84
|
+
const storage = {
|
|
85
|
+
getParsedSyncRules() {
|
|
86
|
+
return new SqlSyncRules('bucket_definitions: {}');
|
|
87
|
+
},
|
|
88
|
+
watchCheckpointChanges: async function* (options) {
|
|
89
|
+
throw new Error('Simulated storage error');
|
|
90
|
+
}
|
|
91
|
+
} as Partial<SyncRulesBucketStorage>;
|
|
92
|
+
const serviceContext = mockServiceContext(storage);
|
|
93
|
+
|
|
94
|
+
// Create a custom format to capture log info objects (which include defaultMeta)
|
|
95
|
+
const capturedLogs: any[] = [];
|
|
96
|
+
const captureFormat = winston.format((info) => {
|
|
97
|
+
// Capture the info object which includes defaultMeta merged in
|
|
98
|
+
capturedLogs.push({ ...info });
|
|
99
|
+
return info;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Create a test logger with the capture format
|
|
103
|
+
const testLogger = winston.createLogger({
|
|
104
|
+
format: winston.format.combine(captureFormat(), winston.format.json()),
|
|
105
|
+
transports: [new winston.transports.Console()]
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const context: Context = {
|
|
109
|
+
logger: testLogger,
|
|
110
|
+
service_context: serviceContext,
|
|
111
|
+
token_payload: {
|
|
112
|
+
exp: new Date().getTime() / 1000 + 10000,
|
|
113
|
+
iat: new Date().getTime() / 1000 - 10000,
|
|
114
|
+
sub: 'test-user'
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const request: BasicRouterRequest = {
|
|
119
|
+
headers: {
|
|
120
|
+
'accept-encoding': 'gzip'
|
|
121
|
+
},
|
|
122
|
+
hostname: '',
|
|
123
|
+
protocol: 'http'
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const inputMeta = {
|
|
127
|
+
test: 'test',
|
|
128
|
+
long_meta: 'a'.repeat(1000)
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const response = await (syncStreamed.handler({
|
|
132
|
+
context,
|
|
133
|
+
params: {
|
|
134
|
+
app_metadata: inputMeta,
|
|
135
|
+
parameters: {
|
|
136
|
+
user_name: 'bob',
|
|
137
|
+
nested_object: {
|
|
138
|
+
nested_key: 'b'.repeat(1000)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
request
|
|
143
|
+
}) as Promise<RouterResponse>);
|
|
144
|
+
expect(response.status).toEqual(200);
|
|
145
|
+
const stream = response.data as Readable;
|
|
146
|
+
const r = await drainWithTimeout(stream).catch((error) => error);
|
|
147
|
+
expect(r.message).toContain('Simulated storage error');
|
|
148
|
+
|
|
149
|
+
// Find the "Sync stream started" log entry
|
|
150
|
+
const syncStartedLog = capturedLogs.find((log) => log.message === 'Sync stream started');
|
|
151
|
+
expect(syncStartedLog).toBeDefined();
|
|
152
|
+
|
|
153
|
+
// Verify that app_metadata from defaultMeta is present in the log
|
|
154
|
+
expect(syncStartedLog?.app_metadata).toBeDefined();
|
|
155
|
+
expect(syncStartedLog?.app_metadata).toEqual(limitParamsForLogging(inputMeta));
|
|
156
|
+
// Should trim long metadata
|
|
157
|
+
expect(syncStartedLog?.app_metadata.long_meta.length).toEqual(
|
|
158
|
+
DEFAULT_PARAM_LOGGING_FORMAT_OPTIONS.maxStringLength
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Verify the explicit log parameters
|
|
162
|
+
expect(syncStartedLog?.client_params).toEqual(
|
|
163
|
+
expect.objectContaining({
|
|
164
|
+
user_name: 'bob'
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
expect(typeof syncStartedLog?.client_params.nested_object).toEqual('string');
|
|
169
|
+
expect(syncStartedLog?.client_params.nested_object.length).toEqual(
|
|
170
|
+
DEFAULT_PARAM_LOGGING_FORMAT_OPTIONS.maxStringLength
|
|
171
|
+
);
|
|
172
|
+
});
|
|
80
173
|
});
|
|
81
174
|
});
|
|
82
175
|
|