Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/five-fireants-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@eth-optimism/common-ts': minor
---

Minor upgrade to BaseServiceV2 to expose a full customizable server, instead of just metrics.
9 changes: 7 additions & 2 deletions packages/common-ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,26 @@
"@eth-optimism/core-utils": "0.8.6",
"@sentry/node": "^6.3.1",
"bcfg": "^0.1.7",
"body-parser": "^1.20.0",
"commander": "^9.0.0",
"dotenv": "^16.0.0",
"envalid": "^7.2.2",
"ethers": "^5.6.8",
"express": "^4.17.1",
"express-prom-bundle": "^6.4.1",
"lodash": "^4.17.21",
"morgan": "^1.10.0",
"pino": "^6.11.3",
"pino-multi-stream": "^5.3.0",
"pino-sentry": "^0.7.0",
"prom-client": "^13.1.0"
"prom-client": "^13.1.0",
"qs": "^6.10.5"
},
"devDependencies": {
"@ethersproject/abstract-provider": "^5.6.1",
"@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-multi-stream": "^5.1.1",
"chai": "^4.3.4",
Expand Down
141 changes: 99 additions & 42 deletions packages/common-ts/src/base-service/base-service-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import { Command, Option } from 'commander'
import { ValidatorSpec, Spec, cleanEnv } from 'envalid'
import { sleep } from '@eth-optimism/core-utils'
import snakeCase from 'lodash/snakeCase'
import express from 'express'
import express, { Router } 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 } from '../common/logger'
import { Metric } from './metrics'
Expand All @@ -19,8 +22,8 @@ export type Options = {

export type StandardOptions = {
loopIntervalMs?: number
metricsServerPort?: number
metricsServerHostname?: string
port?: number
hostname?: string
}

export type OptionsSpec<TOptions extends Options> = {
Expand All @@ -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.
*/
Expand Down Expand Up @@ -71,6 +76,11 @@ export abstract class BaseServiceV2<
*/
protected done: boolean

/**
* Whether or not the service is currently healthy.
*/
protected healthy: boolean

/**
* Logger class for this service.
*/
Expand All @@ -97,19 +107,19 @@ export abstract class BaseServiceV2<
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.
Expand All @@ -122,8 +132,8 @@ export abstract class BaseServiceV2<
* @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.metricsServerPort Port for the metrics server. Defaults to 7300.
* @param params.metricsServerHostname Hostname for the metrics server. Defaults to 0.0.0.0.
* @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.
*/
constructor(params: {
name: string
Expand All @@ -132,8 +142,8 @@ export abstract class BaseServiceV2<
options?: Partial<TOptions>
loop?: boolean
loopIntervalMs?: number
metricsServerPort?: number
metricsServerHostname?: string
port?: number
hostname?: string
}) {
this.loop = params.loop !== undefined ? params.loop : true
this.state = {} as TServiceState
Expand All @@ -148,15 +158,15 @@ export abstract class BaseServiceV2<
desc: 'Loop interval in milliseconds',
default: params.loopIntervalMs || 0,
},
metricsServerPort: {
port: {
validator: validators.num,
desc: 'Port for the metrics server',
default: params.metricsServerPort || 7300,
desc: 'Port for the app server',
default: params.port || 7300,
},
metricsServerHostname: {
hostname: {
validator: validators.str,
desc: 'Hostname for the metrics server',
default: params.metricsServerHostname || '0.0.0.0',
desc: 'Hostname for the app server',
default: params.hostname || '0.0.0.0',
},
}

Expand Down Expand Up @@ -268,12 +278,13 @@ export abstract class BaseServiceV2<

// Create the metrics server.
this.metricsRegistry = prometheus.register
this.metricsServerPort = this.options.metricsServerPort
this.metricsServerHostname = this.options.metricsServerHostname
this.port = this.options.port
this.hostname = this.options.hostname

// Set up everything else.
this.loopIntervalMs = this.options.loopIntervalMs
this.logger = new Logger({ name: params.name })
this.healthy = true

// Gracefully handle stop signals.
const maxSignalCount = 3
Expand Down Expand Up @@ -307,30 +318,69 @@ export abstract class BaseServiceV2<
public async run(): Promise<void> {
this.done = false

// Start the metrics server if not yet running.
if (!this.metricsServer) {
this.logger.info('starting metrics server')
// Start the app server if not yet running.
if (!this.server) {
this.logger.info('starting app server')

// Start building the app.
const app = express()

// Body parsing.
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(' ')
})
)

await new Promise((resolve) => {
const app = express()
// Metrics.
// Will expose a /metrics endpoint by default.
app.use(
promBundle({
promRegistry: this.metricsRegistry,
includeMethod: true,
includePath: true,
includeStatusCode: true,
})
)

app.get('/metrics', async (_, res) => {
res.status(200).send(await this.metricsRegistry.metrics())
// Health status.
app.get('/healthz', async (req, res) => {
return res.json({
ok: this.healthy,
})
})

this.metricsServer = app.listen(
this.metricsServerPort,
this.metricsServerHostname,
() => {
resolve(null)
}
)
// Registery user routes.
if (this.routes) {
const router = express.Router()
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(`metrics started`, {
port: this.metricsServerPort,
hostname: this.metricsServerHostname,
route: '/metrics',
this.logger.info(`app server started`, {
port: this.port,
hostname: this.hostname,
})
}

Expand Down Expand Up @@ -381,15 +431,15 @@ export abstract class BaseServiceV2<
}

// Shut down the metrics server if it's running.
if (this.metricsServer) {
if (this.server) {
this.logger.info('stopping metrics server')
await new Promise((resolve) => {
this.metricsServer.close(() => {
this.server.close(() => {
resolve(null)
})
})
this.logger.info('metrics server stopped')
this.metricsServer = undefined
this.server = undefined
}
}

Expand All @@ -398,6 +448,13 @@ export abstract class BaseServiceV2<
*/
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.
*/
Expand Down
Loading