@powersync/service-core 1.10.1 → 1.11.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.
Files changed (96) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/api/api-index.d.ts +1 -0
  3. package/dist/api/api-index.js +1 -0
  4. package/dist/api/api-index.js.map +1 -1
  5. package/dist/api/api-metrics.d.ts +11 -0
  6. package/dist/api/api-metrics.js +30 -0
  7. package/dist/api/api-metrics.js.map +1 -0
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +2 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/metrics/MetricsEngine.d.ts +21 -0
  12. package/dist/metrics/MetricsEngine.js +79 -0
  13. package/dist/metrics/MetricsEngine.js.map +1 -0
  14. package/dist/metrics/metrics-index.d.ts +5 -0
  15. package/dist/metrics/metrics-index.js +6 -0
  16. package/dist/metrics/metrics-index.js.map +1 -0
  17. package/dist/metrics/metrics-interfaces.d.ts +36 -0
  18. package/dist/metrics/metrics-interfaces.js +6 -0
  19. package/dist/metrics/metrics-interfaces.js.map +1 -0
  20. package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.d.ts +10 -0
  21. package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.js +51 -0
  22. package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.js.map +1 -0
  23. package/dist/metrics/open-telemetry/util.d.ts +6 -0
  24. package/dist/metrics/open-telemetry/util.js +62 -0
  25. package/dist/metrics/open-telemetry/util.js.map +1 -0
  26. package/dist/metrics/register-metrics.d.ts +11 -0
  27. package/dist/metrics/register-metrics.js +44 -0
  28. package/dist/metrics/register-metrics.js.map +1 -0
  29. package/dist/replication/AbstractReplicationJob.d.ts +2 -0
  30. package/dist/replication/AbstractReplicationJob.js.map +1 -1
  31. package/dist/replication/AbstractReplicator.d.ts +3 -0
  32. package/dist/replication/AbstractReplicator.js +3 -0
  33. package/dist/replication/AbstractReplicator.js.map +1 -1
  34. package/dist/replication/ReplicationModule.d.ts +7 -0
  35. package/dist/replication/ReplicationModule.js +1 -0
  36. package/dist/replication/ReplicationModule.js.map +1 -1
  37. package/dist/replication/replication-index.d.ts +1 -0
  38. package/dist/replication/replication-index.js +1 -0
  39. package/dist/replication/replication-index.js.map +1 -1
  40. package/dist/replication/replication-metrics.d.ts +11 -0
  41. package/dist/replication/replication-metrics.js +39 -0
  42. package/dist/replication/replication-metrics.js.map +1 -0
  43. package/dist/routes/endpoints/socket-route.js +5 -5
  44. package/dist/routes/endpoints/socket-route.js.map +1 -1
  45. package/dist/routes/endpoints/sync-stream.js +6 -6
  46. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  47. package/dist/storage/storage-index.d.ts +1 -0
  48. package/dist/storage/storage-index.js +1 -0
  49. package/dist/storage/storage-index.js.map +1 -1
  50. package/dist/storage/storage-metrics.d.ts +4 -0
  51. package/dist/storage/storage-metrics.js +56 -0
  52. package/dist/storage/storage-metrics.js.map +1 -0
  53. package/dist/sync/RequestTracker.d.ts +3 -0
  54. package/dist/sync/RequestTracker.js +8 -3
  55. package/dist/sync/RequestTracker.js.map +1 -1
  56. package/dist/sync/sync.js +8 -1
  57. package/dist/sync/sync.js.map +1 -1
  58. package/dist/system/ServiceContext.d.ts +3 -3
  59. package/dist/system/ServiceContext.js +7 -3
  60. package/dist/system/ServiceContext.js.map +1 -1
  61. package/dist/util/config/compound-config-collector.js +1 -0
  62. package/dist/util/config/compound-config-collector.js.map +1 -1
  63. package/dist/util/config/types.d.ts +1 -0
  64. package/dist/util/env.d.ts +0 -1
  65. package/dist/util/env.js +0 -4
  66. package/dist/util/env.js.map +1 -1
  67. package/package.json +7 -7
  68. package/src/api/api-index.ts +1 -0
  69. package/src/api/api-metrics.ts +35 -0
  70. package/src/index.ts +2 -2
  71. package/src/metrics/MetricsEngine.ts +98 -0
  72. package/src/metrics/metrics-index.ts +5 -0
  73. package/src/metrics/metrics-interfaces.ts +41 -0
  74. package/src/metrics/open-telemetry/OpenTelemetryMetricsFactory.ts +66 -0
  75. package/src/metrics/open-telemetry/util.ts +80 -0
  76. package/src/metrics/register-metrics.ts +56 -0
  77. package/src/replication/AbstractReplicationJob.ts +2 -0
  78. package/src/replication/AbstractReplicator.ts +7 -0
  79. package/src/replication/ReplicationModule.ts +10 -0
  80. package/src/replication/replication-index.ts +1 -0
  81. package/src/replication/replication-metrics.ts +45 -0
  82. package/src/routes/endpoints/socket-route.ts +6 -5
  83. package/src/routes/endpoints/sync-stream.ts +7 -6
  84. package/src/storage/storage-index.ts +1 -0
  85. package/src/storage/storage-metrics.ts +67 -0
  86. package/src/sync/RequestTracker.ts +9 -3
  87. package/src/sync/sync.ts +8 -1
  88. package/src/system/ServiceContext.ts +9 -4
  89. package/src/util/config/compound-config-collector.ts +1 -0
  90. package/src/util/config/types.ts +1 -0
  91. package/src/util/env.ts +0 -4
  92. package/tsconfig.tsbuildinfo +1 -1
  93. package/dist/metrics/Metrics.d.ts +0 -30
  94. package/dist/metrics/Metrics.js +0 -202
  95. package/dist/metrics/Metrics.js.map +0 -1
  96. package/src/metrics/Metrics.ts +0 -255
