@promster/metrics
Version:
Metrics utilities used by all other server integrations
418 lines (394 loc) • 19.2 kB
JavaScript
;
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]; }
});
});