Commit c68fb1e2 authored by mergify[bot]'s avatar mergify[bot] Committed by GitHub

Merge pull request #4375 from ethereum-optimism/sc/cmn-clean-base-service

feat(cmn): clean up BaseServiceV2 options
parents 300d2fdf 9b289185
---
'@eth-optimism/common-ts': minor
'@eth-optimism/drippie-mon': minor
'@eth-optimism/fault-detector': minor
'@eth-optimism/replica-healthcheck': minor
'@eth-optimism/data-transport-layer': patch
---
Refactors BaseServiceV2 slightly, merges standard options with regular options
...@@ -3,61 +3,36 @@ import { Server } from 'net' ...@@ -3,61 +3,36 @@ import { Server } from 'net'
import Config from 'bcfg' import Config from 'bcfg'
import * as dotenv from 'dotenv' import * as dotenv from 'dotenv'
import { Command, Option } from 'commander' import { Command, Option } from 'commander'
import { ValidatorSpec, Spec, cleanEnv } from 'envalid' import { cleanEnv } from 'envalid'
import snakeCase from 'lodash/snakeCase' import snakeCase from 'lodash/snakeCase'
import express, { Router } from 'express' import express from 'express'
import prometheus, { Registry } from 'prom-client' import prometheus, { Registry } from 'prom-client'
import promBundle from 'express-prom-bundle' import promBundle from 'express-prom-bundle'
import bodyParser from 'body-parser' import bodyParser from 'body-parser'
import morgan from 'morgan' import morgan from 'morgan'
import { Logger, LogLevel } from '../common/logger' import { ExpressRouter } from './router'
import { Metric, Gauge, Counter } from './metrics' import { Logger } from '../common/logger'
import { validators } from './validators' import {
Metrics,
export type Options = { MetricsSpec,
[key: string]: any StandardMetrics,
} makeStdMetricsSpec,
} from './metrics'
export type StandardOptions = { import {
loopIntervalMs?: number Options,
port?: number OptionsSpec,
hostname?: string StandardOptions,
logLevel?: LogLevel stdOptionsSpec,
} getPublicOptions,
} from './options'
export type OptionsSpec<TOptions extends Options> = {
[P in keyof Required<TOptions>]: {
validator: (spec?: Spec<TOptions[P]>) => ValidatorSpec<TOptions[P]>
desc: string
default?: TOptions[P]
public?: boolean
}
}
export type MetricsV2 = Record<any, Metric>
export type StandardMetrics = {
metadata: Gauge
unhandledErrors: Counter
}
export type MetricsSpec<TMetrics extends MetricsV2> = {
[P in keyof Required<TMetrics>]: {
type: new (configuration: any) => TMetrics[P]
desc: string
labels?: string[]
}
}
export type ExpressRouter = Router
/** /**
* BaseServiceV2 is an advanced but simple base class for long-running TypeScript services. * BaseServiceV2 is an advanced but simple base class for long-running TypeScript services.
*/ */
export abstract class BaseServiceV2< export abstract class BaseServiceV2<
TOptions extends Options, TOptions extends Options,
TMetrics extends MetricsV2, TMetrics extends Metrics,
TServiceState TServiceState
> { > {
/** /**
...@@ -133,17 +108,13 @@ export abstract class BaseServiceV2< ...@@ -133,17 +108,13 @@ export abstract class BaseServiceV2<
/** /**
* @param params Options for the construction of the service. * @param params Options for the construction of the service.
* @param params.name Name for the service. This name will determine the prefix used for logging, * @param params.name Name for the service.
* metrics, and loading environment variables. * @param params.optionsSpec Settings for input options.
* @param params.optionsSpec Settings for input options. You must specify at least a * @param params.metricsSpec Settings that define which metrics are collected.
* description for each option.
* @param params.metricsSpec Settings that define which metrics are collected. All metrics that
* you plan to collect must be defined within this object.
* @param params.options Options to pass to the service. * @param params.options Options to pass to the service.
* @param params.loops Whether or not the service should loop. Defaults to true. * @param params.loops Whether or not the service should loop. Defaults to true.
* @param params.loopIntervalMs Loop interval in milliseconds. Defaults to zero. * @param params.useEnv Whether or not to load options from the environment. Defaults to true.
* @param params.port Port for the app server. Defaults to 7300. * @param params.useArgv Whether or not to load options from the command line. Defaults to true.
* @param params.hostname Hostname for the app server. Defaults to 0.0.0.0.
*/ */
constructor( constructor(
private readonly params: { private readonly params: {
...@@ -151,73 +122,23 @@ export abstract class BaseServiceV2< ...@@ -151,73 +122,23 @@ export abstract class BaseServiceV2<
version: string version: string
optionsSpec: OptionsSpec<TOptions> optionsSpec: OptionsSpec<TOptions>
metricsSpec: MetricsSpec<TMetrics> metricsSpec: MetricsSpec<TMetrics>
options?: Partial<TOptions> options?: Partial<TOptions & StandardOptions>
loop?: boolean loop?: boolean
loopIntervalMs?: number
port?: number
hostname?: string
logLevel?: LogLevel
} }
) { ) {
this.loop = params.loop !== undefined ? params.loop : true this.loop = params.loop !== undefined ? params.loop : true
this.state = {} as TServiceState this.state = {} as TServiceState
const stdOptionsSpec: OptionsSpec<StandardOptions> = { // Add standard options spec to user options spec.
loopIntervalMs: {
validator: validators.num,
desc: 'Loop interval in milliseconds',
default: params.loopIntervalMs || 0,
public: true,
},
port: {
validator: validators.num,
desc: 'Port for the app server',
default: params.port || 7300,
public: true,
},
hostname: {
validator: validators.str,
desc: 'Hostname for the app server',
default: params.hostname || '0.0.0.0',
public: true,
},
logLevel: {
validator: validators.logLevel,
desc: 'Log level',
default: params.logLevel || 'debug',
public: true,
},
}
// Add default options to options spec.
;(params.optionsSpec as any) = { ;(params.optionsSpec as any) = {
...(params.optionsSpec || {}), ...params.optionsSpec,
...stdOptionsSpec, ...stdOptionsSpec,
} }
// List of options that can safely be logged.
const publicOptionNames = Object.entries(params.optionsSpec)
.filter(([, spec]) => {
return spec.public
})
.map(([key]) => {
return key
})
// Add default metrics to metrics spec. // Add default metrics to metrics spec.
;(params.metricsSpec as any) = { ;(params.metricsSpec as any) = {
...(params.metricsSpec || {}), ...params.metricsSpec,
...makeStdMetricsSpec(params.optionsSpec),
// Users cannot set these options.
metadata: {
type: Gauge,
desc: 'Service metadata',
labels: ['name', 'version'].concat(publicOptionNames),
},
unhandledErrors: {
type: Counter,
desc: 'Unhandled errors',
},
} }
/** /**
...@@ -328,12 +249,12 @@ export abstract class BaseServiceV2< ...@@ -328,12 +249,12 @@ export abstract class BaseServiceV2<
this.hostname = this.options.hostname this.hostname = this.options.hostname
// Set up everything else. // Set up everything else.
this.healthy = true
this.loopIntervalMs = this.options.loopIntervalMs this.loopIntervalMs = this.options.loopIntervalMs
this.logger = new Logger({ this.logger = new Logger({
name: params.name, name: params.name,
level: this.options.logLevel, level: this.options.logLevel,
}) })
this.healthy = true
// Gracefully handle stop signals. // Gracefully handle stop signals.
const maxSignalCount = 3 const maxSignalCount = 3
...@@ -364,7 +285,7 @@ export abstract class BaseServiceV2< ...@@ -364,7 +285,7 @@ export abstract class BaseServiceV2<
{ {
name: params.name, name: params.name,
version: params.version, version: params.version,
...publicOptionNames.reduce((acc, key) => { ...getPublicOptions(params.optionsSpec).reduce((acc, key) => {
if (key in stdOptionsSpec) { if (key in stdOptionsSpec) {
acc[key] = this.options[key].toString() acc[key] = this.options[key].toString()
} else { } else {
......
/* Imports: Internal */ /* Imports: Internal */
import { Logger } from '../common/logger' import { Logger } from '../common/logger'
import { Metrics } from '../common/metrics' import { LegacyMetrics } from '../common/metrics'
type OptionSettings<TOptions> = { type OptionSettings<TOptions> = {
[P in keyof TOptions]?: { [P in keyof TOptions]?: {
...@@ -11,7 +11,7 @@ type OptionSettings<TOptions> = { ...@@ -11,7 +11,7 @@ type OptionSettings<TOptions> = {
type BaseServiceOptions<T> = T & { type BaseServiceOptions<T> = T & {
logger?: Logger logger?: Logger
metrics?: Metrics metrics?: LegacyMetrics
} }
/** /**
...@@ -22,7 +22,7 @@ export class BaseService<T> { ...@@ -22,7 +22,7 @@ export class BaseService<T> {
protected name: string protected name: string
protected options: T protected options: T
protected logger: Logger protected logger: Logger
protected metrics: Metrics protected metrics: LegacyMetrics
protected initialized = false protected initialized = false
protected running = false protected running = false
......
...@@ -2,3 +2,5 @@ export * from './base-service' ...@@ -2,3 +2,5 @@ export * from './base-service'
export * from './base-service-v2' export * from './base-service-v2'
export * from './validators' export * from './validators'
export * from './metrics' export * from './metrics'
export * from './options'
export * from './router'
...@@ -5,8 +5,59 @@ import { ...@@ -5,8 +5,59 @@ import {
Summary as PSummary, Summary as PSummary,
} from 'prom-client' } from 'prom-client'
import { OptionsSpec, getPublicOptions } from './options'
// Prometheus metrics re-exported.
export class Gauge extends PGauge<string> {} export class Gauge extends PGauge<string> {}
export class Counter extends PCounter<string> {} export class Counter extends PCounter<string> {}
export class Histogram extends PHistogram<string> {} export class Histogram extends PHistogram<string> {}
export class Summary extends PSummary<string> {} export class Summary extends PSummary<string> {}
export type Metric = Gauge | Counter | Histogram | Summary export type Metric = Gauge | Counter | Histogram | Summary
/**
* Metrics that are available for a given service.
*/
export type Metrics = Record<any, Metric>
/**
* Specification for metrics.
*/
export type MetricsSpec<TMetrics extends Metrics> = {
[P in keyof Required<TMetrics>]: {
type: new (configuration: any) => TMetrics[P]
desc: string
labels?: string[]
}
}
/**
* Standard metrics that are always available.
*/
export type StandardMetrics = {
metadata: Gauge
unhandledErrors: Counter
}
/**
* Generates a standard metrics specification. Needs to be a function because the labels for
* service metadata are dynamic dependent on the list of given options.
*
* @param options Options to include in the service metadata.
* @returns Metrics specification.
*/
export const makeStdMetricsSpec = (
optionsSpec: OptionsSpec<any>
): MetricsSpec<StandardMetrics> => {
return {
// Users cannot set these options.
metadata: {
type: Gauge,
desc: 'Service metadata',
labels: ['name', 'version'].concat(getPublicOptions(optionsSpec)),
},
unhandledErrors: {
type: Counter,
desc: 'Unhandled errors',
},
}
}
import { ValidatorSpec, Spec } from 'envalid'
import { LogLevel } from '../common/logger'
import { validators } from './validators'
/**
* Options for a service.
*/
export type Options = {
[key: string]: any
}
/**
* Specification for options.
*/
export type OptionsSpec<TOptions extends Options> = {
[P in keyof Required<TOptions>]: {
validator: (spec?: Spec<TOptions[P]>) => ValidatorSpec<TOptions[P]>
desc: string
default?: TOptions[P]
public?: boolean
}
}
/**
* Standard options shared by all services.
*/
export type StandardOptions = {
loopIntervalMs?: number
port?: number
hostname?: string
logLevel?: LogLevel
}
/**
* Specification for standard options.
*/
export const stdOptionsSpec: OptionsSpec<StandardOptions> = {
loopIntervalMs: {
validator: validators.num,
desc: 'Loop interval in milliseconds, only applies if service is set to loop',
default: 0,
public: true,
},
port: {
validator: validators.num,
desc: 'Port for the app server',
default: 7300,
public: true,
},
hostname: {
validator: validators.str,
desc: 'Hostname for the app server',
default: '0.0.0.0',
public: true,
},
logLevel: {
validator: validators.logLevel,
desc: 'Log level',
default: 'debug',
public: true,
},
}
/**
* Gets the list of public option names from an options specification.
*
* @param optionsSpec Options specification.
* @returns List of public option names.
*/
export const getPublicOptions = (
optionsSpec: OptionsSpec<Options>
): string[] => {
return Object.keys(optionsSpec).filter((key) => {
return optionsSpec[key].public
})
}
import { Router } from 'express'
/**
* Express router re-exported.
*/
export type ExpressRouter = Router
...@@ -14,7 +14,7 @@ export interface MetricsOptions { ...@@ -14,7 +14,7 @@ export interface MetricsOptions {
labels?: Object labels?: Object
} }
export class Metrics { export class LegacyMetrics {
options: MetricsOptions options: MetricsOptions
client: typeof prometheus client: typeof prometheus
registry: Registry registry: Registry
......
/* Imports: External */ /* Imports: External */
import { fromHexString, getChainId, sleep } from '@eth-optimism/core-utils' import { fromHexString, getChainId, sleep } from '@eth-optimism/core-utils'
import { BaseService, Metrics } from '@eth-optimism/common-ts' import { BaseService, LegacyMetrics } from '@eth-optimism/common-ts'
import { TypedEvent } from '@eth-optimism/contracts/dist/types/common' import { TypedEvent } from '@eth-optimism/contracts/dist/types/common'
import { BaseProvider, StaticJsonRpcProvider } from '@ethersproject/providers' import { BaseProvider, StaticJsonRpcProvider } from '@ethersproject/providers'
import { LevelUp } from 'levelup' import { LevelUp } from 'levelup'
...@@ -31,7 +31,7 @@ interface L1IngestionMetrics { ...@@ -31,7 +31,7 @@ interface L1IngestionMetrics {
const registerMetrics = ({ const registerMetrics = ({
client, client,
registry, registry,
}: Metrics): L1IngestionMetrics => ({ }: LegacyMetrics): L1IngestionMetrics => ({
highestSyncedL1Block: new client.Gauge({ highestSyncedL1Block: new client.Gauge({
name: 'data_transport_layer_highest_synced_l1_block', name: 'data_transport_layer_highest_synced_l1_block',
help: 'Highest Synced L1 Block Number', help: 'Highest Synced L1 Block Number',
...@@ -52,7 +52,7 @@ const registerMetrics = ({ ...@@ -52,7 +52,7 @@ const registerMetrics = ({
export interface L1IngestionServiceOptions export interface L1IngestionServiceOptions
extends L1DataTransportServiceOptions { extends L1DataTransportServiceOptions {
db: LevelUp db: LevelUp
metrics: Metrics metrics: LegacyMetrics
} }
const optionSettings = { const optionSettings = {
......
/* Imports: External */ /* Imports: External */
import { BaseService, Metrics } from '@eth-optimism/common-ts' import { BaseService, LegacyMetrics } from '@eth-optimism/common-ts'
import { StaticJsonRpcProvider } from '@ethersproject/providers' import { StaticJsonRpcProvider } from '@ethersproject/providers'
import { getChainId, sleep, toRpcHexString } from '@eth-optimism/core-utils' import { getChainId, sleep, toRpcHexString } from '@eth-optimism/core-utils'
import { BigNumber } from 'ethers' import { BigNumber } from 'ethers'
...@@ -22,7 +22,7 @@ interface L2IngestionMetrics { ...@@ -22,7 +22,7 @@ interface L2IngestionMetrics {
const registerMetrics = ({ const registerMetrics = ({
client, client,
registry, registry,
}: Metrics): L2IngestionMetrics => ({ }: LegacyMetrics): L2IngestionMetrics => ({
highestSyncedL2Block: new client.Gauge({ highestSyncedL2Block: new client.Gauge({
name: 'data_transport_layer_highest_synced_l2_block', name: 'data_transport_layer_highest_synced_l2_block',
help: 'Highest Synced L2 Block Number', help: 'Highest Synced L2 Block Number',
......
/* Imports: External */ /* Imports: External */
import { BaseService, Metrics } from '@eth-optimism/common-ts' import { BaseService, LegacyMetrics } from '@eth-optimism/common-ts'
import { LevelUp } from 'levelup' import { LevelUp } from 'levelup'
import level from 'level' import level from 'level'
import { Counter } from 'prom-client' import { Counter } from 'prom-client'
...@@ -27,7 +27,7 @@ export interface L1DataTransportServiceOptions { ...@@ -27,7 +27,7 @@ export interface L1DataTransportServiceOptions {
l2RpcProviderUser?: string l2RpcProviderUser?: string
l2RpcProviderPassword?: string l2RpcProviderPassword?: string
l1SyncShutoffBlock?: number l1SyncShutoffBlock?: number
metrics?: Metrics metrics?: LegacyMetrics
dbPath: string dbPath: string
logsPerPollingInterval: number logsPerPollingInterval: number
pollingInterval: number pollingInterval: number
...@@ -66,7 +66,7 @@ export class L1DataTransportService extends BaseService<L1DataTransportServiceOp ...@@ -66,7 +66,7 @@ export class L1DataTransportService extends BaseService<L1DataTransportServiceOp
l1IngestionService?: L1IngestionService l1IngestionService?: L1IngestionService
l2IngestionService?: L2IngestionService l2IngestionService?: L2IngestionService
l1TransportServer: L1TransportServer l1TransportServer: L1TransportServer
metrics: Metrics metrics: LegacyMetrics
failureCounter: Counter<string> failureCounter: Counter<string>
} = {} as any } = {} as any
...@@ -81,7 +81,7 @@ export class L1DataTransportService extends BaseService<L1DataTransportServiceOp ...@@ -81,7 +81,7 @@ export class L1DataTransportService extends BaseService<L1DataTransportServiceOp
this.logger.info(`L2 chain ID is: ${this.options.l2ChainId}`) this.logger.info(`L2 chain ID is: ${this.options.l2ChainId}`)
this.logger.info(`BSS HF1 will activate at: ${bssHf1Index}`) this.logger.info(`BSS HF1 will activate at: ${bssHf1Index}`)
this.state.metrics = new Metrics({ this.state.metrics = new LegacyMetrics({
labels: { labels: {
environment: this.options.nodeEnv, environment: this.options.nodeEnv,
network: this.options.ethNetworkName, network: this.options.ethNetworkName,
......
/* Imports: External */ /* Imports: External */
import { BaseService, Logger, Metrics } from '@eth-optimism/common-ts' import { BaseService, Logger, LegacyMetrics } from '@eth-optimism/common-ts'
import express, { Request, Response } from 'express' import express, { Request, Response } from 'express'
import promBundle from 'express-prom-bundle' import promBundle from 'express-prom-bundle'
import cors from 'cors' import cors from 'cors'
...@@ -27,7 +27,7 @@ import { L1DataTransportServiceOptions } from '../main/service' ...@@ -27,7 +27,7 @@ import { L1DataTransportServiceOptions } from '../main/service'
export interface L1TransportServerOptions export interface L1TransportServerOptions
extends L1DataTransportServiceOptions { extends L1DataTransportServiceOptions {
db: LevelUp db: LevelUp
metrics: Metrics metrics: LegacyMetrics
} }
const optionSettings = { const optionSettings = {
......
import { import {
BaseServiceV2, BaseServiceV2,
StandardOptions,
Gauge, Gauge,
Counter, Counter,
validators, validators,
...@@ -30,13 +31,15 @@ export class DrippieMonService extends BaseServiceV2< ...@@ -30,13 +31,15 @@ export class DrippieMonService extends BaseServiceV2<
DrippieMonMetrics, DrippieMonMetrics,
DrippieMonState DrippieMonState
> { > {
constructor(options?: Partial<DrippieMonOptions>) { constructor(options?: Partial<DrippieMonOptions & StandardOptions>) {
super({ super({
version, version,
name: 'drippie-mon', name: 'drippie-mon',
loop: true, loop: true,
loopIntervalMs: 60_000, options: {
options, loopIntervalMs: 60_000,
...options,
},
optionsSpec: { optionsSpec: {
rpc: { rpc: {
validator: validators.provider, validator: validators.provider,
......
import { import {
BaseServiceV2, BaseServiceV2,
StandardOptions,
ExpressRouter, ExpressRouter,
Gauge, Gauge,
validators, validators,
...@@ -39,13 +40,15 @@ type State = { ...@@ -39,13 +40,15 @@ 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 & StandardOptions>) {
super({ super({
version, version,
name: 'fault-detector', name: 'fault-detector',
loop: true, loop: true,
loopIntervalMs: 1000, options: {
options, loopIntervalMs: 1000,
...options,
},
optionsSpec: { optionsSpec: {
l1RpcProvider: { l1RpcProvider: {
validator: validators.provider, validator: validators.provider,
......
import { Provider, Block } from '@ethersproject/abstract-provider' import { Provider, Block } from '@ethersproject/abstract-provider'
import { import {
BaseServiceV2, BaseServiceV2,
StandardOptions,
Counter, Counter,
Gauge, Gauge,
validators, validators,
...@@ -32,12 +33,14 @@ export class HealthcheckService extends BaseServiceV2< ...@@ -32,12 +33,14 @@ export class HealthcheckService extends BaseServiceV2<
HealthcheckMetrics, HealthcheckMetrics,
HealthcheckState HealthcheckState
> { > {
constructor(options?: Partial<HealthcheckOptions>) { constructor(options?: Partial<HealthcheckOptions & StandardOptions>) {
super({ super({
version, version,
name: 'healthcheck', name: 'healthcheck',
loopIntervalMs: 5000, options: {
options, loopIntervalMs: 5000,
...options,
},
optionsSpec: { optionsSpec: {
referenceRpcProvider: { referenceRpcProvider: {
validator: validators.provider, validator: validators.provider,
......
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