Skip to content
This repository was archived by the owner on Oct 3, 2023. It is now read-only.

Commit 5b9283e

Browse files
committed
set Correlation-Context header
1 parent ef5712f commit 5b9283e

File tree

3 files changed

+173
-19
lines changed

3 files changed

+173
-19
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.
44

55
## Unreleased
66

7+
- Add support for HTTP tags propagation.
78

89
## 0.0.14 - 2019-06-04
910
- Exporter/Stats/Stackdriver: Add support for exemplar

packages/opencensus-instrumentation-http/src/http.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@ import {
2626
TagMap,
2727
TagTtl,
2828
TraceOptions,
29+
serializeTextFormat,
30+
deserializeTextFormat
2931
} from '@opencensus/core';
3032
import {
3133
ClientRequest,
34+
IncomingHttpHeaders,
3235
IncomingMessage,
3336
request,
3437
RequestOptions,
@@ -37,6 +40,7 @@ import {
3740
import * as semver from 'semver';
3841
import * as shimmer from 'shimmer';
3942
import * as url from 'url';
43+
4044
import * as stats from './http-stats';
4145
import { HttpPluginConfig, IgnoreMatcher } from './types';
4246

@@ -56,6 +60,8 @@ const UNLIMITED_PROPAGATION_MD = {
5660
};
5761

5862
const TAG_VALUE_MAX_LENGTH = 255;
63+
/** A correlation context header under which TagMap is stored as a text value */
64+
export const CORRELATION_CONTEXT = 'Correlation-Context';
5965

6066
/** Http instrumentation plugin for Opencensus */
6167
export class HttpPlugin extends BasePlugin {
@@ -261,7 +267,7 @@ export class HttpPlugin extends BasePlugin {
261267
const host = headers.host || 'localhost';
262268
const userAgent = (headers['user-agent'] ||
263269
headers['User-Agent']) as string;
264-
const tags = new TagMap();
270+
const tags = HttpPlugin.getTagContext(headers) || new TagMap();
265271

266272
rootSpan.addAttribute(
267273
HttpPlugin.ATTRIBUTE_HTTP_HOST,
@@ -483,8 +489,19 @@ export class HttpPlugin extends BasePlugin {
483489
? headers['user-agent'] || headers['User-Agent']
484490
: null;
485491

486-
const tags = new TagMap();
487-
tags.set(stats.HTTP_CLIENT_METHOD, { value: method });
492+
// record stats: new RPCs on client-side inherit the tag context from
493+
// the current Context.
494+
const tags =
495+
plugin.stats ? plugin.stats.getCurrentTagContext() : new TagMap();
496+
if (tags.tags.size > 0) {
497+
if (plugin.hasExpectHeader(options) && options.headers) {
498+
options.headers[CORRELATION_CONTEXT] = serializeTextFormat(tags);
499+
} else {
500+
request.setHeader(CORRELATION_CONTEXT, serializeTextFormat(tags));
501+
}
502+
}
503+
504+
tags.set(stats.HTTP_CLIENT_METHOD, {value: method});
488505

489506
const host = options.hostname || options.host || 'localhost';
490507
span.addAttribute(HttpPlugin.ATTRIBUTE_HTTP_HOST, host);
@@ -602,6 +619,20 @@ export class HttpPlugin extends BasePlugin {
602619
} catch (ignore) {}
603620
}
604621

622+
/**
623+
* Returns a TagMap on incoming HTTP header if it exists and is well-formed,
624+
* or null otherwise.
625+
* @param headers The incoming HTTP header object from which TagMap should be
626+
* retrieved.
627+
*/
628+
static getTagContext(headers: IncomingHttpHeaders): TagMap|null {
629+
const contextValue = (headers[CORRELATION_CONTEXT.toLocaleLowerCase()] ||
630+
headers[CORRELATION_CONTEXT]) as string;
631+
// Entry doesn't exist.
632+
if (!contextValue) return null;
633+
return deserializeTextFormat(contextValue);
634+
}
635+
605636
/**
606637
* Returns whether the Expect header is on the given options object.
607638
* @param options Options for http.request.

packages/opencensus-instrumentation-http/test/test-http.ts

Lines changed: 138 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ import * as http from 'http';
3838
import * as nock from 'nock';
3939
import * as shimmer from 'shimmer';
4040
import * as url from 'url';
41-
42-
import { HttpPlugin, plugin } from '../src/';
41+
import {HttpPlugin, plugin} from '../src/';
4342
import * as stats from '../src/http-stats';
4443

4544
function doNock(
@@ -181,13 +180,18 @@ function assertCustomAttribute(
181180
}
182181

183182
function assertClientStats(
184-
testExporter: TestExporter,
185-
httpStatusCode: number,
186-
httpMethod: string
187-
) {
183+
testExporter: TestExporter, httpStatusCode: number, httpMethod: string,
184+
tagCtx?: TagMap) {
188185
const tags = new TagMap();
189-
tags.set(stats.HTTP_CLIENT_METHOD, { value: httpMethod });
190-
tags.set(stats.HTTP_CLIENT_STATUS, { value: `${httpStatusCode}` });
186+
tags.set(stats.HTTP_CLIENT_METHOD, {value: httpMethod});
187+
tags.set(stats.HTTP_CLIENT_STATUS, {value: `${httpStatusCode}`});
188+
189+
if (tagCtx) {
190+
tagCtx.tags.forEach((tagValue: TagValue, tagKey: TagKey) => {
191+
tags.set(tagKey, tagValue);
192+
});
193+
}
194+
191195
assert.strictEqual(testExporter.registeredViews.length, 8);
192196
assert.strictEqual(testExporter.recordedMeasurements.length, 1);
193197
assert.strictEqual(
@@ -199,15 +203,19 @@ function assertClientStats(
199203
}
200204

201205
function assertServerStats(
202-
testExporter: TestExporter,
203-
httpStatusCode: number,
204-
httpMethod: string,
205-
path: string
206-
) {
206+
testExporter: TestExporter, httpStatusCode: number, httpMethod: string,
207+
path: string, tagCtx?: TagMap) {
207208
const tags = new TagMap();
208-
tags.set(stats.HTTP_SERVER_METHOD, { value: httpMethod });
209-
tags.set(stats.HTTP_SERVER_STATUS, { value: `${httpStatusCode}` });
210-
tags.set(stats.HTTP_SERVER_ROUTE, { value: path });
209+
tags.set(stats.HTTP_SERVER_METHOD, {value: httpMethod});
210+
tags.set(stats.HTTP_SERVER_STATUS, {value: `${httpStatusCode}`});
211+
tags.set(stats.HTTP_SERVER_ROUTE, {value: path});
212+
213+
if (tagCtx) {
214+
tagCtx.tags.forEach((tagValue: TagValue, tagKey: TagKey) => {
215+
tags.set(tagKey, tagValue);
216+
});
217+
}
218+
211219
assert.strictEqual(testExporter.registeredViews.length, 8);
212220
assert.strictEqual(testExporter.recordedMeasurements.length, 1);
213221
assert.strictEqual(
@@ -357,6 +365,60 @@ describe('HttpPlugin', () => {
357365
});
358366
});
359367

368+
it('should create a child span for GET requests with tag context', () => {
369+
const testPath = '/outgoing/rootSpan/childs/1';
370+
doNock(urlHost, testPath, 200, 'Ok');
371+
const tags = new TagMap();
372+
tags.set({name: 'testKey1'}, {value: 'value1'});
373+
tags.set({name: 'testKey2'}, {value: 'value2'});
374+
return globalStats.withTagContext(tags, async () => {
375+
return tracer.startRootSpan(
376+
{name: 'TestRootSpan'}, async (root: Span) => {
377+
await httpRequest.get(`${urlHost}${testPath}`).then((result) => {
378+
assert.ok(root.name.indexOf('TestRootSpan') >= 0);
379+
assert.strictEqual(root.spans.length, 1);
380+
const [span] = root.spans;
381+
assert.ok(span.name.indexOf(testPath) >= 0);
382+
assert.strictEqual(root.traceId, span.traceId);
383+
assertSpanAttributes(span, 200, 'GET', hostName, testPath);
384+
assert.strictEqual(span.messageEvents.length, 1);
385+
const [messageEvent] = span.messageEvents;
386+
assert.strictEqual(messageEvent.type, MessageEventType.SENT);
387+
assert.strictEqual(messageEvent.id, 1);
388+
assertClientStats(testExporter, 200, 'GET', tags);
389+
});
390+
});
391+
});
392+
});
393+
394+
it('should create a child span for GET requests with empty tag context',
395+
() => {
396+
const testPath = '/outgoing/rootSpan/childs/1';
397+
doNock(urlHost, testPath, 200, 'Ok');
398+
const tags = new TagMap();
399+
return globalStats.withTagContext(tags, async () => {
400+
return tracer.startRootSpan(
401+
{name: 'TestRootSpan'}, async (root: Span) => {
402+
await httpRequest.get(`${urlHost}${testPath}`)
403+
.then((result) => {
404+
assert.ok(root.name.indexOf('TestRootSpan') >= 0);
405+
assert.strictEqual(root.spans.length, 1);
406+
const [span] = root.spans;
407+
assert.ok(span.name.indexOf(testPath) >= 0);
408+
assert.strictEqual(root.traceId, span.traceId);
409+
assertSpanAttributes(
410+
span, 200, 'GET', hostName, testPath);
411+
assert.strictEqual(span.messageEvents.length, 1);
412+
const [messageEvent] = span.messageEvents;
413+
assert.strictEqual(
414+
messageEvent.type, MessageEventType.SENT);
415+
assert.strictEqual(messageEvent.id, 1);
416+
assertClientStats(testExporter, 200, 'GET');
417+
});
418+
});
419+
});
420+
});
421+
360422
for (let i = 0; i < httpErrorCodes.length; i++) {
361423
it(`should test a child spans for GET requests with http error ${
362424
httpErrorCodes[i]
@@ -563,6 +625,66 @@ describe('HttpPlugin', () => {
563625
);
564626
});
565627
});
628+
it('should create a root span for incoming requests with Correlation Context header',
629+
async () => {
630+
const testPath = '/incoming/rootSpan/';
631+
const options = {
632+
host: 'localhost',
633+
path: testPath,
634+
port: serverPort,
635+
headers:
636+
{'User-Agent': 'Android', 'Correlation-Context': 'k1=v1,k2=v2'}
637+
};
638+
639+
const expectedTagsFromHeaders = new TagMap();
640+
expectedTagsFromHeaders.set({name: 'k1'}, {value: 'v1'});
641+
expectedTagsFromHeaders.set({name: 'k2'}, {value: 'v2'});
642+
643+
shimmer.unwrap(http, 'get');
644+
shimmer.unwrap(http, 'request');
645+
nock.enableNetConnect();
646+
647+
assert.strictEqual(spanVerifier.endedSpans.length, 0);
648+
649+
await httpRequest.get(options).then((result) => {
650+
assert.ok(spanVerifier.endedSpans[0].name.indexOf(testPath) >= 0);
651+
assert.strictEqual(spanVerifier.endedSpans.length, 1);
652+
const [span] = spanVerifier.endedSpans;
653+
assertSpanAttributes(
654+
span, 200, 'GET', 'localhost', testPath, 'Android');
655+
assertServerStats(
656+
testExporter, 200, 'GET', testPath, expectedTagsFromHeaders);
657+
});
658+
});
659+
660+
it('should handle incoming requests with long request url path',
661+
async () => {
662+
const testPath = '/test&code=' +
663+
'a'.repeat(300);
664+
const options = {
665+
host: 'localhost',
666+
path: testPath,
667+
port: serverPort,
668+
headers: {'User-Agent': 'Android'}
669+
};
670+
shimmer.unwrap(http, 'get');
671+
shimmer.unwrap(http, 'request');
672+
nock.enableNetConnect();
673+
674+
assert.strictEqual(spanVerifier.endedSpans.length, 0);
675+
676+
await httpRequest.get(options).then((result) => {
677+
assert.strictEqual(spanVerifier.endedSpans.length, 1);
678+
assert.ok(spanVerifier.endedSpans[0].name.indexOf(testPath) >= 0);
679+
const [span] = spanVerifier.endedSpans;
680+
assertSpanAttributes(
681+
span, 200, 'GET', 'localhost', testPath, 'Android');
682+
assertServerStats(
683+
testExporter, 200, 'GET',
684+
'/test&code=' +
685+
'a'.repeat(244));
686+
});
687+
});
566688

567689
it('custom attributes should show up on server spans', async () => {
568690
const testPath = '/incoming/rootSpan/';

0 commit comments

Comments
 (0)