Skip to content

Commit

Permalink
feat(cmn): clean up BaseServiceV2 options
Browse files Browse the repository at this point in the history
Cleans up the system of options for BaseServiceV2. Includes some
refactoring to reduce the size of base-service-v2.ts. Primary change
from a user point of view is that standard options and regular options
get merged. Makes it more clear how user inputted options will override
defaults provided by the service.
  • Loading branch information
smartcontracts committed Dec 12, 2022
1 parent 9b74fd6 commit 9b28918
Show file tree
Hide file tree
Showing 16 changed files with 208 additions and 138 deletions.
9 changes: 9 additions & 0 deletions .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
139 changes: 30 additions & 109 deletions packages/common-ts/src/base-service/base-service-v2.ts
Expand Up @@ -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
> {
/**
Expand Down Expand Up @@ -133,91 +108,37 @@ 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: {
name: string
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),
}

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions 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<TOptions> = {
[P in keyof TOptions]?: {
Expand All @@ -11,7 +11,7 @@ type OptionSettings<TOptions> = {

type BaseServiceOptions<T> = T & {
logger?: Logger
metrics?: Metrics
metrics?: LegacyMetrics
}

/**
Expand All @@ -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

Expand Down
2 changes: 2 additions & 0 deletions packages/common-ts/src/base-service/index.ts
Expand Up @@ -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'
51 changes: 51 additions & 0 deletions packages/common-ts/src/base-service/metrics.ts
Expand Up @@ -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',
},
}
}
77 changes: 77 additions & 0 deletions 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<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
})
}
6 changes: 6 additions & 0 deletions packages/common-ts/src/base-service/router.ts
@@ -0,0 +1,6 @@
import { Router } from 'express'

/**
* Express router re-exported.
*/
export type ExpressRouter = Router

0 comments on commit 9b28918

Please sign in to comment.