@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/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, MeterProvider } from '@opentelemetry/api';
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'>;
@@ -24,9 +24,6 @@
24
24
  "server": {
25
25
  "port": 8000,
26
26
  "internalPort": 3000,
27
- "hostname": "localhost",
28
- "metrics": {
29
- "enabled": true
30
- }
27
+ "hostname": "localhost"
31
28
  }
32
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openapi-typescript-infra/service",
3
- "version": "2.7.6",
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.0",
100
+ "@openapi-typescript-infra/coconfig": "^4.2.1",
100
101
  "@semantic-release/changelog": "^6.0.3",
101
- "@semantic-release/commit-analyzer": "^10.0.4",
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": "^11.0.7",
105
+ "@semantic-release/release-notes-generator": "^12.0.0",
105
106
  "@types/cookie-parser": "^1.4.4",
106
- "@types/eventsource": "1.1.11",
107
- "@types/express": "^4.17.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.198",
110
- "@types/minimist": "^1.2.2",
111
- "@types/node": "^20.6.2",
112
- "@types/supertest": "^2.0.12",
113
- "@typescript-eslint/eslint-plugin": "^6.7.0",
114
- "@typescript-eslint/parser": "^6.7.0",
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.49.0",
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.4"
126
+ "vitest": "^0.34.5"
127
+ },
128
+ "resolutions": {
129
+ "qs": "^6.11.0"
126
130
  },
127
131
  "packageManager": "yarn@3.2.3"
128
132
  }
@@ -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,
@@ -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 { ConfigurationItemEnabled, ConfigurationSchema } from '../config/schema';
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
- try {
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
- if (metricInfo.exporter) {
353
+ const prometheusExporter = getGlobalPrometheusExporter();
354
+ if (prometheusExporter) {
427
355
  locals.internalApp.get(
428
356
  '/metrics',
429
- metricInfo.exporter.getMetricsRequestHandler.bind(metricInfo.exporter),
357
+ prometheusExporter.getMetricsRequestHandler.bind(prometheusExporter),
430
358
  );
431
359
  locals.logger.info('Metrics exporter started');
432
360
  } else {
@@ -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
- const sdk = new opentelemetry.NodeSDK({
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 sdk.shutdown();
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, MeterProvider } from '@opentelemetry/api';
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