Commit 84a8934c authored by smartcontracts's avatar smartcontracts Committed by GitHub

feat(cmn): have BSV2 export metadata metrics (#2736)

Updates BaseServiceV2 to require a version string. Also now exports
parameters in a synthetic metric but ignores any parameters explicitly
marked secret.
parent d9e39931
---
'@eth-optimism/common-ts': minor
'@eth-optimism/drippie-mon': minor
'@eth-optimism/fault-detector': minor
'@eth-optimism/message-relayer': minor
'@eth-optimism/replica-healthcheck': minor
---
BaseServiceV2 exposes service name and version as standard synthetic metric
...@@ -13,7 +13,7 @@ import bodyParser from 'body-parser' ...@@ -13,7 +13,7 @@ import bodyParser from 'body-parser'
import morgan from 'morgan' import morgan from 'morgan'
import { Logger } from '../common/logger' import { Logger } from '../common/logger'
import { Metric } from './metrics' import { Metric, Gauge, Counter } from './metrics'
import { validators } from './validators' import { validators } from './validators'
export type Options = { export type Options = {
...@@ -31,6 +31,7 @@ export type OptionsSpec<TOptions extends Options> = { ...@@ -31,6 +31,7 @@ export type OptionsSpec<TOptions extends Options> = {
validator: (spec?: Spec<TOptions[P]>) => ValidatorSpec<TOptions[P]> validator: (spec?: Spec<TOptions[P]>) => ValidatorSpec<TOptions[P]>
desc: string desc: string
default?: TOptions[P] default?: TOptions[P]
secret?: boolean
} }
} }
...@@ -38,6 +39,11 @@ export type MetricsV2 = { ...@@ -38,6 +39,11 @@ export type MetricsV2 = {
[key: string]: Metric [key: string]: Metric
} }
export type StandardMetrics = {
metadata: Gauge
unhandledErrors: Counter
}
export type MetricsSpec<TMetrics extends MetricsV2> = { export type MetricsSpec<TMetrics extends MetricsV2> = {
[P in keyof Required<TMetrics>]: { [P in keyof Required<TMetrics>]: {
type: new (configuration: any) => TMetrics[P] type: new (configuration: any) => TMetrics[P]
...@@ -99,7 +105,7 @@ export abstract class BaseServiceV2< ...@@ -99,7 +105,7 @@ export abstract class BaseServiceV2<
/** /**
* Metrics. * Metrics.
*/ */
protected readonly metrics: TMetrics protected readonly metrics: TMetrics & StandardMetrics
/** /**
* Registry for prometheus metrics. * Registry for prometheus metrics.
...@@ -137,6 +143,7 @@ export abstract class BaseServiceV2< ...@@ -137,6 +143,7 @@ export abstract class BaseServiceV2<
*/ */
constructor(params: { constructor(params: {
name: string name: string
version: string
optionsSpec: OptionsSpec<TOptions> optionsSpec: OptionsSpec<TOptions>
metricsSpec: MetricsSpec<TMetrics> metricsSpec: MetricsSpec<TMetrics>
options?: Partial<TOptions> options?: Partial<TOptions>
...@@ -170,6 +177,31 @@ export abstract class BaseServiceV2< ...@@ -170,6 +177,31 @@ export abstract class BaseServiceV2<
}, },
} }
// List of options that can safely be logged.
const publicOptionNames = Object.entries(params.optionsSpec)
.filter(([, spec]) => {
return spec.secret !== true
})
.map(([key]) => {
return key
})
// Add default metrics to metrics spec.
;(params.metricsSpec as any) = {
...(params.metricsSpec || {}),
// Users cannot set these options.
metadata: {
type: Gauge,
desc: 'Service metadata',
labels: ['name', 'version'].concat(publicOptionNames),
},
unhandledErrors: {
type: Counter,
desc: 'Unhandled errors',
},
}
/** /**
* Special snake_case function which accounts for the common strings "L1" and "L2" which would * Special snake_case function which accounts for the common strings "L1" and "L2" which would
* normally be split into "L_1" and "L_2" by the snake_case function. * normally be split into "L_1" and "L_2" by the snake_case function.
...@@ -274,7 +306,7 @@ export abstract class BaseServiceV2< ...@@ -274,7 +306,7 @@ export abstract class BaseServiceV2<
labelNames: spec.labels || [], labelNames: spec.labels || [],
}) })
return acc return acc
}, {}) as TMetrics }, {}) as TMetrics & StandardMetrics
// Create the metrics server. // Create the metrics server.
this.metricsRegistry = prometheus.register this.metricsRegistry = prometheus.register
...@@ -309,6 +341,19 @@ export abstract class BaseServiceV2< ...@@ -309,6 +341,19 @@ export abstract class BaseServiceV2<
// Handle stop signals. // Handle stop signals.
process.on('SIGTERM', stop) process.on('SIGTERM', stop)
process.on('SIGINT', stop) process.on('SIGINT', stop)
// Set metadata synthetic metric.
this.metrics.metadata.set(
{
name: params.name,
version: params.version,
...publicOptionNames.reduce((acc, key) => {
acc[key] = config.str(key)
return acc
}, {}),
},
1
)
} }
/** /**
...@@ -330,21 +375,7 @@ export abstract class BaseServiceV2< ...@@ -330,21 +375,7 @@ export abstract class BaseServiceV2<
app.use(bodyParser.urlencoded({ extended: true })) app.use(bodyParser.urlencoded({ extended: true }))
// Logging. // Logging.
app.use( app.use(morgan('short'))
morgan((tokens, req, res) => {
return [
tokens.method(req, res),
tokens.url(req, res),
tokens.status(req, res),
JSON.stringify(req.body),
'\n',
tokens.res(req, res, 'content-length'),
'-',
tokens['response-time'](req, res),
'ms',
].join(' ')
})
)
// Metrics. // Metrics.
// Will expose a /metrics endpoint by default. // Will expose a /metrics endpoint by default.
...@@ -397,6 +428,7 @@ export abstract class BaseServiceV2< ...@@ -397,6 +428,7 @@ export abstract class BaseServiceV2<
try { try {
await this.main() await this.main()
} catch (err) { } catch (err) {
this.metrics.unhandledErrors.inc()
this.logger.error('caught an unhandled exception', { this.logger.error('caught an unhandled exception', {
message: err.message, message: err.message,
stack: err.stack, stack: err.stack,
......
...@@ -14,7 +14,6 @@ type DrippieMonOptions = { ...@@ -14,7 +14,6 @@ type DrippieMonOptions = {
} }
type DrippieMonMetrics = { type DrippieMonMetrics = {
metadata: Gauge
isExecutable: Gauge isExecutable: Gauge
executedDripCount: Gauge executedDripCount: Gauge
unexpectedRpcErrors: Counter unexpectedRpcErrors: Counter
...@@ -31,6 +30,8 @@ export class DrippieMonService extends BaseServiceV2< ...@@ -31,6 +30,8 @@ export class DrippieMonService extends BaseServiceV2<
> { > {
constructor(options?: Partial<DrippieMonOptions>) { constructor(options?: Partial<DrippieMonOptions>) {
super({ super({
// eslint-disable-next-line @typescript-eslint/no-var-requires
version: require('../package.json').version,
name: 'drippie-mon', name: 'drippie-mon',
loop: true, loop: true,
loopIntervalMs: 60_000, loopIntervalMs: 60_000,
...@@ -39,6 +40,7 @@ export class DrippieMonService extends BaseServiceV2< ...@@ -39,6 +40,7 @@ export class DrippieMonService extends BaseServiceV2<
rpc: { rpc: {
validator: validators.provider, validator: validators.provider,
desc: 'Provider for network where Drippie is deployed', desc: 'Provider for network where Drippie is deployed',
secret: true,
}, },
drippieAddress: { drippieAddress: {
validator: validators.str, validator: validators.str,
...@@ -46,11 +48,6 @@ export class DrippieMonService extends BaseServiceV2< ...@@ -46,11 +48,6 @@ export class DrippieMonService extends BaseServiceV2<
}, },
}, },
metricsSpec: { metricsSpec: {
metadata: {
type: Gauge,
desc: 'Drippie Monitor metadata',
labels: ['version', 'address'],
},
isExecutable: { isExecutable: {
type: Gauge, type: Gauge,
desc: 'Whether or not the drip is currently executable', desc: 'Whether or not the drip is currently executable',
...@@ -76,15 +73,6 @@ export class DrippieMonService extends BaseServiceV2< ...@@ -76,15 +73,6 @@ export class DrippieMonService extends BaseServiceV2<
DrippieArtifact.abi, DrippieArtifact.abi,
this.options.rpc this.options.rpc
) )
this.metrics.metadata.set(
{
// eslint-disable-next-line @typescript-eslint/no-var-requires
version: require('../package.json').version,
address: this.options.drippieAddress,
},
1
)
} }
protected async main(): Promise<void> { protected async main(): Promise<void> {
......
...@@ -32,6 +32,8 @@ type State = { ...@@ -32,6 +32,8 @@ type State = {
export class FaultDetector extends BaseServiceV2<Options, Metrics, State> { export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
constructor(options?: Partial<Options>) { constructor(options?: Partial<Options>) {
super({ super({
// eslint-disable-next-line @typescript-eslint/no-var-requires
version: require('../package.json').version,
name: 'fault-detector', name: 'fault-detector',
loop: true, loop: true,
loopIntervalMs: 1000, loopIntervalMs: 1000,
...@@ -40,10 +42,12 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> { ...@@ -40,10 +42,12 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
l1RpcProvider: { l1RpcProvider: {
validator: validators.provider, validator: validators.provider,
desc: 'Provider for interacting with L1', desc: 'Provider for interacting with L1',
secret: true,
}, },
l2RpcProvider: { l2RpcProvider: {
validator: validators.provider, validator: validators.provider,
desc: 'Provider for interacting with L2', desc: 'Provider for interacting with L2',
secret: true,
}, },
startBatchIndex: { startBatchIndex: {
validator: validators.num, validator: validators.num,
......
...@@ -37,20 +37,25 @@ export class MessageRelayerService extends BaseServiceV2< ...@@ -37,20 +37,25 @@ export class MessageRelayerService extends BaseServiceV2<
> { > {
constructor(options?: Partial<MessageRelayerOptions>) { constructor(options?: Partial<MessageRelayerOptions>) {
super({ super({
name: 'Message_Relayer', // eslint-disable-next-line @typescript-eslint/no-var-requires
version: require('../package.json').version,
name: 'message-relayer',
options, options,
optionsSpec: { optionsSpec: {
l1RpcProvider: { l1RpcProvider: {
validator: validators.provider, validator: validators.provider,
desc: 'Provider for interacting with L1.', desc: 'Provider for interacting with L1.',
secret: true,
}, },
l2RpcProvider: { l2RpcProvider: {
validator: validators.provider, validator: validators.provider,
desc: 'Provider for interacting with L2.', desc: 'Provider for interacting with L2.',
secret: true,
}, },
l1Wallet: { l1Wallet: {
validator: validators.wallet, validator: validators.wallet,
desc: 'Wallet used to interact with L1.', desc: 'Wallet used to interact with L1.',
secret: true,
}, },
fromL2TransactionIndex: { fromL2TransactionIndex: {
validator: validators.num, validator: validators.num,
......
...@@ -32,17 +32,21 @@ export class HealthcheckService extends BaseServiceV2< ...@@ -32,17 +32,21 @@ export class HealthcheckService extends BaseServiceV2<
> { > {
constructor(options?: Partial<HealthcheckOptions>) { constructor(options?: Partial<HealthcheckOptions>) {
super({ super({
name: 'Healthcheck', // eslint-disable-next-line @typescript-eslint/no-var-requires
version: require('../package.json').version,
name: 'healthcheck',
loopIntervalMs: 5000, loopIntervalMs: 5000,
options, options,
optionsSpec: { optionsSpec: {
referenceRpcProvider: { referenceRpcProvider: {
validator: validators.provider, validator: validators.provider,
desc: 'Provider for interacting with L1', desc: 'Provider for interacting with L1',
secret: true,
}, },
targetRpcProvider: { targetRpcProvider: {
validator: validators.provider, validator: validators.provider,
desc: 'Provider for interacting with L2', desc: 'Provider for interacting with L2',
secret: true,
}, },
onDivergenceWaitMs: { onDivergenceWaitMs: {
validator: validators.num, validator: validators.num,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment