@ossy/platform 1.31.3 → 1.32.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossy/platform",
3
- "version": "1.31.3",
3
+ "version": "1.32.0",
4
4
  "description": "Ossy application server runtime",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,9 +30,10 @@
30
30
  "license": "MIT",
31
31
  "dependencies": {
32
32
  "@aws-sdk/client-ses": "^3.0.0",
33
- "@ossy/event-store": "^1.0.1",
34
- "@ossy/router": "^1.32.3",
35
- "@ossy/sdk": "^1.32.3",
33
+ "@ossy/event-store": "^1.1.0",
34
+ "@ossy/observability": "^1.1.0",
35
+ "@ossy/router": "^1.33.0",
36
+ "@ossy/sdk": "^1.33.0",
36
37
  "cookie-parser": "^1.4.7",
37
38
  "dotenv": ">=16.0.0 <18.0.0",
38
39
  "express": ">=5.0.0 <6.0.0",
@@ -43,5 +44,5 @@
43
44
  "src",
44
45
  "Dockerfile"
45
46
  ],
46
- "gitHead": "b8cbdc545a659e96af8948aa08a89316a2c2d193"
47
+ "gitHead": "9a8a1bb0466d35001425d241d77c2c9ab31e11d3"
47
48
  }
@@ -1,3 +1,7 @@
1
+ import { createLogger } from '@ossy/observability'
2
+
3
+ const log = createLogger('platform')
4
+
1
5
  /** @type {Record<string, unknown>} */
2
6
  const _clients = {}
3
7
 
