@openapi-typescript-infra/service 2.7.6 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +2 -1
- package/.trunk/trunk.yaml +5 -5
- package/CHANGELOG.md +14 -0
- package/build/bootstrap.d.ts +1 -1
- package/build/config/schema.d.ts +0 -1
- package/build/express-app/app.js +6 -60
- package/build/express-app/app.js.map +1 -1
- package/build/telemetry/index.d.ts +13 -1
- package/build/telemetry/index.js +39 -9
- package/build/telemetry/index.js.map +1 -1
- package/build/tsconfig.build.tsbuildinfo +1 -1
- package/build/types.d.ts +1 -2
- package/config/config.json +1 -4
- package/package.json +18 -14
- package/src/config/schema.ts +0 -1
- package/src/express-app/app.ts +7 -79
- package/src/telemetry/index.ts +39 -8
- package/src/types.ts +1 -2
package/build/types.d.ts
CHANGED
|
@@ -5,11 +5,10 @@ import type pino from 'pino';
|
|
|
5
5
|
import type { Request, Response } from 'express';
|
|
6
6
|
import type { Application } from 'express-serve-static-core';
|
|
7
7
|
import type { middleware } from 'express-openapi-validator';
|
|
8
|
-
import type { Meter
|
|
8
|
+
import type { Meter } from '@opentelemetry/api';
|
|
9
9
|
import type { ConfigStore } from './config/types';
|
|
10
10
|
export interface InternalLocals extends Record<string, unknown> {
|
|
11
11
|
server?: Server;
|
|
12
|
-
meterProvider: MeterProvider;
|
|
13
12
|
mainApp: ServiceExpress;
|
|
14
13
|
}
|
|
15
14
|
export type ServiceLogger = pino.BaseLogger & Pick<pino.Logger, 'isLevelEnabled'>;
|
package/config/config.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openapi-typescript-infra/service",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "An opinionated framework for building configuration driven services - web, api, or ob. Uses OpenAPI, pino logging, express, confit, Typescript and vitest.",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"@opentelemetry/instrumentation": "^0.43.0",
|
|
68
68
|
"@opentelemetry/instrumentation-dns": "^0.32.2",
|
|
69
69
|
"@opentelemetry/instrumentation-express": "^0.33.1",
|
|
70
|
+
"@opentelemetry/instrumentation-fetch": "^0.43.0",
|
|
70
71
|
"@opentelemetry/instrumentation-generic-pool": "^0.32.2",
|
|
71
72
|
"@opentelemetry/instrumentation-graphql": "^0.35.1",
|
|
72
73
|
"@opentelemetry/instrumentation-http": "^0.43.0",
|
|
@@ -96,24 +97,24 @@
|
|
|
96
97
|
"devDependencies": {
|
|
97
98
|
"@commitlint/cli": "^17.7.1",
|
|
98
99
|
"@commitlint/config-conventional": "^17.7.0",
|
|
99
|
-
"@openapi-typescript-infra/coconfig": "^4.1
|
|
100
|
+
"@openapi-typescript-infra/coconfig": "^4.2.1",
|
|
100
101
|
"@semantic-release/changelog": "^6.0.3",
|
|
101
|
-
"@semantic-release/commit-analyzer": "^
|
|
102
|
+
"@semantic-release/commit-analyzer": "^11.0.0",
|
|
102
103
|
"@semantic-release/exec": "^6.0.3",
|
|
103
104
|
"@semantic-release/git": "^10.0.1",
|
|
104
|
-
"@semantic-release/release-notes-generator": "^
|
|
105
|
+
"@semantic-release/release-notes-generator": "^12.0.0",
|
|
105
106
|
"@types/cookie-parser": "^1.4.4",
|
|
106
|
-
"@types/eventsource": "1.1.
|
|
107
|
-
"@types/express": "^4.17.
|
|
107
|
+
"@types/eventsource": "1.1.12",
|
|
108
|
+
"@types/express": "^4.17.18",
|
|
108
109
|
"@types/glob": "^8.1.0",
|
|
109
|
-
"@types/lodash": "^4.14.
|
|
110
|
-
"@types/minimist": "^1.2.
|
|
111
|
-
"@types/node": "^20.
|
|
112
|
-
"@types/supertest": "^2.0.
|
|
113
|
-
"@typescript-eslint/eslint-plugin": "^6.7.
|
|
114
|
-
"@typescript-eslint/parser": "^6.7.
|
|
110
|
+
"@types/lodash": "^4.14.199",
|
|
111
|
+
"@types/minimist": "^1.2.3",
|
|
112
|
+
"@types/node": "^20.7.0",
|
|
113
|
+
"@types/supertest": "^2.0.13",
|
|
114
|
+
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
|
115
|
+
"@typescript-eslint/parser": "^6.7.3",
|
|
115
116
|
"coconfig": "^0.13.3",
|
|
116
|
-
"eslint": "^8.
|
|
117
|
+
"eslint": "^8.50.0",
|
|
117
118
|
"eslint-config-prettier": "^9.0.0",
|
|
118
119
|
"eslint-plugin-import": "^2.28.1",
|
|
119
120
|
"pino-pretty": "^10.2.0",
|
|
@@ -122,7 +123,10 @@
|
|
|
122
123
|
"ts-node": "^10.9.1",
|
|
123
124
|
"tsconfig-paths": "^4.2.0",
|
|
124
125
|
"typescript": "^5.2.2",
|
|
125
|
-
"vitest": "^0.34.
|
|
126
|
+
"vitest": "^0.34.5"
|
|
127
|
+
},
|
|
128
|
+
"resolutions": {
|
|
129
|
+
"qs": "^6.11.0"
|
|
126
130
|
},
|
|
127
131
|
"packageManager": "yarn@3.2.3"
|
|
128
132
|
}
|
package/src/config/schema.ts
CHANGED
|
@@ -60,7 +60,6 @@ export interface ConfigurationSchema extends Record<string, unknown> {
|
|
|
60
60
|
server: {
|
|
61
61
|
internalPort?: number;
|
|
62
62
|
port?: number;
|
|
63
|
-
metrics: ConfigurationItemEnabled;
|
|
64
63
|
// To enable HTTPS on the main service, set the key and cert to the
|
|
65
64
|
// actual key material (not the path). Use shortstop file: handler.
|
|
66
65
|
// Note that generally it's better to offload tls termination,
|
package/src/express-app/app.ts
CHANGED
|
@@ -6,11 +6,7 @@ import path from 'path';
|
|
|
6
6
|
import express from 'express';
|
|
7
7
|
import { pino } from 'pino';
|
|
8
8
|
import cookieParser from 'cookie-parser';
|
|
9
|
-
import { MeterProvider } from '@opentelemetry/sdk-metrics';
|
|
10
|
-
import { Resource } from '@opentelemetry/resources';
|
|
11
|
-
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
|
|
12
9
|
import { metrics } from '@opentelemetry/api';
|
|
13
|
-
import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
|
|
14
10
|
import { createTerminus } from '@godaddy/terminus';
|
|
15
11
|
import type { RequestHandler, Response } from 'express';
|
|
16
12
|
|
|
@@ -29,70 +25,13 @@ import type {
|
|
|
29
25
|
ServiceOptions,
|
|
30
26
|
ServiceStartOptions,
|
|
31
27
|
} from '../types';
|
|
32
|
-
import {
|
|
28
|
+
import { ConfigurationSchema } from '../config/schema';
|
|
33
29
|
import { getNodeEnv, isDev } from '../env';
|
|
30
|
+
import { getGlobalPrometheusExporter } from '../telemetry/index';
|
|
34
31
|
|
|
35
32
|
import { loadRoutes } from './route-loader';
|
|
36
33
|
import { startInternalApp } from './internal-server';
|
|
37
34
|
|
|
38
|
-
const METRICS_KEY = Symbol('PrometheusMetricsInfo');
|
|
39
|
-
|
|
40
|
-
interface InternalMetricsInfo {
|
|
41
|
-
meterProvider: MeterProvider;
|
|
42
|
-
exporter?: PrometheusExporter;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
interface AppWithMetrics {
|
|
46
|
-
[METRICS_KEY]?: InternalMetricsInfo;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function enableMetrics<SLocals extends ServiceLocals = ServiceLocals>(
|
|
50
|
-
app: ServiceExpress<SLocals>,
|
|
51
|
-
name: string,
|
|
52
|
-
) {
|
|
53
|
-
const meterProvider = new MeterProvider({
|
|
54
|
-
resource: new Resource({
|
|
55
|
-
[SemanticResourceAttributes.SERVICE_NAME]: name,
|
|
56
|
-
}),
|
|
57
|
-
});
|
|
58
|
-
metrics.setGlobalMeterProvider(meterProvider);
|
|
59
|
-
app.locals.meter = meterProvider.getMeter(name);
|
|
60
|
-
|
|
61
|
-
const metricsConfig = app.locals.config.get<ConfigurationItemEnabled>('server:metrics');
|
|
62
|
-
const value: InternalMetricsInfo = { meterProvider };
|
|
63
|
-
if (metricsConfig?.enabled) {
|
|
64
|
-
const finalConfig = {
|
|
65
|
-
...metricsConfig,
|
|
66
|
-
preventServerStart: true,
|
|
67
|
-
};
|
|
68
|
-
// There is what I would consider a bug in OpenTelemetry metrics
|
|
69
|
-
// wherein adding metrics BEFORE the metricReader is added results
|
|
70
|
-
// in those metrics screaming into the void. So, we need to add
|
|
71
|
-
// this up front and then just tie it to the internal express
|
|
72
|
-
// app if and when "listen" is called.
|
|
73
|
-
const exporter = new PrometheusExporter(finalConfig);
|
|
74
|
-
meterProvider.addMetricReader(exporter);
|
|
75
|
-
value.exporter = exporter;
|
|
76
|
-
} else {
|
|
77
|
-
app.locals.logger.info('No metrics will be exported');
|
|
78
|
-
}
|
|
79
|
-
// Squirrel it away for later
|
|
80
|
-
Object.defineProperty(app.locals, METRICS_KEY, {
|
|
81
|
-
value,
|
|
82
|
-
enumerable: false,
|
|
83
|
-
configurable: true,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function endMetrics<SLocals extends ServiceLocals = ServiceLocals>(
|
|
88
|
-
app: ServiceExpress<SLocals>,
|
|
89
|
-
) {
|
|
90
|
-
const { internalApp, logger } = app.locals;
|
|
91
|
-
const meterProvider = internalApp?.locals.meterProvider as MeterProvider | undefined;
|
|
92
|
-
await meterProvider?.shutdown();
|
|
93
|
-
logger.info('Metrics shutdown');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
35
|
function isSyncLogging() {
|
|
97
36
|
if (process.env.LOG_SYNC) {
|
|
98
37
|
return true;
|
|
@@ -169,13 +108,7 @@ export async function startApp<
|
|
|
169
108
|
await serviceImpl.attach(app);
|
|
170
109
|
}
|
|
171
110
|
|
|
172
|
-
|
|
173
|
-
await enableMetrics(app, name);
|
|
174
|
-
} catch (error) {
|
|
175
|
-
logger.error(error, 'Could not enable metrics.');
|
|
176
|
-
throw error;
|
|
177
|
-
}
|
|
178
|
-
|
|
111
|
+
app.locals.meter = metrics.getMeterProvider().getMeter(name);
|
|
179
112
|
if (config.get('trustProxy')) {
|
|
180
113
|
app.set('trust proxy', config.get('trustProxy'));
|
|
181
114
|
}
|
|
@@ -313,7 +246,6 @@ export async function shutdownApp(app: ServiceExpress) {
|
|
|
313
246
|
const { logger } = app.locals;
|
|
314
247
|
try {
|
|
315
248
|
await app.locals.service.stop?.(app);
|
|
316
|
-
await endMetrics(app);
|
|
317
249
|
logger.info('App shutdown complete');
|
|
318
250
|
} catch (error) {
|
|
319
251
|
logger.warn(error, 'Shutdown failed');
|
|
@@ -375,7 +307,6 @@ export async function listen<SLocals extends ServiceLocals = ServiceLocals>(
|
|
|
375
307
|
onShutdown() {
|
|
376
308
|
return Promise.resolve()
|
|
377
309
|
.then(() => service.stop?.(app))
|
|
378
|
-
.then(() => endMetrics(app))
|
|
379
310
|
.then(shutdownHandler || Promise.resolve)
|
|
380
311
|
.then(() => logger.info('Graceful shutdown complete'))
|
|
381
312
|
.catch((error) => logger.error(error, 'Error terminating tracing'))
|
|
@@ -401,9 +332,6 @@ export async function listen<SLocals extends ServiceLocals = ServiceLocals>(
|
|
|
401
332
|
logger.error(error, 'Main service listener error');
|
|
402
333
|
});
|
|
403
334
|
|
|
404
|
-
const metricInfo = (app.locals as AppWithMetrics)[METRICS_KEY] as InternalMetricsInfo;
|
|
405
|
-
delete (app.locals as AppWithMetrics)[METRICS_KEY];
|
|
406
|
-
|
|
407
335
|
// TODO handle rejection/error?
|
|
408
336
|
const listenPromise = new Promise<void>((accept) => {
|
|
409
337
|
server.listen(port, () => {
|
|
@@ -412,21 +340,21 @@ export async function listen<SLocals extends ServiceLocals = ServiceLocals>(
|
|
|
412
340
|
|
|
413
341
|
const serverConfig = locals.config.get('server') as ConfigurationSchema['server'];
|
|
414
342
|
// Ok now start the internal port if we have one.
|
|
415
|
-
if (serverConfig?.internalPort) {
|
|
343
|
+
if (serverConfig?.internalPort || serverConfig?.internalPort === 0) {
|
|
416
344
|
startInternalApp(app, serverConfig.internalPort)
|
|
417
345
|
.then((internalApp) => {
|
|
418
346
|
locals.internalApp = internalApp;
|
|
419
|
-
internalApp.locals.meterProvider = metricInfo.meterProvider;
|
|
420
347
|
locals.logger.info(
|
|
421
348
|
{ port: serverConfig.internalPort },
|
|
422
349
|
'Internal metadata server started',
|
|
423
350
|
);
|
|
424
351
|
})
|
|
425
352
|
.then(() => {
|
|
426
|
-
|
|
353
|
+
const prometheusExporter = getGlobalPrometheusExporter();
|
|
354
|
+
if (prometheusExporter) {
|
|
427
355
|
locals.internalApp.get(
|
|
428
356
|
'/metrics',
|
|
429
|
-
|
|
357
|
+
prometheusExporter.getMetricsRequestHandler.bind(prometheusExporter),
|
|
430
358
|
);
|
|
431
359
|
locals.logger.info('Metrics exporter started');
|
|
432
360
|
} else {
|
package/src/telemetry/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { diag, DiagConsoleLogger, DiagLogLevel } from '@opentelemetry/api';
|
|
2
2
|
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
|
|
3
3
|
import * as opentelemetry from '@opentelemetry/sdk-node';
|
|
4
|
+
import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
|
|
4
5
|
|
|
5
6
|
import type {
|
|
6
7
|
DelayLoadServiceStartOptions,
|
|
@@ -31,17 +32,47 @@ function getExporter() {
|
|
|
31
32
|
return new DummySpanExporter();
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
let prometheusExporter: PrometheusExporter | undefined;
|
|
36
|
+
let telemetrySdk: opentelemetry.NodeSDK | undefined;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* OpenTelemetry is not friendly to the idea of stopping
|
|
40
|
+
* and starting itself, it seems. So we can only keep a global
|
|
41
|
+
* instance of the infrastructure no matter how many times
|
|
42
|
+
* you start/stop your service (this is mostly only relevant for testing).
|
|
43
|
+
* In addition, since we have to load it right away before configuration
|
|
44
|
+
* is available, we can't use configuration to decide anything.
|
|
45
|
+
*/
|
|
46
|
+
export function startGlobalTelemetry(serviceName: string) {
|
|
47
|
+
if (!prometheusExporter) {
|
|
48
|
+
prometheusExporter = new PrometheusExporter({ preventServerStart: true });
|
|
49
|
+
telemetrySdk = new opentelemetry.NodeSDK({
|
|
50
|
+
serviceName,
|
|
51
|
+
autoDetectResources: true,
|
|
52
|
+
traceExporter: getExporter(),
|
|
53
|
+
metricReader: prometheusExporter,
|
|
54
|
+
instrumentations: [getAutoInstrumentations()],
|
|
55
|
+
});
|
|
56
|
+
telemetrySdk.start();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getGlobalPrometheusExporter() {
|
|
61
|
+
return prometheusExporter;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function shutdownGlobalTelemetry() {
|
|
65
|
+
await prometheusExporter?.shutdown();
|
|
66
|
+
await telemetrySdk?.shutdown();
|
|
67
|
+
telemetrySdk = undefined;
|
|
68
|
+
prometheusExporter = undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
34
71
|
export async function startWithTelemetry<
|
|
35
72
|
SLocals extends ServiceLocals = ServiceLocals,
|
|
36
73
|
RLocals extends RequestLocals = RequestLocals,
|
|
37
74
|
>(options: DelayLoadServiceStartOptions) {
|
|
38
|
-
|
|
39
|
-
serviceName: options.name,
|
|
40
|
-
autoDetectResources: true,
|
|
41
|
-
traceExporter: getExporter(),
|
|
42
|
-
instrumentations: [getAutoInstrumentations()],
|
|
43
|
-
});
|
|
44
|
-
await sdk.start();
|
|
75
|
+
startGlobalTelemetry(options.name);
|
|
45
76
|
|
|
46
77
|
// eslint-disable-next-line import/no-unresolved, @typescript-eslint/no-var-requires
|
|
47
78
|
const { startApp, listen } = require('../express-app/app.js') as {
|
|
@@ -60,7 +91,7 @@ export async function startWithTelemetry<
|
|
|
60
91
|
app.locals.logger.info('OpenTelemetry enabled');
|
|
61
92
|
|
|
62
93
|
const server = await listen(app, async () => {
|
|
63
|
-
await
|
|
94
|
+
await shutdownGlobalTelemetry();
|
|
64
95
|
app.locals.logger.info('OpenTelemetry shut down');
|
|
65
96
|
});
|
|
66
97
|
return { app, server };
|
package/src/types.ts
CHANGED
|
@@ -4,13 +4,12 @@ import type pino from 'pino';
|
|
|
4
4
|
import type { Request, Response } from 'express';
|
|
5
5
|
import type { Application } from 'express-serve-static-core';
|
|
6
6
|
import type { middleware } from 'express-openapi-validator';
|
|
7
|
-
import type { Meter
|
|
7
|
+
import type { Meter } from '@opentelemetry/api';
|
|
8
8
|
|
|
9
9
|
import type { ConfigStore } from './config/types';
|
|
10
10
|
|
|
11
11
|
export interface InternalLocals extends Record<string, unknown> {
|
|
12
12
|
server?: Server;
|
|
13
|
-
meterProvider: MeterProvider;
|
|
14
13
|
mainApp: ServiceExpress;
|
|
15
14
|
}
|
|
16
15
|
|