Commit d9e39931 authored by smartcontracts's avatar smartcontracts Committed by GitHub

feat(cmn): have BaseService expose a full server (#2725)

Updates BaseServiceV2 to expose a full server instead of simply exposing
the server for metrics. Services can now add new custom routes to this
server. Mainly useful because we're already running the metrics server,
so we might as well allow people to add more things to it.
parent 6648fc95
---
'@eth-optimism/common-ts': minor
---
Minor upgrade to BaseServiceV2 to expose a full customizable server, instead of just metrics.
...@@ -34,21 +34,26 @@ ...@@ -34,21 +34,26 @@
"@eth-optimism/core-utils": "0.8.6", "@eth-optimism/core-utils": "0.8.6",
"@sentry/node": "^6.3.1", "@sentry/node": "^6.3.1",
"bcfg": "^0.1.7", "bcfg": "^0.1.7",
"body-parser": "^1.20.0",
"commander": "^9.0.0", "commander": "^9.0.0",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"envalid": "^7.2.2", "envalid": "^7.2.2",
"ethers": "^5.6.8", "ethers": "^5.6.8",
"express": "^4.17.1", "express": "^4.17.1",
"express-prom-bundle": "^6.4.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"morgan": "^1.10.0",
"pino": "^6.11.3", "pino": "^6.11.3",
"pino-multi-stream": "^5.3.0", "pino-multi-stream": "^5.3.0",
"pino-sentry": "^0.7.0", "pino-sentry": "^0.7.0",
"prom-client": "^13.1.0" "prom-client": "^13.1.0",
"qs": "^6.10.5"
}, },
"devDependencies": { "devDependencies": {
"@ethersproject/abstract-provider": "^5.6.1", "@ethersproject/abstract-provider": "^5.6.1",
"@ethersproject/abstract-signer": "^5.6.2", "@ethersproject/abstract-signer": "^5.6.2",
"@types/express": "^4.17.12", "@types/express": "^4.17.13",
"@types/morgan": "^1.9.3",
"@types/pino": "^6.3.6", "@types/pino": "^6.3.6",
"@types/pino-multi-stream": "^5.1.1", "@types/pino-multi-stream": "^5.1.1",
"chai": "^4.3.4", "chai": "^4.3.4",
......
...@@ -6,8 +6,11 @@ import { Command, Option } from 'commander' ...@@ -6,8 +6,11 @@ import { Command, Option } from 'commander'
import { ValidatorSpec, Spec, cleanEnv } from 'envalid' import { ValidatorSpec, Spec, cleanEnv } from 'envalid'
import { sleep } from '@eth-optimism/core-utils' import { sleep } from '@eth-optimism/core-utils'
import snakeCase from 'lodash/snakeCase' import snakeCase from 'lodash/snakeCase'
import express from 'express' import express, { Router } from 'express'
import prometheus, { Registry } from 'prom-client' import prometheus, { Registry } from 'prom-client'
import promBundle from 'express-prom-bundle'
import bodyParser from 'body-parser'
import morgan from 'morgan'
import { Logger } from '../common/logger' import { Logger } from '../common/logger'
import { Metric } from './metrics' import { Metric } from './metrics'
...@@ -19,8 +22,8 @@ export type Options = { ...@@ -19,8 +22,8 @@ export type Options = {
export type StandardOptions = { export type StandardOptions = {
loopIntervalMs?: number loopIntervalMs?: number
metricsServerPort?: number port?: number
metricsServerHostname?: string hostname?: string
} }
export type OptionsSpec<TOptions extends Options> = { export type OptionsSpec<TOptions extends Options> = {
...@@ -43,6 +46,8 @@ export type MetricsSpec<TMetrics extends MetricsV2> = { ...@@ -43,6 +46,8 @@ export type MetricsSpec<TMetrics extends MetricsV2> = {
} }
} }
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.
*/ */
...@@ -71,6 +76,11 @@ export abstract class BaseServiceV2< ...@@ -71,6 +76,11 @@ export abstract class BaseServiceV2<
*/ */
protected done: boolean protected done: boolean
/**
* Whether or not the service is currently healthy.
*/
protected healthy: boolean
/** /**
* Logger class for this service. * Logger class for this service.
*/ */
...@@ -97,19 +107,19 @@ export abstract class BaseServiceV2< ...@@ -97,19 +107,19 @@ export abstract class BaseServiceV2<
protected readonly metricsRegistry: Registry protected readonly metricsRegistry: Registry
/** /**
* Metrics server. * App server.
*/ */
protected metricsServer: Server protected server: Server
/** /**
* Port for the metrics server. * Port for the app server.
*/ */
protected readonly metricsServerPort: number protected readonly port: number
/** /**
* Hostname for the metrics server. * Hostname for the app server.
*/ */
protected readonly metricsServerHostname: string protected readonly hostname: string
/** /**
* @param params Options for the construction of the service. * @param params Options for the construction of the service.
...@@ -122,8 +132,8 @@ export abstract class BaseServiceV2< ...@@ -122,8 +132,8 @@ export abstract class BaseServiceV2<
* @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.loopIntervalMs Loop interval in milliseconds. Defaults to zero.
* @param params.metricsServerPort Port for the metrics server. Defaults to 7300. * @param params.port Port for the app server. Defaults to 7300.
* @param params.metricsServerHostname Hostname for the metrics server. Defaults to 0.0.0.0. * @param params.hostname Hostname for the app server. Defaults to 0.0.0.0.
*/ */
constructor(params: { constructor(params: {
name: string name: string
...@@ -132,8 +142,8 @@ export abstract class BaseServiceV2< ...@@ -132,8 +142,8 @@ export abstract class BaseServiceV2<
options?: Partial<TOptions> options?: Partial<TOptions>
loop?: boolean loop?: boolean
loopIntervalMs?: number loopIntervalMs?: number
metricsServerPort?: number port?: number
metricsServerHostname?: string hostname?: string
}) { }) {
this.loop = params.loop !== undefined ? params.loop : true this.loop = params.loop !== undefined ? params.loop : true
this.state = {} as TServiceState this.state = {} as TServiceState
...@@ -148,15 +158,15 @@ export abstract class BaseServiceV2< ...@@ -148,15 +158,15 @@ export abstract class BaseServiceV2<
desc: 'Loop interval in milliseconds', desc: 'Loop interval in milliseconds',
default: params.loopIntervalMs || 0, default: params.loopIntervalMs || 0,
}, },
metricsServerPort: { port: {
validator: validators.num, validator: validators.num,
desc: 'Port for the metrics server', desc: 'Port for the app server',
default: params.metricsServerPort || 7300, default: params.port || 7300,
}, },
metricsServerHostname: { hostname: {
validator: validators.str, validator: validators.str,
desc: 'Hostname for the metrics server', desc: 'Hostname for the app server',
default: params.metricsServerHostname || '0.0.0.0', default: params.hostname || '0.0.0.0',
}, },
} }
...@@ -268,12 +278,13 @@ export abstract class BaseServiceV2< ...@@ -268,12 +278,13 @@ export abstract class BaseServiceV2<
// Create the metrics server. // Create the metrics server.
this.metricsRegistry = prometheus.register this.metricsRegistry = prometheus.register
this.metricsServerPort = this.options.metricsServerPort this.port = this.options.port
this.metricsServerHostname = this.options.metricsServerHostname this.hostname = this.options.hostname
// Set up everything else. // Set up everything else.
this.loopIntervalMs = this.options.loopIntervalMs this.loopIntervalMs = this.options.loopIntervalMs
this.logger = new Logger({ name: params.name }) this.logger = new Logger({ name: params.name })
this.healthy = true
// Gracefully handle stop signals. // Gracefully handle stop signals.
const maxSignalCount = 3 const maxSignalCount = 3
...@@ -307,30 +318,69 @@ export abstract class BaseServiceV2< ...@@ -307,30 +318,69 @@ export abstract class BaseServiceV2<
public async run(): Promise<void> { public async run(): Promise<void> {
this.done = false this.done = false
// Start the metrics server if not yet running. // Start the app server if not yet running.
if (!this.metricsServer) { if (!this.server) {
this.logger.info('starting metrics server') this.logger.info('starting app server')
await new Promise((resolve) => { // Start building the app.
const app = express() const app = express()
app.get('/metrics', async (_, res) => { // Body parsing.
res.status(200).send(await this.metricsRegistry.metrics()) app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
// Logging.
app.use(
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(' ')
}) })
)
this.metricsServer = app.listen( // Metrics.
this.metricsServerPort, // Will expose a /metrics endpoint by default.
this.metricsServerHostname, app.use(
() => { promBundle({
resolve(null) promRegistry: this.metricsRegistry,
} includeMethod: true,
includePath: true,
includeStatusCode: true,
})
) )
// Health status.
app.get('/healthz', async (req, res) => {
return res.json({
ok: this.healthy,
})
}) })
this.logger.info(`metrics started`, { // Registery user routes.
port: this.metricsServerPort, if (this.routes) {
hostname: this.metricsServerHostname, const router = express.Router()
route: '/metrics', this.routes(router)
app.use('/api', router)
}
// Wait for server to come up.
await new Promise((resolve) => {
this.server = app.listen(this.port, this.hostname, () => {
resolve(null)
})
})
this.logger.info(`app server started`, {
port: this.port,
hostname: this.hostname,
}) })
} }
...@@ -381,15 +431,15 @@ export abstract class BaseServiceV2< ...@@ -381,15 +431,15 @@ export abstract class BaseServiceV2<
} }
// Shut down the metrics server if it's running. // Shut down the metrics server if it's running.
if (this.metricsServer) { if (this.server) {
this.logger.info('stopping metrics server') this.logger.info('stopping metrics server')
await new Promise((resolve) => { await new Promise((resolve) => {
this.metricsServer.close(() => { this.server.close(() => {
resolve(null) resolve(null)
}) })
}) })
this.logger.info('metrics server stopped') this.logger.info('metrics server stopped')
this.metricsServer = undefined this.server = undefined
} }
} }
...@@ -398,6 +448,13 @@ export abstract class BaseServiceV2< ...@@ -398,6 +448,13 @@ export abstract class BaseServiceV2<
*/ */
protected init?(): Promise<void> protected init?(): Promise<void>
/**
* Initialization function for router.
*
* @param router Express router.
*/
protected routes?(router: ExpressRouter): Promise<void>
/** /**
* Main function. Runs repeatedly when run() is called. * Main function. Runs repeatedly when run() is called.
*/ */
......
This diff is collapsed.
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