diff --git a/.changeset/perfect-years-rush.md b/.changeset/perfect-years-rush.md new file mode 100644 index 000000000000..1a7a918fbd52 --- /dev/null +++ b/.changeset/perfect-years-rush.md @@ -0,0 +1,9 @@ +--- +'@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 diff --git a/packages/common-ts/src/base-service/base-service-v2.ts b/packages/common-ts/src/base-service/base-service-v2.ts index 0c446bbb5f1e..494aa8b077af 100644 --- a/packages/common-ts/src/base-service/base-service-v2.ts +++ b/packages/common-ts/src/base-service/base-service-v2.ts @@ -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 = { - [P in keyof Required]: { - validator: (spec?: Spec) => ValidatorSpec - desc: string - default?: TOptions[P] - public?: boolean - } -} - -export type MetricsV2 = Record - -export type StandardMetrics = { - metadata: Gauge - unhandledErrors: Counter -} - -export type MetricsSpec = { - [P in keyof Required]: { - 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 metricsSpec: MetricsSpec - options?: Partial + options?: Partial 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 = { - 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 { diff --git a/packages/common-ts/src/base-service/base-service.ts b/packages/common-ts/src/base-service/base-service.ts index a478c8c511dd..ee816be55c36 100644 --- a/packages/common-ts/src/base-service/base-service.ts +++ b/packages/common-ts/src/base-service/base-service.ts @@ -1,6 +1,6 @@ /* Imports: Internal */ import { Logger } from '../common/logger' -import { Metrics } from '../common/metrics' +import { LegacyMetrics } from '../common/metrics' type OptionSettings = { [P in keyof TOptions]?: { @@ -11,7 +11,7 @@ type OptionSettings = { type BaseServiceOptions = T & { logger?: Logger - metrics?: Metrics + metrics?: LegacyMetrics } /** @@ -22,7 +22,7 @@ export class BaseService { protected name: string protected options: T protected logger: Logger - protected metrics: Metrics + protected metrics: LegacyMetrics protected initialized = false protected running = false diff --git a/packages/common-ts/src/base-service/index.ts b/packages/common-ts/src/base-service/index.ts index 11c37633cc9c..6383c019ee7f 100644 --- a/packages/common-ts/src/base-service/index.ts +++ b/packages/common-ts/src/base-service/index.ts @@ -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' diff --git a/packages/common-ts/src/base-service/metrics.ts b/packages/common-ts/src/base-service/metrics.ts index 17c3418d8616..f3cd48f07fa8 100644 --- a/packages/common-ts/src/base-service/metrics.ts +++ b/packages/common-ts/src/base-service/metrics.ts @@ -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 {} export class Counter extends PCounter {} export class Histogram extends PHistogram {} export class Summary extends PSummary {} export type Metric = Gauge | Counter | Histogram | Summary + +/** + * Metrics that are available for a given service. + */ +export type Metrics = Record + +/** + * Specification for metrics. + */ +export type MetricsSpec = { + [P in keyof Required]: { + 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 +): MetricsSpec => { + return { + // Users cannot set these options. + metadata: { + type: Gauge, + desc: 'Service metadata', + labels: ['name', 'version'].concat(getPublicOptions(optionsSpec)), + }, + unhandledErrors: { + type: Counter, + desc: 'Unhandled errors', + }, + } +} diff --git a/packages/common-ts/src/base-service/options.ts b/packages/common-ts/src/base-service/options.ts new file mode 100644 index 000000000000..fac6e6de56eb --- /dev/null +++ b/packages/common-ts/src/base-service/options.ts @@ -0,0 +1,77 @@ +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 = { + [P in keyof Required]: { + validator: (spec?: Spec) => ValidatorSpec + 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 = { + 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 +): string[] => { + return Object.keys(optionsSpec).filter((key) => { + return optionsSpec[key].public + }) +} diff --git a/packages/common-ts/src/base-service/router.ts b/packages/common-ts/src/base-service/router.ts new file mode 100644 index 000000000000..e5bbaabd4e5f --- /dev/null +++ b/packages/common-ts/src/base-service/router.ts @@ -0,0 +1,6 @@ +import { Router } from 'express' + +/** + * Express router re-exported. + */ +export type ExpressRouter = Router diff --git a/packages/common-ts/src/common/metrics.ts b/packages/common-ts/src/common/metrics.ts index 94d321e7f369..bf2bdb213009 100644 --- a/packages/common-ts/src/common/metrics.ts +++ b/packages/common-ts/src/common/metrics.ts @@ -14,7 +14,7 @@ export interface MetricsOptions { labels?: Object } -export class Metrics { +export class LegacyMetrics { options: MetricsOptions client: typeof prometheus registry: Registry diff --git a/packages/data-transport-layer/src/services/l1-ingestion/service.ts b/packages/data-transport-layer/src/services/l1-ingestion/service.ts index ffb2f46d9d9c..306a15cfa49b 100644 --- a/packages/data-transport-layer/src/services/l1-ingestion/service.ts +++ b/packages/data-transport-layer/src/services/l1-ingestion/service.ts @@ -1,6 +1,6 @@ /* 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 = { diff --git a/packages/data-transport-layer/src/services/l2-ingestion/service.ts b/packages/data-transport-layer/src/services/l2-ingestion/service.ts index 87e4fb1753b4..4bbb070067d7 100644 --- a/packages/data-transport-layer/src/services/l2-ingestion/service.ts +++ b/packages/data-transport-layer/src/services/l2-ingestion/service.ts @@ -1,5 +1,5 @@ /* 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', diff --git a/packages/data-transport-layer/src/services/main/service.ts b/packages/data-transport-layer/src/services/main/service.ts index 98f93c9d8775..39319effe8ae 100644 --- a/packages/data-transport-layer/src/services/main/service.ts +++ b/packages/data-transport-layer/src/services/main/service.ts @@ -1,5 +1,5 @@ /* 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 } = {} as any @@ -81,7 +81,7 @@ export class L1DataTransportService extends BaseService { - constructor(options?: Partial) { + constructor(options?: Partial) { super({ version, name: 'drippie-mon', loop: true, - loopIntervalMs: 60_000, - options, + options: { + loopIntervalMs: 60_000, + ...options, + }, optionsSpec: { rpc: { validator: validators.provider, diff --git a/packages/fault-detector/src/service.ts b/packages/fault-detector/src/service.ts index e7b4bc173175..4a0566a76722 100644 --- a/packages/fault-detector/src/service.ts +++ b/packages/fault-detector/src/service.ts @@ -1,5 +1,6 @@ import { BaseServiceV2, + StandardOptions, ExpressRouter, Gauge, validators, @@ -39,13 +40,15 @@ type State = { } export class FaultDetector extends BaseServiceV2 { - constructor(options?: Partial) { + constructor(options?: Partial) { super({ version, name: 'fault-detector', loop: true, - loopIntervalMs: 1000, - options, + options: { + loopIntervalMs: 1000, + ...options, + }, optionsSpec: { l1RpcProvider: { validator: validators.provider, diff --git a/packages/replica-healthcheck/src/service.ts b/packages/replica-healthcheck/src/service.ts index 939c8743f6b1..d56f519cb2e3 100644 --- a/packages/replica-healthcheck/src/service.ts +++ b/packages/replica-healthcheck/src/service.ts @@ -1,6 +1,7 @@ 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) { + constructor(options?: Partial) { super({ version, name: 'healthcheck', - loopIntervalMs: 5000, - options, + options: { + loopIntervalMs: 5000, + ...options, + }, optionsSpec: { referenceRpcProvider: { validator: validators.provider, diff --git a/yarn.lock b/yarn.lock index 9e78676bfa01..c23b4115225f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3207,11 +3207,6 @@ resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.7.3.tgz#f1d606e2827d409053f3e908ba4eb8adb1dd6995" integrity sha512-+wuegAMaLcZnLCJIvrVUDzA9z/Wp93f0Dla/4jJvIhijRrPabjQbZe6fWiECLaJyfn5ci9fqf9vTw3xpQOad2A== -"@openzeppelin/contracts-upgradeable@4.8.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.8.0.tgz#26688982f46969018e3ed3199e72a07c8d114275" - integrity sha512-5GeFgqMiDlqGT8EdORadp1ntGF0qzWZLmEY7Wbp/yVhN7/B3NNzCxujuI77ktlyG81N3CUZP8cZe3ZAQ/cW10w== - "@openzeppelin/contracts@3.4.1-solc-0.7-2": version "3.4.1-solc-0.7-2" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.1-solc-0.7-2.tgz#371c67ebffe50f551c3146a9eec5fe6ffe862e92"