diff --git a/src/config/index.ts b/src/config/index.ts index a5fe325..c0b69d0 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -30,10 +30,16 @@ export interface HeadSamplingOperationMatcher { grpcClient?: GrpcSamplingOperationMatcherClient; } +export interface HttpQueryParamMatcher { + name: string; + valueExact?: string; +} + export interface HttpSamplingOperationMatcherServer { route?: string; routePrefix?: string; method?: string; + queryParams?: HttpQueryParamMatcher[]; } export interface HttpSamplingOperationMatcherClient { diff --git a/src/sampler/OdigosScopedHeadSampler.ts b/src/sampler/OdigosScopedHeadSampler.ts index 813c41a..0885160 100644 --- a/src/sampler/OdigosScopedHeadSampler.ts +++ b/src/sampler/OdigosScopedHeadSampler.ts @@ -5,6 +5,7 @@ import { parseHttpServerAttributes, parseHttpClientAttributes, parseGrpcAttribut import { samplingDecisionByPercentage } from "./percentage"; import { HeadSamplingOperationKind, ParsedGrpcRule, ParsedHeadSamplingConfig, ParsedHttpRule } from "./types"; import { createGrpcMethodMatcher, createGrpcServiceMatcher, createHttpMethodMatcher, createHttpPathMatcher, createHttpServerAddressMatcher as createServerAddressMatcher } from "./path-matching"; +import { createHttpQueryParamsMatcher } from "./query-params-matching"; const spanKindToString = (spanKind: SpanKind): string => { return SpanKind[spanKind] ?? String(spanKind); @@ -29,12 +30,14 @@ export function parseHeadSamplingConfig(config: HeadSamplingConfig, logger: Diag } else if (rule.operation.httpServer) { const pathMatcher = createHttpPathMatcher(rule.operation.httpServer.route, rule.operation.httpServer.routePrefix); const methodMatcher = createHttpMethodMatcher(rule.operation.httpServer.method); - httpServerRules.push({ pathMatcher, methodMatcher, rule }); + const queryParamsMatcher = createHttpQueryParamsMatcher(rule.operation.httpServer.queryParams); + httpServerRules.push({ pathMatcher, methodMatcher, queryParamsMatcher, rule }); } else if (rule.operation.httpClient) { const pathMatcher = createHttpPathMatcher(rule.operation.httpClient.templatedPath, rule.operation.httpClient.templatedPathPrefix); const methodMatcher = createHttpMethodMatcher(rule.operation.httpClient.method); const serverAddressMatcher = createServerAddressMatcher(rule.operation.httpClient.serverAddress); - httpClientRules.push({ pathMatcher, methodMatcher, serverAddressMatcher, rule }); + const queryParamsMatcher = createHttpQueryParamsMatcher(undefined); + httpClientRules.push({ pathMatcher, methodMatcher, queryParamsMatcher, serverAddressMatcher, rule }); } else if (rule.operation.grpcServer) { const methodMatcher = createGrpcMethodMatcher(rule.operation.grpcServer.method); const serviceMatcher = createGrpcServiceMatcher(rule.operation.grpcServer.service); @@ -178,6 +181,7 @@ export class OdigosScopedHeadSampler implements Sampler { return this.config.httpServerRules .filter(parsedRule => parsedRule.methodMatcher.match(upperCaseMethod)) .filter(parsedRule => parsedRule.pathMatcher.match(routeOrPath, segments)) + .filter(parsedRule => parsedRule.queryParamsMatcher.match(parsed.queryParams)) .map(parsedRule => parsedRule.rule); } diff --git a/src/sampler/query-params-matching.ts b/src/sampler/query-params-matching.ts new file mode 100644 index 0000000..f7ca97e --- /dev/null +++ b/src/sampler/query-params-matching.ts @@ -0,0 +1,48 @@ +import { HttpQueryParamMatcher } from "../config"; +import { HttpQueryParamsMatcher } from "./types"; + +class HttpQueryParamsMatcherAlwaysTrue implements HttpQueryParamsMatcher { + match(_queryParams: string | undefined): boolean { + return true; + } +} + +class SingleQueryParamMatcher { + constructor(private name: string, private valueExact: string | undefined) {} + + match(parsedParams: URLSearchParams): boolean { + const values = parsedParams.getAll(this.name); + if (values.length === 0) { + return false; + } + if (this.valueExact === undefined) { + return true; + } + return values.some(value => value === this.valueExact); + } +} + +class HttpQueryParamsMatcherComposite implements HttpQueryParamsMatcher { + private matchers: SingleQueryParamMatcher[]; + + constructor(queryParams: HttpQueryParamMatcher[]) { + this.matchers = queryParams.map( + queryParam => new SingleQueryParamMatcher(queryParam.name, queryParam.valueExact), + ); + } + + match(queryParams: string | undefined): boolean { + if (queryParams === undefined || queryParams === '') { + return false; + } + const parsedParams = new URLSearchParams(queryParams); + return this.matchers.every(matcher => matcher.match(parsedParams)); + } +} + +export const createHttpQueryParamsMatcher = (queryParams: HttpQueryParamMatcher[] | undefined): HttpQueryParamsMatcher => { + if (!queryParams || queryParams.length === 0) { + return new HttpQueryParamsMatcherAlwaysTrue(); + } + return new HttpQueryParamsMatcherComposite(queryParams); +} diff --git a/src/sampler/types.ts b/src/sampler/types.ts index 5f154fc..31d3aee 100644 --- a/src/sampler/types.ts +++ b/src/sampler/types.ts @@ -20,12 +20,17 @@ export interface ExactStringMatcher { match(value: string | undefined): boolean; } +export interface HttpQueryParamsMatcher { + match(queryParams: string | undefined): boolean; +} + // a raw rule contains just a path text. it is parsed to make matching streamlined and efficient. // this struct is created per rule to make that happen export type ParsedHttpRule = { pathMatcher: HttpPathMatcher; methodMatcher: HttpMethodMatcher; + queryParamsMatcher: HttpQueryParamsMatcher; serverAddressMatcher?: HttpServerAddressMatcher; rule: NoisyOperationSamplingConfig; } diff --git a/src/sampler/utils.ts b/src/sampler/utils.ts index 0e69172..331e1c1 100644 --- a/src/sampler/utils.ts +++ b/src/sampler/utils.ts @@ -1,5 +1,5 @@ import { Attributes } from "@opentelemetry/api"; -import { ATTR_HTTP_ROUTE, ATTR_HTTP_REQUEST_METHOD, ATTR_SERVER_ADDRESS, ATTR_URL_PATH } from "@opentelemetry/semantic-conventions"; +import { ATTR_HTTP_ROUTE, ATTR_HTTP_REQUEST_METHOD, ATTR_SERVER_ADDRESS, ATTR_URL_PATH, ATTR_URL_QUERY } from "@opentelemetry/semantic-conventions"; import { SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_TARGET, SEMATTRS_HTTP_ROUTE, SEMATTRS_NET_PEER_NAME, SEMATTRS_RPC_SYSTEM } from "@opentelemetry/semantic-conventions"; const ATTR_URL_TEMPLATE = "url.template"; @@ -32,19 +32,23 @@ export const getHttpRouteFromAttributes = (attributes: Attributes): string | und return undefined; } -export const getHttpPathFromAttributes = (attributes: Attributes): string | undefined => { +export const getHttpPathAndQueryParamsFromAttributes = (attributes: Attributes): { path: string, queryParams?: string } | undefined => { const pathNew = attributes[ATTR_URL_PATH]; if (pathNew) { - return pathNew.toString(); + const path = pathNew.toString(); + const urlQuery = attributes[ATTR_URL_QUERY]; + const queryParams = urlQuery?.toString(); + return { path, queryParams }; } const httpTargetLegacy = attributes[SEMATTRS_HTTP_TARGET]; if (httpTargetLegacy) { const httpTarget = httpTargetLegacy.toString(); if (httpTarget.includes('?')) { - return httpTarget.split('?')[0]; + const [path, queryParams] = httpTarget.split('?'); + return { path, queryParams }; } else { - return httpTarget; + return { path: httpTarget }; } } @@ -78,6 +82,7 @@ export interface ParsedHttpServerAttributes { method: string; route?: string; path?: string; + queryParams?: string; } export const parseHttpServerAttributes = (attributes: Attributes): ParsedHttpServerAttributes | undefined => { @@ -85,10 +90,12 @@ export const parseHttpServerAttributes = (attributes: Attributes): ParsedHttpSer if (!method) { return undefined; } + const pathAndQueryParams = getHttpPathAndQueryParamsFromAttributes(attributes); return { method, route: getHttpRouteFromAttributes(attributes), - path: getHttpPathFromAttributes(attributes), + path: pathAndQueryParams?.path, + queryParams: pathAndQueryParams?.queryParams, }; } @@ -97,6 +104,7 @@ export interface ParsedHttpClientAttributes { path?: string; templatedPath?: string; serverAddress?: string; + queryParams?: string; } export const parseHttpClientAttributes = (attributes: Attributes): ParsedHttpClientAttributes | undefined => { @@ -104,9 +112,11 @@ export const parseHttpClientAttributes = (attributes: Attributes): ParsedHttpCli if (!method) { return undefined; } + const pathAndQueryParams = getHttpPathAndQueryParamsFromAttributes(attributes); return { method, - path: getHttpPathFromAttributes(attributes), + path: pathAndQueryParams?.path, + queryParams: pathAndQueryParams?.queryParams, templatedPath: getHttpTemplatedPathFromAttributes(attributes), serverAddress: getServerAddressFromAttributes(attributes), };