@maestro-js/agent-skills 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.
@@ -0,0 +1,236 @@
1
+ ---
2
+ name: 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
+ ---
5
+
6
+ # Metrics
7
+
8
+ ## Overview
9
+
10
+ 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.
11
+
12
+ ## Quick Setup
13
+
14
+ ```ts
15
+ import { Metrics } from '@maestro-js/metrics'
16
+ import { Log } from '@maestro-js/log'
17
+ import * as Sentry from '@sentry/node'
18
+
19
+ // 1. Create a typed logger for metrics messages
20
+ const metricsLogger = Log.create<[Metrics.LogMessage]>()
21
+
22
+ // 2. Register the metrics service
23
+ Metrics.Provider.register('default', Metrics.Provider.create({ logger: metricsLogger }))
24
+
25
+ // 3. Attach the Sentry transport (or any custom transport)
26
+ metricsLogger.addTransport(Metrics.transports.sentryMetrics(Sentry.metrics))
27
+
28
+ // 4. Emit metrics
29
+ Metrics.increment('order_placed', 1, { tags: { region: 'us-east' } })
30
+ ```
31
+
32
+ ## API Reference
33
+
34
+ ### Provider
35
+
36
+ #### `Metrics.Provider.create(config)`
37
+
38
+ Create a metrics service instance.
39
+
40
+ ```ts
41
+ Metrics.Provider.create({
42
+ logger: Log.Logger<[Metrics.LogMessage]>
43
+ })
44
+ ```
45
+
46
+ 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.
47
+
48
+ #### `Metrics.Provider.register(name, service)`
49
+
50
+ Register a metrics service instance in the named registry.
51
+
52
+ ```ts
53
+ Metrics.Provider.register('default', Metrics.Provider.create({ logger: metricsLogger }))
54
+ ```
55
+
56
+ ### Metric Methods
57
+
58
+ All four methods are available directly on the `Metrics` facade (resolving the `'default'` provider) or on a named provider instance.
59
+
60
+ #### `Metrics.increment(name, value?, data?)`
61
+
62
+ Emit a counter metric, incrementing by `value` (defaults to 1 when omitted).
63
+
64
+ ```ts
65
+ Metrics.increment('user_signup') // increment by 1
66
+ Metrics.increment('items_purchased', 3, { tags: { store: 'web' } }) // increment by 3
67
+ ```
68
+
69
+ #### `Metrics.distribution(name, value, data?)`
70
+
71
+ Emit a distribution metric for statistical aggregation (p50, p95, p99).
72
+
73
+ ```ts
74
+ Metrics.distribution('api_response_time', 142, { unit: 'millisecond' })
75
+ Metrics.distribution('payload_size', 2048, { unit: 'byte', tags: { endpoint: '/upload' } })
76
+ ```
77
+
78
+ #### `Metrics.set(name, value, data?)`
79
+
80
+ Emit a set metric tracking unique string or integer values.
81
+
82
+ ```ts
83
+ Metrics.set('unique_customers', 'cust_123')
84
+ Metrics.set('unique_customers', 'cust_456', { tags: { channel: 'mobile' } })
85
+ ```
86
+
87
+ #### `Metrics.gauge(name, value, data?)`
88
+
89
+ Emit a gauge metric capturing a point-in-time value that can increase or decrease.
90
+
91
+ ```ts
92
+ Metrics.gauge('active_connections', 47)
93
+ Metrics.gauge('queue_depth', 312, { tags: { queue: 'email' } })
94
+ ```
95
+
96
+ ### MetricsAdditionalData
97
+
98
+ Optional metadata passed as the last argument to any metric method.
99
+
100
+ ```ts
101
+ interface MetricsAdditionalData {
102
+ unit?: MeasurementUnit // 'millisecond', 'byte', 'percent', 'none', etc.
103
+ tags?: Record<string, Primitive> // arbitrary key-value pairs for grouping
104
+ timestamp?: number // custom Unix timestamp
105
+ }
106
+ ```
107
+
108
+ **Supported unit categories:**
109
+
110
+ - **Duration**: `'nanosecond'`, `'microsecond'`, `'millisecond'`, `'second'`, `'minute'`, `'hour'`, `'day'`, `'week'`
111
+ - **Information**: `'bit'`, `'byte'`, `'kilobyte'`, `'megabyte'`, `'gigabyte'`, `'terabyte'`, etc.
112
+ - **Fraction**: `'ratio'`, `'percent'`
113
+ - **None**: `''`, `'none'`
114
+
115
+ ### MetricsLogMessage
116
+
117
+ 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.
118
+
119
+ ```ts
120
+ type MetricsLogMessage =
121
+ | { op: 'increment'; name: string; value: number | undefined; data: MetricsAdditionalData | undefined }
122
+ | { op: 'distribution'; name: string; value: number; data: MetricsAdditionalData | undefined }
123
+ | { op: 'set'; name: string; value: number | string; data: MetricsAdditionalData | undefined }
124
+ | { op: 'gauge'; name: string; value: number; data: MetricsAdditionalData | undefined }
125
+ ```
126
+
127
+ ## Transports
128
+
129
+ ### Built-in: Sentry Metrics
130
+
131
+ Route all metrics to Sentry via `Metrics.transports.sentryMetrics()`.
132
+
133
+ ```ts
134
+ import * as Sentry from '@sentry/node'
135
+
136
+ const metricsLogger = Log.create<[Metrics.LogMessage]>()
137
+ metricsLogger.addTransport(Metrics.transports.sentryMetrics(Sentry.metrics))
138
+ ```
139
+
140
+ The transport accepts any object matching the `Metrics.MetricFunctions` interface (the four metric methods), so it works with any Sentry-compatible metrics API.
141
+
142
+ ### Custom Transports
143
+
144
+ Write a custom transport by implementing `Log.Transport<[Metrics.LogMessage]>`. The `write` method receives a log message whose `data[0]` is the `MetricsLogMessage`.
145
+
146
+ ```ts
147
+ function datadogTransport(client: DatadogClient): Log.Transport<[Metrics.LogMessage]> {
148
+ return {
149
+ write(message) {
150
+ const metric = message.data[0]
151
+ if (metric.op === 'increment') {
152
+ client.increment(metric.name, metric.value ?? 1, metric.data?.tags)
153
+ } else if (metric.op === 'distribution') {
154
+ client.distribution(metric.name, metric.value, metric.data?.tags)
155
+ } else if (metric.op === 'gauge') {
156
+ client.gauge(metric.name, metric.value, metric.data?.tags)
157
+ }
158
+ // handle 'set' as needed
159
+ }
160
+ }
161
+ }
162
+
163
+ metricsLogger.addTransport(datadogTransport(myDatadogClient))
164
+ ```
165
+
166
+ ## Common Patterns
167
+
168
+ ### Tagging metrics by request context
169
+
170
+ ```ts
171
+ Metrics.increment('api_request', 1, {
172
+ tags: { method: 'POST', path: '/orders', status: 201 }
173
+ })
174
+
175
+ Metrics.distribution('db_query_time', 23, {
176
+ unit: 'millisecond',
177
+ tags: { table: 'orders', operation: 'insert' }
178
+ })
179
+ ```
180
+
181
+ ### Tracking unique visitors
182
+
183
+ ```ts
184
+ Metrics.set('unique_visitors', sessionId, {
185
+ tags: { page: '/checkout' }
186
+ })
187
+ ```
188
+
189
+ ### Monitoring resource usage
190
+
191
+ ```ts
192
+ Metrics.gauge('memory_usage_mb', process.memoryUsage().heapUsed / 1024 / 1024, {
193
+ unit: 'megabyte'
194
+ })
195
+
196
+ Metrics.gauge('active_websocket_connections', connectionPool.size)
197
+ ```
198
+
199
+ ### Multiple metric providers
200
+
201
+ Register named providers for different metric pipelines.
202
+
203
+ ```ts
204
+ const appLogger = Log.create<[Metrics.LogMessage]>()
205
+ const perfLogger = Log.create<[Metrics.LogMessage]>()
206
+
207
+ appLogger.addTransport(Metrics.transports.sentryMetrics(Sentry.metrics))
208
+ perfLogger.addTransport(datadogTransport(myDatadogClient))
209
+
210
+ Metrics.Provider.register('default', Metrics.Provider.create({ logger: appLogger }))
211
+ Metrics.Provider.register('perf', Metrics.Provider.create({ logger: perfLogger }))
212
+
213
+ // Facade calls use 'default'
214
+ Metrics.increment('order_placed')
215
+ ```
216
+
217
+ ## Cross-Package Integration
218
+
219
+ ### Depends on
220
+
221
+ - **@maestro-js/service-registry** -- Provider pattern, named registry, deferred Proxy resolution.
222
+ - **@maestro-js/log** -- Typed logger and transport system. Metrics emissions are structured log messages routed through Log transports.
223
+
224
+ ### Works with
225
+
226
+ - **@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.
227
+
228
+ ## Package Info
229
+
230
+ - **Package name**: `@maestro-js/metrics`
231
+ - **Source**: `packages/metrics/src/index.ts`
232
+ - **Types**: `packages/metrics/src/metrics-types.ts`
233
+ - **Sentry transport**: `packages/metrics/src/sentry-metrics-transport.ts`
234
+ - **Tests**: None yet
235
+ - **Build**: `pnpm --filter @maestro-js/metrics build`
236
+ - **Typecheck**: `pnpm --filter @maestro-js/metrics typecheck`
@@ -0,0 +1,284 @@
1
+ ---
2
+ name: queue
3
+ description: "Database-backed job queue for deferring work beyond the current request with scheduled execution, concurrency control, and distributed tracing. Use when working with @maestro-js/queue, including dispatching jobs, processing queues, configuring the db driver, writing queue consumers, handling retries/stuck jobs, integrating with mail or recurring-jobs, and understanding the queue database schema."
4
+ ---
5
+
6
+ # @maestro-js/queue
7
+
8
+ Database-backed job queue that stores messages in MySQL via a pluggable driver and processes them asynchronously. Follows the Provider pattern from `@maestro-js/service-registry` with a producer/consumer split supporting scheduled execution, concurrency, and distributed trace propagation.
9
+
10
+ ## Quick Setup
11
+
12
+ ```ts
13
+ import { Queue } from '@maestro-js/queue'
14
+ import { Db } from '@maestro-js/db'
15
+ import { Log } from '@maestro-js/log'
16
+
17
+ // Register the queue service with the db driver
18
+ Queue.Provider.register(
19
+ 'default',
20
+ Queue.Provider.create({
21
+ driver: Queue.drivers.db({
22
+ db: Db.provider('default'),
23
+ databaseTable: 'jobs'
24
+ }),
25
+ logger: Log
26
+ })
27
+ )
28
+
29
+ // Create a named queue with a work function
30
+ const orderQueue = Queue.create({
31
+ name: 'fulfill-orders',
32
+ workFn: async (body) => {
33
+ await fulfillOrder(body.orderId)
34
+ return { fulfilled: true }
35
+ },
36
+ concurrency: 3
37
+ })
38
+
39
+ // Enqueue a message
40
+ await orderQueue.addMessage({ orderId: 'ord_123' }, { scheduledDate: null })
41
+
42
+ // Start processing
43
+ orderQueue.startProcessing()
44
+ ```
45
+
46
+ ## Database Schema
47
+
48
+ The db driver expects a MySQL table (name configurable via `databaseTable`). The column-to-field mapping used internally:
49
+
50
+ | Column | Type | Description |
51
+ | --------------- | ------------- | -------------------------------------------------------- |
52
+ | `id` | int (PK, AI) | Message ID |
53
+ | `batch_id` | int / NULL | Lock batch for concurrent dequeue |
54
+ | `job_type` | varchar | Queue name string |
55
+ | `body` | text (JSON) | Serialized message body |
56
+ | `trace` | text (JSON) | Serialized `DehydratedTrace` for distributed tracing |
57
+ | `status` | enum | `waiting`, `processing`, `success`, `error`, `stuck` |
58
+ | `scheduledDate` | datetime | Earliest time the message can be processed |
59
+ | `enqueuedDate` | datetime | When the message was inserted |
60
+ | `started_ts` | datetime | When processing began |
61
+ | `result` | text | JSON result on success, error stack on failure |
62
+ | `recovered` | tinyint | 1 if the message was recovered from stuck state |
63
+ | `running_time` | int / NULL | Seconds elapsed during processing |
64
+ | `unique_key` | int / NULL | MD5-derived dedup key (nullable) |
65
+ | `priority` | bigint / NULL | Lower value = higher priority (defaults to `Date.now()`) |
66
+
67
+ The driver requires a `FORCE INDEX (locking_update)` index on `(batch_id, status, job_type, scheduledDate)` for efficient locking queries.
68
+
69
+ ## API Reference
70
+
71
+ ### Provider Setup
72
+
73
+ ```ts
74
+ Queue.Provider.create(config: {
75
+ driver: Queue.Driver
76
+ logger: Log.Logger<[Queue.LogMessage]>
77
+ tracing?: Queue.Tracing // optional distributed tracing adapter
78
+ }): Queue.QueueService
79
+ ```
80
+
81
+ ```ts
82
+ Queue.Provider.register(name: Queue.Provider.Key, service: Queue.QueueService): void
83
+ ```
84
+
85
+ ### Drivers
86
+
87
+ ```ts
88
+ Queue.drivers.db(config: {
89
+ db: { update, select, insert } // Db provider interface
90
+ databaseTable: string
91
+ extraFields?: string[] // additional columns to insert
92
+ fastestPollingRateMs?: number // default 100
93
+ slowestPollingRateMs?: number // default 10_000
94
+ pollingBackoffRate?: number // default 1.1
95
+ }): Queue.Driver
96
+ ```
97
+
98
+ ### Queue Instance Methods
99
+
100
+ Create a queue:
101
+
102
+ ```ts
103
+ const queue = Queue.create<MessageBody, Result>({
104
+ name: string,
105
+ workFn: (body: MessageBody) => Promise<Result>,
106
+ logger?: Log.LogFunctions<[Queue.LogMessage]>, // additional per-queue logger
107
+ concurrency?: number // default 1
108
+ })
109
+ ```
110
+
111
+ Dispatch a message:
112
+
113
+ ```ts
114
+ await queue.addMessage(body, {
115
+ scheduledDate: Iso.Instant | null, // null = process immediately
116
+ tracingContext?: DehydratedTrace | null
117
+ })
118
+ ```
119
+
120
+ Update a waiting message:
121
+
122
+ ```ts
123
+ await queue.updateMessage({
124
+ messageId: number,
125
+ scheduledDate: Iso.Instant,
126
+ body: MessageBody
127
+ })
128
+ ```
129
+
130
+ List messages (paginated):
131
+
132
+ ```ts
133
+ const messages = await queue.listMessages({ pageSize: 50, pageNumber: 0 })
134
+ ```
135
+
136
+ Processing control:
137
+
138
+ ```ts
139
+ queue.startProcessing() // begin polling and processing
140
+ queue.stopProcessing() // stop polling
141
+ await queue.processUntilEmpty({ pollingMs?: number }) // process then resolve when empty
142
+ ```
143
+
144
+ ### Service-Level Methods
145
+
146
+ ```ts
147
+ Queue.list() // names of all registered queues
148
+ Queue.get<Body, Result>(name) // retrieve a queue by name
149
+ Queue.startProcessing() // start all queues
150
+ Queue.stopProcessing() // stop all queues
151
+ ```
152
+
153
+ ### Message Statuses
154
+
155
+ | Status | Meaning |
156
+ | ------------ | -------------------------------------------------------------- |
157
+ | `waiting` | Queued and eligible for processing when scheduled time arrives |
158
+ | `processing` | Locked by a worker and currently executing |
159
+ | `success` | Work function completed without error |
160
+ | `error` | Work function threw or timed out |
161
+ | `stuck` | Processing exceeded timeout; recovered or marked stuck |
162
+
163
+ ### Log Events
164
+
165
+ The queue emits structured log messages with these `event` values:
166
+
167
+ - `messageEnqueued` -- message added to the queue
168
+ - `messageDequeued` -- message picked up by a worker
169
+ - `messageProcessed` -- work function completed successfully
170
+ - `messageFailed` -- work function threw an error
171
+ - `queueStarted` -- queue processing started
172
+ - `queueStopped` -- queue processing stopped
173
+
174
+ ## Common Patterns
175
+
176
+ ### Schedule a Future Job
177
+
178
+ ```ts
179
+ import { instantFns } from 'iso-fns2'
180
+
181
+ // Schedule 1 hour from now
182
+ const oneHourLater = instantFns.add(instantFns.now(), { hours: 1 })
183
+ await queue.addMessage({ task: 'reminder' }, { scheduledDate: oneHourLater })
184
+ ```
185
+
186
+ ### Deduplication with Unique Keys
187
+
188
+ The db driver supports unique keys via the `addMessage` options on the driver level. When a duplicate `uniqueKey` is inserted, the row is not duplicated -- instead the priority is updated if the new priority is higher (lower number).
189
+
190
+ ### Process Until Empty (Worker Scripts)
191
+
192
+ ```ts
193
+ // Useful for one-off batch processing or tests
194
+ await queue.processUntilEmpty({ pollingMs: 1000 })
195
+ // Resolves when no waiting/processing messages remain
196
+ ```
197
+
198
+ ### Concurrency Control
199
+
200
+ ```ts
201
+ // High-concurrency queue for lightweight jobs
202
+ const emailQueue = Queue.create({
203
+ name: 'send-emails',
204
+ workFn: async (body) => sendEmail(body),
205
+ concurrency: 10
206
+ })
207
+ ```
208
+
209
+ ### Stuck Job Recovery
210
+
211
+ The db driver automatically recovers stuck jobs every 60 seconds. Jobs in `processing` status that exceed the timeout (default 45 minutes) are reset to `waiting` and reprocessed. Set `shouldRecoverStuckJobs: false` in the driver `start` options to mark them as `stuck` instead of recovering.
212
+
213
+ ### Distributed Tracing
214
+
215
+ Pass a `tracing` adapter when creating the service to propagate trace context across producer/consumer boundaries:
216
+
217
+ ```ts
218
+ Queue.Provider.register(
219
+ 'default',
220
+ Queue.Provider.create({
221
+ driver: Queue.drivers.db({ db: Db.provider('default'), databaseTable: 'jobs' }),
222
+ logger: Log,
223
+ tracing: {
224
+ hydrate: (trace, callback) => {
225
+ /* restore trace context */ return callback()
226
+ },
227
+ dehydrate: () => {
228
+ /* return current trace context */ return null
229
+ },
230
+ startActiveSpan: (name, fn) => fn(),
231
+ getActiveSpan: () => undefined
232
+ }
233
+ })
234
+ )
235
+ ```
236
+
237
+ The trace data is serialized as `DehydratedTrace` (traceId, spanId, traceFlags, traceState) and stored in the `trace` column.
238
+
239
+ ## Testing
240
+
241
+ This package has no tests yet (`pnpm --filter @maestro-js/queue test` prints "No tests yet"). To test queue behavior in consuming packages, mock the driver interface:
242
+
243
+ ```ts
244
+ const mockDriver: Queue.Driver = {
245
+ start: () => {},
246
+ stop: () => {},
247
+ listMessages: async () => [],
248
+ addMessage: async () => ({ id: 1 }),
249
+ updateMessage: async () => {},
250
+ isQueueEmpty: async () => true
251
+ }
252
+
253
+ Queue.Provider.register(
254
+ 'default',
255
+ Queue.Provider.create({
256
+ driver: mockDriver,
257
+ logger: Log
258
+ })
259
+ )
260
+ ```
261
+
262
+ ## Cross-Package Integration
263
+
264
+ ### Mail (async email sending)
265
+
266
+ The `@maestro-js/mail` package uses Queue to dispatch email-sending jobs asynchronously. Mail enqueues messages; a queue consumer processes them via the configured mail driver.
267
+
268
+ ### Recurring Jobs (cron scheduling)
269
+
270
+ The `@maestro-js/recurring-jobs` package uses Queue to execute scheduled/cron-based jobs. It dispatches queue messages on a schedule and relies on the queue's processing infrastructure.
271
+
272
+ ### Db (database driver)
273
+
274
+ The db driver requires a `Db` provider that exposes `select`, `update`, and `insert` methods. Pass `Db.provider('default')` as the `db` config option.
275
+
276
+ ### Tracing
277
+
278
+ Integrate with `@maestro-js/tracing` by implementing the `Queue.Tracing` interface to propagate OpenTelemetry-compatible trace context across the producer/consumer boundary.
279
+
280
+ ## Source Files
281
+
282
+ - `packages/queue/src/index.ts` -- Provider pattern, facade, queue creation and lifecycle
283
+ - `packages/queue/src/queue-types.ts` -- TypeScript interfaces for QueueMessage, QueueDriver, QueueTracing, log events
284
+ - `packages/queue/src/db-queue-driver.ts` -- MySQL-backed driver with polling, batched locking, stuck job recovery