@@ -22,13 +26,13 @@ export const IntegrationService = {
22
26
  const { id, credentials = [], connect } = mod
23
27
 
24
28
  if (typeof id !== 'string' || id.trim() === '') {
25
- console.warn('[IntegrationService] Skipping integration with missing or invalid "id"')
29
+ log.warn('[IntegrationService] Skipping integration with missing or invalid "id"')
26
30
  continue
27
31
  }
28
32
 
29
33
  const missing = credentials.filter((key) => !env[key])
30
34
  if (missing.length > 0) {
31
- console.warn(
35
+ log.warn(
32
36
  `[IntegrationService] Skipping integration "${id}" — missing env var(s): ${missing.join(', ')}`,
33
37
  )
34
38
  continue
@@ -36,11 +40,10 @@ export const IntegrationService = {
36
40
 
37
41
  try {
38
42
  _clients[id] = await connect({ env })
39
- console.log(`[IntegrationService] Connected integration "${id}"`)
43
+ log.info(`[IntegrationService] Connected integration "${id}"`)
40
44
  } catch (err) {
41
- console.warn(
42
- `[IntegrationService] Integration "${id}" connect() failed — skipping:`,
43
- err && err.message ? err.message : err,
45
+ log.warn(
46
+ `[IntegrationService] Integration "${id}" connect() failed — skipping: ${err && err.message ? err.message : err}`,
44
47
  )
45
48
  }
46
49
  }
@@ -1,3 +1,7 @@
1
+ import { createLogger } from '@ossy/observability'
2
+
3
+ const log = createLogger('platform')
4
+
1
5
  /** Node/Express can join duplicate workspace headers as `id, id` — API expects a single id. */
2
6
  function normalizeWorkspaceIdHeader (value) {
3
7
  if (!value || value === 'undefined') return undefined
@@ -39,14 +43,14 @@ export function ProxyInternal () {
39
43
  }
40
44
 
41
45
  if (req.originalUrl.startsWith('/@ossy/users/me/app-settings') && req.method === 'GET') {
42
- console.log('[@ossy/platform][proxy] GET /@ossy/users/me/app-settings')
46
+ log.info('[@ossy/platform][proxy] GET /@ossy/users/me/app-settings')
43
47
  const userSettings = JSON.parse(req.signedCookies?.['x-ossy-user-settings'] || '{}')
44
48
  res.status(200)
45
49
  res.json(userSettings)
46
50
  return
47
51
  }
48
52
 
49
- console.log(`[@ossy/platform][proxy] ${req.method} ${req.originalUrl}`)
53
+ log.info(`[@ossy/platform][proxy] ${req.method} ${req.originalUrl}`)
50
54
 
51
55
  const domain = process.env.OSSY_API_URL || 'https://api.ossy.se'
52
56
  const url = `${domain}${req.originalUrl?.replace('/@ossy', '/api/v0')}`
@@ -79,7 +83,7 @@ export function ProxyInternal () {
79
83
  try {
80
84
  data = trimmed === '' ? null : JSON.parse(trimmed)
81
85
  } catch (error) {
82
- console.log('[@ossy/platform][proxy][error]', error)
86
+ log.error('[@ossy/platform][proxy][error]', undefined, error)
83
87
  res.removeHeader('content-length')
84
88
  const st = response.status
85
89
  if (st === 401 || st === 403 || st === 404) {
@@ -103,7 +107,7 @@ export function ProxyInternal () {
103
107
  })
104
108
  })
105
109
  .catch((error) => {
106
- console.log('[@ossy/platform][proxy][error]', error)
110
+ log.error('[@ossy/platform][proxy][error]', undefined, error)
107
111
  const status = error.status
108
112
  res.status(status || 500)
109
113
  res.json({ message: error.message || 'Internal Server Error' })
@@ -1,3 +1,7 @@
1
+ import { createLogger } from '@ossy/observability'
2
+
3
+ const log = createLogger('platform')
4
+
1
5
  /** @type {object[]} */
2
6
  const systemTemplates = []
3
7
 
@@ -12,7 +16,7 @@ export function registerResourceTemplate(template) {
12
16
  if (typeof template.id !== 'string' || template.id.trim() === '') return
13
17
  if (systemTemplates.find(t => t.id === template.id)) return
14
18
  systemTemplates.push(template)
15
- console.log(`[INFO][ResourceTemplateRegistry] Registered system template: ${template.id}`)
19
+ log.info(`[ResourceTemplateRegistry] Registered system template: ${template.id}`)
16
20
  }
17
21
 
18
22
  /**
package/src/runtime.js CHANGED
@@ -7,6 +7,9 @@ import { Router as OssyRouter } from '@ossy/router'
7
7
  import { loadManifest, resolveEntryUrl } from './server.js'
8
8
  import { ProxyInternal } from './proxy-internal.js'
9
9
  import { loadSite, invalidateSite } from './site-loader.js'
10
+ import { createLogger } from '@ossy/observability'
11
+
12
+ const log = createLogger('platform')
10
13
 
11
14
  const DEFAULT_PORT = 3000
12
15
 
@@ -119,7 +122,7 @@ export async function startRuntime ({ port } = {}) {
119
122
  try {
120
123
  context = await getSiteContext(domain)
121
124
  } catch (err) {
122
- console.error(`[@ossy/platform] Failed to load site for ${domain}:`, err.message)
125
+ log.error(`[@ossy/platform] Failed to load site for ${domain}`, undefined, err)
123
126
  res.status(503).type('text').send(`Site not available: ${domain}`)
124
127
  return
125
128
  }
@@ -177,7 +180,7 @@ export async function startRuntime ({ port } = {}) {
177
180
  const html = await mod.render(props)
178
181
  res.status(200).type('html').send(html)
179
182
  } catch (err) {
180
- console.error(`[@ossy/platform] Request error for ${domain}${requestUrl}:`, err)
183
+ log.error(`[@ossy/platform] Request error for ${domain}${requestUrl}`, undefined, err)
181
184
  if (!res.headersSent) res.status(500).type('text').send('Internal Server Error')
182
185
  }
183
186
  })
@@ -188,8 +191,8 @@ export async function startRuntime ({ port } = {}) {
188
191
  httpServer.once('error', reject)
189
192
  })
190
193
 
191
- console.log(`[@ossy/platform] Runtime running on http://localhost:${resolvedPort}`)
192
- console.log('[@ossy/platform] Press Ctrl+C to stop.')
194
+ log.info(`[@ossy/platform] Runtime running on http://localhost:${resolvedPort}`)
195
+ log.info('[@ossy/platform] Press Ctrl+C to stop.')
193
196
 
194
197
  const closeServer = () => new Promise((resolve) => server.close(() => resolve()))
195
198
 
@@ -197,7 +200,7 @@ export async function startRuntime ({ port } = {}) {
197
200
  const handleShutdown = async (signal) => {
198
201
  if (shuttingDown) return
199
202
  shuttingDown = true
200
- console.log(`\n[@ossy/platform] Received ${signal}, shutting down…`)
203
+ log.info(`[@ossy/platform] Received ${signal}, shutting down…`)
201
204
  try { await closeServer() } finally { process.exit(0) }
202
205
  }
203
206
  process.on('SIGINT', () => handleShutdown('SIGINT'))
@@ -208,6 +211,6 @@ export async function startRuntime ({ port } = {}) {
208
211
 
209
212
  // Run directly: node src/runtime.js
210
213
  startRuntime().catch((err) => {
211
- console.error('[@ossy/platform] Runtime failed to start:', err)
214
+ log.error('[@ossy/platform] Runtime failed to start', undefined, err)
212
215
  process.exit(1)
213
216
  })
package/src/server.js CHANGED
@@ -12,6 +12,9 @@ import { TaskService } from './tasks/task-service.js'
12
12
  import { ChangeStream } from './tasks/change-stream.js'
13
13
  import { registerResourceTemplate } from './resources/resource-template.registry.js'
14
14
  import { IntegrationService } from './integration.service.js'
15
+ import { createLogger } from '@ossy/observability'
16
+
17
+ const log = createLogger('@ossy/platform')
15
18
 
16
19
  const DEFAULT_PORT = 3000
17
20
  const MANIFEST_FILE = 'manifest.json'
@@ -55,7 +58,7 @@ export function loadManifest (buildDir) {
55
58
  const startups = Array.isArray(manifest.startups) ? manifest.startups : []
56
59
  for (const e of entries) {
57
60
  if ((e.type === 'page' || e.type === 'api') && !validRoutable(e)) {
58
- console.warn(`[@ossy/platform][server] Skipping ${e.type} "${e.id}" — manifest entry has no path.`)
61
+ log.warn(`Skipping ${e.type} "${e.id}" — manifest entry has no path.`)
59
62
  }
60
63
  }
61
64
  return {
@@ -106,7 +109,7 @@ export async function startServer (options = {}) {
106
109
  TaskService.registerTask(mod)
107
110
  }
108
111
  } catch (err) {
109
- console.error(`[@ossy/platform][server] Failed to load task "${task.id}":`, err.message)
112
+ log.error(`Failed to load task "${task.id}"`, undefined, err)
110
113
  }
111
114
  }
112
115
 
@@ -123,7 +126,7 @@ export async function startServer (options = {}) {
123
126
  try {
124
127
  integrationModules.push(await import(resolveEntryUrl(intEntry.entry, buildDir)))
125
128
  } catch (err) {
126
- console.warn(`[@ossy/platform][server] Failed to import integration "${intEntry.id}":`, err.message)
129
+ log.warn(`Failed to import integration "${intEntry.id}"`, undefined, err)
127
130
  }
128
131
  }
129
132
  await IntegrationService.load(integrationModules, process.env)
@@ -136,12 +139,12 @@ export async function startServer (options = {}) {
136
139
  const mod = await import(resolveEntryUrl(agg.entry, buildDir))
137
140
  AggregateRebuild.registerAggregate(mod)
138
141
  } catch (err) {
139
- console.error(`[@ossy/platform][server] Failed to load aggregate "${agg.id}":`, err.message)
142
+ log.error(`Failed to load aggregate "${agg.id}"`, undefined, err)
140
143
  }
141
144
  }
142
145
 
143
146
  AggregateRebuild.BuildAndSaveAll().catch((error) => {
144
- console.error('[@ossy/platform][server] BuildAndSaveAll failed — is MongoDB reachable?', error)
147
+ log.error('BuildAndSaveAll failed — is MongoDB reachable?', undefined, error)
145
148
  })
146
149
  }
147
150
 
@@ -152,7 +155,7 @@ export async function startServer (options = {}) {
152
155
  await mod.run({ env: process.env })
153
156
  }
154
157
  } catch (err) {
155
- console.error(`[@ossy/platform][server] Failed to run startup "${startup.id}":`, err.message)
158
+ log.error(`Failed to run startup "${startup.id}"`, undefined, err)
156
159
  }
157
160
  }
158
161
 
@@ -258,7 +261,7 @@ export async function startServer (options = {}) {
258
261
 
259
262
  res.status(404).send('Not found')
260
263
  } catch (err) {
261
- console.error('[@ossy/platform][server] Request handling failed:', err)
264
+ log.error('Request handling failed', undefined, err)
262
265
  if (!res.headersSent) {
263
266
  res.status(500).type('text').send('Internal Server Error')
264
267
  }
@@ -271,8 +274,8 @@ export async function startServer (options = {}) {
271
274
  httpServer.once('error', reject)
272
275
  })
273
276
 
274
- console.log(`[@ossy/platform][server] Running on http://localhost:${port}`)
275
- console.log('[@ossy/platform][server] Press Ctrl+C to stop.')
277
+ log.info(`Running on http://localhost:${port}`)
278
+ log.info('Press Ctrl+C to stop.')
276
279
 
277
280
  const closeServer = () => new Promise((resolve) => server.close(() => resolve()))
278
281
 
@@ -280,7 +283,7 @@ export async function startServer (options = {}) {
280
283
  const handleShutdown = async (signal) => {
281
284
  if (shuttingDown) return
282
285
  shuttingDown = true
283
- console.log(`\n[@ossy/platform][server] Received ${signal}, shutting down...`)
286
+ log.info(`Received ${signal}, shutting down...`)
284
287
  try {
285
288
  await closeServer()
286
289
  } finally {
@@ -2,6 +2,9 @@ import path from 'path'
2
2
  import os from 'os'
3
3
  import { mkdir, writeFile } from 'fs/promises'
4
4
  import { SDK, ResourcesList } from '@ossy/sdk'
5
+ import { createLogger } from '@ossy/observability'
6
+
7
+ const log = createLogger('platform')
5
8
 
6
9
  const CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes
7
10
  const DOWNLOAD_CONCURRENCY = 10
@@ -125,7 +128,7 @@ export async function loadSite (domain) {
125
128
  const buildDir = path.join(BASE_DIR, domain)
126
129
  const cmsLocation = `/@ossy/apps/${domain}`
127
130
 
128
- console.log(`[@ossy/platform] Loading site for ${domain} (workspace ${workspaceId}) from CMS ${cmsLocation}…`)
131
+ log.info(`[@ossy/platform] Loading site for ${domain} (workspace ${workspaceId}) from CMS ${cmsLocation}…`)
129
132
 
130
133
  const sdk = SDK.of({
131
134
  apiUrl: API_URL,
@@ -146,7 +149,7 @@ export async function loadSite (domain) {
146
149
 
147
150
  await runWithConcurrency(tasks, DOWNLOAD_CONCURRENCY)
148
151
 
149
- console.log(`[@ossy/platform] Site ${domain} ready — ${files.length} file(s) in ${buildDir}`)
152
+ log.info(`[@ossy/platform] Site ${domain} ready — ${files.length} file(s) in ${buildDir}`)
150
153
 
151
154
  cache.set(domain, { buildDir, workspaceId, loadedAt: Date.now() })
152
155
  return buildDir
@@ -158,5 +161,5 @@ export async function loadSite (domain) {
158
161
  */
159
162
  export function invalidateSite (domain) {
160
163
  cache.delete(domain)
161
- console.log(`[@ossy/platform] Cache invalidated for ${domain}`)
164
+ log.info(`[@ossy/platform] Cache invalidated for ${domain}`)
162
165
  }
@@ -1,4 +1,7 @@
1
1
  import { TaskService } from './task-service.js'
2
+ import { createLogger } from '@ossy/observability'
3
+
4
+ const log = createLogger('platform')
2
5
 
3
6
  function isMongoTopologyClosedError(error) {
4
7
  if (!error || typeof error !== 'object') return false
@@ -24,7 +27,7 @@ export class ChangeStream {
24
27
  ChangeStream._reconnectAttempts = 0
25
28
  ChangeStream._dbUrl = dbUrl
26
29
  ChangeStream._open().catch((error) => {
27
- console.error('[ChangeStream] Change stream could not be opened', error)
30
+ log.error('[ChangeStream] Change stream could not be opened', undefined, error)
28
31
  ChangeStream._scheduleReconnect()
29
32
  })
30
33
  }
@@ -61,7 +64,7 @@ export class ChangeStream {
61
64
  if (prev) {
62
65
  prev.close().catch(() => {})
63
66
  }
64
- console.log('[ChangeStream] New MongoClient instance created')
67
+ log.info('[ChangeStream] New MongoClient instance created')
65
68
  }
66
69
 
67
70
  static _scheduleReconnect() {
@@ -70,19 +73,19 @@ export class ChangeStream {
70
73
  const delaySecs = Math.min(Math.pow(2, ChangeStream._reconnectAttempts), 30)
71
74
  ChangeStream._reconnectAttempts++
72
75
 
73
- console.log(`[ChangeStream] Reconnecting in ${delaySecs}s...`)
76
+ log.info(`[ChangeStream] Reconnecting in ${delaySecs}s...`)
74
77
 
75
78
  setTimeout(() => {
76
79
  if (ChangeStream._stopped) return
77
80
  ChangeStream._open().catch((error) => {
78
- console.error('[ChangeStream] Change stream could not be opened', error)
81
+ log.error('[ChangeStream] Change stream could not be opened', undefined, error)
79
82
  ChangeStream._scheduleReconnect()
80
83
  })
81
84
  }, delaySecs * 1000)
82
85
  }
83
86
 
84
87
  static async _open() {
85
- console.log('[ChangeStream] Watching for changes')
88
+ log.info('[ChangeStream] Watching for changes')
86
89
 
87
90
  const dbName = process.env.DB_NAME || 'test'
88
91
  const client = await ChangeStream._getClient()
@@ -95,7 +98,7 @@ export class ChangeStream {
95
98
  const wasReconnecting = ChangeStream._reconnectAttempts > 0
96
99
  ChangeStream._reconnectAttempts = 0
97
100
  if (wasReconnecting) {
98
- console.log('[ChangeStream] Reconnected.')
101
+ log.info('[ChangeStream] Reconnected.')
99
102
  }
100
103
 
101
104
  // Ensures only one reconnect is scheduled if both 'error' and 'close' fire for the same failure.
@@ -108,7 +111,7 @@ export class ChangeStream {
108
111
  }
109
112
 
110
113
  changeStream.on('error', (error) => {
111
- console.error('[ChangeStream] Change stream error (server keeps running)', error)
114
+ log.error('[ChangeStream] Change stream error (server keeps running)', undefined, error)
112
115
  if (isMongoTopologyClosedError(error)) {
113
116
  ChangeStream._resetClient().catch(() => {})
114
117
  }
@@ -116,17 +119,17 @@ export class ChangeStream {
116
119
  })
117
120
 
118
121
  changeStream.on('change', (change) => {
119
- console.log('[ChangeStream] Change detected')
122
+ log.info('[ChangeStream] Change detected')
120
123
  TaskService.dispatch(change.fullDocument)
121
124
  })
122
125
 
123
126
  changeStream.on('close', () => {
124
- console.log('[ChangeStream] close detected')
127
+ log.info('[ChangeStream] close detected')
125
128
  scheduleOnce()
126
129
  })
127
130
 
128
131
  changeStream.on('end', () => {
129
- console.log('[ChangeStream] end detected')
132
+ log.info('[ChangeStream] end detected')
130
133
  scheduleOnce()
131
134
  })
132
135
  }
@@ -1,5 +1,8 @@
1
1
  import { discoverTaskFiles } from './task-loader.js'
2
2
  import { TaskService } from './task-service.js'
3
+ import { createLogger } from '@ossy/observability'
4
+
5
+ const log = createLogger('platform')
3
6
 
4
7
  export async function loadAndRegisterTasks({ cwd }) {
5
8
  const taskFiles = await discoverTaskFiles({ cwd })
@@ -11,7 +14,7 @@ export async function loadAndRegisterTasks({ cwd }) {
11
14
  TaskService.registerTask(taskModule)
12
15
  }
13
16
  } catch (err) {
14
- console.error(`[TaskService] Failed to load task ${fileUrl}:`, err.message)
17
+ log.error(`[TaskService] Failed to load task ${fileUrl}`, { message: err.message }, err)
15
18
  }
16
19
  }
17
20
  }
@@ -1,8 +1,10 @@
1
1
  import { matchesGlob } from './glob.js'
2
2
  import { matchesCron } from './cron.js'
3
3
  import { IntegrationService } from '../integration.service.js'
4
+ import { createLogger, metrics } from '@ossy/observability'
4
5
 
5
6
  const SCHEDULER_INTERVAL_MS = 60_000
7
+ const _serviceLog = createLogger('TaskService')
6
8
 
7
9
  export class TaskService {
8
10
 
@@ -41,17 +43,17 @@ export class TaskService {
41
43
  const metadata = taskModule.metadata
42
44
 
43
45
  if (typeof handler !== 'function') {
44
- console.error(`[TaskService] Task "${metadata?.id}" has no exported "run" function — skipping`)
46
+ _serviceLog.error(`Task "${metadata?.id}" has no exported "run" function — skipping`)
45
47
  return
46
48
  }
47
49
  if (!metadata?.id) {
48
- console.error('[TaskService] Task has no metadata.id — skipping')
50
+ _serviceLog.error('Task has no metadata.id — skipping')
49
51
  return
50
52
  }
51
53
 
52
54
  TaskService._tasks.push({ metadata, handler })
53
- console.log(
54
- `[TaskService] Registered task "${metadata.id}" with ${metadata.triggers?.length ?? 0} trigger(s)` +
55
+ _serviceLog.info(
56
+ `Registered task "${metadata.id}" with ${metadata.triggers?.length ?? 0} trigger(s)` +
55
57
  (metadata.schedule ? ` and schedule "${metadata.schedule}"` : ''),
56
58
  )
57
59
  }
@@ -72,13 +74,19 @@ export class TaskService {
72
74
 
73
75
  if (!matched) continue
74
76
 
75
- console.log(`[TaskService] Dispatching task "${metadata.id}"`)
77
+ _serviceLog.info(`Dispatching task "${metadata.id}"`)
76
78
 
79
+ const _taskId = metadata.id
80
+ const _start = Date.now()
77
81
  Promise.resolve()
78
- .then(() => handler({ event, sdk: effectiveSdk, integrations: IntegrationService }))
82
+ .then(() => handler({ event, sdk: effectiveSdk, integrations: IntegrationService, log: createLogger(metadata.id) }))
79
83
  .catch(error =>
80
- console.error(`[TaskService] Task "${metadata.id}" failed`, error),
84
+ _serviceLog.error(`Task "${metadata.id}" failed`, undefined, error),
81
85
  )
86
+ .finally(() => {
87
+ try { metrics.timing('task.duration', Date.now() - _start, { task: _taskId }) } catch {}
88
+ try { metrics.increment('task.run', { task: _taskId }) } catch {}
89
+ })
82
90
  }
83
91
  }
84
92
 
@@ -91,7 +99,7 @@ export class TaskService {
91
99
  const scheduledTasks = TaskService._tasks.filter(t => !!t.metadata.schedule)
92
100
  if (scheduledTasks.length === 0) return
93
101
 
94
- console.log(`[TaskService] Scheduler started for ${scheduledTasks.length} task(s)`)
102
+ _serviceLog.info(`Scheduler started for ${scheduledTasks.length} task(s)`)
95
103
 
96
104
  TaskService._stopped = false
97
105
  TaskService._schedulerInterval = setInterval(() => {
@@ -102,13 +110,19 @@ export class TaskService {
102
110
  for (const { metadata, handler } of scheduledTasks) {
103
111
  if (!matchesCron(metadata.schedule, now)) continue
104
112
 
105
- console.log(`[TaskService] Schedule fired for task "${metadata.id}"`)
113
+ _serviceLog.info(`Schedule fired for task "${metadata.id}"`)
106
114
 
115
+ const _taskId = metadata.id
116
+ const _start = Date.now()
107
117
  Promise.resolve()
108
- .then(() => handler({ event: { type: 'scheduled', taskId: metadata.id }, sdk: TaskService._sdk, integrations: IntegrationService }))
118
+ .then(() => handler({ event: { type: 'scheduled', taskId: metadata.id }, sdk: TaskService._sdk, integrations: IntegrationService, log: createLogger(metadata.id) }))
109
119
  .catch(error =>
110
- console.error(`[TaskService] Scheduled task "${metadata.id}" failed`, error),
120
+ _serviceLog.error(`Scheduled task "${metadata.id}" failed`, undefined, error),
111
121
  )
122
+ .finally(() => {
123
+ try { metrics.timing('task.duration', Date.now() - _start, { task: _taskId }) } catch {}
124
+ try { metrics.increment('task.run', { task: _taskId }) } catch {}
125
+ })
112
126
  }
113
127
  }, SCHEDULER_INTERVAL_MS)
114
128
  }