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'
import Config from 'bcfg'
import * as dotenv from 'dotenv'
import { Command, Option } from 'commander'
import { ValidatorSpec, Spec, cleanEnv } from 'envalid'
import { cleanEnv } from 'envalid'
import snakeCase from 'lodash/snakeCase'
import express, { Router } from 'express'
import express from 'express'
import prometheus, { Registry } from 'prom-client'
import promBundle from 'express-prom-bundle'
import bodyParser from 'body-parser'
import morgan from 'morgan'
import { Logger, LogLevel } from '../common/logger'
import { Metric, Gauge, Counter } from './metrics'
import { validators } from './validators'
export type Options = {
[key: string]: any
}
export type StandardOptions = {
loopIntervalMs?: number
port?: number
hostname?: string
logLevel?: LogLevel
}
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
import { ExpressRouter } from './router'
import { Logger } from '../common/logger'
import {
Metrics,
MetricsSpec,
StandardMetrics,
makeStdMetricsSpec,
} from './metrics'
import {
Options,
OptionsSpec,
StandardOptions,
stdOptionsSpec,
getPublicOptions,
} from './options'
/**
* BaseServiceV2 is an advanced but simple base class for long-running TypeScript services.
*/
export abstract class BaseServiceV2<
TOptions extends Options,
TMetrics extends MetricsV2,
TMetrics extends Metrics,
TServiceState
> {
/**
......@@ -133,17 +108,13 @@ export abstract class BaseServiceV2<
/**
* @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,
* metrics, and loading environment variables.
* @param params.optionsSpec Settings for input options. You must specify at least a
* 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.name Name for the service.
* @param params.optionsSpec Settings for input options.
* @param params.metricsSpec Settings that define which metrics are collected.
* @param params.options Options to pass to the service.
* @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.port Port for the app server. Defaults to 7300.
* @param params.hostname Hostname for the app server. Defaults to 0.0.0.0.
* @param params.useEnv Whether or not to load options from the environment. Defaults to true.
* @param params.useArgv Whether or not to load options from the command line. Defaults to true.
*/
constructor(
private readonly params: {
......@@ -151,73 +122,23 @@ export abstract class BaseServiceV2<
version: string
optionsSpec: OptionsSpec<TOptions>
metricsSpec: MetricsSpec<TMetrics>
options?: Partial<TOptions>
options?: Partial<TOptions & StandardOptions>
loop?: boolean
loopIntervalMs?: number
port?: number
hostname?: string
logLevel?: LogLevel
}
) {
this.loop = params.loop !== undefined ? params.loop : true
this.state = {} as TServiceState
const stdOptionsSpec: OptionsSpec<StandardOptions> = {
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.
// Add standard options spec to user options spec.
;(params.optionsSpec as any) = {
...(params.optionsSpec || {}),
...params.optionsSpec,
...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.
;(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',
},
...params.metricsSpec,
...makeStdMetricsSpec(params.optionsSpec),
}
/**
......@@ -328,12 +249,12 @@ export abstract class BaseServiceV2<
this.hostname = this.options.hostname
// Set up everything else.
this.healthy = true
this.loopIntervalMs = this.options.loopIntervalMs
this.logger = new Logger({
name: params.name,
level: this.options.logLevel,
})
this.healthy = true
// Gracefully handle stop signals.
const maxSignalCount = 3
......@@ -364,7 +285,7 @@ export abstract class BaseServiceV2<
{
name: params.name,
version: params.version,
...publicOptionNames.reduce((acc, key) => {
...getPublicOptions(params.optionsSpec).reduce((acc, key) => {
if (key in stdOptionsSpec) {
acc[key] = this.options[key].toString()
} else {
......
/* Imports: Internal */
import { Logger } from '../common/logger'
import { Metrics } from '../common/metrics'
import { LegacyMetrics } from '../common/metrics'
type OptionSettings<TOptions> = {
[P in keyof TOptions]?: {
......@@ -11,7 +11,7 @@ type OptionSettings<TOptions> = {
type BaseServiceOptions<T> = T & {
logger?: Logger
metrics?: Metrics
metrics?: LegacyMetrics
}
/**
......@@ -22,7 +22,7 @@ export class BaseService<T> {
protected name: string
protected options: T
protected logger: Logger
protected metrics: Metrics
protected metrics: LegacyMetrics
protected initialized = false
protected running = false
......
......@@ -2,3 +2,5 @@ export * from './base-service'
export * from './base-service-v2'
export * from './validators'
export * from './metrics'
export * from './options'
export * from './router'
......@@ -5,8 +5,59 @@ import {
Summary as PSummary,
} from 'prom-client'
import { OptionsSpec, getPublicOptions } from './options'
// Prometheus metrics re-exported.
export class Gauge extends PGauge<string> {}
export class Counter extends PCounter<string> {}
export class Histogram extends PHistogram<string> {}
export class Summary extends PSummary<string> {}
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 {
labels?: Object
}
export class Metrics {
export class LegacyMetrics {
options: MetricsOptions
client: typeof prometheus
registry: Registry
......
/* Imports: External */
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 { BaseProvider, StaticJsonRpcProvider } from '@ethersproject/providers'
import { LevelUp } from 'levelup'
......@@ -31,7 +31,7 @@ interface L1IngestionMetrics {
const registerMetrics = ({
client,
registry,
}: Metrics): L1IngestionMetrics => ({
}: LegacyMetrics): L1IngestionMetrics => ({
highestSyncedL1Block: new client.Gauge({
name: 'data_transport_layer_highest_synced_l1_block',
help: 'Highest Synced L1 Block Number',
......@@ -52,7 +52,7 @@ const registerMetrics = ({
export interface L1IngestionServiceOptions
extends L1DataTransportServiceOptions {
db: LevelUp
metrics: Metrics
metrics: LegacyMetrics
}
const optionSettings = {
......
/* Imports: External */
import { BaseService, Metrics } from '@eth-optimism/common-ts'
import { BaseService, LegacyMetrics } from '@eth-optimism/common-ts'
import { StaticJsonRpcProvider } from '@ethersproject/providers'
import { getChainId, sleep, toRpcHexString } from '@eth-optimism/core-utils'
import { BigNumber } from 'ethers'
......@@ -22,7 +22,7 @@ interface L2IngestionMetrics {
const registerMetrics = ({
client,
registry,
}: Metrics): L2IngestionMetrics => ({
}: LegacyMetrics): L2IngestionMetrics => ({
highestSyncedL2Block: new client.Gauge({
name: 'data_transport_layer_highest_synced_l2_block',
help: 'Highest Synced L2 Block Number',
......
/* Imports: External */
import { BaseService, Metrics } from '@eth-optimism/common-ts'
import { BaseService, LegacyMetrics } from '@eth-optimism/common-ts'
import { LevelUp } from 'levelup'
import level from 'level'
import { Counter } from 'prom-client'
......@@ -27,7 +27,7 @@ export interface L1DataTransportServiceOptions {
l2RpcProviderUser?: string
l2RpcProviderPassword?: string
l1SyncShutoffBlock?: number
metrics?: Metrics
metrics?: LegacyMetrics
dbPath: string
logsPerPollingInterval: number
pollingInterval: number
......@@ -66,7 +66,7 @@ export class L1DataTransportService extends BaseService<L1DataTransportServiceOp
l1IngestionService?: L1IngestionService
l2IngestionService?: L2IngestionService
l1TransportServer: L1TransportServer
metrics: Metrics
metrics: LegacyMetrics
failureCounter: Counter<string>
} = {} as any
......@@ -81,7 +81,7 @@ export class L1DataTransportService extends BaseService<L1DataTransportServiceOp
this.logger.info(`L2 chain ID is: ${this.options.l2ChainId}`)
this.logger.info(`BSS HF1 will activate at: ${bssHf1Index}`)
this.state.metrics = new Metrics({
this.state.metrics = new LegacyMetrics({
labels: {
environment: this.options.nodeEnv,
network: this.options.ethNetworkName,
......
/* 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 promBundle from 'express-prom-bundle'
import cors from 'cors'
......@@ -27,7 +27,7 @@ import { L1DataTransportServiceOptions } from '../main/service'
export interface L1TransportServerOptions
extends L1DataTransportServiceOptions {
db: LevelUp
metrics: Metrics
metrics: LegacyMetrics
}
const optionSettings = {
......
import {
BaseServiceV2,
StandardOptions,
Gauge,
Counter,
validators,
......@@ -30,13 +31,15 @@ export class DrippieMonService extends BaseServiceV2<
DrippieMonMetrics,
DrippieMonState
> {
constructor(options?: Partial<DrippieMonOptions>) {
constructor(options?: Partial<DrippieMonOptions & StandardOptions>) {
super({
version,
name: 'drippie-mon',
loop: true,
options: {
loopIntervalMs: 60_000,
options,
...options,
},
optionsSpec: {
rpc: {
validator: validators.provider,
......
import {
BaseServiceV2,
StandardOptions,
ExpressRouter,
Gauge,
validators,
......@@ -39,13 +40,15 @@ type State = {
}
export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
constructor(options?: Partial<Options>) {
constructor(options?: Partial<Options & StandardOptions>) {
super({
version,
name: 'fault-detector',
loop: true,
options: {
loopIntervalMs: 1000,
options,
...options,
},
optionsSpec: {
l1RpcProvider: {
validator: validators.provider,
......
import { Provider, Block } from '@ethersproject/abstract-provider'
import {
BaseServiceV2,
StandardOptions,
Counter,
Gauge,
validators,
......@@ -32,12 +33,14 @@ export class HealthcheckService extends BaseServiceV2<
HealthcheckMetrics,
HealthcheckState
> {
constructor(options?: Partial<HealthcheckOptions>) {
constructor(options?: Partial<HealthcheckOptions & StandardOptions>) {
super({
version,
name: 'healthcheck',
options: {
loopIntervalMs: 5000,
options,
...options,
},
optionsSpec: {
referenceRpcProvider: {
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