@adalo/metrics 0.0.0-staging.1

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 (85) hide show
  1. package/.env.example +14 -0
  2. package/.eslintignore +3 -0
  3. package/.eslintrc +61 -0
  4. package/.github/pull_request_template.md +14 -0
  5. package/.github/workflows/code-style.yml +29 -0
  6. package/.github/workflows/deploy-staging.yml +34 -0
  7. package/.github/workflows/deploy.yml +29 -0
  8. package/.github/workflows/tests.yml +17 -0
  9. package/.idea/codeStyles/Project.xml +101 -0
  10. package/.idea/codeStyles/codeStyleConfig.xml +5 -0
  11. package/.idea/git_toolbox_prj.xml +15 -0
  12. package/.idea/inspectionProfiles/Project_Default.xml +6 -0
  13. package/.idea/jsLibraryMappings.xml +6 -0
  14. package/.idea/prettier.xml +6 -0
  15. package/.idea/vcs.xml +6 -0
  16. package/.prettierrc +10 -0
  17. package/README-health.md +234 -0
  18. package/README.md +120 -0
  19. package/__tests__/metricsRedisClient.test.js +138 -0
  20. package/babel.config.js +20 -0
  21. package/lib/health/databaseChecker.d.ts +43 -0
  22. package/lib/health/databaseChecker.d.ts.map +1 -0
  23. package/lib/health/databaseChecker.js +189 -0
  24. package/lib/health/databaseChecker.js.map +1 -0
  25. package/lib/health/healthCheckCache.d.ts +59 -0
  26. package/lib/health/healthCheckCache.d.ts.map +1 -0
  27. package/lib/health/healthCheckCache.js +187 -0
  28. package/lib/health/healthCheckCache.js.map +1 -0
  29. package/lib/health/healthCheckClient.d.ts +124 -0
  30. package/lib/health/healthCheckClient.d.ts.map +1 -0
  31. package/lib/health/healthCheckClient.js +324 -0
  32. package/lib/health/healthCheckClient.js.map +1 -0
  33. package/lib/health/healthCheckUtils.d.ts +52 -0
  34. package/lib/health/healthCheckUtils.d.ts.map +1 -0
  35. package/lib/health/healthCheckUtils.js +129 -0
  36. package/lib/health/healthCheckUtils.js.map +1 -0
  37. package/lib/health/healthCheckWorker.d.ts +2 -0
  38. package/lib/health/healthCheckWorker.d.ts.map +1 -0
  39. package/lib/health/healthCheckWorker.js +70 -0
  40. package/lib/health/healthCheckWorker.js.map +1 -0
  41. package/lib/index.d.ts +10 -0
  42. package/lib/index.d.ts.map +1 -0
  43. package/lib/index.js +105 -0
  44. package/lib/index.js.map +1 -0
  45. package/lib/metrics/baseMetricsClient.d.ts +174 -0
  46. package/lib/metrics/baseMetricsClient.d.ts.map +1 -0
  47. package/lib/metrics/baseMetricsClient.js +428 -0
  48. package/lib/metrics/baseMetricsClient.js.map +1 -0
  49. package/lib/metrics/metricsClient.d.ts +95 -0
  50. package/lib/metrics/metricsClient.d.ts.map +1 -0
  51. package/lib/metrics/metricsClient.js +239 -0
  52. package/lib/metrics/metricsClient.js.map +1 -0
  53. package/lib/metrics/metricsDatabaseClient.d.ts +74 -0
  54. package/lib/metrics/metricsDatabaseClient.d.ts.map +1 -0
  55. package/lib/metrics/metricsDatabaseClient.js +218 -0
  56. package/lib/metrics/metricsDatabaseClient.js.map +1 -0
  57. package/lib/metrics/metricsQueueRedisClient.d.ts +57 -0
  58. package/lib/metrics/metricsQueueRedisClient.d.ts.map +1 -0
  59. package/lib/metrics/metricsQueueRedisClient.js +277 -0
  60. package/lib/metrics/metricsQueueRedisClient.js.map +1 -0
  61. package/lib/metrics/metricsRedisClient.d.ts +71 -0
  62. package/lib/metrics/metricsRedisClient.d.ts.map +1 -0
  63. package/lib/metrics/metricsRedisClient.js +370 -0
  64. package/lib/metrics/metricsRedisClient.js.map +1 -0
  65. package/lib/redisUtils.d.ts +53 -0
  66. package/lib/redisUtils.d.ts.map +1 -0
  67. package/lib/redisUtils.js +140 -0
  68. package/lib/redisUtils.js.map +1 -0
  69. package/package.json +66 -0
  70. package/scripts/README.md +43 -0
  71. package/scripts/clearMetrics.js +6 -0
  72. package/src/health/databaseChecker.js +183 -0
  73. package/src/health/healthCheckCache.js +216 -0
  74. package/src/health/healthCheckClient.js +347 -0
  75. package/src/health/healthCheckUtils.js +125 -0
  76. package/src/health/healthCheckWorker.js +71 -0
  77. package/src/index.ts +9 -0
  78. package/src/metrics/baseMetricsClient.js +494 -0
  79. package/src/metrics/metricsClient.js +284 -0
  80. package/src/metrics/metricsDatabaseClient.js +236 -0
  81. package/src/metrics/metricsQueueRedisClient.js +352 -0
  82. package/src/metrics/metricsRedisClient.js +417 -0
  83. package/src/redisUtils.js +155 -0
  84. package/tsconfig.json +19 -0
  85. package/tsconfig.types.json +11 -0
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const {
6
+ BaseMetricsClient
7
+ } = require('./baseMetricsClient');
8
+
9
+ /**
10
+ * MetricsClient handles Prometheus metrics collection and push.
11
+ * Supports gauges, counters, default metrics, and custom metrics.
12
+ * Extends BaseMetricsClient for common functionality.
13
+ */
14
+ class MetricsClient extends BaseMetricsClient {
15
+ /**
16
+ * @param {Object} config
17
+ * @param {string} [config.appName] Name of the application
18
+ * @param {string} [config.dynoId] Dyno/instance ID
19
+ * @param {string} [config.processType] Process type (web, worker, etc.)
20
+ * @param {boolean} [config.enabled] Enable metrics collection
21
+ * @param {boolean} [config.httpMetricsEnabled=false] Enable HTTP request metrics (app_requests_total, app_requests_total_duration)
22
+ * @param {boolean} [config.logValues] Log metrics values to console
23
+ * @param {string} [config.pushgatewayUrl] PushGateway URL
24
+ * @param {string} [config.pushgatewaySecret] PushGateway secret token
25
+ * @param {number} [config.intervalSec] Interval in seconds for pushing metrics
26
+ * @param {boolean} [config.removeOldMetrics] Enable to clear metrics by service name
27
+ * @param {function} [config.startupValidation] Add to validate on start push.
28
+ * @param {boolean} [config.disablePushgateway] Disable pushing to Pushgateway (use HTTP scraping instead)
29
+ */
30
+ constructor(config = {}) {
31
+ super(config);
32
+ this.httpMetricsEnabled = config.httpMetricsEnabled ?? process.env.METRICS_HTTP_ENABLED === 'true' ?? false;
33
+ this._lastUsageMicros = 0;
34
+ this._lastCheckTime = Date.now();
35
+ this._initDefaultMetrics();
36
+ }
37
+
38
+ /**
39
+ * Register all built-in default Gauges and Counters.
40
+ * @private
41
+ */
42
+ _initDefaultMetrics = () => {
43
+ this.createGauge({
44
+ name: 'app_process_cpu_usage_percent',
45
+ help: 'Current CPU usage of the Node.js process in percent',
46
+ updateFn: this.getCpuUsagePercent
47
+ });
48
+ this.createGauge({
49
+ name: 'app_available_cpu_count',
50
+ help: 'How many CPU cores are available to this process',
51
+ updateFn: this.getAvailableCPUs
52
+ });
53
+ this.createGauge({
54
+ name: 'app_container_memory_usage_bytes',
55
+ help: 'Current container RAM usage from cgroup',
56
+ updateFn: this.getContainerMemoryUsage
57
+ });
58
+ this.createGauge({
59
+ name: 'app_event_loop_lag_ms',
60
+ help: 'Estimated event loop lag in milliseconds',
61
+ updateFn: this.measureLag
62
+ });
63
+ this.createGauge({
64
+ name: 'app_container_memory_limit_bytes',
65
+ help: 'Max RAM available to container from cgroup (memory.max)',
66
+ updateFn: this.getContainerMemoryLimit
67
+ });
68
+ this.createGauge({
69
+ name: 'app_uptime_seconds',
70
+ help: 'How long the process has been running',
71
+ updateFn: process.uptime
72
+ });
73
+ if (this.httpMetricsEnabled) {
74
+ this.createCounter({
75
+ name: 'app_requests_total',
76
+ help: 'Total number of HTTP requests',
77
+ labelNames: this.withDefaultLabels(['method', 'route', 'appId', 'databaseId', 'status_code'])
78
+ });
79
+ this.createCounter({
80
+ name: 'app_requests_total_duration',
81
+ help: 'Total duration of HTTP requests in milliseconds',
82
+ labelNames: this.withDefaultLabels(['method', 'route', 'appId', 'databaseId', 'status_code'])
83
+ });
84
+ }
85
+ };
86
+
87
+ /**
88
+ * Get CPU usage percent (cgroup-aware)
89
+ * @returns {number}
90
+ */
91
+ getCpuUsagePercent = () => {
92
+ try {
93
+ const stat = fs.readFileSync('/sys/fs/cgroup/cpu.stat', 'utf-8');
94
+ const match = stat.match(/usage_usec (\d+)/);
95
+ if (!match) return 0;
96
+ const now = Date.now();
97
+ const currentUsage = parseInt(match[1], 10);
98
+ if (this._lastUsageMicros === 0) {
99
+ this._lastUsageMicros = currentUsage;
100
+ this._lastCheckTime = now;
101
+ return 0;
102
+ }
103
+ const deltaUsage = currentUsage - this._lastUsageMicros;
104
+ const deltaTime = now - this._lastCheckTime;
105
+ this._lastUsageMicros = currentUsage;
106
+ this._lastCheckTime = now;
107
+ return deltaUsage / (deltaTime * 1000) * 100;
108
+ } catch {
109
+ return 0;
110
+ }
111
+ };
112
+
113
+ /**
114
+ * Get available CPU cores.
115
+ * @returns {number}
116
+ */
117
+ getAvailableCPUs() {
118
+ try {
119
+ const cpuMaxPath = '/sys/fs/cgroup/cpu.max';
120
+ if (fs.existsSync(cpuMaxPath)) {
121
+ const [quotaStr, periodStr] = fs.readFileSync(cpuMaxPath, 'utf8').trim().split(' ');
122
+ if (quotaStr === 'max') return os.cpus().length;
123
+ return parseInt(quotaStr, 10) / parseInt(periodStr, 10);
124
+ }
125
+ return os.cpus().length;
126
+ } catch {
127
+ return 1;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Get container memory usage in bytes.
133
+ * @returns {number}
134
+ */
135
+ getContainerMemoryUsage() {
136
+ try {
137
+ return parseInt(fs.readFileSync('/sys/fs/cgroup/memory.current', 'utf-8').trim(), 10);
138
+ } catch {
139
+ return process.memoryUsage().rss;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Get container memory limit in bytes.
145
+ * @returns {number}
146
+ */
147
+ getContainerMemoryLimit() {
148
+ try {
149
+ const path = '/sys/fs/cgroup/memory.max';
150
+ if (fs.existsSync(path)) {
151
+ const val = fs.readFileSync(path, 'utf-8').trim();
152
+ if (val !== 'max') {
153
+ const parsed = parseInt(val, 10);
154
+ if (parsed && parsed < os.totalmem()) return parsed;
155
+ }
156
+ }
157
+ return os.totalmem();
158
+ } catch {
159
+ return os.totalmem();
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Measure event loop lag in ms.
165
+ * @returns {Promise<number>}
166
+ */
167
+ measureLag() {
168
+ return new Promise(resolve => {
169
+ const start = Date.now();
170
+ setImmediate(() => resolve(Date.now() - start));
171
+ });
172
+ }
173
+
174
+ /**
175
+ * Increment the HTTP requests counter with detailed request information.
176
+ *
177
+ * @param {Object} params - The parameters for the request counter.
178
+ * @param {string} params.method - HTTP method (GET, POST, etc.).
179
+ * @param {string} params.route - The full requested URL or route.
180
+ * @param {number} params.status_code - HTTP response status code.
181
+ * @param {string} [params.appId=''] - Optional application identifier.
182
+ * @param {string} [params.databaseId=''] - Optional database identifier.
183
+ * @param {number} params.duration - Request duration in milliseconds.
184
+ */
185
+ trackHttpRequest({
186
+ method,
187
+ route,
188
+ status_code,
189
+ appId = '',
190
+ databaseId = '',
191
+ duration
192
+ }) {
193
+ if (!this.httpMetricsEnabled) return;
194
+ this.countersFunctions?.app_requests_total({
195
+ method,
196
+ route,
197
+ status_code,
198
+ appId,
199
+ databaseId
200
+ });
201
+ this.countersFunctions?.app_requests_total_duration({
202
+ method,
203
+ route,
204
+ status_code,
205
+ appId,
206
+ databaseId
207
+ }, duration);
208
+ }
209
+
210
+ /**
211
+ * Express middleware to track HTTP requests.
212
+ * Track the `app_requests_total` and `app_requests_total_duration` metric.
213
+ */
214
+ trackHttpRequestMiddleware = (req, res, next) => {
215
+ if (!this.enabled || !this.httpMetricsEnabled || req.method === 'OPTIONS') {
216
+ next();
217
+ return;
218
+ }
219
+ const start = Date.now();
220
+ res.on('finish', () => {
221
+ const route = req.route?.path || req.path || 'unknown';
222
+ const appId = req.params?.appId || req.body?.appId || req.query?.appId || '';
223
+ const databaseId = req.params?.databaseId || req.body?.databaseId || req.query?.databaseId || req.params?.datasourceId || req.body?.datasourceId || req.query?.datasourceId || '';
224
+ this.trackHttpRequest({
225
+ method: req.method,
226
+ route,
227
+ status_code: res.statusCode,
228
+ appId,
229
+ databaseId,
230
+ duration: Date.now() - start
231
+ });
232
+ });
233
+ next();
234
+ };
235
+ }
236
+ module.exports = {
237
+ MetricsClient
238
+ };
239
+ //# sourceMappingURL=metricsClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metricsClient.js","names":["fs","require","os","BaseMetricsClient","MetricsClient","constructor","config","httpMetricsEnabled","process","env","METRICS_HTTP_ENABLED","_lastUsageMicros","_lastCheckTime","Date","now","_initDefaultMetrics","createGauge","name","help","updateFn","getCpuUsagePercent","getAvailableCPUs","getContainerMemoryUsage","measureLag","getContainerMemoryLimit","uptime","createCounter","labelNames","withDefaultLabels","stat","readFileSync","match","currentUsage","parseInt","deltaUsage","deltaTime","cpuMaxPath","existsSync","quotaStr","periodStr","trim","split","cpus","length","memoryUsage","rss","path","val","parsed","totalmem","Promise","resolve","start","setImmediate","trackHttpRequest","method","route","status_code","appId","databaseId","duration","countersFunctions","app_requests_total","app_requests_total_duration","trackHttpRequestMiddleware","req","res","next","enabled","on","params","body","query","datasourceId","statusCode","module","exports"],"sources":["../../src/metrics/metricsClient.js"],"sourcesContent":["const fs = require('fs')\nconst os = require('os')\nconst { BaseMetricsClient } = require('./baseMetricsClient')\n\n/**\n * MetricsClient handles Prometheus metrics collection and push.\n * Supports gauges, counters, default metrics, and custom metrics.\n * Extends BaseMetricsClient for common functionality.\n */\nclass MetricsClient extends BaseMetricsClient {\n /**\n * @param {Object} config\n * @param {string} [config.appName] Name of the application\n * @param {string} [config.dynoId] Dyno/instance ID\n * @param {string} [config.processType] Process type (web, worker, etc.)\n * @param {boolean} [config.enabled] Enable metrics collection\n * @param {boolean} [config.httpMetricsEnabled=false] Enable HTTP request metrics (app_requests_total, app_requests_total_duration)\n * @param {boolean} [config.logValues] Log metrics values to console\n * @param {string} [config.pushgatewayUrl] PushGateway URL\n * @param {string} [config.pushgatewaySecret] PushGateway secret token\n * @param {number} [config.intervalSec] Interval in seconds for pushing metrics\n * @param {boolean} [config.removeOldMetrics] Enable to clear metrics by service name\n * @param {function} [config.startupValidation] Add to validate on start push.\n * @param {boolean} [config.disablePushgateway] Disable pushing to Pushgateway (use HTTP scraping instead)\n */\n constructor(config = {}) {\n super(config)\n\n this.httpMetricsEnabled =\n config.httpMetricsEnabled ??\n process.env.METRICS_HTTP_ENABLED === 'true' ??\n false\n\n this._lastUsageMicros = 0\n this._lastCheckTime = Date.now()\n\n this._initDefaultMetrics()\n }\n\n /**\n * Register all built-in default Gauges and Counters.\n * @private\n */\n _initDefaultMetrics = () => {\n this.createGauge({\n name: 'app_process_cpu_usage_percent',\n help: 'Current CPU usage of the Node.js process in percent',\n updateFn: this.getCpuUsagePercent,\n })\n\n this.createGauge({\n name: 'app_available_cpu_count',\n help: 'How many CPU cores are available to this process',\n updateFn: this.getAvailableCPUs,\n })\n\n this.createGauge({\n name: 'app_container_memory_usage_bytes',\n help: 'Current container RAM usage from cgroup',\n updateFn: this.getContainerMemoryUsage,\n })\n\n this.createGauge({\n name: 'app_event_loop_lag_ms',\n help: 'Estimated event loop lag in milliseconds',\n updateFn: this.measureLag,\n })\n\n this.createGauge({\n name: 'app_container_memory_limit_bytes',\n help: 'Max RAM available to container from cgroup (memory.max)',\n updateFn: this.getContainerMemoryLimit,\n })\n\n this.createGauge({\n name: 'app_uptime_seconds',\n help: 'How long the process has been running',\n updateFn: process.uptime,\n })\n\n if (this.httpMetricsEnabled) {\n this.createCounter({\n name: 'app_requests_total',\n help: 'Total number of HTTP requests',\n labelNames: this.withDefaultLabels([\n 'method',\n 'route',\n 'appId',\n 'databaseId',\n 'status_code',\n ]),\n })\n\n this.createCounter({\n name: 'app_requests_total_duration',\n help: 'Total duration of HTTP requests in milliseconds',\n labelNames: this.withDefaultLabels([\n 'method',\n 'route',\n 'appId',\n 'databaseId',\n 'status_code',\n ]),\n })\n }\n }\n\n /**\n * Get CPU usage percent (cgroup-aware)\n * @returns {number}\n */\n getCpuUsagePercent = () => {\n try {\n const stat = fs.readFileSync('/sys/fs/cgroup/cpu.stat', 'utf-8')\n const match = stat.match(/usage_usec (\\d+)/)\n if (!match) return 0\n\n const now = Date.now()\n const currentUsage = parseInt(match[1], 10)\n\n if (this._lastUsageMicros === 0) {\n this._lastUsageMicros = currentUsage\n this._lastCheckTime = now\n return 0\n }\n\n const deltaUsage = currentUsage - this._lastUsageMicros\n const deltaTime = now - this._lastCheckTime\n\n this._lastUsageMicros = currentUsage\n this._lastCheckTime = now\n\n return (deltaUsage / (deltaTime * 1000)) * 100\n } catch {\n return 0\n }\n }\n\n /**\n * Get available CPU cores.\n * @returns {number}\n */\n getAvailableCPUs() {\n try {\n const cpuMaxPath = '/sys/fs/cgroup/cpu.max'\n if (fs.existsSync(cpuMaxPath)) {\n const [quotaStr, periodStr] = fs\n .readFileSync(cpuMaxPath, 'utf8')\n .trim()\n .split(' ')\n if (quotaStr === 'max') return os.cpus().length\n return parseInt(quotaStr, 10) / parseInt(periodStr, 10)\n }\n return os.cpus().length\n } catch {\n return 1\n }\n }\n\n /**\n * Get container memory usage in bytes.\n * @returns {number}\n */\n getContainerMemoryUsage() {\n try {\n return parseInt(\n fs.readFileSync('/sys/fs/cgroup/memory.current', 'utf-8').trim(),\n 10\n )\n } catch {\n return process.memoryUsage().rss\n }\n }\n\n /**\n * Get container memory limit in bytes.\n * @returns {number}\n */\n getContainerMemoryLimit() {\n try {\n const path = '/sys/fs/cgroup/memory.max'\n if (fs.existsSync(path)) {\n const val = fs.readFileSync(path, 'utf-8').trim()\n if (val !== 'max') {\n const parsed = parseInt(val, 10)\n if (parsed && parsed < os.totalmem()) return parsed\n }\n }\n return os.totalmem()\n } catch {\n return os.totalmem()\n }\n }\n\n /**\n * Measure event loop lag in ms.\n * @returns {Promise<number>}\n */\n measureLag() {\n return new Promise(resolve => {\n const start = Date.now()\n setImmediate(() => resolve(Date.now() - start))\n })\n }\n\n /**\n * Increment the HTTP requests counter with detailed request information.\n *\n * @param {Object} params - The parameters for the request counter.\n * @param {string} params.method - HTTP method (GET, POST, etc.).\n * @param {string} params.route - The full requested URL or route.\n * @param {number} params.status_code - HTTP response status code.\n * @param {string} [params.appId=''] - Optional application identifier.\n * @param {string} [params.databaseId=''] - Optional database identifier.\n * @param {number} params.duration - Request duration in milliseconds.\n */\n trackHttpRequest({\n method,\n route,\n status_code,\n appId = '',\n databaseId = '',\n duration,\n }) {\n if (!this.httpMetricsEnabled) return\n\n this.countersFunctions?.app_requests_total({\n method,\n route,\n status_code,\n appId,\n databaseId,\n })\n this.countersFunctions?.app_requests_total_duration(\n {\n method,\n route,\n status_code,\n appId,\n databaseId,\n },\n duration\n )\n }\n\n /**\n * Express middleware to track HTTP requests.\n * Track the `app_requests_total` and `app_requests_total_duration` metric.\n */\n trackHttpRequestMiddleware = (req, res, next) => {\n if (!this.enabled || !this.httpMetricsEnabled || req.method === 'OPTIONS') {\n next()\n return\n }\n\n const start = Date.now()\n res.on('finish', () => {\n const route = req.route?.path || req.path || 'unknown'\n const appId =\n req.params?.appId || req.body?.appId || req.query?.appId || ''\n const databaseId =\n req.params?.databaseId ||\n req.body?.databaseId ||\n req.query?.databaseId ||\n req.params?.datasourceId ||\n req.body?.datasourceId ||\n req.query?.datasourceId ||\n ''\n\n this.trackHttpRequest({\n method: req.method,\n route,\n status_code: res.statusCode,\n appId,\n databaseId,\n duration: Date.now() - start,\n })\n })\n\n next()\n }\n}\n\nmodule.exports = { MetricsClient }\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * DatabaseMetricsClient collects Postgres connection metrics
3
+ * and pushes them to Prometheus Pushgateway.
4
+ *
5
+ * @extends BaseMetricsClient
6
+ */
7
+ export class DatabaseMetricsClient extends BaseMetricsClient {
8
+ /**
9
+ * @param {Object} options
10
+ * @param {string} options.databaseUrl - Required main database URL
11
+ * @param {string} options.databaseName - Main database name for metrics
12
+ * @param {Object<string, string>} [options.additional_database_urls] - Optional additional DBs, keyed by custom name with URL as value
13
+ * @param {string} [options.appName] - Application name (from BaseMetricsClient)
14
+ * @param {string} [options.dynoId] - Dyno/instance ID (from BaseMetricsClient)
15
+ * @param {string} [options.processType] - Process type (from BaseMetricsClient)
16
+ * @param {boolean} [options.enabled] - Enable metrics collection (from BaseMetricsClient)
17
+ * @param {boolean} [options.logValues] - Log metrics values (from BaseMetricsClient)
18
+ * @param {string} [options.pushgatewayUrl] - PushGateway URL (from BaseMetricsClient)
19
+ * @param {string} [options.pushgatewaySecret] - PushGateway secret token (from BaseMetricsClient)
20
+ * @param {number} [options.intervalSec] - Interval in seconds for pushing metrics
21
+ * @param {boolean} [options.removeOldMetrics] - Remove old metrics by service
22
+ * @param {function} [options.startupValidation] - Function to validate startup
23
+ * @param {boolean} [options.disablePushgateway] - Disable pushing to Pushgateway (use HTTP scraping instead)
24
+ */
25
+ constructor({ databaseUrl, databaseName, additional_database_urls, ...metricsConfig }?: {
26
+ databaseUrl: string;
27
+ databaseName: string;
28
+ additional_database_urls?: {
29
+ [x: string]: string;
30
+ } | undefined;
31
+ appName?: string | undefined;
32
+ dynoId?: string | undefined;
33
+ processType?: string | undefined;
34
+ enabled?: boolean | undefined;
35
+ logValues?: boolean | undefined;
36
+ pushgatewayUrl?: string | undefined;
37
+ pushgatewaySecret?: string | undefined;
38
+ intervalSec?: number | undefined;
39
+ removeOldMetrics?: boolean | undefined;
40
+ startupValidation?: Function | undefined;
41
+ disablePushgateway?: boolean | undefined;
42
+ });
43
+ databasePools: any[];
44
+ /** Gauge for Database connections */
45
+ databaseConnectionsGauge: import("prom-client").Gauge<string>;
46
+ _addPool: (url: any, dbKey: any) => Pool;
47
+ /**
48
+ * @param {Pool} pool - PG connection pool
49
+ * @returns {Promise<{ current: number, max: number, dbName: string }>}
50
+ */
51
+ getDBConnectionsAndName: (pool: Pool) => Promise<{
52
+ current: number;
53
+ max: number;
54
+ dbName: string;
55
+ }>;
56
+ /**
57
+ * Collect database connection metrics for all configured pools
58
+ * @returns {Promise<void>}
59
+ */
60
+ collectDatabaseMetrics: () => Promise<void>;
61
+ /**
62
+ * Push database metrics to Prometheus Pushgateway
63
+ * @returns {Promise<void>}
64
+ */
65
+ pushDatabaseMetrics: () => Promise<void>;
66
+ /**
67
+ * Start periodic collection.
68
+ * @param {number} [intervalSec=this.intervalSec] - Interval in seconds
69
+ */
70
+ startPush: (intervalSec?: number | undefined) => void;
71
+ }
72
+ import { BaseMetricsClient } from "./baseMetricsClient";
73
+ import { Pool } from "pg";
74
+ //# sourceMappingURL=metricsDatabaseClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metricsDatabaseClient.d.ts","sourceRoot":"","sources":["../../src/metrics/metricsDatabaseClient.js"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH;IACE;;;;;;;;;;;;;;;;OAgBG;IACH;QAf2B,WAAW,EAA3B,MAAM;QACU,YAAY,EAA5B,MAAM;QAC2B,wBAAwB;;;QACxC,OAAO;QACP,MAAM;QACN,WAAW;QACV,OAAO;QACP,SAAS;QACV,cAAc;QACd,iBAAiB;QACjB,WAAW;QACV,gBAAgB;QACf,iBAAiB;QAClB,kBAAkB;OAkF9C;IAtBC,qBAAuB;IAUvB,qCAAqC;IACrC,8DAQE;IAKJ,yCAKC;IAED;;;OAGG;IACH,gCAHW,IAAI,KACF,QAAQ;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CA4BrE;IAED;;;OAGG;IACH,8BAFa,QAAQ,IAAI,CAAC,CAsBzB;IAED;;;OAGG;IACH,2BAFa,QAAQ,IAAI,CAAC,CAqBzB;IAED;;;OAGG;IACH,sDAMC;CAwBF"}
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+
3
+ const {
4
+ Pool
5
+ } = require('pg');
6
+ const {
7
+ BaseMetricsClient
8
+ } = require('./baseMetricsClient');
9
+
10
+ /**
11
+ * DatabaseMetricsClient collects Postgres connection metrics
12
+ * and pushes them to Prometheus Pushgateway.
13
+ *
14
+ * @extends BaseMetricsClient
15
+ */
16
+ class DatabaseMetricsClient extends BaseMetricsClient {
17
+ /**
18
+ * @param {Object} options
19
+ * @param {string} options.databaseUrl - Required main database URL
20
+ * @param {string} options.databaseName - Main database name for metrics
21
+ * @param {Object<string, string>} [options.additional_database_urls] - Optional additional DBs, keyed by custom name with URL as value
22
+ * @param {string} [options.appName] - Application name (from BaseMetricsClient)
23
+ * @param {string} [options.dynoId] - Dyno/instance ID (from BaseMetricsClient)
24
+ * @param {string} [options.processType] - Process type (from BaseMetricsClient)
25
+ * @param {boolean} [options.enabled] - Enable metrics collection (from BaseMetricsClient)
26
+ * @param {boolean} [options.logValues] - Log metrics values (from BaseMetricsClient)
27
+ * @param {string} [options.pushgatewayUrl] - PushGateway URL (from BaseMetricsClient)
28
+ * @param {string} [options.pushgatewaySecret] - PushGateway secret token (from BaseMetricsClient)
29
+ * @param {number} [options.intervalSec] - Interval in seconds for pushing metrics
30
+ * @param {boolean} [options.removeOldMetrics] - Remove old metrics by service
31
+ * @param {function} [options.startupValidation] - Function to validate startup
32
+ * @param {boolean} [options.disablePushgateway] - Disable pushing to Pushgateway (use HTTP scraping instead)
33
+ */
34
+ constructor({
35
+ databaseUrl,
36
+ databaseName,
37
+ additional_database_urls = {},
38
+ ...metricsConfig
39
+ } = {}) {
40
+ const intervalSec = metricsConfig.intervalSec || parseInt(process.env.METRICS_DATABASE_INTERVAL_SEC || '', 10) || 60;
41
+ const tmpAppName = metricsConfig.appName || process.env.BUILD_APP_NAME || 'unknown-app';
42
+ const mainDbName = databaseName || `${tmpAppName}_db`;
43
+ const startupValidation = async () => {
44
+ if (!databaseUrl) {
45
+ console.error(`[database-metrics] ❌ METRICS_DATABASE_URL is required`);
46
+ return false;
47
+ }
48
+ try {
49
+ const mainPool = new Pool({
50
+ connectionString: databaseUrl
51
+ });
52
+ await mainPool.query('SELECT 1');
53
+ await mainPool.end();
54
+ console.info(`[database-metrics] ✓ Main database OK: ${mainDbName}`);
55
+ } catch (err) {
56
+ console.error(`[database-metrics] ❌ Cannot connect to main database: ${err.message}`);
57
+ return false;
58
+ }
59
+ for (const [dbKey, url] of Object.entries(additional_database_urls)) {
60
+ try {
61
+ const p = new Pool({
62
+ connectionString: url
63
+ });
64
+ await p.query('SELECT 1');
65
+ await p.end();
66
+ console.info(`[database-metrics] ✓ Additional database OK: ${dbKey}`);
67
+ } catch (err) {
68
+ console.error(`[database-metrics] ⚠ Skipping additional database: ${dbKey}`);
69
+ console.error(`[database-metrics] ${err.message}`);
70
+ }
71
+ }
72
+ console.info(`[database-metrics] Database metrics collection starting`);
73
+ return true;
74
+ };
75
+ super({
76
+ ...metricsConfig,
77
+ processType: metricsConfig.processType || 'database-metrics',
78
+ intervalSec,
79
+ startupValidation
80
+ });
81
+ this.databasePools = [];
82
+ if (databaseUrl) {
83
+ this._addPool(databaseUrl, mainDbName);
84
+ }
85
+ for (const [dbKey, url] of Object.entries(additional_database_urls)) {
86
+ this._addPool(url, dbKey);
87
+ }
88
+
89
+ /** Gauge for Database connections */
90
+ this.databaseConnectionsGauge = this.createGauge({
91
+ name: 'app_database_connections',
92
+ help: 'Postgres database connections',
93
+ labelNames: this.withDefaultLabels(['max_connections', 'database_name', 'db_key'])
94
+ });
95
+ this._setCleanupHandlers();
96
+ }
97
+ _addPool = (url, dbKey) => {
98
+ const pool = new Pool({
99
+ connectionString: url
100
+ });
101
+ pool.dbKey = dbKey;
102
+ this.databasePools.push(pool);
103
+ return pool;
104
+ };
105
+
106
+ /**
107
+ * @param {Pool} pool - PG connection pool
108
+ * @returns {Promise<{ current: number, max: number, dbName: string }>}
109
+ */
110
+ getDBConnectionsAndName = async pool => {
111
+ try {
112
+ const currentRes = await pool.query('SELECT COUNT(*) AS current FROM pg_stat_activity WHERE datname = current_database()');
113
+ const current = parseInt(currentRes.rows[0]?.current || 0, 10);
114
+ const maxRes = await pool.query("SELECT current_setting('max_connections') AS max");
115
+ const max = parseInt(maxRes.rows[0]?.max || 0, 10);
116
+ let dbName = pool.options?.database;
117
+ if (!dbName && pool.options?.connectionString) {
118
+ try {
119
+ const url = new URL(pool.options.connectionString);
120
+ dbName = url.pathname.replace(/^\//, '');
121
+ } catch {
122
+ dbName = pool.options.connectionString;
123
+ }
124
+ }
125
+ return {
126
+ current,
127
+ max,
128
+ dbName
129
+ };
130
+ } catch (err) {
131
+ return {
132
+ current: 0,
133
+ max: 0,
134
+ dbName: pool.options?.database || ''
135
+ };
136
+ }
137
+ };
138
+
139
+ /**
140
+ * Collect database connection metrics for all configured pools
141
+ * @returns {Promise<void>}
142
+ */
143
+ collectDatabaseMetrics = async () => {
144
+ for (const pool of this.databasePools) {
145
+ try {
146
+ const {
147
+ current,
148
+ max,
149
+ dbName
150
+ } = await this.getDBConnectionsAndName(pool);
151
+ this.databaseConnectionsGauge.set({
152
+ ...this.getDefaultLabels(),
153
+ database_name: pool.dbKey,
154
+ max_connections: String(max),
155
+ db_key: dbName
156
+ }, current);
157
+ } catch (err) {
158
+ console.warn(`[database-metrics] Failed to collect: ${err.message}`);
159
+ }
160
+ }
161
+ };
162
+
163
+ /**
164
+ * Push database metrics to Prometheus Pushgateway
165
+ * @returns {Promise<void>}
166
+ */
167
+ pushDatabaseMetrics = async () => {
168
+ try {
169
+ await this.collectDatabaseMetrics();
170
+ await this.gatewayPush();
171
+ this.clearAllCounters();
172
+ if (this.metricsLogValues) {
173
+ const metricObjects = await this.registry.getMetricsAsJSON();
174
+ console.info(`[database-metrics] Collected DB metrics`, JSON.stringify(metricObjects, null, 2));
175
+ }
176
+ } catch (error) {
177
+ console.error(`[database-metrics] Failed to collect DB metrics: ${error.message}`);
178
+ throw error;
179
+ }
180
+ };
181
+
182
+ /**
183
+ * Start periodic collection.
184
+ * @param {number} [intervalSec=this.intervalSec] - Interval in seconds
185
+ */
186
+ startPush = (intervalSec = this.intervalSec) => {
187
+ this._startPush(intervalSec, () => {
188
+ this.pushDatabaseMetrics().catch(err => {
189
+ console.error(`[database-metrics] Failed to push DB metrics:`, err);
190
+ });
191
+ });
192
+ };
193
+
194
+ /**
195
+ * Cleanup database pools and exit process
196
+ * @returns {Promise<void>}
197
+ */
198
+ cleanup = async () => {
199
+ try {
200
+ if (this.databasePools) {
201
+ for (const pool of this.databasePools) {
202
+ await pool.end();
203
+ }
204
+ }
205
+ } catch (err) {
206
+ console.error('[database-metrics] Error during cleanup:', err);
207
+ }
208
+ process.exit(0);
209
+ };
210
+ _setCleanupHandlers = () => {
211
+ process.on('SIGINT', this.cleanup);
212
+ process.on('SIGTERM', this.cleanup);
213
+ };
214
+ }
215
+ module.exports = {
216
+ DatabaseMetricsClient
217
+ };
218
+ //# sourceMappingURL=metricsDatabaseClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metricsDatabaseClient.js","names":["Pool","require","BaseMetricsClient","DatabaseMetricsClient","constructor","databaseUrl","databaseName","additional_database_urls","metricsConfig","intervalSec","parseInt","process","env","METRICS_DATABASE_INTERVAL_SEC","tmpAppName","appName","BUILD_APP_NAME","mainDbName","startupValidation","console","error","mainPool","connectionString","query","end","info","err","message","dbKey","url","Object","entries","p","processType","databasePools","_addPool","databaseConnectionsGauge","createGauge","name","help","labelNames","withDefaultLabels","_setCleanupHandlers","pool","push","getDBConnectionsAndName","currentRes","current","rows","maxRes","max","dbName","options","database","URL","pathname","replace","collectDatabaseMetrics","set","getDefaultLabels","database_name","max_connections","String","db_key","warn","pushDatabaseMetrics","gatewayPush","clearAllCounters","metricsLogValues","metricObjects","registry","getMetricsAsJSON","JSON","stringify","startPush","_startPush","catch","cleanup","exit","on","module","exports"],"sources":["../../src/metrics/metricsDatabaseClient.js"],"sourcesContent":["const { Pool } = require('pg')\nconst { BaseMetricsClient } = require('./baseMetricsClient')\n\n/**\n * DatabaseMetricsClient collects Postgres connection metrics\n * and pushes them to Prometheus Pushgateway.\n *\n * @extends BaseMetricsClient\n */\nclass DatabaseMetricsClient extends BaseMetricsClient {\n /**\n * @param {Object} options\n * @param {string} options.databaseUrl - Required main database URL\n * @param {string} options.databaseName - Main database name for metrics\n * @param {Object<string, string>} [options.additional_database_urls] - Optional additional DBs, keyed by custom name with URL as value\n * @param {string} [options.appName] - Application name (from BaseMetricsClient)\n * @param {string} [options.dynoId] - Dyno/instance ID (from BaseMetricsClient)\n * @param {string} [options.processType] - Process type (from BaseMetricsClient)\n * @param {boolean} [options.enabled] - Enable metrics collection (from BaseMetricsClient)\n * @param {boolean} [options.logValues] - Log metrics values (from BaseMetricsClient)\n * @param {string} [options.pushgatewayUrl] - PushGateway URL (from BaseMetricsClient)\n * @param {string} [options.pushgatewaySecret] - PushGateway secret token (from BaseMetricsClient)\n * @param {number} [options.intervalSec] - Interval in seconds for pushing metrics\n * @param {boolean} [options.removeOldMetrics] - Remove old metrics by service\n * @param {function} [options.startupValidation] - Function to validate startup\n * @param {boolean} [options.disablePushgateway] - Disable pushing to Pushgateway (use HTTP scraping instead)\n */\n constructor({\n databaseUrl,\n databaseName,\n additional_database_urls = {},\n ...metricsConfig\n } = {}) {\n const intervalSec =\n metricsConfig.intervalSec ||\n parseInt(process.env.METRICS_DATABASE_INTERVAL_SEC || '', 10) ||\n 60\n\n const tmpAppName =\n metricsConfig.appName || process.env.BUILD_APP_NAME || 'unknown-app'\n const mainDbName = databaseName || `${tmpAppName}_db`\n\n const startupValidation = async () => {\n if (!databaseUrl) {\n console.error(`[database-metrics] ❌ METRICS_DATABASE_URL is required`)\n return false\n }\n\n try {\n const mainPool = new Pool({ connectionString: databaseUrl })\n await mainPool.query('SELECT 1')\n await mainPool.end()\n console.info(`[database-metrics] ✓ Main database OK: ${mainDbName}`)\n } catch (err) {\n console.error(\n `[database-metrics] ❌ Cannot connect to main database: ${err.message}`\n )\n return false\n }\n\n for (const [dbKey, url] of Object.entries(additional_database_urls)) {\n try {\n const p = new Pool({ connectionString: url })\n await p.query('SELECT 1')\n await p.end()\n console.info(`[database-metrics] ✓ Additional database OK: ${dbKey}`)\n } catch (err) {\n console.error(\n `[database-metrics] ⚠ Skipping additional database: ${dbKey}`\n )\n console.error(`[database-metrics] ${err.message}`)\n }\n }\n\n console.info(`[database-metrics] Database metrics collection starting`)\n return true\n }\n\n super({\n ...metricsConfig,\n processType: metricsConfig.processType || 'database-metrics',\n intervalSec,\n startupValidation,\n })\n\n this.databasePools = []\n\n if (databaseUrl) {\n this._addPool(databaseUrl, mainDbName)\n }\n\n for (const [dbKey, url] of Object.entries(additional_database_urls)) {\n this._addPool(url, dbKey)\n }\n\n /** Gauge for Database connections */\n this.databaseConnectionsGauge = this.createGauge({\n name: 'app_database_connections',\n help: 'Postgres database connections',\n labelNames: this.withDefaultLabels([\n 'max_connections',\n 'database_name',\n 'db_key',\n ]),\n })\n\n this._setCleanupHandlers()\n }\n\n _addPool = (url, dbKey) => {\n const pool = new Pool({ connectionString: url })\n pool.dbKey = dbKey\n this.databasePools.push(pool)\n return pool\n }\n\n /**\n * @param {Pool} pool - PG connection pool\n * @returns {Promise<{ current: number, max: number, dbName: string }>}\n */\n getDBConnectionsAndName = async pool => {\n try {\n const currentRes = await pool.query(\n 'SELECT COUNT(*) AS current FROM pg_stat_activity WHERE datname = current_database()'\n )\n const current = parseInt(currentRes.rows[0]?.current || 0, 10)\n\n const maxRes = await pool.query(\n \"SELECT current_setting('max_connections') AS max\"\n )\n const max = parseInt(maxRes.rows[0]?.max || 0, 10)\n\n let dbName = pool.options?.database\n if (!dbName && pool.options?.connectionString) {\n try {\n const url = new URL(pool.options.connectionString)\n dbName = url.pathname.replace(/^\\//, '')\n } catch {\n dbName = pool.options.connectionString\n }\n }\n\n return { current, max, dbName }\n } catch (err) {\n return { current: 0, max: 0, dbName: pool.options?.database || '' }\n }\n }\n\n /**\n * Collect database connection metrics for all configured pools\n * @returns {Promise<void>}\n */\n collectDatabaseMetrics = async () => {\n for (const pool of this.databasePools) {\n try {\n const { current, max, dbName } = await this.getDBConnectionsAndName(\n pool\n )\n\n this.databaseConnectionsGauge.set(\n {\n ...this.getDefaultLabels(),\n database_name: pool.dbKey,\n max_connections: String(max),\n db_key: dbName,\n },\n current\n )\n } catch (err) {\n console.warn(`[database-metrics] Failed to collect: ${err.message}`)\n }\n }\n }\n\n /**\n * Push database metrics to Prometheus Pushgateway\n * @returns {Promise<void>}\n */\n pushDatabaseMetrics = async () => {\n try {\n await this.collectDatabaseMetrics()\n await this.gatewayPush()\n this.clearAllCounters()\n\n if (this.metricsLogValues) {\n const metricObjects = await this.registry.getMetricsAsJSON()\n console.info(\n `[database-metrics] Collected DB metrics`,\n JSON.stringify(metricObjects, null, 2)\n )\n }\n } catch (error) {\n console.error(\n `[database-metrics] Failed to collect DB metrics: ${error.message}`\n )\n throw error\n }\n }\n\n /**\n * Start periodic collection.\n * @param {number} [intervalSec=this.intervalSec] - Interval in seconds\n */\n startPush = (intervalSec = this.intervalSec) => {\n this._startPush(intervalSec, () => {\n this.pushDatabaseMetrics().catch(err => {\n console.error(`[database-metrics] Failed to push DB metrics:`, err)\n })\n })\n }\n\n /**\n * Cleanup database pools and exit process\n * @returns {Promise<void>}\n */\n cleanup = async () => {\n try {\n if (this.databasePools) {\n for (const pool of this.databasePools) {\n await pool.end()\n }\n }\n } catch (err) {\n console.error('[database-metrics] Error during cleanup:', err)\n }\n\n process.exit(0)\n }\n\n _setCleanupHandlers = () => {\n process.on('SIGINT', this.cleanup)\n process.on('SIGTERM', this.cleanup)\n }\n}\n\nmodule.exports = { DatabaseMetricsClient }\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * QueueRedisMetricsClient extends RedisMetricsClient to collect
3
+ * Redis and Bee Queue metrics periodically and push them to Prometheus Pushgateway.
4
+ *
5
+ * @extends RedisMetricsClient
6
+ */
7
+ export class QueueRedisMetricsClient extends RedisMetricsClient {
8
+ getConfiguredQueueNames: () => string[];
9
+ startupValidation: () => boolean;
10
+ /** Cache for queue objects to avoid multiple connections */
11
+ queueCache: Map<any, any>;
12
+ /** Gauge for queue jobs by status */
13
+ queueJobsGauge: import("prom-client").Gauge<string>;
14
+ /**
15
+ * Execute a Redis command in a client-type safe way.
16
+ *
17
+ * @param {string[]} args Command args array, e.g. ['LLEN', 'key']
18
+ * @returns {Promise<any>}
19
+ */
20
+ _send: (args: string[]) => Promise<any>;
21
+ _toNumber: (v: any) => number;
22
+ _getQueueType: (queueName: any) => Promise<"bull" | "bee">;
23
+ _getBullHealth: (queueName: any) => Promise<{
24
+ waiting: number;
25
+ active: number;
26
+ succeeded: number;
27
+ failed: number;
28
+ delayed: number;
29
+ }>;
30
+ _getBeeQueueHealth: (queueName: any) => Promise<{
31
+ waiting: number;
32
+ active: number;
33
+ succeeded: number;
34
+ failed: number;
35
+ delayed: number;
36
+ }>;
37
+ /**
38
+ * Collect metrics for a single queue and set gauges.
39
+ *
40
+ * Supports:
41
+ * - Bull (default): keys like `bull:<queue>:wait`, `...:active`, `...:completed`, `...:failed`, `...:delayed`
42
+ * - Bee-Queue: keys like `bq:<queue>:waiting`, `...:active`, `...:succeeded`, `...:failed`, `...:delayed`
43
+ *
44
+ * Auto-detects queue type unless `METRICS_QUEUE_TYPE` is set to `bull` or `bee`.
45
+ *
46
+ * @param {string} queueName - Name of the queue
47
+ * @returns {Promise<void>}
48
+ */
49
+ collectSingleQueueMetrics: (queueName: string) => Promise<void>;
50
+ /**
51
+ * Collect metrics for all queues and Redis, then push to Pushgateway
52
+ * @returns {Promise<void>}
53
+ */
54
+ collectQueueMetrics: () => Promise<void>;
55
+ }
56
+ import { RedisMetricsClient } from "./metricsRedisClient";
57
+ //# sourceMappingURL=metricsQueueRedisClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metricsQueueRedisClient.d.ts","sourceRoot":"","sources":["../../src/metrics/metricsQueueRedisClient.js"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH;IAgEI,wCAAsD;IACtD,iCAA0C;IAE1C,4DAA4D;IAa5D,0BAA2B;IAE3B,qCAAqC;IACrC,oDAIE;IAKJ;;;;;OAKG;IACH,cAHW,MAAM,EAAE,KACN,QAAQ,GAAG,CAAC,CA6BxB;IAED,8BAGC;IAED,2DA6BC;IAED;;;;;;OAmBC;IAED;;;;;;OAwBC;IAED;;;;;;;;;;;OAWG;IACH,uCAHW,MAAM,KACJ,QAAQ,IAAI,CAAC,CAyCzB;IAED;;;OAGG;IACH,2BAFa,QAAQ,IAAI,CAAC,CAuCzB;CAoCF"}