Skip to content

Commit 692b22f

Browse files
authored
feat: add /metrics endpoint to gateways SOFIE-456 (Sofie-Automation#1723)
1 parent 38fae25 commit 692b22f

20 files changed

Lines changed: 367 additions & 40 deletions

File tree

packages/live-status-gateway/src/config.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ let deviceToken: string = process.env.DEVICE_TOKEN || ''
1111
let disableWatchdog: boolean = process.env.DISABLE_WATCHDOG === '1' || false
1212
let unsafeSSL: boolean = process.env.UNSAFE_SSL === '1' || false
1313
const certs: string[] = (process.env.CERTIFICATES || '').split(';') || []
14+
let healthPort: number | undefined = parseInt(process.env.HEALTH_PORT + '') || undefined
1415

1516
let prevProcessArg = ''
1617
process.argv.forEach((val) => {
@@ -37,12 +38,14 @@ process.argv.forEach((val) => {
3738
} else if (val.match(/-unsafeSSL/i)) {
3839
// Will cause the Node applocation to blindly accept all certificates. Not recommenced unless in local, controlled networks.
3940
unsafeSSL = true
41+
} else if (prevProcessArg.match(/-healthPort/i)) {
42+
healthPort = parseInt(val)
4043
}
4144
prevProcessArg = nextPrevProcessArg + ''
4245
})
4346

4447
const config: Config = {
45-
process: {
48+
certificates: {
4649
unsafeSSL: unsafeSSL,
4750
certificates: certs.filter((c) => c !== undefined && c !== null && c.length !== 0),
4851
},
@@ -55,6 +58,9 @@ const config: Config = {
5558
port: port,
5659
watchdog: !disableWatchdog,
5760
},
61+
health: {
62+
port: healthPort,
63+
},
5864
}
5965

6066
export { config, logPath, logLevel, disableWatchdog }

packages/live-status-gateway/src/connector.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
11
import { CoreHandler, CoreConfig } from './coreHandler.js'
22
import { Logger } from 'winston'
3-
import { loadDDPTLSOptions } from '@sofie-automation/server-core-integration'
43
import { PeripheralDeviceId } from '@sofie-automation/shared-lib/dist/core/model/Ids'
54
import { LiveStatusServer } from './liveStatusServer.js'
5+
import {
6+
CertificatesConfig,
7+
HealthConfig,
8+
HealthEndpoints,
9+
IConnector,
10+
loadDDPTLSOptions,
11+
stringifyError,
12+
} from '@sofie-automation/server-core-integration'
613

714
export interface Config {
8-
process: ProcessConfig
15+
certificates: CertificatesConfig
916
device: DeviceConfig
1017
core: CoreConfig
18+
health: HealthConfig
1119
}
12-
export interface ProcessConfig {
13-
/** Will cause the Node applocation to blindly accept all certificates. Not recommenced unless in local, controlled networks. */
14-
unsafeSSL: boolean
15-
/** Paths to certificates to load, for SSL-connections */
16-
certificates: string[]
17-
}
20+
1821
export interface DeviceConfig {
1922
deviceId: PeripheralDeviceId
2023
deviceToken: string
2124
}
22-
export class Connector {
25+
export class Connector implements IConnector {
26+
public initialized = false
27+
public initializedError: string | undefined = undefined
28+
2329
private coreHandler: CoreHandler | undefined
2430
private _logger: Logger
2531
private _liveStatusServer: LiveStatusServer | undefined
@@ -31,11 +37,13 @@ export class Connector {
3137
public async init(config: Config): Promise<void> {
3238
try {
3339
this._logger.info('Initializing Process...')
34-
const tlsOptions = loadDDPTLSOptions(this._logger, config.process)
40+
const tlsOptions = loadDDPTLSOptions(this._logger, config.certificates)
3541
this._logger.info('Process initialized')
3642

3743
this._logger.info('Initializing Core...')
3844
this.coreHandler = new CoreHandler(this._logger, config.device)
45+
new HealthEndpoints(this, this.coreHandler, config.health)
46+
3947
await this.coreHandler.init(config.core, tlsOptions)
4048
this._logger.info('Core initialized')
4149

@@ -51,6 +59,8 @@ export class Connector {
5159
this._logger.error(e)
5260
this._logger.error(e.stack)
5361

62+
this.initializedError = stringifyError(e)
63+
5464
try {
5565
if (this.coreHandler) {
5666
this.coreHandler.destroy().catch(this._logger.error)

packages/live-status-gateway/src/coreHandler.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
CoreOptions,
55
DDPConnectorOptions,
66
DDPTLSOptions,
7+
ICoreHandler,
78
Observer,
89
PeripheralDevicePubSub,
910
PeripheralDevicePubSubCollections,
@@ -36,7 +37,7 @@ export interface CoreConfig {
3637
/**
3738
* Represents a connection between the Gateway and Core
3839
*/
39-
export class CoreHandler {
40+
export class CoreHandler implements ICoreHandler {
4041
core!: CoreConnection<
4142
CorelibPubSubTypes & PeripheralDevicePubSubTypes,
4243
CorelibPubSubCollections & PeripheralDevicePubSubCollections
@@ -45,12 +46,7 @@ export class CoreHandler {
4546
public _observers: Array<any> = []
4647
public deviceSettings: LiveStatusGatewayConfig = {}
4748

48-
public errorReporting = false
49-
public multithreading = false
50-
public reportAllCommands = false
51-
5249
private _deviceOptions: DeviceConfig
53-
private _onConnected?: () => any
5450
private _executedFunctions = new Set<PeripheralDeviceCommandId>()
5551
private _coreConfig?: CoreConfig
5652

@@ -59,6 +55,10 @@ export class CoreHandler {
5955
private _statusInitialized = false
6056
private _statusDestroyed = false
6157

58+
public get connectedToCore(): boolean {
59+
return this.core && this.core.connected
60+
}
61+
6262
constructor(logger: Logger, deviceOptions: DeviceConfig) {
6363
this.logger = logger
6464
this._deviceOptions = deviceOptions
@@ -77,7 +77,6 @@ export class CoreHandler {
7777
this.setupObserversAndSubscriptions().catch((e) => {
7878
this.logger.error('Core Error during setupObserversAndSubscriptions:', e)
7979
})
80-
if (this._onConnected) this._onConnected()
8180
})
8281
this.core.onDisconnected(() => {
8382
this.logger.warn('Core Disconnected!')
@@ -97,7 +96,6 @@ export class CoreHandler {
9796

9897
this.logger.info('Core id: ' + this.core.deviceId)
9998
await this.setupObserversAndSubscriptions()
100-
if (this._onConnected) this._onConnected()
10199

102100
this._statusInitialized = true
103101
await this.updateCoreStatus()
@@ -180,9 +178,6 @@ export class CoreHandler {
180178

181179
return options
182180
}
183-
onConnected(fcn: () => any): void {
184-
this._onConnected = fcn
185-
}
186181

187182
onDeviceChanged(): void {
188183
const col = this.core.getCollection(PeripheralDevicePubSubCollectionsNames.peripheralDeviceForDevice)
@@ -319,7 +314,10 @@ export class CoreHandler {
319314
this.logger.info('getDevicesInfo')
320315
return []
321316
}
322-
async updateCoreStatus(): Promise<any> {
317+
getCoreStatus(): {
318+
statusCode: StatusCode
319+
messages: string[]
320+
} {
323321
let statusCode = StatusCode.GOOD
324322
const messages: Array<string> = []
325323

@@ -331,11 +329,13 @@ export class CoreHandler {
331329
statusCode = StatusCode.BAD
332330
messages.push('Shut down')
333331
}
334-
335-
return this.core.setStatus({
336-
statusCode: statusCode,
337-
messages: messages,
338-
})
332+
return {
333+
statusCode,
334+
messages,
335+
}
336+
}
337+
async updateCoreStatus(): Promise<any> {
338+
return this.core.setStatus(this.getCoreStatus())
339339
}
340340
private _getVersions() {
341341
const versions: { [packageName: string]: string } = {}

packages/live-status-gateway/src/liveStatusServer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { NotificationsHandler } from './collections/notifications/notificationsH
3434
import { NotificationsTopic } from './topics/notificationsTopic.js'
3535
import { PlaylistNotificationsHandler } from './collections/notifications/playlistNotificationsHandler.js'
3636
import { RundownNotificationsHandler } from './collections/notifications/rundownNotificationsHandler.js'
37+
import { wsConnectionsGauge } from './wsMetrics.js'
3738

3839
export interface CollectionHandlers {
3940
studioHandler: StudioHandler
@@ -153,8 +154,10 @@ export class LiveStatusServer {
153154
this._logger.info(`Closing websocket`)
154155
rootChannel.removeSubscriber(ws)
155156
this._clients.delete(ws)
157+
wsConnectionsGauge.set(this._clients.size)
156158
})
157159
this._clients.add(ws)
160+
wsConnectionsGauge.set(this._clients.size)
158161

159162
if (typeof request.url === 'string' && request.url === '/') {
160163
rootChannel.addSubscriber(ws)

packages/live-status-gateway/src/topics/root.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
SubscriptionStatus,
1111
SubscriptionName,
1212
} from '@sofie-automation/live-status-gateway-api'
13+
import { activeSubscriptionsGauge, subscriptionSubscribersGauge } from '../wsMetrics.js'
1314

1415
enum PublishMsg {
1516
ping = 'ping',
@@ -41,6 +42,7 @@ export class RootChannel extends WebSocketTopicBase implements WebSocketTopic {
4142
removeSubscriber(ws: WebSocket): void {
4243
super.removeSubscriber(ws)
4344
this._topics.forEach((h) => h.removeSubscriber(ws))
45+
this._updateSubscriptionMetrics()
4446
}
4547

4648
processMessage(ws: WebSocket, msg: object): void {
@@ -74,6 +76,16 @@ export class RootChannel extends WebSocketTopicBase implements WebSocketTopic {
7476
if (Object.values<string>(SubscriptionName).includes(channel)) this._topics.set(channel, topic)
7577
}
7678

79+
private _updateSubscriptionMetrics(): void {
80+
let total = 0
81+
for (const [name, topic] of this._topics) {
82+
const count = topic.subscriberCount
83+
subscriptionSubscribersGauge.set({ subscription: name }, count)
84+
total += count
85+
}
86+
activeSubscriptionsGauge.set(total)
87+
}
88+
7789
subscribe(ws: WebSocket, name: SubscriptionName, reqid: number): void {
7890
const topic = this._topics.get(name)
7991
const curUnsubscribed =
@@ -91,6 +103,7 @@ export class RootChannel extends WebSocketTopicBase implements WebSocketTopic {
91103
})
92104
)
93105
topic.addSubscriber(ws)
106+
this._updateSubscriptionMetrics()
94107
} else {
95108
this.sendMessage(
96109
ws,
@@ -112,6 +125,7 @@ export class RootChannel extends WebSocketTopicBase implements WebSocketTopic {
112125
const curSubscribed = topic && topic.hasSubscriber(ws) && Object.values<string>(SubscriptionName).includes(name)
113126
if (curSubscribed) {
114127
topic.removeSubscriber(ws)
128+
this._updateSubscriptionMetrics()
115129
this.sendMessage(
116130
ws,
117131
literal<SubscriptionStatusSuccess>({

packages/live-status-gateway/src/wsHandler.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export abstract class WebSocketTopicBase {
2323
: this.sendStatusToAll
2424
}
2525

26+
get subscriberCount(): number {
27+
return this._subscribers.size
28+
}
29+
2630
addSubscriber(ws: WebSocket): void {
2731
this._logger.info(`${this._name} adding a websocket subscriber`)
2832
this._subscribers.add(ws)
@@ -77,6 +81,7 @@ export abstract class WebSocketTopicBase {
7781
}
7882

7983
export interface WebSocketTopic {
84+
subscriberCount: number
8085
addSubscriber(ws: WebSocket): void
8186
hasSubscriber(ws: WebSocket): boolean
8287
removeSubscriber(ws: WebSocket): void
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { MetricsGauge } from '@sofie-automation/server-core-integration/dist/lib/prometheus'
2+
3+
export const wsConnectionsGauge = new MetricsGauge({
4+
name: 'sofie_lsg_websocket_connections',
5+
help: 'Number of open WebSocket connections',
6+
})
7+
8+
export const activeSubscriptionsGauge = new MetricsGauge({
9+
name: 'sofie_lsg_active_subscriptions_total',
10+
help: 'Total number of active subscriptions across all topics',
11+
})
12+
13+
export const subscriptionSubscribersGauge = new MetricsGauge({
14+
name: 'sofie_lsg_subscription_subscribers',
15+
help: 'Number of subscribers per subscription',
16+
labelNames: ['subscription'] as const,
17+
})

packages/mos-gateway/src/CoreMosDeviceHandler.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ import { PartialDeep } from 'type-fest'
3535
import type { CoreHandler } from './coreHandler.js'
3636
import { CoreConnectionChild } from '@sofie-automation/server-core-integration/dist/lib/CoreConnectionChild'
3737
import { Queue } from '@sofie-automation/server-core-integration/dist/lib/queue'
38+
import {
39+
mosDeviceConnectedGauge,
40+
mosMessagesFailedCounter,
41+
mosMessagesReceivedCounter,
42+
mosQueueDepthGauge,
43+
} from './mosMetrics.js'
3844

3945
function deepMatch(object: any, attrs: any, deep: boolean): boolean {
4046
const keys = Object.keys(attrs)
@@ -197,6 +203,18 @@ export class CoreMosDeviceHandler {
197203
messages: messages,
198204
})
199205
.catch((e) => this._coreParentHandler.logger.warn('Error when setting status:' + e))
206+
207+
const deviceId = this._mosDevice.idPrimary
208+
mosDeviceConnectedGauge.set(
209+
{ device_id: deviceId, connection: 'primary' },
210+
connectionStatus.PrimaryConnected ? 1 : 0
211+
)
212+
if (this._mosDevice.idSecondary) {
213+
mosDeviceConnectedGauge.set(
214+
{ device_id: deviceId, connection: 'secondary' },
215+
connectionStatus.SecondaryConnected ? 1 : 0
216+
)
217+
}
200218
}
201219
async getMachineInfo(): Promise<IMOSListMachInfo> {
202220
const info: IMOSListMachInfo = {
@@ -456,8 +474,15 @@ export class CoreMosDeviceHandler {
456474
return this.fixMosData(attr)
457475
}) as any
458476

477+
const deviceId = this._mosDevice.idPrimary
478+
const commandName = methodName as string
479+
mosMessagesReceivedCounter.inc({ device_id: deviceId, command: commandName })
480+
mosQueueDepthGauge.inc({ device_id: deviceId })
481+
459482
// Make the commands be sent sequantially:
460483
return this._messageQueue.putOnQueue(async () => {
484+
mosQueueDepthGauge.dec({ device_id: deviceId })
485+
461486
// Log info about the sent command:
462487
let msg = 'Command: ' + methodName
463488
const attr0 = attrs[0] as any | undefined
@@ -476,6 +501,7 @@ export class CoreMosDeviceHandler {
476501
const res = (this.core.coreMethods[methodName] as any)(...attrs)
477502
return res.catch((e: any) => {
478503
this._coreParentHandler.logger.info('MOS command rejected: ' + ((e && JSON.stringify(e)) || e))
504+
mosMessagesFailedCounter.inc({ device_id: deviceId, command: commandName })
479505
throw e
480506
})
481507
})

packages/mos-gateway/src/coreHandler.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ export class CoreHandler implements ICoreHandler {
3535
core: CoreConnection | undefined
3636
logger: Winston.Logger
3737
public _observers: Array<Observer<any>> = []
38-
public connectedToCore = false
3938
private _deviceOptions: DeviceConfig
4039
private _coreMosHandlers: Array<CoreMosDeviceHandler> = []
4140
private _onConnected?: () => any
@@ -44,6 +43,10 @@ export class CoreHandler implements ICoreHandler {
4443
private _executedFunctions = new Set<PeripheralDeviceCommandId>()
4544
private _coreConfig?: CoreConfig
4645

46+
public get connectedToCore(): boolean {
47+
return !!this.core && this.core.connected
48+
}
49+
4750
public static async create(
4851
logger: Winston.Logger,
4952
config: CoreConfig,
@@ -67,12 +70,10 @@ export class CoreHandler implements ICoreHandler {
6770

6871
this.core.onConnected(() => {
6972
this.logger.info('Core Connected!')
70-
this.connectedToCore = true
7173
if (this._isInitialized) this.onConnectionRestored()
7274
})
7375
this.core.onDisconnected(() => {
7476
this.logger.info('Core Disconnected!')
75-
this.connectedToCore = false
7677
})
7778
this.core.onError((err) => {
7879
this.logger.error('Core Error: ' + (typeof err === 'string' ? err : err.message || err.toString()))

0 commit comments

Comments
 (0)