@@ -0,0 +1,35 @@
1
+ import { MetricsEngine } from '../metrics/MetricsEngine.js';
2
+ import { APIMetric } from '@powersync/service-types';
3
+
4
+ /**
5
+ * Create and register the core API metrics.
6
+ * @param engine
7
+ */
8
+ export function createCoreAPIMetrics(engine: MetricsEngine): void {
9
+ engine.createCounter({
10
+ name: APIMetric.DATA_SYNCED_BYTES,
11
+ description: 'Uncompressed size of synced data',
12
+ unit: 'bytes'
13
+ });
14
+
15
+ engine.createCounter({
16
+ name: APIMetric.OPERATIONS_SYNCED,
17
+ description: 'Number of operations synced'
18
+ });
19
+
20
+ engine.createUpDownCounter({
21
+ name: APIMetric.CONCURRENT_CONNECTIONS,
22
+ description: 'Number of concurrent sync connections'
23
+ });
24
+ }
25
+
26
+ /**
27
+ * Initialise the core API metrics. This should be called after the metrics have been created.
28
+ * @param engine
29
+ */
30
+ export function initializeCoreAPIMetrics(engine: MetricsEngine): void {
31
+ const concurrent_connections = engine.getUpDownCounter(APIMetric.CONCURRENT_CONNECTIONS);
32
+
33
+ // Initialize the metric, so that it reports a value before connections have been opened.
34
+ concurrent_connections.add(0);
35
+ }
package/src/index.ts CHANGED
@@ -12,8 +12,8 @@ export * as entry from './entry/entry-index.js';
12
12
  // Re-export framework for easy use of Container API
13
13
  export * as framework from '@powersync/lib-services-framework';
14
14
 
15
- export * from './metrics/Metrics.js';
16
- export * as metrics from './metrics/Metrics.js';
15
+ export * from './metrics/metrics-index.js';
16
+ export * as metrics from './metrics/metrics-index.js';
17
17
 
18
18
  export * from './migrations/migrations-index.js';
19
19
  export * as migrations from './migrations/migrations-index.js';
