Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 6 additions & 2 deletions src/sampler/OdigosScopedHeadSampler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}

Expand Down
48 changes: 48 additions & 0 deletions src/sampler/query-params-matching.ts
Original file line number Diff line number Diff line change
@@ -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);
}
5 changes: 5 additions & 0 deletions src/sampler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
24 changes: 17 additions & 7 deletions src/sampler/utils.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 };
}
}

Expand Down Expand Up @@ -78,17 +82,20 @@ export interface ParsedHttpServerAttributes {
method: string;
route?: string;
path?: string;
queryParams?: string;
}

export const parseHttpServerAttributes = (attributes: Attributes): ParsedHttpServerAttributes | undefined => {
const method = getHttpMethodFromAttributes(attributes);
if (!method) {
return undefined;
}
const pathAndQueryParams = getHttpPathAndQueryParamsFromAttributes(attributes);
return {
method,
route: getHttpRouteFromAttributes(attributes),
path: getHttpPathFromAttributes(attributes),
path: pathAndQueryParams?.path,
queryParams: pathAndQueryParams?.queryParams,
};
}

Expand All @@ -97,16 +104,19 @@ export interface ParsedHttpClientAttributes {
path?: string;
templatedPath?: string;
serverAddress?: string;
queryParams?: string;
}

export const parseHttpClientAttributes = (attributes: Attributes): ParsedHttpClientAttributes | undefined => {
const method = getHttpMethodFromAttributes(attributes);
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),
};
Expand Down
Loading