@maestro-js/metrics 1.0.0-alpha.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/README.md ADDED
@@ -0,0 +1,231 @@
1
+ # Metrics
2
+
3
+ ## Overview
4
+
5
+ The Metrics package provides unified application metrics collection that routes through the Log transport system. Emit counters, distributions, sets, and gauges as structured log messages, then attach transports to forward metrics to external systems like Sentry.
6
+
7
+ ## Quick Setup
8
+
9
+ ```ts
10
+ import { Metrics } from '@maestro-js/metrics'
11
+ import { Log } from '@maestro-js/log'
12
+ import * as Sentry from '@sentry/node'
13
+
14
+ // 1. Create a typed logger for metrics messages
15
+ const metricsLogger = Log.create<[Metrics.LogMessage]>()
16
+
17
+ // 2. Register the metrics service
18
+ Metrics.Provider.register('default', Metrics.Provider.create({ logger: metricsLogger }))
19
+
20
+ // 3. Attach the Sentry transport (or any custom transport)
21
+ metricsLogger.addTransport(Metrics.transports.sentryMetrics(Sentry.metrics))
22
+
23
+ // 4. Emit metrics
24
+ Metrics.increment('order_placed', 1, { tags: { region: 'us-east' } })
25
+ ```
26
+
27
+ ## API Reference
28
+
29
+ ### Provider
30
+
31
+ #### `Metrics.Provider.create(config)`
32
+
33
+ Create a metrics service instance.
34
+
35
+ ```ts
36
+ Metrics.Provider.create({
37
+ logger: Log.Logger<[Metrics.LogMessage]>
38
+ })
39
+ ```
40
+
41
+ The `logger` parameter is a typed `Log.Logger` that accepts `Metrics.LogMessage` entries. All metric emissions flow through this logger, so output destination is controlled entirely by the transports attached to it.
42
+
43
+ #### `Metrics.Provider.register(name, service)`
44
+
45
+ Register a metrics service instance in the named registry.
46
+
47
+ ```ts
48
+ Metrics.Provider.register('default', Metrics.Provider.create({ logger: metricsLogger }))
49
+ ```
50
+
51
+ ### Metric Methods
52
+
53
+ All four methods are available directly on the `Metrics` facade (resolving the `'default'` provider) or on a named provider instance.
54
+
55
+ #### `Metrics.increment(name, value?, data?)`
56
+
57
+ Emit a counter metric, incrementing by `value` (defaults to 1 when omitted).
58
+
59
+ ```ts
60
+ Metrics.increment('user_signup') // increment by 1
61
+ Metrics.increment('items_purchased', 3, { tags: { store: 'web' } }) // increment by 3
62
+ ```
63
+
64
+ #### `Metrics.distribution(name, value, data?)`
65
+
66
+ Emit a distribution metric for statistical aggregation (p50, p95, p99).
67
+
68
+ ```ts
69
+ Metrics.distribution('api_response_time', 142, { unit: 'millisecond' })
70
+ Metrics.distribution('payload_size', 2048, { unit: 'byte', tags: { endpoint: '/upload' } })
71
+ ```
72
+
73
+ #### `Metrics.set(name, value, data?)`
74
+
75
+ Emit a set metric tracking unique string or integer values.
76
+
77
+ ```ts
78
+ Metrics.set('unique_customers', 'cust_123')
79
+ Metrics.set('unique_customers', 'cust_456', { tags: { channel: 'mobile' } })
80
+ ```
81
+
82
+ #### `Metrics.gauge(name, value, data?)`
83
+
84
+ Emit a gauge metric capturing a point-in-time value that can increase or decrease.
85
+
86
+ ```ts
87
+ Metrics.gauge('active_connections', 47)
88
+ Metrics.gauge('queue_depth', 312, { tags: { queue: 'email' } })
89
+ ```
90
+
91
+ ### MetricsAdditionalData
92
+
93
+ Optional metadata passed as the last argument to any metric method.
94
+
95
+ ```ts
96
+ interface MetricsAdditionalData {
97
+ unit?: MeasurementUnit // 'millisecond', 'byte', 'percent', 'none', etc.
98
+ tags?: Record<string, Primitive> // arbitrary key-value pairs for grouping
99
+ timestamp?: number // custom Unix timestamp
100
+ }
101
+ ```
102
+
103
+ **Supported unit categories:**
104
+
105
+ - **Duration**: `'nanosecond'`, `'microsecond'`, `'millisecond'`, `'second'`, `'minute'`, `'hour'`, `'day'`, `'week'`
106
+ - **Information**: `'bit'`, `'byte'`, `'kilobyte'`, `'megabyte'`, `'gigabyte'`, `'terabyte'`, etc.
107
+ - **Fraction**: `'ratio'`, `'percent'`
108
+ - **None**: `''`, `'none'`
109
+
110
+ ### MetricsLogMessage
111
+
112
+ Discriminated union of the four metric operations, keyed by `op`. Each variant carries `name`, `value`, and `data` fields. Used to type the logger and transports.
113
+
114
+ ```ts
115
+ type MetricsLogMessage =
116
+ | { op: 'increment'; name: string; value: number | undefined; data: MetricsAdditionalData | undefined }
117
+ | { op: 'distribution'; name: string; value: number; data: MetricsAdditionalData | undefined }
118
+ | { op: 'set'; name: string; value: number | string; data: MetricsAdditionalData | undefined }
119
+ | { op: 'gauge'; name: string; value: number; data: MetricsAdditionalData | undefined }
120
+ ```
121
+
122
+ ## Transports
123
+
124
+ ### Built-in: Sentry Metrics
125
+
126
+ Route all metrics to Sentry via `Metrics.transports.sentryMetrics()`.
127
+
128
+ ```ts
129
+ import * as Sentry from '@sentry/node'
130
+
131
+ const metricsLogger = Log.create<[Metrics.LogMessage]>()
132
+ metricsLogger.addTransport(Metrics.transports.sentryMetrics(Sentry.metrics))
133
+ ```
134
+
135
+ The transport accepts any object matching the `Metrics.MetricFunctions` interface (the four metric methods), so it works with any Sentry-compatible metrics API.
136
+
137
+ ### Custom Transports
138
+
139
+ Write a custom transport by implementing `Log.Transport<[Metrics.LogMessage]>`. The `write` method receives a log message whose `data[0]` is the `MetricsLogMessage`.
140
+
141
+ ```ts
142
+ function datadogTransport(client: DatadogClient): Log.Transport<[Metrics.LogMessage]> {
143
+ return {
144
+ write(message) {
145
+ const metric = message.data[0]
146
+ if (metric.op === 'increment') {
147
+ client.increment(metric.name, metric.value ?? 1, metric.data?.tags)
148
+ } else if (metric.op === 'distribution') {
149
+ client.distribution(metric.name, metric.value, metric.data?.tags)
150
+ } else if (metric.op === 'gauge') {
151
+ client.gauge(metric.name, metric.value, metric.data?.tags)
152
+ }
153
+ // handle 'set' as needed
154
+ }
155
+ }
156
+ }
157
+
158
+ metricsLogger.addTransport(datadogTransport(myDatadogClient))
159
+ ```
160
+
161
+ ## Common Patterns
162
+
163
+ ### Tagging metrics by request context
164
+
165
+ ```ts
166
+ Metrics.increment('api_request', 1, {
167
+ tags: { method: 'POST', path: '/orders', status: 201 }
168
+ })
169
+
170
+ Metrics.distribution('db_query_time', 23, {
171
+ unit: 'millisecond',
172
+ tags: { table: 'orders', operation: 'insert' }
173
+ })
174
+ ```
175
+
176
+ ### Tracking unique visitors
177
+
178
+ ```ts
179
+ Metrics.set('unique_visitors', sessionId, {
180
+ tags: { page: '/checkout' }
181
+ })
182
+ ```
183
+
184
+ ### Monitoring resource usage
185
+
186
+ ```ts
187
+ Metrics.gauge('memory_usage_mb', process.memoryUsage().heapUsed / 1024 / 1024, {
188
+ unit: 'megabyte'
189
+ })
190
+
191
+ Metrics.gauge('active_websocket_connections', connectionPool.size)
192
+ ```
193
+
194
+ ### Multiple metric providers
195
+
196
+ Register named providers for different metric pipelines.
197
+
198
+ ```ts
199
+ const appLogger = Log.create<[Metrics.LogMessage]>()
200
+ const perfLogger = Log.create<[Metrics.LogMessage]>()
201
+
202
+ appLogger.addTransport(Metrics.transports.sentryMetrics(Sentry.metrics))
203
+ perfLogger.addTransport(datadogTransport(myDatadogClient))
204
+
205
+ Metrics.Provider.register('default', Metrics.Provider.create({ logger: appLogger }))
206
+ Metrics.Provider.register('perf', Metrics.Provider.create({ logger: perfLogger }))
207
+
208
+ // Facade calls use 'default'
209
+ Metrics.increment('order_placed')
210
+ ```
211
+
212
+ ## Cross-Package Integration
213
+
214
+ ### Depends on
215
+
216
+ - **@maestro-js/service-registry** -- Provider pattern, named registry, deferred Proxy resolution.
217
+ - **@maestro-js/log** -- Typed logger and transport system. Metrics emissions are structured log messages routed through Log transports.
218
+
219
+ ### Works with
220
+
221
+ - **@maestro-js/log** -- Metrics piggybacks on the Log transport architecture. Create a dedicated `Log.Logger<[Metrics.LogMessage]>` for metrics, then attach transports to route emissions to Sentry, Datadog, or any custom destination. The same logger can have multiple transports to fan out metrics to several backends simultaneously.
222
+
223
+ ## Package Info
224
+
225
+ - **Package name**: `@maestro-js/metrics`
226
+ - **Source**: `packages/metrics/src/index.ts`
227
+ - **Types**: `packages/metrics/src/metrics-types.ts`
228
+ - **Sentry transport**: `packages/metrics/src/sentry-metrics-transport.ts`
229
+ - **Tests**: None yet
230
+ - **Build**: `pnpm --filter @maestro-js/metrics build`
231
+ - **Typecheck**: `pnpm --filter @maestro-js/metrics typecheck`
@@ -0,0 +1,139 @@
1
+ import { Log } from '@maestro-js/log';
2
+
3
+ /**
4
+ * A time duration.
5
+ */
6
+ type DurationUnit = 'nanosecond' | 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week';
7
+ /**
8
+ * Size of information derived from bytes.
9
+ */
10
+ type InformationUnit = 'bit' | 'byte' | 'kilobyte' | 'kibibyte' | 'megabyte' | 'mebibyte' | 'gigabyte' | 'terabyte' | 'tebibyte' | 'petabyte' | 'exabyte' | 'exbibyte';
11
+ /**
12
+ * Fractions such as percentages.
13
+ */
14
+ type FractionUnit = 'ratio' | 'percent';
15
+ /**
16
+ * Untyped value without a unit.
17
+ */
18
+ type NoneUnit = '' | 'none';
19
+ type MeasurementUnit = LiteralUnion<DurationUnit | InformationUnit | FractionUnit | NoneUnit>;
20
+ type LiteralUnion<T extends string> = T | Omit<T, T>;
21
+ type Primitive = number | string | boolean | bigint | symbol | null | undefined;
22
+ interface MetricsAdditionalData {
23
+ unit?: MeasurementUnit;
24
+ tags?: Record<string, Primitive>;
25
+ timestamp?: number;
26
+ }
27
+ type MetricsLogMessage = {
28
+ op: 'increment';
29
+ name: string;
30
+ value: number | undefined;
31
+ data: MetricsAdditionalData | undefined;
32
+ } | {
33
+ op: 'distribution';
34
+ name: string;
35
+ value: number;
36
+ data: MetricsAdditionalData | undefined;
37
+ } | {
38
+ op: 'set';
39
+ name: string;
40
+ value: number | string;
41
+ data: MetricsAdditionalData | undefined;
42
+ } | {
43
+ op: 'gauge';
44
+ name: string;
45
+ value: number;
46
+ data: MetricsAdditionalData | undefined;
47
+ };
48
+ interface MetricFunctions {
49
+ increment(name: string, value?: number, data?: MetricsAdditionalData): void;
50
+ distribution(name: string, value: number, data?: MetricsAdditionalData): void;
51
+ set(name: string, value: number | string, data?: MetricsAdditionalData): void;
52
+ gauge(name: string, value: number, data?: MetricsAdditionalData): void;
53
+ }
54
+
55
+ declare function sentryMetricsTransport(sentryMetrics: MetricFunctions): Log.Transport<[MetricsLogMessage]>;
56
+
57
+ declare function create(config: Metrics.Provider.MetricsServiceConfig): {
58
+ increment: (name: string, value?: number, data?: MetricsAdditionalData) => void;
59
+ distribution: (name: string, value: number, data?: MetricsAdditionalData) => void;
60
+ set: (name: string, value: number | string, data?: MetricsAdditionalData) => void;
61
+ gauge: (name: string, value: number, data?: MetricsAdditionalData) => void;
62
+ };
63
+ /**
64
+ * Unified metrics collection piped through the Log transport system.
65
+ *
66
+ * Four metric types cover the full spectrum of application observability:
67
+ * counters ({@link increment}), distributions ({@link distribution}),
68
+ * sets ({@link set}), and gauges ({@link gauge}). All emissions flow through
69
+ * a typed `Log.Logger` as structured `MetricsLogMessage` entries, so output
70
+ * destination is controlled entirely by attaching Log transports.
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * const metricsLogger = Log.create<[Metrics.LogMessage]>()
75
+ * Metrics.Provider.register('default', Metrics.Provider.create({ logger: metricsLogger }))
76
+ * metricsLogger.addTransport(Metrics.transports.sentryMetrics(Sentry.metrics))
77
+ *
78
+ * Metrics.increment('order_placed', 1, { tags: { region: 'us-east' } })
79
+ * Metrics.distribution('payment_latency', 142, { unit: 'millisecond' })
80
+ * Metrics.set('unique_customers', 'cust_123')
81
+ * Metrics.gauge('active_connections', 47)
82
+ * ```
83
+ */
84
+ declare namespace Metrics {
85
+ /** Discriminated union of the four metric operations, keyed by `op` */
86
+ type LogMessage = MetricsLogMessage;
87
+ /** Interface matching the four metric methods, used to type external metric providers like Sentry */
88
+ type MetricFunctions = MetricFunctions;
89
+ namespace Provider {
90
+ interface MetricsServiceConfig {
91
+ logger: Log.Logger<[MetricsLogMessage]>;
92
+ }
93
+ }
94
+ /**
95
+ * The four standard metric operations for application observability.
96
+ *
97
+ * Each method emits a structured log message through the configured logger.
98
+ * Attach Log transports to the logger to route metrics to external systems
99
+ * like Sentry, Datadog, or custom analytics pipelines. Use `MetricsAdditionalData`
100
+ * to attach tags, measurement units, and timestamps to any emission.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * Metrics.increment('user_signup', 1, { tags: { plan: 'pro' }, unit: 'none' })
105
+ * Metrics.distribution('response_time', 200, { unit: 'millisecond' })
106
+ * Metrics.set('unique_visitors', 'user_abc')
107
+ * Metrics.gauge('queue_depth', 312)
108
+ * ```
109
+ */
110
+ interface MetricsService {
111
+ /** Emits a counter metric, incrementing by `value` (defaults to 1) */
112
+ increment: (name: string, value?: number, data?: MetricsAdditionalData) => void;
113
+ /** Emits a distribution metric for statistical aggregation (p50, p95, p99) */
114
+ distribution: (name: string, value: number, data?: MetricsAdditionalData) => void;
115
+ /** Emits a set metric tracking unique string or integer values */
116
+ set: (name: string, value: number | string, data?: MetricsAdditionalData) => void;
117
+ /** Emits a gauge metric capturing a point-in-time value that can increase or decrease */
118
+ gauge: (name: string, value: number, data?: MetricsAdditionalData) => void;
119
+ }
120
+ }
121
+ /**
122
+ * Unified metrics collection piped through the Log transport system.
123
+ * Call `Metrics.Provider.create({ logger })` and register it, then attach transports to the logger.
124
+ */
125
+ declare const Metrics: {
126
+ Provider: {
127
+ create: typeof create;
128
+ register: (name: "default", item: Metrics.MetricsService) => void;
129
+ };
130
+ increment: (name: string, value?: number, data?: MetricsAdditionalData) => void;
131
+ distribution: (name: string, value: number, data?: MetricsAdditionalData) => void;
132
+ set: (name: string, value: number | string, data?: MetricsAdditionalData) => void;
133
+ gauge: (name: string, value: number, data?: MetricsAdditionalData) => void;
134
+ transports: {
135
+ sentryMetrics: typeof sentryMetricsTransport;
136
+ };
137
+ };
138
+
139
+ export { Metrics };
package/dist/index.js ADDED
@@ -0,0 +1,80 @@
1
+ // src/index.ts
2
+ import "@maestro-js/log";
3
+ import { ServiceRegistry } from "@maestro-js/service-registry";
4
+
5
+ // src/sentry-metrics-transport.ts
6
+ import "@maestro-js/log";
7
+ function sentryMetricsTransport(sentryMetrics) {
8
+ return {
9
+ write(message) {
10
+ const metric = message.data[0];
11
+ if (metric.op === "increment") {
12
+ sentryMetrics.increment(metric.name, metric.value, metric.data);
13
+ } else if (metric.op === "set") {
14
+ sentryMetrics.set(metric.name, metric.value, metric.data);
15
+ } else if (metric.op === "distribution") {
16
+ sentryMetrics.distribution(metric.name, metric.value, metric.data);
17
+ } else if (metric.op === "gauge") {
18
+ sentryMetrics.gauge(metric.name, metric.value, metric.data);
19
+ }
20
+ }
21
+ };
22
+ }
23
+
24
+ // src/index.ts
25
+ function create(config) {
26
+ const logger = config.logger;
27
+ function increment(name, value, data) {
28
+ logger.info({
29
+ op: "increment",
30
+ name,
31
+ value,
32
+ data
33
+ });
34
+ }
35
+ function distribution(name, value, data) {
36
+ logger.info({
37
+ op: "distribution",
38
+ name,
39
+ value,
40
+ data
41
+ });
42
+ }
43
+ function set(name, value, data) {
44
+ logger.info({
45
+ op: "set",
46
+ name,
47
+ value,
48
+ data
49
+ });
50
+ }
51
+ function gauge(name, value, data) {
52
+ logger.info({
53
+ op: "gauge",
54
+ name,
55
+ value,
56
+ data
57
+ });
58
+ }
59
+ return { increment, distribution, set, gauge };
60
+ }
61
+ var transports = {
62
+ sentryMetrics: sentryMetricsTransport
63
+ };
64
+ var registry = ServiceRegistry.createRegistry(ServiceRegistry.proxyFunctionsForObject);
65
+ var Provider = {
66
+ create,
67
+ register: registry.register
68
+ };
69
+ var facade = registry.resolve("default");
70
+ var Metrics = {
71
+ Provider,
72
+ increment: facade.increment,
73
+ distribution: facade.distribution,
74
+ set: facade.set,
75
+ gauge: facade.gauge,
76
+ transports
77
+ };
78
+ export {
79
+ Metrics
80
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@maestro-js/metrics",
3
+ "description": "Use when working with @maestro-js/metrics. Unified metrics collection piped through the Log transport system. Covers four metric types (counters, distributions, sets, gauges), provider setup, logger integration, Sentry transport, and custom transport authoring. Trigger on metrics emission, observability instrumentation, Sentry metrics integration, or application telemetry tasks.",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "default": "./dist/index.js"
9
+ }
10
+ },
11
+ "dependencies": {
12
+ "@maestro-js/service-registry": "1.0.0-alpha.0",
13
+ "@maestro-js/log": "1.0.0-alpha.0"
14
+ },
15
+ "version": "1.0.0-alpha.0",
16
+ "publishConfig": {
17
+ "access": "restricted"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "license": "UNLICENSED",
23
+ "engines": {
24
+ "node": ">=22.18.0"
25
+ },
26
+ "repository": "https://github.com/Marcato-Partners/maestro-js",
27
+ "scripts": {
28
+ "build": "tsup --config ../../tsup.config.ts",
29
+ "typecheck": "tsc --noEmit",
30
+ "test": "echo 'No tests yet'",
31
+ "format": "prettier --write src/",
32
+ "lint": "prettier --check src/"
33
+ }
34
+ }