@promster/metrics
Version:
Metrics utilities used by all other server integrations
336 lines (335 loc) • 16 kB
JavaScript
import { once } from "es-toolkit/function";
import * as Prometheus from "prom-client";
import merge from "merge-options";
import { gcStats } from "@chainsafe/prometheus-gc-stats";
import url from "node:url";
import UrlValueParser from "url-value-parser";
//#region src/environment/kubernetes.ts
const isRunningInKubernetes = () => Boolean(process.env.KUBERNETES_SERVICE_HOST);
//#endregion
//#region src/environment/skip-metrics-in-environment.ts
const skipMetricsInEnvironment = (options) => options.detectKubernetes === true && !isRunningInKubernetes();
//#endregion
//#region src/client/client.ts
const defaultRegister = Prometheus.register;
const configure = once((options) => {
if (!skipMetricsInEnvironment(options)) Prometheus.collectDefaultMetrics(options);
});
//#endregion
//#region src/create-gc-metrics/create-gc-metrics.ts
const asArray$2 = (maybeArray) => Array.isArray(maybeArray) ? maybeArray : [maybeArray];
const defaultOptions$4 = {
getLabelValues: () => ({}),
labels: [],
metricPrefix: "",
metricNames: { up: ["nodejs_up"] },
gcCollectionInterval: 6e3
};
const getMetrics$2 = (options) => ({ up: asArray$2(options.metricNames.up).map((nameOfUpMetric) => new Prometheus.Gauge({
name: `${options.metricPrefix}${nameOfUpMetric}`,
help: "1 = nodejs server is up, 0 = nodejs server is not up"
})) });
const createGcMetrics = (options) => {
const defaultedOptions = merge(defaultOptions$4, options);
configure({ prefix: defaultedOptions.metricPrefix });
return getMetrics$2(defaultedOptions);
};
createGcMetrics.defaultOptions = defaultOptions$4;
//#endregion
//#region src/create-gc-observer/create-gc-observer.ts
const defaultOptions$3 = { disableGcMetrics: false };
const createGcObserver = once((_metrics, options) => () => {
gcStats(defaultRegister, {
collectionInterval: options.gcCollectionInterval,
prefix: options.metricPrefix
});
});
createGcObserver.defaultOptions = defaultOptions$3;
//#endregion
//#region src/create-graphql-metrics/create-graphql-metrics.ts
const defaultGraphQlPercentiles = [
.5,
.9,
.95,
.98,
.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) => new Prometheus.Histogram({
name: `${options.metricPrefix}${nameOfGraphQlParseDuration}`,
help: "The GraphQL request parse time in seconds.",
labelNames: defaultLabels$1.concat(options.labels).toSorted(),
buckets: options.metricPercentiles?.graphQlParseDuration || defaultGraphQlPercentiles
})) : void 0,
graphQlValidationDuration: shouldObserveGraphQlValidationDurationAsHistogram(options) ? asArray$1(options.metricNames.graphQlValidationDuration).map((nameOfGraphQlValidationDuration) => new Prometheus.Histogram({
name: `${options.metricPrefix}${nameOfGraphQlValidationDuration}`,
help: "The GraphQL request validation time in seconds.",
labelNames: defaultLabels$1.concat(options.labels).toSorted(),
buckets: options.metricPercentiles?.graphQlValidationDuration || defaultGraphQlPercentiles
})) : void 0,
graphQlResolveFieldDuration: shouldObserveGraphQlResolveFieldDurationAsHistogram(options) ? asArray$1(options.metricNames.graphQlResolveFieldDuration).map((nameOfGraphQlResolveFieldDuration) => new Prometheus.Histogram({
name: `${options.metricPrefix}${nameOfGraphQlResolveFieldDuration}`,
help: "The GraphQL field resolving time in seconds.",
labelNames: defaultLabels$1.concat(["field_name"]).concat(options.labels).toSorted(),
buckets: options.metricPercentiles?.graphQlResolveFieldDuration || defaultGraphQlPercentiles
})) : void 0,
graphQlRequestDuration: shouldObserveGraphQlRequestDurationAsHistogram(options) ? asArray$1(options.metricNames.graphQlRequestDuration).map((nameOfGraphQlRequestDuration) => new Prometheus.Histogram({
name: `${options.metricPrefix}${nameOfGraphQlRequestDuration}`,
help: "The GraphQL request duration time in seconds.",
labelNames: defaultLabels$1.concat(options.labels).toSorted(),
buckets: options.metricPercentiles?.graphQlRequestDuration || defaultGraphQlPercentiles
})) : void 0,
graphQlErrorsTotal: shouldObserveGraphQlErrorsTotalAsCounter(options) ? asArray$1(options.metricNames.graphQlErrorsTotal).map((nameOfGraphQlErrorsCount) => new Prometheus.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).toSorted()
})) : void 0
});
const createGraphQlMetrics = (options) => {
const defaultedOptions = merge(defaultOptions$2, options);
configure({ prefix: defaultedOptions.metricPrefix });
return getMetrics$1(defaultedOptions);
};
createGraphQlMetrics.defaultOptions = defaultOptions$2;
//#endregion
//#region src/create-http-metrics/create-http-metrics.ts
const defaultHttpRequestDurationPercentileInSeconds = [
.5,
.9,
.95,
.98,
.99
];
const defaultHttpRequestDurationInSeconds = [
.05,
.1,
.3,
.5,
.8,
1,
1.5,
2,
3,
10
];
const defaultHttpContentLengthInBytes = [
1e5,
2e5,
5e5,
1e6,
15e5,
2e6,
3e6,
5e6,
1e7
];
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) => new Prometheus.Histogram({
name: `${options.metricPrefix}${nameOfHttpContentLengthMetric}`,
help: "The HTTP request content length in bytes.",
labelNames: defaultLabels.concat(options.labels).toSorted(),
buckets: options.metricBuckets?.httpRequestContentLengthInBytes || defaultHttpContentLengthInBytes
})) : void 0,
httpResponseContentLengthInBytes: shouldObserveHttpContentLengthAsHistogram(options) ? asArray(options.metricNames.httpResponseContentLengthInBytes).map((nameOfHttpContentLengthMetric) => new Prometheus.Histogram({
name: `${options.metricPrefix}${nameOfHttpContentLengthMetric}`,
help: "The HTTP response content length in bytes.",
labelNames: defaultLabels.concat(options.labels).toSorted(),
buckets: options.metricBuckets?.httpResponseContentLengthInBytes || defaultHttpContentLengthInBytes
})) : void 0
});
const getHttpRequestLatencyMetricsInSeconds = (options) => ({
httpRequestDurationPerPercentileInSeconds: shouldObserveHttpRequestsAsSummary(options) ? asArray(options.metricNames.httpRequestDurationPerPercentileInSeconds).map((nameOfHttpRequestDurationPerPercentileInSeconds) => new Prometheus.Summary({
name: `${options.metricPrefix}${nameOfHttpRequestDurationPerPercentileInSeconds}`,
help: "The HTTP request latencies in seconds.",
labelNames: defaultLabels.concat(options.labels).toSorted(),
percentiles: options.metricPercentiles?.httpRequestDurationPerPercentileInSeconds || defaultHttpRequestDurationPercentileInSeconds
})) : void 0,
httpRequestDurationInSeconds: shouldObserveHttpRequestsAsHistogram(options) ? asArray(options.metricNames.httpRequestDurationInSeconds).map((nameOfHttpRequestDurationInSecondsMetric) => new Prometheus.Histogram({
name: `${options.metricPrefix}${nameOfHttpRequestDurationInSecondsMetric}`,
help: "The HTTP request latencies in seconds.",
labelNames: defaultLabels.concat(options.labels).toSorted(),
buckets: options.metricBuckets?.httpRequestDurationInSeconds || defaultHttpRequestDurationInSeconds
})) : void 0
});
const getHttpRequestCounterMetric = (options) => ({ httpRequestsTotal: shouldObserveHttpRequestsAsCounter(options) ? asArray(options.metricNames.httpRequestsTotal).map((nameOfHttpRequestsTotalMetric) => new Prometheus.Counter({
name: `${options.metricPrefix}${nameOfHttpRequestsTotalMetric}`,
help: "The total HTTP requests.",
labelNames: defaultLabels.concat(options.labels).toSorted()
})) : void 0 });
const createHttpMetrics = (options) => {
const defaultedOptions = merge(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;
//#endregion
//#region src/end-measurement-from/end-measurement-from.ts
const NS_PER_SEC = 1e9;
function endMeasurementFrom(start) {
const [seconds, nanoseconds] = process.hrtime(start);
return { durationS: (seconds * NS_PER_SEC + nanoseconds) / NS_PER_SEC };
}
//#endregion
//#region src/sort-labels/sort-labels.ts
function sortLabels(unsortedLabels) {
return Object.keys(unsortedLabels).toSorted((a, b) => {
if (a < b) return -1;
if (a > b) return 1;
return 0;
}).reduce((sortedLabels, labelName) => {
sortedLabels[labelName] = unsortedLabels[labelName];
return sortedLabels;
}, {});
}
//#endregion
//#region src/create-request-recorder/create-request-recorder.ts
const defaultOptions = { detectKubernetes: false };
function isTiming(timing) {
return !Array.isArray(timing);
}
const createRequestRecorder = (metrics, options = defaultOptions) => {
const shouldSkipMetricsByEnvironment = skipMetricsInEnvironment(merge(defaultOptions, options));
return (timing, recordingOptions) => {
const durationS = isTiming(timing) ? timing.end().value().seconds : endMeasurementFrom(timing).durationS;
const labels = sortLabels(recordingOptions.labels);
if (!shouldSkipMetricsByEnvironment && durationS !== void 0) {
if (metrics.httpRequestDurationInSeconds) for (const httpRequestDurationInSecondsMetricType of metrics.httpRequestDurationInSeconds) httpRequestDurationInSecondsMetricType.observe(labels, durationS);
}
if (!shouldSkipMetricsByEnvironment && durationS !== void 0) {
if (metrics.httpRequestDurationPerPercentileInSeconds) for (const httpRequestDurationPerPercentileInSecondsMetricType of metrics.httpRequestDurationPerPercentileInSeconds) httpRequestDurationPerPercentileInSecondsMetricType.observe(labels, durationS);
}
if (!shouldSkipMetricsByEnvironment && durationS !== void 0) {
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;
//#endregion
//#region src/normalizers/method/method.ts
const normalizeMethod = (method) => method.toLowerCase();
//#endregion
//#region src/normalizers/path/path.ts
const urlValueParser = new UrlValueParser();
const normalizePath = (path) => {
const parsedPathname = url.parse(path).pathname;
if (!parsedPathname) return "";
return urlValueParser.replacePathValues(parsedPathname, "#val");
};
//#endregion
//#region src/normalizers/status-code/status-code.ts
const normalizeStatusCode = (statusCode) => statusCode;
//#endregion
//#region src/normalizers/index.ts
const defaultNormalizers = {
normalizeStatusCode,
normalizePath,
normalizeMethod
};
//#endregion
//#region src/summary/summary.ts
const getSummary = async () => defaultRegister.metrics();
const getContentType = () => defaultRegister.contentType;
//#endregion
//#region src/timing/timing.ts
var Timing = 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: void 0 };
return { seconds: Number(endTime - startTime) / Number(Timing.NS_PER_SEC) };
}
reset() {
this.#startTime = process.hrtime.bigint();
this.#endTime = void 0;
return this;
}
end() {
this.#endTime = process.hrtime.bigint();
return this;
}
};
const timing = { start() {
return new Timing();
} };
//#endregion
export { Prometheus, createGcMetrics, createGcObserver, createGraphQlMetrics, createHttpMetrics, createRequestRecorder, defaultNormalizers, defaultRegister, endMeasurementFrom, getContentType, getSummary, isRunningInKubernetes, normalizeMethod, normalizePath, normalizeStatusCode, skipMetricsInEnvironment, sortLabels, timing };
//# sourceMappingURL=index.js.map