From a31bbf127cb8b5e7399eb48f72c5018068066d28 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Thu, 8 Aug 2024 10:06:39 +0200 Subject: [PATCH 01/10] added support for older openmetrics versions --- doc/api.md | 4 +-- lib/services/stats/statsRegistry.js | 51 ++++++++++++++++++++++------- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/doc/api.md b/doc/api.md index 41f38f375..80198cfe2 100644 --- a/doc/api.md +++ b/doc/api.md @@ -2321,8 +2321,8 @@ _**Response body**_ Returns the current value of the server stats, -- If `Accept` header contains `application/openmetrics-text`, the response has content-type - `application/openmetrics-text; version=1.0.0; charset=utf-8` +- If `Accept` header contains `application/openmetrics-text; version=(1.0.0|0.0.4|0.0.1,text/plain)`, the response has content-type + `application/openmetrics-text; version=; charset=utf-8` - Else, If `Accept` header is missing or supports `text/plain` (explicitly or by `*/*`) , the response has content-type `text/plain; version=0.0.4; charset=utf-8` (legacy format for [prometheus](https://prometheus.io)) - In any other case, returns an error message with `406` status. diff --git a/lib/services/stats/statsRegistry.js b/lib/services/stats/statsRegistry.js index 9bf1923d6..ddacaa3ee 100644 --- a/lib/services/stats/statsRegistry.js +++ b/lib/services/stats/statsRegistry.js @@ -72,6 +72,24 @@ function globalLoad(values, callback) { callback(null); } +/** + * Matches a list of requested values to a list of supported values. + * Returns the first supported value that matches any of the requested values. + * Returns null if there is no match + * + * @param {Array} requestedValues List of requested values. + * @param {Array} supportedValues List of supported values. + */ +function matchPreferredValue(requestedValues, supportedValues) { + for (let i = 0; i < supportedValues.length; i++) { + const supportedValue = supportedValues[i]; + if (_.contains(requestedValues, supportedValue)) { + return supportedValue; + } + } + return null; +} + /** * Predefined http handler that returns current openmetrics data */ @@ -83,8 +101,14 @@ function openmetricsHandler(req, res) { // - For prometheus compatible collectors, it SHOULD BE 'text/plain; version=0.0.4; charset=utf-8'. See: // https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md let contentType = 'application/openmetrics-text; version=1.0.0; charset=utf-8'; - let version = null; - let charset = null; + // Some versions of prometheus have been observed to send several + // version options, e.g.: + // Accept: application/openmetrics-text; version=0.0.1,text/plain;version=0.0.4;q=0.5,*/*;q=0.1 + // So we need to keep a list of requested values, and a preferred order for supported values. + const requestedVersions = []; + const requestedCharsets = []; + const supportedVersions = ['1.0.0', '0.0.4', '0.0.1,text/plain']; + const supportedCharsets = ['utf-8']; // To identify openmetrics collectors, we need to parse the `Accept` header. // An openmetrics-based collectors SHOULD use an `Accept` header such as: // `Accept: application/openmetrics-text; version=1.0.0; charset=utf-8' @@ -101,9 +125,9 @@ function openmetricsHandler(req, res) { const current = parts[i]; const trimmed = current.trim(); if (trimmed.startsWith('version=')) { - version = trimmed.substring(8); + requestedVersions.push(trimmed.substring(8)) } else if (trimmed.startsWith('charset=')) { - charset = trimmed.substring(8); + requestedCharsets.push(trimmed.substring(8)); } else { unparsed.push(current); } @@ -113,20 +137,23 @@ function openmetricsHandler(req, res) { req.headers['accept'] = unparsed.join(';'); } } - // charset MUST BE utf-8 - if (charset && charset !== 'utf-8') { - logger.error(statsContext, 'Unsupported charset: %s', charset); + if (requestedCharsets.length > 0 && !matchPreferredValue(requestedCharsets, supportedCharsets)) { + logger.error(statsContext, 'Unsupported charset: %s', requestedCharsets); res.status(406).send('Unsupported charset'); return; } switch (req.accepts(['text/plain', 'application/openmetrics-text'])) { case 'application/openmetrics-text': - // Version MUST BE 1.0.0 for openmetrics - if (version && version !== '1.0.0') { - logger.error(statsContext, 'Unsupported openmetrics version: %s', version); - res.status(406).send('Unsupported openmetrics version'); - return; + let version = supportedVersions[0]; + if (requestedVersions.length > 0) { + version = matchPreferredValue(requestedVersions, supportedVersions); + if (!version) { + logger.error(statsContext, 'Unsupported openmetrics version: %s', requestedVersions); + res.status(406).send('Unsupported openmetrics version'); + return; + } } + contentType = `text/plain; version=${version}; charset=utf-8`; break; case 'text/plain': contentType = 'text/plain; version=0.0.4; charset=utf-8'; From 66882b324f855d8d2cb4351a43c823fadb46f639 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Thu, 8 Aug 2024 10:18:09 +0200 Subject: [PATCH 02/10] fix openmetrics version --- lib/services/stats/statsRegistry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/stats/statsRegistry.js b/lib/services/stats/statsRegistry.js index ddacaa3ee..3efd75a52 100644 --- a/lib/services/stats/statsRegistry.js +++ b/lib/services/stats/statsRegistry.js @@ -153,7 +153,7 @@ function openmetricsHandler(req, res) { return; } } - contentType = `text/plain; version=${version}; charset=utf-8`; + contentType = `application/openmetrics-text; version=${version}; charset=utf-8`; break; case 'text/plain': contentType = 'text/plain; version=0.0.4; charset=utf-8'; From 2e7c625adcd91ddcfb3c373b7d3510fc19413c23 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Thu, 8 Aug 2024 10:36:07 +0200 Subject: [PATCH 03/10] imporve accept header --- lib/services/stats/statsRegistry.js | 35 ++++++++++++++++------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/lib/services/stats/statsRegistry.js b/lib/services/stats/statsRegistry.js index 3efd75a52..703b38716 100644 --- a/lib/services/stats/statsRegistry.js +++ b/lib/services/stats/statsRegistry.js @@ -107,7 +107,7 @@ function openmetricsHandler(req, res) { // So we need to keep a list of requested values, and a preferred order for supported values. const requestedVersions = []; const requestedCharsets = []; - const supportedVersions = ['1.0.0', '0.0.4', '0.0.1,text/plain']; + const supportedVersions = ['1.0.0', '0.0.1']; const supportedCharsets = ['utf-8']; // To identify openmetrics collectors, we need to parse the `Accept` header. // An openmetrics-based collectors SHOULD use an `Accept` header such as: @@ -119,23 +119,25 @@ function openmetricsHandler(req, res) { // So we must parse these key-value pairs ourselves, and remove them from the // header before handling it to `requests.accept`. if (req.headers.accept) { - const parts = req.headers.accept.split(';'); - let unparsed = []; - for (let i = 0; i < parts.length; i++) { - const current = parts[i]; - const trimmed = current.trim(); - if (trimmed.startsWith('version=')) { - requestedVersions.push(trimmed.substring(8)) - } else if (trimmed.startsWith('charset=')) { - requestedCharsets.push(trimmed.substring(8)); - } else { - unparsed.push(current); + const mediaTypes = req.headers.accept.split(','); + for (let i = 0; i < mediaTypes.length; i++) { + const parts = mediaType.split(';'); + let unparsed = []; + for (let j = 0; j < parts.length; i++) { + const current = parts[j]; + const trimmed = current.trim(); + if (trimmed.startsWith('version=')) { + requestedVersions.push(trimmed.substring(8)) + } else if (trimmed.startsWith('charset=')) { + requestedCharsets.push(trimmed.substring(8)); + } else { + unparsed.push(current); + } } + mediaTypes[i] = unparsed.join(';'); } - if (unparsed.length < parts.length) { - delete req.headers['accept']; - req.headers['accept'] = unparsed.join(';'); - } + delete req.headers['accept']; + req.headers['accept'] = mediaTypes.join(','); } if (requestedCharsets.length > 0 && !matchPreferredValue(requestedCharsets, supportedCharsets)) { logger.error(statsContext, 'Unsupported charset: %s', requestedCharsets); @@ -156,6 +158,7 @@ function openmetricsHandler(req, res) { contentType = `application/openmetrics-text; version=${version}; charset=utf-8`; break; case 'text/plain': + // Text/plain only supports version 0.0.4 contentType = 'text/plain; version=0.0.4; charset=utf-8'; break; default: From 09271496b59d21a49cc3030766b1281241230d99 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Thu, 8 Aug 2024 10:41:44 +0200 Subject: [PATCH 04/10] fixed undefined var --- lib/services/stats/statsRegistry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/stats/statsRegistry.js b/lib/services/stats/statsRegistry.js index 703b38716..5f6d632b3 100644 --- a/lib/services/stats/statsRegistry.js +++ b/lib/services/stats/statsRegistry.js @@ -121,7 +121,7 @@ function openmetricsHandler(req, res) { if (req.headers.accept) { const mediaTypes = req.headers.accept.split(','); for (let i = 0; i < mediaTypes.length; i++) { - const parts = mediaType.split(';'); + const parts = mediaTypes[i].split(';'); let unparsed = []; for (let j = 0; j < parts.length; i++) { const current = parts[j]; From f9ebf3b64191b74e0eb3333b323f968384f3d109 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Thu, 8 Aug 2024 10:44:16 +0200 Subject: [PATCH 05/10] fix typo --- lib/services/stats/statsRegistry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/stats/statsRegistry.js b/lib/services/stats/statsRegistry.js index 5f6d632b3..f5b213f83 100644 --- a/lib/services/stats/statsRegistry.js +++ b/lib/services/stats/statsRegistry.js @@ -123,7 +123,7 @@ function openmetricsHandler(req, res) { for (let i = 0; i < mediaTypes.length; i++) { const parts = mediaTypes[i].split(';'); let unparsed = []; - for (let j = 0; j < parts.length; i++) { + for (let j = 0; j < parts.length; j++) { const current = parts[j]; const trimmed = current.trim(); if (trimmed.startsWith('version=')) { From 6ddfd9a2e2c87cc50f528dfb99db58f7d621f49c Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Thu, 8 Aug 2024 10:50:50 +0200 Subject: [PATCH 06/10] fix doc --- doc/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api.md b/doc/api.md index 80198cfe2..42aca3b60 100644 --- a/doc/api.md +++ b/doc/api.md @@ -2321,7 +2321,7 @@ _**Response body**_ Returns the current value of the server stats, -- If `Accept` header contains `application/openmetrics-text; version=(1.0.0|0.0.4|0.0.1,text/plain)`, the response has content-type +- If `Accept` header contains `application/openmetrics-text; version=(1.0.0|0.0.1)`, the response has content-type `application/openmetrics-text; version=; charset=utf-8` - Else, If `Accept` header is missing or supports `text/plain` (explicitly or by `*/*`) , the response has content-type `text/plain; version=0.0.4; charset=utf-8` (legacy format for [prometheus](https://prometheus.io)) From 373c87f8d07aff5786a6b4832d953306c9ba4bb5 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Thu, 8 Aug 2024 10:58:16 +0200 Subject: [PATCH 07/10] fixed lint issue --- lib/services/stats/statsRegistry.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/services/stats/statsRegistry.js b/lib/services/stats/statsRegistry.js index f5b213f83..ec4618244 100644 --- a/lib/services/stats/statsRegistry.js +++ b/lib/services/stats/statsRegistry.js @@ -144,18 +144,18 @@ function openmetricsHandler(req, res) { res.status(406).send('Unsupported charset'); return; } + let selectedVersion = supportedVersions[0]; switch (req.accepts(['text/plain', 'application/openmetrics-text'])) { case 'application/openmetrics-text': - let version = supportedVersions[0]; if (requestedVersions.length > 0) { - version = matchPreferredValue(requestedVersions, supportedVersions); - if (!version) { + selectedVersion = matchPreferredValue(requestedVersions, supportedVersions); + if (!selectedVersion) { logger.error(statsContext, 'Unsupported openmetrics version: %s', requestedVersions); res.status(406).send('Unsupported openmetrics version'); return; } } - contentType = `application/openmetrics-text; version=${version}; charset=utf-8`; + contentType = `application/openmetrics-text; version=${selectedVersion}; charset=utf-8`; break; case 'text/plain': // Text/plain only supports version 0.0.4 From d45a4e2f824c7f7ebceeb6c0a8bac3bae592fb14 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Thu, 8 Aug 2024 15:13:34 +0200 Subject: [PATCH 08/10] test accept headers --- lib/services/stats/statsRegistry.js | 155 +++++++++--------- test/unit/statsRegistry/openmetrics-test.js | 167 ++++++++++++++++++++ 2 files changed, 249 insertions(+), 73 deletions(-) create mode 100644 test/unit/statsRegistry/openmetrics-test.js diff --git a/lib/services/stats/statsRegistry.js b/lib/services/stats/statsRegistry.js index ec4618244..acdcf27b7 100644 --- a/lib/services/stats/statsRegistry.js +++ b/lib/services/stats/statsRegistry.js @@ -73,18 +73,72 @@ function globalLoad(values, callback) { } /** - * Matches a list of requested values to a list of supported values. - * Returns the first supported value that matches any of the requested values. - * Returns null if there is no match - * - * @param {Array} requestedValues List of requested values. - * @param {Array} supportedValues List of supported values. + * Chooses the appropiate content type and version based on Accept header + * + * @param {String} accepts The accepts header */ -function matchPreferredValue(requestedValues, supportedValues) { - for (let i = 0; i < supportedValues.length; i++) { - const supportedValue = supportedValues[i]; - if (_.contains(requestedValues, supportedValue)) { - return supportedValue; +function matchContentType(accepts) { + const requestedType = []; + for (const expression of accepts.split(',')) { + const parts = expression.split(';').map((part) => part.trim()); + const mediaType = parts[0]; + let version = null; + let charset = null; + let preference = null; + for (let part of parts.slice(1)) { + if (part.startsWith('version=')) { + version = part.substring(8).trim(); + } else if (part.startsWith('charset=')) { + charset = part.substring(8).trim(); + } else if (part.startsWith('q=')) { + preference = parseFloat(part.substring(2).trim()); + } + } + requestedType.push({ + mediaType: mediaType, + version: version, + charset: charset, + preference: preference || 1 + }); + } + // If both text/plain and openmetrics are accepted, + // prefer openmetrics + const mediaTypePref = { + 'application/openmetrics-text': 1, + 'text/plain': 0.5 + } + // sort requests by priority descending + requestedType.sort(function (a, b) { + if (a.preference === b.preference) { + // same priority, sort by media type. + return (mediaTypePref[b.mediaType] || 0) - (mediaTypePref[a.mediaType] || 0); + } + // Beware! preference difference is usually a decima + return b.preference - a.preference; + }); + for (const req of requestedType) { + switch(req.mediaType) { + case 'application/openmetrics-text': + req.version = req.version || '1.0.0'; + req.charset = req.charset || 'utf-8'; + if ( + (req.version === '1.0.0' || req.version === '0.0.1') && + (req.charset === 'utf-8')) { + return req; + } + break; + case 'text/plain': + case 'text/*': + case '*/*': + req.version = req.version || '0.0.4'; + req.charset = req.charset || 'utf-8'; + if ( + (req.version === '0.0.4') && + (req.charset === 'utf-8')) { + req.mediaType = 'text/plain'; + return req; + } + break; } } return null; @@ -100,72 +154,26 @@ function openmetricsHandler(req, res) { // https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#overall-structure // - For prometheus compatible collectors, it SHOULD BE 'text/plain; version=0.0.4; charset=utf-8'. See: // https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md - let contentType = 'application/openmetrics-text; version=1.0.0; charset=utf-8'; - // Some versions of prometheus have been observed to send several - // version options, e.g.: + // - Caveat: Some versions of prometheus have been observed to send multivalued Accept headers such as // Accept: application/openmetrics-text; version=0.0.1,text/plain;version=0.0.4;q=0.5,*/*;q=0.1 - // So we need to keep a list of requested values, and a preferred order for supported values. - const requestedVersions = []; - const requestedCharsets = []; - const supportedVersions = ['1.0.0', '0.0.1']; - const supportedCharsets = ['utf-8']; - // To identify openmetrics collectors, we need to parse the `Accept` header. - // An openmetrics-based collectors SHOULD use an `Accept` header such as: - // `Accept: application/openmetrics-text; version=1.0.0; charset=utf-8' - // See: https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/18913 - // - // WORKAROUND: express version 4 does not parse properly the openmetrics Accept header, - // it won't match the regular expressions supported by `express.accepts`. - // So we must parse these key-value pairs ourselves, and remove them from the - // header before handling it to `requests.accept`. - if (req.headers.accept) { - const mediaTypes = req.headers.accept.split(','); - for (let i = 0; i < mediaTypes.length; i++) { - const parts = mediaTypes[i].split(';'); - let unparsed = []; - for (let j = 0; j < parts.length; j++) { - const current = parts[j]; - const trimmed = current.trim(); - if (trimmed.startsWith('version=')) { - requestedVersions.push(trimmed.substring(8)) - } else if (trimmed.startsWith('charset=')) { - requestedCharsets.push(trimmed.substring(8)); - } else { - unparsed.push(current); - } - } - mediaTypes[i] = unparsed.join(';'); - } - delete req.headers['accept']; - req.headers['accept'] = mediaTypes.join(','); - } - if (requestedCharsets.length > 0 && !matchPreferredValue(requestedCharsets, supportedCharsets)) { - logger.error(statsContext, 'Unsupported charset: %s', requestedCharsets); - res.status(406).send('Unsupported charset'); - return; + let reqType = { + mediaType: 'application/openmetrics-text', + version: '1.0.0', + charset: 'utf-8' } - let selectedVersion = supportedVersions[0]; - switch (req.accepts(['text/plain', 'application/openmetrics-text'])) { - case 'application/openmetrics-text': - if (requestedVersions.length > 0) { - selectedVersion = matchPreferredValue(requestedVersions, supportedVersions); - if (!selectedVersion) { - logger.error(statsContext, 'Unsupported openmetrics version: %s', requestedVersions); - res.status(406).send('Unsupported openmetrics version'); - return; - } - } - contentType = `application/openmetrics-text; version=${selectedVersion}; charset=utf-8`; - break; - case 'text/plain': - // Text/plain only supports version 0.0.4 - contentType = 'text/plain; version=0.0.4; charset=utf-8'; - break; - default: - logger.error(statsContext, 'Unsupported accept header: %s', req.headers.accept); - res.status(406).send('Unsupported accept header'); + if (req.headers.accept) { + // WORKAROUND: express version 4 does not parse properly the openmetrics Accept header, + // it won't match the regular expressions supported by `express.accepts`. + // So we must parse these key-value pairs ourselves. + reqType = matchContentType(req.headers.accept); + if (reqType === null) { + logger.error(statsContext, 'Unsupported media type: %s', req.headers.accept); + res.status(406).send('Not Acceptable'); return; + } } + const contentType = `${reqType.mediaType};version=${reqType.version};charset=${reqType.charset}`; + // The actual payload is the same for all supported content types const metrics = []; for (const key in globalStats) { if (globalStats.hasOwnProperty(key)) { @@ -210,3 +218,4 @@ exports.getAllGlobal = getAllGlobal; exports.globalLoad = globalLoad; exports.withStats = withStats; exports.openmetricsHandler = openmetricsHandler; +exports.matchContentType = matchContentType; diff --git a/test/unit/statsRegistry/openmetrics-test.js b/test/unit/statsRegistry/openmetrics-test.js new file mode 100644 index 000000000..dc8a0c7e7 --- /dev/null +++ b/test/unit/statsRegistry/openmetrics-test.js @@ -0,0 +1,167 @@ +/* + * Copyright 2024 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. + * + * fiware-iotagent-lib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::daniel.moranjimenez@telefonica.com + */ + +/* eslint-disable no-unused-vars */ + +const statsRegistry = require('../../../lib/services/stats/statsRegistry'); +const should = require('should'); + +describe('statsRegistry - openmetrics endpoint', function () { + + const testCases = [ + { + description: 'Should accept standard openmetrics 0.0.1 header', + accept: 'application/openmetrics-text; version=0.0.1; charset=utf-8', + contentType: { + mediaType: 'application/openmetrics-text', + version: '0.0.1', + charset: 'utf-8' + } + }, + { + description: 'Should accept standard openmetrics 1.0.0 header', + accept: 'application/openmetrics-text; version=1.0.0; charset=utf-8', + contentType: { + mediaType: 'application/openmetrics-text', + version: '1.0.0', + charset: 'utf-8' + } + }, + { + description: 'Should accept openmetrics with no version', + accept: 'application/openmetrics-text', + contentType: { + mediaType: 'application/openmetrics-text', + version: '1.0.0', + charset: 'utf-8' + } + }, + { + description: 'Should accept text/plain header with version', + accept: 'text/plain; version=0.0.4', + contentType: { + mediaType: 'text/plain', + version: '0.0.4', + charset: 'utf-8' + } + }, + { + description: 'Should accept wildcard header', + accept: '*/*', + contentType: { + mediaType: 'text/plain', + version: '0.0.4', + charset: 'utf-8' + } + }, + { + description: 'Should accept both openmetrics and text/plain, prefer openmetrics', + accept: 'application/openmetrics-text; version=0.0.1; charset=utf-8,text/plain;version=0.0.4', + contentType: { + mediaType: 'application/openmetrics-text', + version: '0.0.1', + charset: 'utf-8' + } + }, + { + description: 'Should accept both text/plain and openmetrics, prefer openmetrics', + accept: 'text/plain,application/openmetrics-text; version=0.0.1; charset=utf-8', + contentType: { + mediaType: 'application/openmetrics-text', + version: '0.0.1', + charset: 'utf-8' + } + }, + { + description: 'Should accept both openmetrics and text/plain, prefer text if preference set', + accept: 'application/openmetrics-text; version=0.0.1; charset=utf-8;q=0.5,text/plain;q=0.7', + contentType: { + mediaType: 'text/plain', + version: '0.0.4', + charset: 'utf-8' + } + }, + { + description: 'Should match version to content-type', + accept: 'application/openmetrics-text; version=0.0.1; charset=utf-8, text/plain;version=1.0.0', + contentType: { + mediaType: 'application/openmetrics-text', + version: '0.0.1', + charset: 'utf-8' + } + }, + { + description: 'Should set default q to 1.0', + accept: 'application/openmetrics-text; version=0.0.1; q=0.5,text/plain;version=0.0.4', + contentType: { + mediaType: 'text/plain', + version: '0.0.4', + charset: 'utf-8' + } + }, + { + description: 'Should accept mixture of content-types and q', + accept: 'application/openmetrics-text; version=0.0.1,text/plain;version=0.0.4;q=0.5,*/*;q=0.1', + contentType: { + mediaType: 'application/openmetrics-text', + version: '0.0.1', + charset: 'utf-8' + } + }, + { + description: 'Should reject Invalid charset', + accept: '*/*; charset=utf-16', + contentType: null + }, + { + description: 'Should reject Invalid openmetrics version', + accept: 'application/openmetrics-text; version=0.0.5', + contentType: null + }, + { + description: 'Should reject Invalid text/plain', + accept: 'text/plain; version=0.0.2', + contentType: null + } + ] + + for (const testCase of testCases) { + describe(testCase.description, function () { + const result = statsRegistry.matchContentType(testCase.accept); + if (testCase.contentType) { + it('should match', function (done) { + should.exist(result); + result.mediaType.should.equal(testCase.contentType.mediaType); + result.version.should.equal(testCase.contentType.version); + result.charset.should.equal(testCase.contentType.charset); + done(); + }); + } else { + it('should not match', function (done) { + should.not.exist(result); + done(); + }); + } + }); + } +}); From bc6caf8b9399f8b97d93a173afd7bd3f11af9ffd Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Thu, 8 Aug 2024 15:19:35 +0200 Subject: [PATCH 09/10] remove wrong comment --- lib/services/stats/statsRegistry.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/services/stats/statsRegistry.js b/lib/services/stats/statsRegistry.js index acdcf27b7..791551275 100644 --- a/lib/services/stats/statsRegistry.js +++ b/lib/services/stats/statsRegistry.js @@ -113,7 +113,6 @@ function matchContentType(accepts) { // same priority, sort by media type. return (mediaTypePref[b.mediaType] || 0) - (mediaTypePref[a.mediaType] || 0); } - // Beware! preference difference is usually a decima return b.preference - a.preference; }); for (const req of requestedType) { From 679813e4f9e0d59dd175805f52a5c43ba06ef220 Mon Sep 17 00:00:00 2001 From: rg2011 <52279456+rg2011@users.noreply.github.com> Date: Fri, 9 Aug 2024 14:09:00 +0200 Subject: [PATCH 10/10] fix literals --- lib/services/stats/statsRegistry.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/services/stats/statsRegistry.js b/lib/services/stats/statsRegistry.js index 791551275..1ce1fdd7b 100644 --- a/lib/services/stats/statsRegistry.js +++ b/lib/services/stats/statsRegistry.js @@ -79,6 +79,9 @@ function globalLoad(values, callback) { */ function matchContentType(accepts) { const requestedType = []; + const vlabel = 'version='; + const clabel = 'charset='; + const qlabel = 'q='; for (const expression of accepts.split(',')) { const parts = expression.split(';').map((part) => part.trim()); const mediaType = parts[0]; @@ -86,25 +89,25 @@ function matchContentType(accepts) { let charset = null; let preference = null; for (let part of parts.slice(1)) { - if (part.startsWith('version=')) { - version = part.substring(8).trim(); - } else if (part.startsWith('charset=')) { - charset = part.substring(8).trim(); - } else if (part.startsWith('q=')) { - preference = parseFloat(part.substring(2).trim()); + if (part.startsWith(vlabel)) { + version = part.substring(vlabel.length).trim(); + } else if (part.startsWith(clabel)) { + charset = part.substring(clabel.length).trim(); + } else if (part.startsWith(qlabel)) { + preference = parseFloat(part.substring(qlabel.length).trim()); } } requestedType.push({ mediaType: mediaType, version: version, charset: charset, - preference: preference || 1 + preference: preference || 1.0 }); } // If both text/plain and openmetrics are accepted, // prefer openmetrics const mediaTypePref = { - 'application/openmetrics-text': 1, + 'application/openmetrics-text': 1.0, 'text/plain': 0.5 } // sort requests by priority descending