Extract prometheus server
This commit is contained in:
parent
53b9688a58
commit
7fd1c5a03c
15 changed files with 168 additions and 177 deletions
|
@ -7,6 +7,7 @@ module.exports = {
|
|||
'prettier/prettier': 'warn',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'error',
|
||||
'prefer-arrow-callback': 'error',
|
||||
'sort-imports': 'warn',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
|
2
TODOs
2
TODOs
|
@ -1 +1 @@
|
|||
- Extract HttpServer
|
||||
- Prefix via configuration
|
|
@ -1,9 +1,9 @@
|
|||
import type { Device } from '../boundaries'
|
||||
import type { Device } from '../../boundaries'
|
||||
import { Logger } from 'homebridge'
|
||||
|
||||
type Pin = string
|
||||
|
||||
export type HapConfig = {
|
||||
export interface HapConfig {
|
||||
pin: Pin
|
||||
refreshInterval: number
|
||||
discoveryTimeout: number
|
|
@ -1,13 +1,13 @@
|
|||
import type { HapDiscover } from './api'
|
||||
import { HAPNodeJSClient } from 'hap-node-client'
|
||||
|
||||
import { Device, DeviceBoundary } from '../boundaries'
|
||||
import { Device, DeviceBoundary } from '../../boundaries'
|
||||
import { Array, Unknown } from 'runtypes'
|
||||
import { Logger } from 'homebridge'
|
||||
|
||||
const MaybeDevices = Array(Unknown)
|
||||
|
||||
type HapConfig = {
|
||||
interface HapConfig {
|
||||
debug: boolean
|
||||
refresh: number
|
||||
timeout: number
|
13
src/adapters/http/api.ts
Normal file
13
src/adapters/http/api.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { HttpServer } from '../../http'
|
||||
|
||||
export interface HttpResponse {
|
||||
statusCode?: number
|
||||
body?: string
|
||||
headers?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface HttpServerController {
|
||||
shutdown(): void
|
||||
}
|
||||
|
||||
export type HttpAdapter = (config: HttpServer) => Promise<HttpServerController>
|
52
src/adapters/http/fastify.ts
Normal file
52
src/adapters/http/fastify.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import Fastify, { FastifyReply, FastifyRequest, HookHandlerDoneFunction } from 'fastify'
|
||||
import { HttpAdapter, HttpResponse } from './api'
|
||||
import { HttpServer } from '../../http'
|
||||
|
||||
function adaptResponseToReply(response: HttpResponse, reply: FastifyReply): void {
|
||||
if (response.statusCode) {
|
||||
void reply.code(response.statusCode)
|
||||
}
|
||||
if (response.body) {
|
||||
void reply.send(response.body)
|
||||
}
|
||||
|
||||
if (response.headers) {
|
||||
void reply.headers(response.headers)
|
||||
}
|
||||
}
|
||||
|
||||
export const serve: HttpAdapter = async (server: HttpServer) => {
|
||||
const fastify = Fastify({
|
||||
logger: server.debug,
|
||||
})
|
||||
|
||||
fastify.addHook('onRequest', (request: FastifyRequest, reply: FastifyReply, next: HookHandlerDoneFunction) => {
|
||||
const response = server.onRequest()
|
||||
|
||||
if (response) {
|
||||
adaptResponseToReply(response, reply)
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
fastify.setErrorHandler(async (error, request: FastifyRequest, reply: FastifyReply) => {
|
||||
adaptResponseToReply(server.onError(error), reply)
|
||||
})
|
||||
|
||||
fastify.setNotFoundHandler(async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
adaptResponseToReply(server.onNotFound(), reply)
|
||||
})
|
||||
|
||||
fastify.get('/metrics', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
adaptResponseToReply(server.onMetrics(), reply)
|
||||
})
|
||||
|
||||
await fastify.listen({ port: server.port, host: '::' })
|
||||
|
||||
return {
|
||||
shutdown() {
|
||||
void fastify.close()
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { Number, String, Literal, Array, Record, Union, Static, Optional, Intersect } from 'runtypes'
|
||||
import { Array, Intersect, Literal, Number, Optional, Record, Static, String, Union } from 'runtypes'
|
||||
|
||||
export const NUMBER_TYPES = ['float', 'int', 'uint8', 'uint16', 'uint32', 'uint64'] as const
|
||||
const NumberTypesLiterals = NUMBER_TYPES.map(Literal)
|
||||
|
|
14
src/http.ts
Normal file
14
src/http.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { HttpResponse } from './adapters/http/api'
|
||||
import { Metric } from './metrics'
|
||||
import { Logger } from 'homebridge'
|
||||
|
||||
export interface HttpServer {
|
||||
port: number
|
||||
debug: boolean
|
||||
log: Logger
|
||||
onRequest(): HttpResponse | undefined
|
||||
onMetrics(): HttpResponse
|
||||
onNotFound(): HttpResponse
|
||||
onError(error: unknown): HttpResponse
|
||||
updateMetrics(metrics: Metric[]): void
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import { Logger } from 'homebridge'
|
||||
|
||||
export type HttpResponse = {
|
||||
statusCode: number
|
||||
body: string
|
||||
headers: Record<string, string>
|
||||
}
|
||||
|
||||
export type HttpServerConfig = {
|
||||
port: number
|
||||
requestInterceptor: () => HttpResponse | undefined
|
||||
metricsController: () => HttpResponse
|
||||
notFoundController: () => HttpResponse
|
||||
errorController: (error: unknown) => HttpResponse
|
||||
logger: Logger
|
||||
}
|
||||
|
||||
export type HttpServerController = {
|
||||
shutdown(): void
|
||||
}
|
||||
|
||||
export type HttpServer = (config: HttpServerConfig) => Promise<HttpServerController>
|
|
@ -1,82 +0,0 @@
|
|||
import Fastify, { FastifyBaseLogger, FastifyReply, FastifyRequest, HookHandlerDoneFunction } from 'fastify'
|
||||
import { HttpResponse, HttpServer } from './api'
|
||||
import { Logger } from 'homebridge'
|
||||
import { Bindings, ChildLoggerOptions } from 'fastify/types/logger'
|
||||
import { LogFn } from 'pino'
|
||||
|
||||
/*class PinoLoggerBridge implements FastifyBaseLogger {
|
||||
|
||||
constructor(private readonly logger: Logger) {
|
||||
}
|
||||
|
||||
child(bindings: Bindings, options: ChildLoggerOptions | undefined): FastifyBaseLogger {
|
||||
return this
|
||||
}
|
||||
|
||||
debug(m: string|unknown, ...args: unknown[]) {
|
||||
this.logger.debug(typeof m === 'string' ? m : JSON.stringify(m), ...args)
|
||||
}
|
||||
|
||||
error: LogFn;
|
||||
fatal: pino.LogFn;
|
||||
info: pino.LogFn;
|
||||
level: pino.LevelWithSilent | string;
|
||||
silent: pino.LogFn;
|
||||
trace: pino.LogFn;
|
||||
warn: pino.LogFn;
|
||||
}*/
|
||||
|
||||
function adaptResponseToReply(response: HttpResponse, reply: FastifyReply): void {
|
||||
if (response.statusCode) {
|
||||
reply.code(response.statusCode)
|
||||
}
|
||||
if (response.body) {
|
||||
reply.send(response.body)
|
||||
}
|
||||
|
||||
if (response.headers) {
|
||||
reply.headers(response.headers)
|
||||
}
|
||||
}
|
||||
|
||||
export const serve: HttpServer = async ({
|
||||
port,
|
||||
requestInterceptor,
|
||||
metricsController,
|
||||
notFoundController,
|
||||
errorController,
|
||||
}) => {
|
||||
const fastify = Fastify({
|
||||
// logger
|
||||
})
|
||||
|
||||
fastify.addHook('onRequest', (request: FastifyRequest, reply: FastifyReply, next: HookHandlerDoneFunction) => {
|
||||
const response = requestInterceptor()
|
||||
|
||||
if (response) {
|
||||
adaptResponseToReply(response, reply)
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
fastify.setErrorHandler(async (error, request: FastifyRequest, reply: FastifyReply) => {
|
||||
adaptResponseToReply(errorController(error), reply)
|
||||
})
|
||||
|
||||
fastify.setNotFoundHandler(async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
adaptResponseToReply(notFoundController(), reply)
|
||||
})
|
||||
|
||||
fastify.get('/metrics', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
adaptResponseToReply(metricsController(), reply)
|
||||
})
|
||||
|
||||
await fastify.listen({ port, host: '::' })
|
||||
|
||||
return {
|
||||
shutdown() {
|
||||
fastify.close()
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import type { Device, Accessory, NumberTypes, Service } from './boundaries'
|
||||
import type { Accessory, Device, Service } from './boundaries'
|
||||
import { isType } from './std'
|
||||
import { NUMBER_TYPES } from './boundaries'
|
||||
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import { API, Logger, PlatformConfig, IndependentPlatformPlugin } from 'homebridge'
|
||||
import { API, IndependentPlatformPlugin, Logger, PlatformConfig } from 'homebridge'
|
||||
|
||||
import { Metric, aggregate } from './metrics'
|
||||
import { discover } from './discovery/hap_node_js_client'
|
||||
import { serve } from './http/fastify'
|
||||
import { HttpServerController } from './http/api'
|
||||
import { MetricsRenderer } from './prometheus'
|
||||
import { aggregate } from './metrics'
|
||||
import { discover } from './adapters/discovery/hap_node_js_client'
|
||||
import { serve } from './adapters/http/fastify'
|
||||
import { HttpServerController } from './adapters/http/api'
|
||||
import { PrometheusServer } from './prometheus'
|
||||
|
||||
export class PrometheusExporterPlatform implements IndependentPlatformPlugin {
|
||||
private metrics: Metric[] = []
|
||||
private metricsDiscovered = false
|
||||
private http: HttpServerController | undefined = undefined
|
||||
private readonly httpServer: PrometheusServer
|
||||
private httpServerController: HttpServerController | undefined = undefined
|
||||
|
||||
constructor(public readonly log: Logger, public readonly config: PlatformConfig, public readonly api: API) {
|
||||
this.log.debug('Initializing platform %s', this.config.platform)
|
||||
|
@ -18,12 +17,22 @@ export class PrometheusExporterPlatform implements IndependentPlatformPlugin {
|
|||
|
||||
this.api.on('shutdown', () => {
|
||||
this.log.debug('Shutting down %s', this.config.platform)
|
||||
if (this.http) {
|
||||
this.http.shutdown()
|
||||
if (this.httpServerController) {
|
||||
this.httpServerController.shutdown()
|
||||
}
|
||||
})
|
||||
|
||||
this.startHttpServer()
|
||||
this.log.debug('Starting probe HTTP server on port %d', this.config.probe_port)
|
||||
|
||||
this.httpServer = new PrometheusServer(this.config.probe_port, this.log, this.config.debug)
|
||||
serve(this.httpServer)
|
||||
.then((httpServerController) => {
|
||||
this.log.debug('HTTP server started on port %d', this.config.probe_port)
|
||||
this.httpServerController = httpServerController
|
||||
})
|
||||
.catch((e) => {
|
||||
this.log.error('Failed to start probe HTTP server on port %d: %o', this.config.probe_port, e)
|
||||
})
|
||||
|
||||
this.api.on('didFinishLaunching', () => {
|
||||
this.log.debug('Finished launching %s', this.config.platform)
|
||||
|
@ -45,55 +54,6 @@ export class PrometheusExporterPlatform implements IndependentPlatformPlugin {
|
|||
this.log.debug('Configuration materialized: %o', this.config)
|
||||
}
|
||||
|
||||
private startHttpServer(): void {
|
||||
this.log.debug('Starting probe HTTP server on port %d', this.config.probe_port)
|
||||
|
||||
const contentTypeHeader = { 'Content-Type': 'text/plain; charset=UTF-8' }
|
||||
serve({
|
||||
port: this.config.probe_port,
|
||||
logger: this.log,
|
||||
requestInterceptor: () => {
|
||||
if (!this.metricsDiscovered) {
|
||||
return {
|
||||
statusCode: 503,
|
||||
headers: { ...contentTypeHeader, 'Retry-After': '10' },
|
||||
body: 'Discovery pending',
|
||||
}
|
||||
}
|
||||
},
|
||||
metricsController: () => {
|
||||
const renderer = new MetricsRenderer('homebridge')
|
||||
const metrics = this.metrics.map((metric) => renderer.render(metric)).join('\n')
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: contentTypeHeader,
|
||||
body: metrics,
|
||||
}
|
||||
},
|
||||
notFoundController: () => ({
|
||||
statusCode: 404,
|
||||
headers: contentTypeHeader,
|
||||
body: 'Not found. Try /metrics',
|
||||
}),
|
||||
errorController: (e) => {
|
||||
this.log.error('HTTP request error: %o', e)
|
||||
return {
|
||||
statusCode: 500,
|
||||
headers: contentTypeHeader,
|
||||
body: 'Server error',
|
||||
}
|
||||
},
|
||||
})
|
||||
.then((http) => {
|
||||
this.log.debug('HTTP server started on port %d', this.config.probe_port)
|
||||
this.http = http
|
||||
})
|
||||
.catch((e) => {
|
||||
this.log.error('Failed to start probe HTTP server on port %d: %o', this.config.probe_port, e)
|
||||
})
|
||||
}
|
||||
|
||||
private startHapDiscovery(): void {
|
||||
this.log.debug('Starting HAP discovery')
|
||||
discover({
|
||||
|
@ -105,9 +65,9 @@ export class PrometheusExporterPlatform implements IndependentPlatformPlugin {
|
|||
debug: this.config.debug,
|
||||
})
|
||||
.then((devices) => {
|
||||
this.metrics = aggregate(devices, new Date())
|
||||
this.metricsDiscovered = true
|
||||
this.log.debug('HAP discovery completed, %d metrics discovered', this.metrics.length)
|
||||
const metrics = aggregate(devices, new Date())
|
||||
this.httpServer.updateMetrics(metrics)
|
||||
this.log.debug('HAP discovery completed, %d metrics discovered', metrics.length)
|
||||
this.startHapDiscovery()
|
||||
})
|
||||
.catch((e) => {
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import { Metric } from './metrics'
|
||||
import { Logger } from 'homebridge'
|
||||
import { HttpResponse } from './adapters/http/api'
|
||||
import { HttpServer } from './http'
|
||||
|
||||
function escapeString(str: string) {
|
||||
return str.replace(/\\/g, '\\\\').replace(/\n/g, '\\n')
|
||||
|
@ -68,3 +71,55 @@ export class MetricsRenderer {
|
|||
return sanitizePrometheusMetricName(this.prefix.replace(/_+$/, '') + '_' + name)
|
||||
}
|
||||
}
|
||||
|
||||
const contentTypeHeader = { 'Content-Type': 'text/plain; charset=UTF-8' }
|
||||
|
||||
export class PrometheusServer implements HttpServer {
|
||||
private metricsInitialized = false
|
||||
private metrics: Metric[] = []
|
||||
|
||||
constructor(public readonly port: number, public readonly log: Logger, public readonly debug: boolean) {}
|
||||
|
||||
onRequest(): HttpResponse | undefined {
|
||||
if (!this.metricsInitialized) {
|
||||
return {
|
||||
statusCode: 503,
|
||||
headers: { ...contentTypeHeader, 'Retry-After': '10' },
|
||||
body: 'Metrics discovery pending',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMetrics(): HttpResponse {
|
||||
const renderer = new MetricsRenderer('homebridge')
|
||||
const metrics = this.metrics.map((metric) => renderer.render(metric)).join('\n')
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: contentTypeHeader,
|
||||
body: metrics,
|
||||
}
|
||||
}
|
||||
|
||||
onNotFound(): HttpResponse {
|
||||
return {
|
||||
statusCode: 404,
|
||||
headers: contentTypeHeader,
|
||||
body: 'Not found. Try /metrics',
|
||||
}
|
||||
}
|
||||
|
||||
onError(error: unknown): HttpResponse {
|
||||
this.log.error('HTTP request error: %o', error)
|
||||
return {
|
||||
statusCode: 500,
|
||||
headers: contentTypeHeader,
|
||||
body: 'Server error',
|
||||
}
|
||||
}
|
||||
|
||||
updateMetrics(metrics: Metric[]): void {
|
||||
this.metrics = metrics
|
||||
this.metricsInitialized = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
type Types = 'string' | 'number' | 'boolean' | 'object'
|
||||
type TypeMap = {
|
||||
interface TypeMap {
|
||||
string: string
|
||||
number: number
|
||||
boolean: boolean
|
||||
|
|
|
@ -14,5 +14,5 @@
|
|||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/"],
|
||||
"exclude": ["**/*.test.ts", "node_modules/hap-nodejs/**/**"]
|
||||
"exclude": []
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue