Skip to content

Commit 0baef34

Browse files
jj22eepichlermarc
andauthored
feat(opentelemetry-sampler-aws-xray): Add Rules Caching and Rules Matching Logic (#2824)
Co-authored-by: Marc Pichler <marc.pichler@dynatrace.com>
1 parent 2d4092e commit 0baef34

File tree

10 files changed

+1160
-29
lines changed

10 files changed

+1160
-29
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// Includes work from:
18+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
19+
// SPDX-License-Identifier: Apache-2.0
20+
21+
import { Attributes, Context, Link, SpanKind } from '@opentelemetry/api';
22+
import {
23+
Sampler,
24+
SamplingResult,
25+
TraceIdRatioBasedSampler,
26+
} from '@opentelemetry/sdk-trace-base';
27+
28+
// FallbackSampler samples 1 req/sec and additional 5% of requests using TraceIdRatioBasedSampler.
29+
export class FallbackSampler implements Sampler {
30+
private fixedRateSampler: TraceIdRatioBasedSampler;
31+
32+
constructor() {
33+
this.fixedRateSampler = new TraceIdRatioBasedSampler(0.05);
34+
}
35+
36+
shouldSample(
37+
context: Context,
38+
traceId: string,
39+
spanName: string,
40+
spanKind: SpanKind,
41+
attributes: Attributes,
42+
links: Link[]
43+
): SamplingResult {
44+
// TODO: implement and use Rate Limiting Sampler
45+
46+
return this.fixedRateSampler.shouldSample(context, traceId);
47+
}
48+
49+
public toString(): string {
50+
return 'FallbackSampler{fallback sampling with sampling config of 1 req/sec and 5% of additional requests}';
51+
}
52+
}

incubator/opentelemetry-sampler-aws-xray/src/remote-sampler.ts

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,17 @@ import {
2929
import {
3030
ParentBasedSampler,
3131
Sampler,
32-
SamplingDecision,
3332
SamplingResult,
3433
} from '@opentelemetry/sdk-trace-base';
3534
import { AWSXRaySamplingClient } from './aws-xray-sampling-client';
35+
import { FallbackSampler } from './fallback-sampler';
3636
import {
3737
AWSXRayRemoteSamplerConfig,
3838
GetSamplingRulesResponse,
3939
SamplingRuleRecord,
4040
} from './types';
41+
import { RuleCache } from './rule-cache';
42+
4143
import { SamplingRuleApplier } from './sampling-rule-applier';
4244

4345
// 5 minute default sampling rules polling interval
@@ -50,12 +52,14 @@ const DEFAULT_AWS_PROXY_ENDPOINT = 'http://localhost:2000';
5052
export class AWSXRayRemoteSampler implements Sampler {
5153
private _root: ParentBasedSampler;
5254
private internalXraySampler: _AWSXRayRemoteSampler;
55+
5356
constructor(samplerConfig: AWSXRayRemoteSamplerConfig) {
5457
this.internalXraySampler = new _AWSXRayRemoteSampler(samplerConfig);
5558
this._root = new ParentBasedSampler({
5659
root: this.internalXraySampler,
5760
});
5861
}
62+
5963
public shouldSample(
6064
context: Context,
6165
traceId: string,
@@ -91,8 +95,11 @@ export class AWSXRayRemoteSampler implements Sampler {
9195
export class _AWSXRayRemoteSampler implements Sampler {
9296
private rulePollingIntervalMillis: number;
9397
private awsProxyEndpoint: string;
98+
private ruleCache: RuleCache;
99+
private fallbackSampler: FallbackSampler;
94100
private samplerDiag: DiagLogger;
95101
private rulePoller: NodeJS.Timeout | undefined;
102+
private clientId: string;
96103
private rulePollingJitterMillis: number;
97104
private samplingClient: AWSXRaySamplingClient;
98105

@@ -117,6 +124,10 @@ export class _AWSXRayRemoteSampler implements Sampler {
117124
this.awsProxyEndpoint = samplerConfig.endpoint
118125
? samplerConfig.endpoint
119126
: DEFAULT_AWS_PROXY_ENDPOINT;
127+
this.fallbackSampler = new FallbackSampler();
128+
// TODO: Use clientId for retrieving Sampling Targets
129+
this.clientId = _AWSXRayRemoteSampler.generateClientId();
130+
this.ruleCache = new RuleCache(samplerConfig.resource);
120131

121132
this.samplingClient = new AWSXRaySamplingClient(
122133
this.awsProxyEndpoint,
@@ -137,8 +148,51 @@ export class _AWSXRayRemoteSampler implements Sampler {
137148
attributes: Attributes,
138149
links: Link[]
139150
): SamplingResult {
140-
// Implementation to be added
141-
return { decision: SamplingDecision.NOT_RECORD };
151+
if (this.ruleCache.isExpired()) {
152+
this.samplerDiag.debug(
153+
'Rule cache is expired, so using fallback sampling strategy'
154+
);
155+
return this.fallbackSampler.shouldSample(
156+
context,
157+
traceId,
158+
spanName,
159+
spanKind,
160+
attributes,
161+
links
162+
);
163+
}
164+
165+
try {
166+
const matchedRule: SamplingRuleApplier | undefined =
167+
this.ruleCache.getMatchedRule(attributes);
168+
if (matchedRule) {
169+
return matchedRule.shouldSample(
170+
context,
171+
traceId,
172+
spanName,
173+
spanKind,
174+
attributes,
175+
links
176+
);
177+
}
178+
} catch (e: unknown) {
179+
this.samplerDiag.debug(
180+
'Unexpected error occurred when trying to match or applying a sampling rule',
181+
e
182+
);
183+
}
184+
185+
this.samplerDiag.debug(
186+
'Using fallback sampler as no rule match was found. This is likely due to a bug, since default rule should always match'
187+
);
188+
return this.fallbackSampler.shouldSample(
189+
context,
190+
traceId,
191+
spanName,
192+
spanKind,
193+
attributes,
194+
links
195+
);
142196
}
143197

144198
public toString(): string {
@@ -180,13 +234,37 @@ export class _AWSXRayRemoteSampler implements Sampler {
180234
}
181235
}
182236
);
183-
184-
// TODO: pass samplingRules to rule cache, temporarily logging the samplingRules array
185-
this.samplerDiag.debug('sampling rules: ', samplingRules);
237+
this.ruleCache.updateRules(samplingRules);
186238
} else {
187239
this.samplerDiag.error(
188240
'SamplingRuleRecords from GetSamplingRules request is not defined'
189241
);
190242
}
191243
}
244+
245+
private static generateClientId(): string {
246+
const hexChars: string[] = [
247+
'0',
248+
'1',
249+
'2',
250+
'3',
251+
'4',
252+
'5',
253+
'6',
254+
'7',
255+
'8',
256+
'9',
257+
'a',
258+
'b',
259+
'c',
260+
'd',
261+
'e',
262+
'f',
263+
];
264+
const clientIdArray: string[] = [];
265+
for (let _ = 0; _ < 24; _ += 1) {
266+
clientIdArray.push(hexChars[Math.floor(Math.random() * hexChars.length)]);
267+
}
268+
return clientIdArray.join('');
269+
}
192270
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// Includes work from:
18+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
19+
// SPDX-License-Identifier: Apache-2.0
20+
21+
import { Attributes } from '@opentelemetry/api';
22+
import { Resource } from '@opentelemetry/resources';
23+
import { SamplingRuleApplier } from './sampling-rule-applier';
24+
25+
// The cache expires 1 hour after the last refresh time.
26+
const RULE_CACHE_TTL_MILLIS: number = 60 * 60 * 1000;
27+
28+
export class RuleCache {
29+
private ruleAppliers: SamplingRuleApplier[];
30+
private lastUpdatedEpochMillis: number;
31+
private samplerResource: Resource;
32+
33+
constructor(samplerResource: Resource) {
34+
this.ruleAppliers = [];
35+
this.samplerResource = samplerResource;
36+
this.lastUpdatedEpochMillis = Date.now();
37+
}
38+
39+
public isExpired(): boolean {
40+
const nowInMillis: number = Date.now();
41+
return nowInMillis > this.lastUpdatedEpochMillis + RULE_CACHE_TTL_MILLIS;
42+
}
43+
44+
public getMatchedRule(
45+
attributes: Attributes
46+
): SamplingRuleApplier | undefined {
47+
// `this.ruleAppliers` should be sorted by priority, so `find()` is used here
48+
// to determine the first highest priority rule that is matched. The last rule
49+
// in the list should be the 'Default' rule with hardcoded priority of 10000.
50+
return this.ruleAppliers.find(
51+
rule =>
52+
rule.matches(attributes, this.samplerResource) ||
53+
rule.samplingRule.RuleName === 'Default'
54+
);
55+
}
56+
57+
private sortRulesByPriority(): void {
58+
this.ruleAppliers.sort(
59+
(rule1: SamplingRuleApplier, rule2: SamplingRuleApplier): number => {
60+
if (rule1.samplingRule.Priority === rule2.samplingRule.Priority) {
61+
return rule1.samplingRule.RuleName < rule2.samplingRule.RuleName
62+
? -1
63+
: 1;
64+
}
65+
return rule1.samplingRule.Priority - rule2.samplingRule.Priority;
66+
}
67+
);
68+
}
69+
70+
public updateRules(newRuleAppliers: SamplingRuleApplier[]): void {
71+
const oldRuleAppliersMap = new Map<string, SamplingRuleApplier>();
72+
73+
this.ruleAppliers.forEach((rule: SamplingRuleApplier) => {
74+
oldRuleAppliersMap.set(rule.samplingRule.RuleName, rule);
75+
});
76+
77+
newRuleAppliers.forEach((newRule: SamplingRuleApplier, index: number) => {
78+
const ruleNameToCheck: string = newRule.samplingRule.RuleName;
79+
const oldRule = oldRuleAppliersMap.get(ruleNameToCheck);
80+
if (oldRule) {
81+
if (newRule.samplingRule.equals(oldRule.samplingRule)) {
82+
newRuleAppliers[index] = oldRule;
83+
}
84+
}
85+
});
86+
this.ruleAppliers = newRuleAppliers;
87+
88+
// sort ruleAppliers by priority and update lastUpdatedEpochMillis
89+
this.sortRulesByPriority();
90+
this.lastUpdatedEpochMillis = Date.now();
91+
}
92+
}

0 commit comments

Comments
 (0)