@@ -0,0 +1,98 @@
1
+ import { logger, ServiceAssertionError } from '@powersync/lib-services-framework';
2
+ import { Counter, UpDownCounter, ObservableGauge, MetricMetadata, MetricsFactory } from './metrics-interfaces.js';
3
+
4
+ export interface MetricsEngineOptions {
5
+ factory: MetricsFactory;
6
+ disable_telemetry_sharing: boolean;
7
+ }
8
+
9
+ export class MetricsEngine {
10
+ private counters: Map<string, Counter>;
11
+ private upDownCounters: Map<string, UpDownCounter>;
12
+ private observableGauges: Map<string, ObservableGauge>;
13
+
14
+ constructor(private options: MetricsEngineOptions) {
15
+ this.counters = new Map();
16
+ this.upDownCounters = new Map();
17
+ this.observableGauges = new Map();
18
+ }
19
+
20
+ private get factory(): MetricsFactory {
21
+ return this.options.factory;
22
+ }
23
+
24
+ createCounter(metadata: MetricMetadata): Counter {
25
+ if (this.counters.has(metadata.name)) {
26
+ logger.warn(`Counter with name ${metadata.name} already created and registered, skipping.`);
27
+ return this.counters.get(metadata.name)!;
28
+ }
29
+
30
+ const counter = this.factory.createCounter(metadata);
31
+ this.counters.set(metadata.name, counter);
32
+ return counter;
33
+ }
34
+ createUpDownCounter(metadata: MetricMetadata): UpDownCounter {
35
+ if (this.upDownCounters.has(metadata.name)) {
36
+ logger.warn(`UpDownCounter with name ${metadata.name} already created and registered, skipping.`);
37
+ return this.upDownCounters.get(metadata.name)!;
38
+ }
39
+
40
+ const upDownCounter = this.factory.createUpDownCounter(metadata);
41
+ this.upDownCounters.set(metadata.name, upDownCounter);
42
+ return upDownCounter;
43
+ }
44
+
45
+ createObservableGauge(metadata: MetricMetadata): ObservableGauge {
46
+ if (this.observableGauges.has(metadata.name)) {
47
+ logger.warn(`ObservableGauge with name ${metadata.name} already created and registered, skipping.`);
48
+ return this.observableGauges.get(metadata.name)!;
49
+ }
50
+
51
+ const observableGauge = this.factory.createObservableGauge(metadata);
52
+ this.observableGauges.set(metadata.name, observableGauge);
53
+ return observableGauge;
54
+ }
55
+
56
+ getCounter(name: string): Counter {
57
+ const counter = this.counters.get(name);
58
+ if (!counter) {
59
+ throw new ServiceAssertionError(`Counter '${name}' has not been created and registered yet.`);
60
+ }
61
+ return counter;
62
+ }
63
+
64
+ getUpDownCounter(name: string): UpDownCounter {
65
+ const upDownCounter = this.upDownCounters.get(name);
66
+ if (!upDownCounter) {
67
+ throw new ServiceAssertionError(`UpDownCounter '${name}' has not been created and registered yet.`);
68
+ }
69
+ return upDownCounter;
70
+ }
71
+
72
+ getObservableGauge(name: string): ObservableGauge {
73
+ const observableGauge = this.observableGauges.get(name);
74
+ if (!observableGauge) {
75
+ throw new ServiceAssertionError(`ObservableGauge '${name}' has not been created and registered yet.`);
76
+ }
77
+ return observableGauge;
78
+ }
79
+
80
+ public async start(): Promise<void> {
81
+ logger.info(
82
+ `
83
+ Attention:
84
+ PowerSync collects completely anonymous telemetry regarding usage.
85
+ This information is used to shape our roadmap to better serve our customers.
86
+ You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
87
+ https://docs.powersync.com/self-hosting/lifecycle-maintenance/telemetry
88
+ `.trim()
89
+ );
90
+ logger.info(`Anonymous telemetry is currently: ${this.options.disable_telemetry_sharing ? 'disabled' : 'enabled'}`);
91
+
92
+ logger.info('Successfully started Metrics Engine.');
93
+ }
94
+
95
+ public async shutdown(): Promise<void> {
96
+ logger.info('Successfully shut down Metrics Engine.');
97
+ }
98
+ }
@@ -0,0 +1,5 @@
1
+ export * from './MetricsEngine.js';
2
+ export * from './metrics-interfaces.js';
3
+ export * from './register-metrics.js';
4
+ export * from './open-telemetry/OpenTelemetryMetricsFactory.js';
5
+ export * from './open-telemetry/util.js';
@@ -0,0 +1,41 @@
1
+ export interface Counter {
2
+ /**
3
+ * Increment the counter by the given value. Only positive numbers are valid.
4
+ * @param value
5
+ */
6
+ add(value: number): void;
7
+ }
8
+
9
+ export interface UpDownCounter {
10
+ /**
11
+ * Increment or decrement(if negative) the counter by the given value.
12
+ * @param value
13
+ */
14
+ add(value: number): void;
15
+ }
16
+
17
+ export interface ObservableGauge {
18
+ /**
19
+ * Set a value provider that provides the value for the gauge at the time of observation.
20
+ * @param valueProvider
21
+ */
22
+ setValueProvider(valueProvider: () => Promise<number | undefined>): void;
23
+ }
24
+
25
+ export enum Precision {
26
+ INT = 'int',
27
+ DOUBLE = 'double'
28
+ }
29
+
30
+ export interface MetricMetadata {
31
+ name: string;
32
+ description?: string;
33
+ unit?: string;
34
+ precision?: Precision;
35
+ }
36
+
37
+ export interface MetricsFactory {
38
+ createCounter(metadata: MetricMetadata): Counter;
39
+ createUpDownCounter(metadata: MetricMetadata): UpDownCounter;
40
+ createObservableGauge(metadata: MetricMetadata): ObservableGauge;
41
+ }
@@ -0,0 +1,66 @@
1
+ import { Meter, ValueType } from '@opentelemetry/api';
2
+ import {
3
+ Counter,
4
+ ObservableGauge,
5
+ UpDownCounter,
6
+ MetricMetadata,
7
+ MetricsFactory,
8
+ Precision
9
+ } from '../metrics-interfaces.js';
10
+
11
+ export class OpenTelemetryMetricsFactory implements MetricsFactory {
12
+ private meter: Meter;
13
+
14
+ constructor(meter: Meter) {
15
+ this.meter = meter;
16
+ }
17
+
18
+ createCounter(metadata: MetricMetadata): Counter {
19
+ return this.meter.createCounter(metadata.name, {
20
+ description: metadata.description,
21
+ unit: metadata.unit,
22
+ valueType: this.toValueType(metadata.precision)
23
+ });
24
+ }
25
+
26
+ createObservableGauge(metadata: MetricMetadata): ObservableGauge {
27
+ const gauge = this.meter.createObservableGauge(metadata.name, {
28
+ description: metadata.description,
29
+ unit: metadata.unit,
30
+ valueType: this.toValueType(metadata.precision)
31
+ });
32
+
33
+ return {
34
+ setValueProvider(valueProvider: () => Promise<number | undefined>) {
35
+ gauge.addCallback(async (result) => {
36
+ const value = await valueProvider();
37
+
38
+ if (value) {
39
+ result.observe(value);
40
+ }
41
+ });
42
+ }
43
+ };
44
+ }
45
+
46
+ createUpDownCounter(metadata: MetricMetadata): UpDownCounter {
47
+ return this.meter.createUpDownCounter(metadata.name, {
48
+ description: metadata.description,
49
+ unit: metadata.unit,
50
+ valueType: this.toValueType(metadata.precision)
51
+ });
52
+ }
53
+
54
+ private toValueType(precision?: Precision): ValueType {
55
+ if (!precision) {
56
+ return ValueType.INT;
57
+ }
58
+
59
+ switch (precision) {
60
+ case Precision.INT:
61
+ return ValueType.INT;
62
+ case Precision.DOUBLE:
63
+ return ValueType.DOUBLE;
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,80 @@
1
+ import { MeterProvider, MetricReader, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
2
+ import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
3
+ import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
4
+ import { Resource } from '@opentelemetry/resources';
5
+ import { ServiceContext } from '../../system/ServiceContext.js';
6
+ import { OpenTelemetryMetricsFactory } from './OpenTelemetryMetricsFactory.js';
7
+ import { MetricsFactory } from '../metrics-interfaces.js';
8
+ import { logger } from '@powersync/lib-services-framework';
9
+
10
+ export interface RuntimeMetadata {
11
+ [key: string]: string | number | undefined;
12
+ }
13
+
14
+ export function createOpenTelemetryMetricsFactory(context: ServiceContext): MetricsFactory {
15
+ const { configuration, lifeCycleEngine, storageEngine } = context;
16
+ const configuredExporters: MetricReader[] = [];
17
+
18
+ if (configuration.telemetry.prometheus_port) {
19
+ const prometheusExporter = new PrometheusExporter({
20
+ port: configuration.telemetry.prometheus_port,
21
+ preventServerStart: true
22
+ });
23
+ configuredExporters.push(prometheusExporter);
24
+
25
+ lifeCycleEngine.withLifecycle(prometheusExporter, {
26
+ start: async () => {
27
+ await prometheusExporter.startServer();
28
+ logger.info(`Prometheus metric export enabled on port:${configuration.telemetry.prometheus_port}`);
29
+ }
30
+ });
31
+ }
32
+
33
+ if (!configuration.telemetry.disable_telemetry_sharing) {
34
+ const periodicExporter = new PeriodicExportingMetricReader({
35
+ exporter: new OTLPMetricExporter({
36
+ url: configuration.telemetry.internal_service_endpoint
37
+ }),
38
+ exportIntervalMillis: 1000 * 60 * 5 // 5 minutes
39
+ });
40
+
41
+ configuredExporters.push(periodicExporter);
42
+ }
43
+
44
+ let resolvedMetadata: (metadata: RuntimeMetadata) => void;
45
+ const runtimeMetadata: Promise<RuntimeMetadata> = new Promise((resolve) => {
46
+ resolvedMetadata = resolve;
47
+ });
48
+
49
+ lifeCycleEngine.withLifecycle(null, {
50
+ start: async () => {
51
+ const bucketStorage = storageEngine.activeBucketStorage;
52
+ try {
53
+ const instanceId = await bucketStorage.getPowerSyncInstanceId();
54
+ resolvedMetadata({ ['instance_id']: instanceId });
55
+ } catch (err) {
56
+ resolvedMetadata({ ['instance_id']: 'Unknown' });
57
+ }
58
+ }
59
+ });
60
+
61
+ const meterProvider = new MeterProvider({
62
+ resource: new Resource(
63
+ {
64
+ ['service']: 'PowerSync'
65
+ },
66
+ runtimeMetadata
67
+ ),
68
+ readers: configuredExporters
69
+ });
70
+
71
+ lifeCycleEngine.withLifecycle(meterProvider, {
72
+ stop: async () => {
73
+ await meterProvider.shutdown();
74
+ }
75
+ });
76
+
77
+ const meter = meterProvider.getMeter('powersync');
78
+
79
+ return new OpenTelemetryMetricsFactory(meter);
80
+ }
@@ -0,0 +1,56 @@
1
+ import { ServiceContextContainer } from '../system/ServiceContext.js';
2
+ import { createOpenTelemetryMetricsFactory } from './open-telemetry/util.js';
3
+ import { MetricsEngine } from './MetricsEngine.js';
4
+ import { createCoreAPIMetrics, initializeCoreAPIMetrics } from '../api/api-metrics.js';
5
+ import { createCoreReplicationMetrics, initializeCoreReplicationMetrics } from '../replication/replication-metrics.js';
6
+ import { createCoreStorageMetrics, initializeCoreStorageMetrics } from '../storage/storage-metrics.js';
7
+
8
+ export enum MetricModes {
9
+ API = 'api',
10
+ REPLICATION = 'replication',
11
+ STORAGE = 'storage'
12
+ }
13
+
14
+ export type MetricsRegistrationOptions = {
15
+ service_context: ServiceContextContainer;
16
+ modes: MetricModes[];
17
+ };
18
+
19
+ export const registerMetrics = async (options: MetricsRegistrationOptions) => {
20
+ const { service_context, modes } = options;
21
+
22
+ const metricsFactory = createOpenTelemetryMetricsFactory(service_context);
23
+ const metricsEngine = new MetricsEngine({
24
+ factory: metricsFactory,
25
+ disable_telemetry_sharing: service_context.configuration.telemetry.disable_telemetry_sharing
26
+ });
27
+ service_context.register(MetricsEngine, metricsEngine);
28
+
29
+ if (modes.includes(MetricModes.API)) {
30
+ createCoreAPIMetrics(metricsEngine);
31
+ initializeCoreAPIMetrics(metricsEngine);
32
+ }
33
+
34
+ if (modes.includes(MetricModes.REPLICATION)) {
35
+ createCoreReplicationMetrics(metricsEngine);
36
+ initializeCoreReplicationMetrics(metricsEngine);
37
+ }
38
+
39
+ if (modes.includes(MetricModes.STORAGE)) {
40
+ createCoreStorageMetrics(metricsEngine);
41
+
42
+ // This requires an instantiated bucket storage, which is only created when the lifecycle starts
43
+ service_context.storageEngine.registerListener({
44
+ storageActivated: (bucketStorage) => {
45
+ initializeCoreStorageMetrics(metricsEngine, bucketStorage);
46
+ }
47
+ });
48
+ }
49
+
50
+ service_context.lifeCycleEngine.withLifecycle(metricsEngine, {
51
+ start: async () => {
52
+ await metricsEngine.start();
53
+ },
54
+ stop: () => metricsEngine.shutdown()
55
+ });
56
+ };
@@ -2,10 +2,12 @@ import { container, logger } from '@powersync/lib-services-framework';
2
2
  import winston from 'winston';
3
3
  import * as storage from '../storage/storage-index.js';
4
4
  import { ErrorRateLimiter } from './ErrorRateLimiter.js';
5
+ import { MetricsEngine } from '../metrics/MetricsEngine.js';
5
6
 
6
7
  export interface AbstractReplicationJobOptions {
7
8
  id: string;
8
9
  storage: storage.SyncRulesBucketStorage;
10
+ metrics: MetricsEngine;
9
11
  lock: storage.ReplicationLock;
10
12
  rateLimiter: ErrorRateLimiter;
11
13
  }
@@ -7,6 +7,7 @@ import { SyncRulesProvider } from '../util/config/sync-rules/sync-rules-provider
7
7
  import { AbstractReplicationJob } from './AbstractReplicationJob.js';
8
8
  import { ErrorRateLimiter } from './ErrorRateLimiter.js';
9
9
  import { ConnectionTestResult } from './ReplicationModule.js';
10
+ import { MetricsEngine } from '../metrics/MetricsEngine.js';
10
11
 
11
12
  // 5 minutes
12
13
  const PING_INTERVAL = 1_000_000_000n * 300n;
@@ -19,6 +20,7 @@ export interface CreateJobOptions {
19
20
  export interface AbstractReplicatorOptions {
20
21
  id: string;
21
22
  storageEngine: StorageEngine;
23
+ metricsEngine: MetricsEngine;
22
24
  syncRuleProvider: SyncRulesProvider;
23
25
  /**
24
26
  * This limits the effect of retries when there is a persistent issue.
@@ -33,6 +35,7 @@ export interface AbstractReplicatorOptions {
33
35
  */
34
36
  export abstract class AbstractReplicator<T extends AbstractReplicationJob = AbstractReplicationJob> {
35
37
  protected logger: winston.Logger;
38
+
36
39
  /**
37
40
  * Map of replication jobs by sync rule id. Usually there is only one running job, but there could be two when
38
41
  * transitioning to a new set of sync rules.
@@ -72,6 +75,10 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
72
75
  return this.options.rateLimiter;
73
76
  }
74
77
 
78
+ protected get metrics() {
79
+ return this.options.metricsEngine;
80
+ }
81
+
75
82
  public async start(): Promise<void> {
76
83
  this.runLoop().catch((e) => {
77
84
  this.logger.error('Data source fatal replication error', e);
@@ -64,6 +64,14 @@ export abstract class ReplicationModule<TConfig extends DataSourceConfig>
64
64
  */
65
65
  protected abstract createReplicator(context: system.ServiceContext): AbstractReplicator;
66
66
 
67
+ /**
68
+ * Any additional initialization specific to the module should be added here. Will be called if necessary after the
69
+ * main initialization has been completed
70
+ * @param context
71
+ * @protected
72
+ */
73
+ protected abstract onInitialized(context: system.ServiceContext): Promise<void>;
74
+
67
75
  public abstract testConnection(config: TConfig): Promise<ConnectionTestResult>;
68
76
 
69
77
  /**
@@ -93,6 +101,8 @@ export abstract class ReplicationModule<TConfig extends DataSourceConfig>
93
101
 
94
102
  context.replicationEngine?.register(this.createReplicator(context));
95
103
  context.routerEngine?.registerAPI(this.createRouteAPIAdapter());
104
+
105
+ await this.onInitialized(context);
96
106
  }
97
107
 
98
108
  protected decodeConfig(config: TConfig): void {
@@ -3,3 +3,4 @@ export * from './AbstractReplicator.js';
3
3
  export * from './ErrorRateLimiter.js';
4
4
  export * from './ReplicationEngine.js';
5
5
  export * from './ReplicationModule.js';
6
+ export * from './replication-metrics.js';
@@ -0,0 +1,45 @@
1
+ import { MetricsEngine } from '../metrics/metrics-index.js';
2
+ import { ReplicationMetric } from '@powersync/service-types';
3
+
4
+ /**
5
+ * Create and register the core replication metrics.
6
+ * @param engine
7
+ */
8
+ export function createCoreReplicationMetrics(engine: MetricsEngine): void {
9
+ engine.createCounter({
10
+ name: ReplicationMetric.DATA_REPLICATED_BYTES,
11
+ description: 'Uncompressed size of replicated data',
12
+ unit: 'bytes'
13
+ });
14
+
15
+ engine.createCounter({
16
+ name: ReplicationMetric.ROWS_REPLICATED,
17
+ description: 'Total number of replicated rows'
18
+ });
19
+
20
+ engine.createCounter({
21
+ name: ReplicationMetric.TRANSACTIONS_REPLICATED,
22
+ description: 'Total number of replicated transactions'
23
+ });
24
+
25
+ engine.createCounter({
26
+ name: ReplicationMetric.CHUNKS_REPLICATED,
27
+ description: 'Total number of replication chunks'
28
+ });
29
+ }
30
+
31
+ /**
32
+ * Initialise the core replication metrics. This should be called after the metrics have been created.
33
+ * @param engine
34
+ */
35
+ export function initializeCoreReplicationMetrics(engine: MetricsEngine): void {
36
+ const data_replicated_bytes = engine.getCounter(ReplicationMetric.DATA_REPLICATED_BYTES);
37
+ const rows_replicated_total = engine.getCounter(ReplicationMetric.ROWS_REPLICATED);
38
+ const transactions_replicated_total = engine.getCounter(ReplicationMetric.TRANSACTIONS_REPLICATED);
39
+ const chunks_replicated_total = engine.getCounter(ReplicationMetric.CHUNKS_REPLICATED);
40
+
41
+ data_replicated_bytes.add(0);
42
+ rows_replicated_total.add(0);
43
+ transactions_replicated_total.add(0);
44
+ chunks_replicated_total.add(0);
45
+ }
@@ -2,18 +2,19 @@ import { ErrorCode, errors, logger, schema } from '@powersync/lib-services-frame
2
2
  import { RequestParameters } from '@powersync/service-sync-rules';
3
3
  import { serialize } from 'bson';
4
4
 
5
- import { Metrics } from '../../metrics/Metrics.js';
6
5
  import * as sync from '../../sync/sync-index.js';
7
6
  import * as util from '../../util/util-index.js';
8
7
  import { SocketRouteGenerator } from '../router-socket.js';
9
8
  import { SyncRoutes } from './sync-stream.js';
10
9
 
10
+ import { APIMetric } from '@powersync/service-types';
11
+
11
12
  export const syncStreamReactive: SocketRouteGenerator = (router) =>
12
13
  router.reactiveStream<util.StreamingSyncRequest, any>(SyncRoutes.STREAM, {
13
14
  validator: schema.createTsCodecValidator(util.StreamingSyncRequest, { allowAdditional: true }),
14
15
  handler: async ({ context, params, responder, observer, initialN, signal: upstreamSignal }) => {
15
16
  const { service_context } = context;
16
- const { routerEngine, syncContext } = service_context;
17
+ const { routerEngine, metricsEngine, syncContext } = service_context;
17
18
 
18
19
  // Create our own controller that we can abort directly
19
20
  const controller = new AbortController();
@@ -69,8 +70,8 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
69
70
  controller.abort();
70
71
  });
71
72
 
72
- Metrics.getInstance().concurrent_connections.add(1);
73
- const tracker = new sync.RequestTracker();
73
+ metricsEngine.getUpDownCounter(APIMetric.CONCURRENT_CONNECTIONS).add(1);
74
+ const tracker = new sync.RequestTracker(metricsEngine);
74
75
  try {
75
76
  for await (const data of sync.streamResponse({
76
77
  syncContext: syncContext,
@@ -147,7 +148,7 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
147
148
  operations_synced: tracker.operationsSynced,
148
149
  data_synced_bytes: tracker.dataSyncedBytes
149
150
  });
150
- Metrics.getInstance().concurrent_connections.add(-1);
151
+ metricsEngine.getUpDownCounter(APIMetric.CONCURRENT_CONNECTIONS).add(-1);
151
152
  }
152
153
  }
153
154
  });
@@ -5,10 +5,11 @@ import { Readable } from 'stream';
5
5
  import * as sync from '../../sync/sync-index.js';
6
6
  import * as util from '../../util/util-index.js';
7
7
 
8
- import { Metrics } from '../../metrics/Metrics.js';
9
8
  import { authUser } from '../auth.js';
10
9
  import { routeDefinition } from '../router.js';
11
10
 
11
+ import { APIMetric } from '@powersync/service-types';
12
+
12
13
  export enum SyncRoutes {
13
14
  STREAM = '/sync/stream'
14
15
  }
@@ -20,7 +21,7 @@ export const syncStreamed = routeDefinition({
20
21
  validator: schema.createTsCodecValidator(util.StreamingSyncRequest, { allowAdditional: true }),
21
22
  handler: async (payload) => {
22
23
  const { service_context } = payload.context;
23
- const { routerEngine, storageEngine, syncContext } = service_context;
24
+ const { routerEngine, storageEngine, metricsEngine, syncContext } = service_context;
24
25
  const headers = payload.request.headers;
25
26
  const userAgent = headers['x-user-agent'] ?? headers['user-agent'];
26
27
  const clientId = payload.params.client_id;
@@ -49,9 +50,9 @@ export const syncStreamed = routeDefinition({
49
50
  const syncRules = bucketStorage.getParsedSyncRules(routerEngine!.getAPI().getParseSyncRulesOptions());
50
51
 
51
52
  const controller = new AbortController();
52
- const tracker = new sync.RequestTracker();
53
+ const tracker = new sync.RequestTracker(metricsEngine);
53
54
  try {
54
- Metrics.getInstance().concurrent_connections.add(1);
55
+ metricsEngine.getUpDownCounter(APIMetric.CONCURRENT_CONNECTIONS).add(1);
55
56
  const stream = Readable.from(
56
57
  sync.transformToBytesTracked(
57
58
  sync.ndjson(
@@ -96,7 +97,7 @@ export const syncStreamed = routeDefinition({
96
97
  data: stream,
97
98
  afterSend: async () => {
98
99
  controller.abort();
99
- Metrics.getInstance().concurrent_connections.add(-1);
100
+ metricsEngine.getUpDownCounter(APIMetric.CONCURRENT_CONNECTIONS).add(-1);
100
101
  logger.info(`Sync stream complete`, {
101
102
  user_id: syncParams.user_id,
102
103
  client_id: clientId,
@@ -108,7 +109,7 @@ export const syncStreamed = routeDefinition({
108
109
  });
109
110
  } catch (ex) {
110
111
  controller.abort();
111
- Metrics.getInstance().concurrent_connections.add(-1);
112
+ metricsEngine.getUpDownCounter(APIMetric.CONCURRENT_CONNECTIONS).add(-1);
112
113
  }
113
114
  }
114
115
  });
@@ -6,6 +6,7 @@ export * from './SourceEntity.js';
6
6
  export * from './SourceTable.js';
7
7
  export * from './StorageEngine.js';
8
8
  export * from './StorageProvider.js';
9
+ export * from './storage-metrics.js';
9
10
  export * from './WriteCheckpointAPI.js';
10
11
  export * from './BucketStorageFactory.js';
11
12
  export * from './BucketStorageBatch.js';