@openapi-typescript-infra/service 2.7.7 → 2.8.1

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.7",
3
+ "version": "2.8.1",
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": {
@@ -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,15 @@ 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
- locals.logger.info(
421
- { port: serverConfig.internalPort },
422
- 'Internal metadata server started',
423
- );
424
- })
425
- .then(() => {
426
- if (metricInfo.exporter) {
347
+ const prometheusExporter = getGlobalPrometheusExporter();
348
+ if (prometheusExporter) {
427
349
  locals.internalApp.get(
428
350
  '/metrics',
429
- metricInfo.exporter.getMetricsRequestHandler.bind(metricInfo.exporter),
351
+ prometheusExporter.getMetricsRequestHandler.bind(prometheusExporter),
430
352
  );
431
353
  locals.logger.info('Metrics exporter started');
432
354
  } else {
@@ -33,5 +33,8 @@ export async function startInternalApp(mainApp: ServiceExpress, port: number) {
33
33
  });
34
34
 
35
35
  await listenPromise;
36
+
37
+ mainApp.locals.logger.info({ port: finalPort }, 'Internal metadata server started');
38
+
36
39
  return app;
37
40
  }
@@ -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