UNPKG

@promster/metrics

Version:

Metrics utilities used by all other server integrations

418 lines (394 loc) 19.2 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var once = require('lodash.once'); var Prometheus = require('prom-client'); var merge = require('merge-options'); var gcStats = require('prometheus-gc-stats'); var url = require('node:url'); var UrlValueParser = require('url-value-parser'); var types = require('@promster/types'); function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return Object.freeze(n); } var once__default = /*#__PURE__*/_interopDefault(once); var Prometheus__namespace = /*#__PURE__*/_interopNamespace(Prometheus); var merge__default = /*#__PURE__*/_interopDefault(merge); var gcStats__default = /*#__PURE__*/_interopDefault(gcStats); var url__default = /*#__PURE__*/_interopDefault(url); var UrlValueParser__default = /*#__PURE__*/_interopDefault(UrlValueParser); const isRunningInKubernetes = () => Boolean(process.env.KUBERNETES_SERVICE_HOST); const skipMetricsInEnvironment = options => options.detectKubernetes === true && !isRunningInKubernetes(); // NOTE: // This is the `globalRegistry` provided by the `prom-client` // We could create multiple registries with `new Prometheus.registry()`. const defaultRegister = Prometheus__namespace.register; const configure = once__default["default"](options => { const shouldSkipMetricsInEnvironment = skipMetricsInEnvironment(options); if (!shouldSkipMetricsInEnvironment) { Prometheus__namespace.collectDefaultMetrics(options); } }); const asArray$2 = maybeArray => Array.isArray(maybeArray) ? maybeArray : [maybeArray]; const defaultOptions$4 = { getLabelValues: () => ({}), labels: [], metricPrefix: '', metricNames: { up: ['nodejs_up'] } }; const getMetrics$2 = options => ({ up: asArray$2(options.metricNames.up).map(nameOfUpMetric => new Prometheus__namespace.Gauge({ name: `${options.metricPrefix}${nameOfUpMetric}`, help: '1 = nodejs server is up, 0 = nodejs server is not up' })) }); const createGcMetrics = options => { const defaultedOptions = merge__default["default"](defaultOptions$4, options); configure({ prefix: defaultedOptions.metricPrefix }); const gcMetrics = getMetrics$2(defaultedOptions); return gcMetrics; }; createGcMetrics.defaultOptions = defaultOptions$4; const defaultOptions$3 = { disableGcMetrics: false }; const createGcObserver = once__default["default"]((_metrics, options) => () => { const startGcStats = gcStats__default["default"](defaultRegister, { prefix: options.metricPrefix }); startGcStats(); }); // @ts-expect-error createGcObserver.defaultOptions = defaultOptions$3; const defaultGraphQlPercentiles = [0.5, 0.9, 0.95, 0.98, 0.99]; const defaultLabels$1 = ['operation_name']; const asArray$1 = maybeArray => Array.isArray(maybeArray) ? maybeArray : [maybeArray]; const shouldObserveGraphQlParseDurationAsHistogram = options => options.metricTypes.includes('graphQlParseDurationHistogram'); const shouldObserveGraphQlValidationDurationAsHistogram = options => options.metricTypes.includes('graphQlValidationDurationHistogram'); const shouldObserveGraphQlResolveFieldDurationAsHistogram = options => options.metricTypes.includes('graphQlResolveFieldDurationHistogram'); const shouldObserveGraphQlRequestDurationAsHistogram = options => options.metricTypes.includes('graphQlRequestDurationHistogram'); const shouldObserveGraphQlErrorsTotalAsCounter = options => options.metricTypes.includes('graphQlErrorsTotal'); const defaultOptions$2 = { getLabelValues: () => ({}), labels: [], metricPrefix: '', metricTypes: ['graphQlParseDurationHistogram', 'graphQlValidationDurationHistogram', 'graphQlResolveFieldDurationHistogram', 'graphQlRequestDurationHistogram', 'graphQlErrorsTotal'], metricNames: { graphQlParseDuration: ['graphql_parse_duration_seconds'], graphQlValidationDuration: ['graphql_validation_duration_seconds'], graphQlResolveFieldDuration: ['graphql_resolve_field_duration_seconds'], graphQlRequestDuration: ['graphql_request_duration_seconds'], graphQlErrorsTotal: ['graphql_errors_total'] }, metricPercentiles: { graphQlParseDuration: defaultGraphQlPercentiles, graphQlValidationDuration: defaultGraphQlPercentiles, graphQlResolveFieldDuration: defaultGraphQlPercentiles, graphQlRequestDuration: defaultGraphQlPercentiles, graphQlErrorsTotal: defaultGraphQlPercentiles } }; const getMetrics$1 = options => ({ graphQlParseDuration: shouldObserveGraphQlParseDurationAsHistogram(options) ? asArray$1(options.metricNames.graphQlParseDuration).map(nameOfGraphQlParseDuration => { var _options$metricPercen; return new Prometheus__namespace.Histogram({ name: `${options.metricPrefix}${nameOfGraphQlParseDuration}`, help: 'The GraphQL request parse time in seconds.', labelNames: defaultLabels$1.concat(options.labels).sort(), buckets: ((_options$metricPercen = options.metricPercentiles) === null || _options$metricPercen === void 0 ? void 0 : _options$metricPercen.graphQlParseDuration) || defaultGraphQlPercentiles }); }) : undefined, graphQlValidationDuration: shouldObserveGraphQlValidationDurationAsHistogram(options) ? asArray$1(options.metricNames.graphQlValidationDuration).map(nameOfGraphQlValidationDuration => { var _options$metricPercen2; return new Prometheus__namespace.Histogram({ name: `${options.metricPrefix}${nameOfGraphQlValidationDuration}`, help: 'The GraphQL request validation time in seconds.', labelNames: defaultLabels$1.concat(options.labels).sort(), buckets: ((_options$metricPercen2 = options.metricPercentiles) === null || _options$metricPercen2 === void 0 ? void 0 : _options$metricPercen2.graphQlValidationDuration) || defaultGraphQlPercentiles }); }) : undefined, graphQlResolveFieldDuration: shouldObserveGraphQlResolveFieldDurationAsHistogram(options) ? asArray$1(options.metricNames.graphQlResolveFieldDuration).map(nameOfGraphQlResolveFieldDuration => { var _options$metricPercen3; return new Prometheus__namespace.Histogram({ name: `${options.metricPrefix}${nameOfGraphQlResolveFieldDuration}`, help: 'The GraphQL field resolving time in seconds.', labelNames: defaultLabels$1.concat(['field_name']).concat(options.labels).sort(), buckets: ((_options$metricPercen3 = options.metricPercentiles) === null || _options$metricPercen3 === void 0 ? void 0 : _options$metricPercen3.graphQlResolveFieldDuration) || defaultGraphQlPercentiles }); }) : undefined, graphQlRequestDuration: shouldObserveGraphQlRequestDurationAsHistogram(options) ? asArray$1(options.metricNames.graphQlRequestDuration).map(nameOfGraphQlRequestDuration => { var _options$metricPercen4; return new Prometheus__namespace.Histogram({ name: `${options.metricPrefix}${nameOfGraphQlRequestDuration}`, help: 'The GraphQL request duration time in seconds.', labelNames: defaultLabels$1.concat(options.labels).sort(), buckets: ((_options$metricPercen4 = options.metricPercentiles) === null || _options$metricPercen4 === void 0 ? void 0 : _options$metricPercen4.graphQlRequestDuration) || defaultGraphQlPercentiles }); }) : undefined, graphQlErrorsTotal: shouldObserveGraphQlErrorsTotalAsCounter(options) ? asArray$1(options.metricNames.graphQlErrorsTotal).map(nameOfGraphQlErrorsCount => new Prometheus__namespace.Counter({ name: `${options.metricPrefix}${nameOfGraphQlErrorsCount}`, help: 'Count of errors while parsing, validating, or executing a GraphQL operation.', labelNames: defaultLabels$1.concat(['phase']).concat(options.labels).sort() })) : undefined }); const createGraphQlMetrics = options => { const defaultedOptions = merge__default["default"](defaultOptions$2, options); configure({ prefix: defaultedOptions.metricPrefix }); const metrics = getMetrics$1(defaultedOptions); return metrics; }; createGraphQlMetrics.defaultOptions = defaultOptions$2; const defaultHttpRequestDurationPercentileInSeconds = [0.5, 0.9, 0.95, 0.98, 0.99]; const defaultHttpRequestDurationInSeconds = [0.05, 0.1, 0.3, 0.5, 0.8, 1, 1.5, 2, 3, 10]; const defaultHttpContentLengthInBytes = [100000, 200000, 500000, 1000000, 1500000, 2000000, 3000000, 5000000, 10000000]; const defaultLabels = ['path', 'status_code', 'method']; const asArray = maybeArray => Array.isArray(maybeArray) ? maybeArray : [maybeArray]; const shouldObserveHttpRequestsAsSummary = options => options.metricTypes.includes('httpRequestsSummary'); const shouldObserveHttpRequestsAsHistogram = options => options.metricTypes.includes('httpRequestsHistogram'); const shouldObserveHttpRequestsAsCounter = options => options.metricTypes.includes('httpRequestsTotal'); const shouldObserveHttpContentLengthAsHistogram = options => options.metricTypes.includes('httpContentLengthHistogram'); const defaultOptions$1 = { getLabelValues: () => ({}), labels: [], metricPrefix: '', metricTypes: ['httpRequestsTotal', 'httpRequestsHistogram'], metricNames: { httpRequestsTotal: ['http_requests_total'], httpRequestDurationPerPercentileInSeconds: ['http_request_duration_per_percentile_seconds'], httpRequestDurationInSeconds: ['http_request_duration_seconds'], httpRequestContentLengthInBytes: ['http_request_content_length_bytes'], httpResponseContentLengthInBytes: ['http_response_content_length_bytes'] }, metricBuckets: { httpRequestContentLengthInBytes: defaultHttpContentLengthInBytes, httpRequestDurationInSeconds: defaultHttpRequestDurationInSeconds }, metricPercentiles: { httpRequestDurationPerPercentileInSeconds: defaultHttpRequestDurationPercentileInSeconds, httpResponseContentLengthInBytes: defaultHttpContentLengthInBytes } }; const getMetrics = options => ({ httpRequestContentLengthInBytes: shouldObserveHttpContentLengthAsHistogram(options) ? asArray(options.metricNames.httpRequestContentLengthInBytes).map(nameOfHttpContentLengthMetric => { var _options$metricBucket; return new Prometheus__namespace.Histogram({ name: `${options.metricPrefix}${nameOfHttpContentLengthMetric}`, help: 'The HTTP request content length in bytes.', labelNames: defaultLabels.concat(options.labels).sort(), buckets: ((_options$metricBucket = options.metricBuckets) === null || _options$metricBucket === void 0 ? void 0 : _options$metricBucket.httpRequestContentLengthInBytes) || defaultHttpContentLengthInBytes }); }) : undefined, httpResponseContentLengthInBytes: shouldObserveHttpContentLengthAsHistogram(options) ? asArray(options.metricNames.httpResponseContentLengthInBytes).map(nameOfHttpContentLengthMetric => { var _options$metricBucket2; return new Prometheus__namespace.Histogram({ name: `${options.metricPrefix}${nameOfHttpContentLengthMetric}`, help: 'The HTTP response content length in bytes.', labelNames: defaultLabels.concat(options.labels).sort(), buckets: ((_options$metricBucket2 = options.metricBuckets) === null || _options$metricBucket2 === void 0 ? void 0 : _options$metricBucket2.httpResponseContentLengthInBytes) || defaultHttpContentLengthInBytes }); }) : undefined }); const getHttpRequestLatencyMetricsInSeconds = options => ({ httpRequestDurationPerPercentileInSeconds: shouldObserveHttpRequestsAsSummary(options) ? asArray(options.metricNames.httpRequestDurationPerPercentileInSeconds).map(nameOfHttpRequestDurationPerPercentileInSeconds => { var _options$metricPercen; return new Prometheus__namespace.Summary({ name: `${options.metricPrefix}${nameOfHttpRequestDurationPerPercentileInSeconds}`, help: 'The HTTP request latencies in seconds.', labelNames: defaultLabels.concat(options.labels).sort(), percentiles: ((_options$metricPercen = options.metricPercentiles) === null || _options$metricPercen === void 0 ? void 0 : _options$metricPercen.httpRequestDurationPerPercentileInSeconds) || defaultHttpRequestDurationPercentileInSeconds }); }) : undefined, httpRequestDurationInSeconds: shouldObserveHttpRequestsAsHistogram(options) ? asArray(options.metricNames.httpRequestDurationInSeconds).map(nameOfHttpRequestDurationInSecondsMetric => { var _options$metricBucket3; return new Prometheus__namespace.Histogram({ name: `${options.metricPrefix}${nameOfHttpRequestDurationInSecondsMetric}`, help: 'The HTTP request latencies in seconds.', labelNames: defaultLabels.concat(options.labels).sort(), buckets: ((_options$metricBucket3 = options.metricBuckets) === null || _options$metricBucket3 === void 0 ? void 0 : _options$metricBucket3.httpRequestDurationInSeconds) || defaultHttpRequestDurationInSeconds }); }) : undefined }); const getHttpRequestCounterMetric = options => ({ httpRequestsTotal: shouldObserveHttpRequestsAsCounter(options) ? asArray(options.metricNames.httpRequestsTotal).map(nameOfHttpRequestsTotalMetric => new Prometheus__namespace.Counter({ name: `${options.metricPrefix}${nameOfHttpRequestsTotalMetric}`, help: 'The total HTTP requests.', labelNames: defaultLabels.concat(options.labels).sort() })) : undefined }); const createHttpMetrics = options => { const defaultedOptions = merge__default["default"](defaultOptions$1, options); configure({ prefix: defaultedOptions.metricPrefix }); const metrics = getMetrics(defaultedOptions); const httpRequestLatencyMetricsInSeconds = getHttpRequestLatencyMetricsInSeconds(defaultedOptions); const httpRequestCounterMetric = getHttpRequestCounterMetric(defaultedOptions); return Object.assign({}, metrics, httpRequestLatencyMetricsInSeconds, httpRequestCounterMetric); }; createHttpMetrics.defaultOptions = defaultOptions$1; const NS_PER_SEC = 1e9; function endMeasurementFrom(start) { const [seconds, nanoseconds] = process.hrtime(start); return { durationS: (seconds * NS_PER_SEC + nanoseconds) / NS_PER_SEC }; } function sortLabels(unsortedLabels) { return Object.keys(unsortedLabels).sort((a, b) => { if (a < b) { return -1; } if (a > b) { return 1; } return 0; }).reduce((sortedLabels, labelName) => { sortedLabels[labelName] = unsortedLabels[labelName]; return sortedLabels; }, {}); } const defaultOptions = { detectKubernetes: false }; function isTiming(timing) { return !Array.isArray(timing); } const createRequestRecorder = (metrics, options = defaultOptions) => { const defaultedRecorderOptions = merge__default["default"](defaultOptions, options); const shouldSkipMetricsByEnvironment = skipMetricsInEnvironment(defaultedRecorderOptions); return (timing, recordingOptions) => { const durationS = isTiming(timing) ? timing.end().value().seconds : endMeasurementFrom(timing).durationS; const labels = sortLabels(recordingOptions.labels); if (!shouldSkipMetricsByEnvironment && durationS !== undefined) { if (metrics.httpRequestDurationInSeconds) { for (const httpRequestDurationInSecondsMetricType of metrics.httpRequestDurationInSeconds) { httpRequestDurationInSecondsMetricType.observe(labels, durationS); } } } if (!shouldSkipMetricsByEnvironment && durationS !== undefined) { if (metrics.httpRequestDurationPerPercentileInSeconds) { for (const httpRequestDurationPerPercentileInSecondsMetricType of metrics.httpRequestDurationPerPercentileInSeconds) { httpRequestDurationPerPercentileInSecondsMetricType.observe(labels, durationS); } } } if (!shouldSkipMetricsByEnvironment && durationS !== undefined) { if (metrics.httpRequestsTotal) { for (const httpRequestsTotalMetricType of metrics.httpRequestsTotal) { httpRequestsTotalMetricType.inc(labels); } } } if (recordingOptions.requestContentLength) { if (metrics.httpRequestContentLengthInBytes) { for (const httpRequestContentLengthInBytesMetricType of metrics.httpRequestContentLengthInBytes) { httpRequestContentLengthInBytesMetricType.observe(labels, recordingOptions.requestContentLength); } } } if (recordingOptions.responseContentLength) { if (metrics.httpResponseContentLengthInBytes) { for (const httpResponseContentLengthInBytesMetricType of metrics.httpResponseContentLengthInBytes) { httpResponseContentLengthInBytesMetricType.observe(labels, recordingOptions.responseContentLength); } } } }; }; createRequestRecorder.defaultOptions = defaultOptions; const normalizeMethod = method => method.toLowerCase(); const urlValueParser = new UrlValueParser__default["default"](); const normalizePath = path => { const parsedPathname = url__default["default"].parse(path).pathname; if (!parsedPathname) { return ''; } return urlValueParser.replacePathValues(parsedPathname, '#val'); }; const normalizeStatusCode = statusCode => statusCode; const defaultNormalizers = { normalizeStatusCode, normalizePath, normalizeMethod }; const getSummary = async () => defaultRegister.metrics(); const getContentType = () => defaultRegister.contentType; class Timing { static NS_PER_SEC = BigInt(1e9); #startTime; #endTime; constructor() { this.reset(); } value() { const startTime = this.#startTime; const endTime = this.#endTime; if (!endTime || !startTime) { return { seconds: undefined }; } return { seconds: Number(endTime - startTime) / Number(Timing.NS_PER_SEC) }; } reset() { this.#startTime = process.hrtime.bigint(); this.#endTime = undefined; return this; } end() { this.#endTime = process.hrtime.bigint(); return this; } } const timing = { start() { return new Timing(); } }; exports.Prometheus = Prometheus__namespace; exports.createGcMetrics = createGcMetrics; exports.createGcObserver = createGcObserver; exports.createGraphQlMetrics = createGraphQlMetrics; exports.createHttpMetrics = createHttpMetrics; exports.createRequestRecorder = createRequestRecorder; exports.defaultNormalizers = defaultNormalizers; exports.defaultRegister = defaultRegister; exports.endMeasurementFrom = endMeasurementFrom; exports.getContentType = getContentType; exports.getSummary = getSummary; exports.isRunningInKubernetes = isRunningInKubernetes; exports.normalizeMethod = normalizeMethod; exports.normalizePath = normalizePath; exports.normalizeStatusCode = normalizeStatusCode; exports.skipMetricsInEnvironment = skipMetricsInEnvironment; exports.sortLabels = sortLabels; exports.timing = timing; Object.keys(types).forEach(function (k) { if (k !== 'default' && !exports.hasOwnProperty(k)) Object.defineProperty(exports, k, { enumerable: true, get: function () { return types[k]; } }); });