Skip to content

Commit b3d4c51

Browse files
committed
Merge branch 'nrk/feat/pgw-health-endpoints' into nrk/release52
# Conflicts: # packages/playout-gateway/package.json # packages/yarn.lock
2 parents 1f0904d + 731f92d commit b3d4c51

File tree

6 files changed

+376
-19
lines changed

6 files changed

+376
-19
lines changed

packages/playout-gateway/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,17 @@
5555
"rundown",
5656
"production"
5757
],
58+
"devDependencies": {
59+
"@types/koa": "^3.0.0",
60+
"@types/koa__router": "^12.0.4"
61+
},
5862
"dependencies": {
63+
"@koa/router": "^14.0.0",
5964
"@sofie-automation/server-core-integration": "1.52.9-nrk",
6065
"@sofie-automation/shared-lib": "1.52.9-nrk",
6166
"debug": "^4.4.0",
6267
"influx": "^5.9.7",
68+
"koa": "^3.0.1",
6369
"timeline-state-resolver": "9.3.2-nightly-test-release52-nrk-20250910-071718-b75f9a20c.0",
6470
"tslib": "^2.8.1",
6571
"underscore": "^1.13.7",

packages/playout-gateway/src/config.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ let influxUser: string | undefined = process.env.INFLUX_USER || 'sofie'
2020
let influxPassword: string | undefined = process.env.INFLUX_PASSWORD || undefined
2121
let influxDatabase: string | undefined = process.env.INFLUX_DB || 'sofie'
2222

23+
let healthPort: number | undefined = parseInt(process.env.HEALTH_PORT + '') || undefined
24+
2325
let prevProcessArg = ''
2426
process.argv.forEach((val) => {
2527
val = val + ''
@@ -50,14 +52,18 @@ process.argv.forEach((val) => {
5052
influxPassword = val
5153
} else if (prevProcessArg.match(/-influxDatabase/i)) {
5254
influxDatabase = val
55+
} else if (prevProcessArg.match(/-healthPort/i)) {
56+
healthPort = parseInt(val)
5357

5458
// arguments with no options:
5559
} else if (val.match(/-disableWatchdog/i)) {
5660
disableWatchdog = true
5761
} else if (val.match(/-disableAtemUpload/i)) {
5862
disableAtemUpload = true
5963
} else if (val.match(/-unsafeSSL/i)) {
60-
// Will cause the Node applocation to blindly accept all certificates. Not recommenced unless in local, controlled networks.
64+
// Will cause the Node application to blindly accept all certificates.
65+
// Not recommenced unless in local, controlled networks.
66+
// Instead use "-certificates cert1 cert2"
6167
unsafeSSL = true
6268
}
6369
prevProcessArg = nextPrevProcessArg + ''
@@ -85,6 +91,9 @@ const config: Config = {
8591
password: influxPassword,
8692
database: influxDatabase,
8793
},
94+
health: {
95+
port: healthPort,
96+
},
8897
}
8998

9099
export { config, logPath, logLevel, disableWatchdog, disableAtemUpload }

packages/playout-gateway/src/connector.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,27 @@ import {
66
CertificatesConfig,
77
PeripheralDeviceId,
88
loadCertificatesFromDisk,
9+
stringifyError,
910
} from '@sofie-automation/server-core-integration'
11+
import { HealthConfig, HealthEndpoints } from './health'
1012

1113
export interface Config {
1214
certificates: CertificatesConfig
1315
device: DeviceConfig
1416
core: CoreConfig
1517
tsr: TSRConfig
1618
influx: InfluxConfig
19+
health: HealthConfig
1720
}
1821

1922
export interface DeviceConfig {
2023
deviceId: PeripheralDeviceId
2124
deviceToken: string
2225
}
2326
export class Connector {
27+
public initialized = false
28+
public initializedError: string | undefined = undefined
29+
2430
private tsrHandler: TSRHandler | undefined
2531
private coreHandler: CoreHandler | undefined
2632
private _logger: Logger
@@ -38,6 +44,8 @@ export class Connector {
3844

3945
this._logger.info('Initializing Core...')
4046
this.coreHandler = new CoreHandler(this._logger, config.device)
47+
new HealthEndpoints(this, this.coreHandler, config.health)
48+
4149
await this.coreHandler.init(config.core, this._certificates)
4250
this._logger.info('Core initialized')
4351

@@ -47,12 +55,15 @@ export class Connector {
4755
this._logger.info('TSR initialized')
4856

4957
this._logger.info('Initialization done')
58+
this.initialized = true
5059
return
5160
} catch (e: any) {
5261
this._logger.error('Error during initialization:')
5362
this._logger.error(e)
5463
this._logger.error(e.stack)
5564

65+
this.initializedError = stringifyError(e)
66+
5667
try {
5768
if (this.coreHandler) {
5869
this.coreHandler.destroy().catch(this._logger.error)

packages/playout-gateway/src/coreHandler.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ export class CoreHandler {
5959
private _statusInitialized = false
6060
private _statusDestroyed = false
6161

62+
public connectedToCore = false
63+
6264
constructor(logger: Logger, deviceOptions: DeviceConfig) {
6365
this.logger = logger
6466
this._deviceOptions = deviceOptions
@@ -73,11 +75,13 @@ export class CoreHandler {
7375

7476
this.core.onConnected(() => {
7577
this.logger.info('Core Connected!')
78+
this.connectedToCore = true
7679

7780
if (this._onConnected) this._onConnected()
7881
})
7982
this.core.onDisconnected(() => {
8083
this.logger.warn('Core Disconnected!')
84+
this.connectedToCore = false
8185
})
8286
this.core.onError((err: any) => {
8387
this.logger.error('Core Error: ' + (typeof err === 'string' ? err : err.message || err.toString() || err))
@@ -389,9 +393,12 @@ export class CoreHandler {
389393

390394
return Object.fromEntries(this._tsrHandler.getDebugStates().entries())
391395
}
392-
async updateCoreStatus(): Promise<any> {
396+
getCoreStatus(): {
397+
statusCode: StatusCode
398+
messages: string[]
399+
} {
393400
let statusCode = StatusCode.GOOD
394-
const messages: Array<string> = []
401+
const messages: string[] = []
395402

396403
if (!this._statusInitialized) {
397404
statusCode = StatusCode.BAD
@@ -401,11 +408,13 @@ export class CoreHandler {
401408
statusCode = StatusCode.BAD
402409
messages.push('Shut down')
403410
}
404-
405-
return this.core.setStatus({
406-
statusCode: statusCode,
407-
messages: messages,
408-
})
411+
return {
412+
statusCode,
413+
messages,
414+
}
415+
}
416+
async updateCoreStatus(): Promise<any> {
417+
return this.core.setStatus(this.getCoreStatus())
409418
}
410419
}
411420

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import * as Koa from 'koa'
2+
import * as Router from '@koa/router'
3+
import { StatusCode } from 'timeline-state-resolver'
4+
import { assertNever } from '@sofie-automation/server-core-integration'
5+
import { CoreHandler } from './coreHandler'
6+
import { Connector } from './connector'
7+
8+
export interface HealthConfig {
9+
/** If set, exposes health HTTP endpoints on the given port */
10+
port?: number
11+
}
12+
13+
/**
14+
* Exposes health endpoints for Kubernetes or other orchestrators to monitor
15+
* see https://kubernetes.io/docs/concepts/configuration/liveness-readiness-startup-probes
16+
*/
17+
export class HealthEndpoints {
18+
private app = new Koa()
19+
constructor(private connector: Connector, private coreHandler: CoreHandler, private config: HealthConfig) {
20+
if (!config.port) return // disabled
21+
22+
const router = new Router()
23+
24+
router.get('/healthz', async (ctx) => {
25+
if (this.connector.initializedError !== undefined) {
26+
ctx.status = 503
27+
ctx.body = `Error during initialization: ${this.connector.initializedError}`
28+
return
29+
}
30+
if (!this.connector.initialized) {
31+
ctx.status = 503
32+
ctx.body = 'Not initialized'
33+
return
34+
}
35+
36+
const coreStatus = this.coreHandler.getCoreStatus()
37+
38+
if (coreStatus.statusCode === StatusCode.UNKNOWN) ctx.status = 503
39+
else if (coreStatus.statusCode === StatusCode.FATAL) ctx.status = 503
40+
else if (coreStatus.statusCode === StatusCode.BAD) ctx.status = 503
41+
else if (coreStatus.statusCode === StatusCode.WARNING_MAJOR) ctx.status = 200
42+
else if (coreStatus.statusCode === StatusCode.WARNING_MINOR) ctx.status = 200
43+
else if (coreStatus.statusCode === StatusCode.GOOD) ctx.status = 200
44+
else assertNever(coreStatus.statusCode)
45+
46+
if (ctx.status !== 200) {
47+
ctx.body = `Status: ${StatusCode[coreStatus.statusCode]}, messages: ${coreStatus.messages.join(', ')}`
48+
} else {
49+
ctx.body = 'OK'
50+
}
51+
})
52+
53+
router.get('/readyz', async (ctx) => {
54+
if (!this.coreHandler.connectedToCore) {
55+
ctx.status = 503
56+
ctx.body = 'Not connected to Core'
57+
return
58+
}
59+
// else
60+
ctx.status = 200
61+
ctx.body = 'READY'
62+
})
63+
64+
this.app.use(router.routes()).use(router.allowedMethods())
65+
this.app.listen(this.config.port)
66+
}
67+
}

0 commit comments

Comments
 (0)