@ossy/platform 1.31.3 → 1.33.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 +6 -5
- package/src/integration.service.js +9 -6
- package/src/proxy-internal.js +8 -4
- package/src/resources/resource-template.registry.js +5 -1
- package/src/runtime.js +9 -6
- package/src/server.js +13 -10
- package/src/site-loader.js +6 -3
- package/src/tasks/change-stream.js +13 -10
- package/src/tasks/task-registry.js +4 -1
- package/src/tasks/task-service.js +25 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/platform",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.33.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
|
|
34
|
-
"@ossy/
|
|
35
|
-
"@ossy/
|
|
33
|
+
"@ossy/event-store": "^1.2.0",
|
|
34
|
+
"@ossy/observability": "^1.2.0",
|
|
35
|
+
"@ossy/router": "^1.34.0",
|
|
36
|
+
"@ossy/sdk": "^1.34.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": "
|
|
47
|
+
"gitHead": "46cdd391ec01ea9210543ee7b14661f77079a7e7"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
+
log.info(`[IntegrationService] Connected integration "${id}"`)
|
|
40
44
|
} catch (err) {
|
|
41
|
-
|
|
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
|
}
|
package/src/proxy-internal.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
142
|
+
log.error(`Failed to load aggregate "${agg.id}"`, undefined, err)
|
|
140
143
|
}
|
|
141
144
|
}
|
|
142
145
|
|
|
143
146
|
AggregateRebuild.BuildAndSaveAll().catch((error) => {
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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
|
-
|
|
286
|
+
log.info(`Received ${signal}, shutting down...`)
|
|
284
287
|
try {
|
|
285
288
|
await closeServer()
|
|
286
289
|
} finally {
|
package/src/site-loader.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
+
log.info('[ChangeStream] Change detected')
|
|
120
123
|
TaskService.dispatch(change.fullDocument)
|
|
121
124
|
})
|
|
122
125
|
|
|
123
126
|
changeStream.on('close', () => {
|
|
124
|
-
|
|
127
|
+
log.info('[ChangeStream] close detected')
|
|
125
128
|
scheduleOnce()
|
|
126
129
|
})
|
|
127
130
|
|
|
128
131
|
changeStream.on('end', () => {
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
+
_serviceLog.error(`Task "${metadata?.id}" has no exported "run" function — skipping`)
|
|
45
47
|
return
|
|
46
48
|
}
|
|
47
49
|
if (!metadata?.id) {
|
|
48
|
-
|
|
50
|
+
_serviceLog.error('Task has no metadata.id — skipping')
|
|
49
51
|
return
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
TaskService._tasks.push({ metadata, handler })
|
|
53
|
-
|
|
54
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|