Skip to content

Commit

Permalink
feat: more filters (traceloop#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomer-friedman authored Feb 22, 2023
1 parent c05bda4 commit e8c3193
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 56 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { jest, describe, it } from '@jest/globals';
import { expectTrace } from '../..';
import { TraceLoop } from '../../trace-loop';

jest.setTimeout(30000);

describe('http request matchers', () => {
describe('when orders-service makes an http call to emails-service', () => {
let traceloop: TraceLoop;
beforeAll(async () => {
traceloop = new TraceLoop();

await traceloop.axiosInstance.post('http://localhost:3000/orders/create');
await traceloop.fetchTraces();
});

it('should contain outbound http call from orders-service', async () => {
expectTrace(
traceloop.serviceByName('orders-service'),
).toSendHttpRequest();
});

it('should contain inbound http call to emails-service', async () => {
expectTrace(
traceloop.serviceByName('emails-service'),
).toReceiveHttpRequest();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ export function toReceiveHttpRequest(service: Service): HttpRequest {
throw new Error(`No HTTP call received by ${serviceName}`);
}

return new HttpRequest(filteredSpans, serviceName, spanKind);
return new HttpRequest(filteredSpans, { serviceName, spanKind });
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ export function toSendHttpRequest(service: Service): HttpRequest {
throw new Error(`No HTTP call was sent by ${serviceName}`);
}

return new HttpRequest(filteredSpans, serviceName, spanKind);
return new HttpRequest(filteredSpans, { serviceName, spanKind });
}
44 changes: 44 additions & 0 deletions packages/expect-opentelemetry/src/matchers/utils/filters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { opentelemetry } from '@traceloop/otel-proto';
import { stringCompare } from './comparators';
import { CompareOptions } from './compare-types';

export const filterByAttributeKey = (
spans: opentelemetry.proto.trace.v1.ISpan[],
attName: string,
) =>
spans.filter((span) => {
span.attributes?.find((attribute) => {
return attribute.key === attName;
});
});

export const filterBySpanKind = (
spans: opentelemetry.proto.trace.v1.ISpan[],
expected: opentelemetry.proto.trace.v1.Span.SpanKind,
) => spans.filter((span) => span.kind === expected);

export const filterByAttributeStringValue = (
spans: opentelemetry.proto.trace.v1.ISpan[],
attName: string,
attValue: string | RegExp,
options?: CompareOptions,
) =>
spans.filter((span) => {
return span.attributes?.find(
(attribute) =>
attribute.key === attName &&
stringCompare(attribute.value?.stringValue, attValue, options),
);
});

export const filterByAttributeIntValue = (
spans: opentelemetry.proto.trace.v1.ISpan[],
attName: string,
attValue: number,
) =>
spans.filter((span) => {
return span.attributes?.find(
(attribute) =>
attribute.key === attName && attribute.value?.intValue === attValue,
);
});
1 change: 1 addition & 0 deletions packages/expect-opentelemetry/src/matchers/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './compare-types';
export * from './comparators';
export * from './filters';
72 changes: 39 additions & 33 deletions packages/expect-opentelemetry/src/resources/http-request.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import deepEqual from 'deep-equal';
import { opentelemetry } from '@traceloop/otel-proto';
import {
CompareOptions,
filterByAttributeIntValue,
filterByAttributeStringValue,
} from '../matchers/utils';

export class HttpRequest {
constructor(
readonly spans: opentelemetry.proto.trace.v1.ISpan[],
private readonly serviceName: string,
private readonly spanKind: opentelemetry.proto.trace.v1.Span.SpanKind,
readonly extra: {
serviceName: string;
spanKind: opentelemetry.proto.trace.v1.Span.SpanKind;
},
) {}

withBody(body: object) {
Expand All @@ -26,71 +33,70 @@ export class HttpRequest {
);
}

return new HttpRequest(filteredSpans, this.serviceName, this.spanKind);
return new HttpRequest(filteredSpans, this.extra);
}

withHeader(key: string, value: string) {
const filteredSpans = this.spans.filter((span) => {
return span.attributes?.find(
(attribute) =>
attribute.key === `http.request.header.${key}` &&
attribute.value?.stringValue === value,
);
});
withHeader(key: string, value: string, options: CompareOptions) {
const filteredSpans = filterByAttributeStringValue(
this.spans,
`http.request.header.${key}`,
value,
options,
);

if (filteredSpans.length === 0) {
throw new Error(
`No HTTP call with header ${key} assigned with ${value} ${this.serviceErrorBySpanKind()}`,
);
}

return new HttpRequest(filteredSpans, this.serviceName, this.spanKind);
return new HttpRequest(filteredSpans, this.extra);
}

ofMethod(method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH') {
const filteredSpans = this.spans.filter((span) => {
return span.attributes?.find(
(attribute) =>
attribute.key === SemanticAttributes.HTTP_METHOD &&
attribute.value?.stringValue === method,
);
});
ofMethod(
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
options?: CompareOptions,
) {
const filteredSpans = filterByAttributeStringValue(
this.spans,
SemanticAttributes.HTTP_METHOD,
method,
options,
);

if (filteredSpans.length === 0) {
throw new Error(
`No HTTP call of method ${method} ${this.serviceErrorBySpanKind()}`,
);
}

return new HttpRequest(filteredSpans, this.serviceName, this.spanKind);
return new HttpRequest(filteredSpans, this.extra);
}

withStatusCode(code: number) {
const filteredSpans = this.spans.filter((span) => {
return span.attributes?.find(
(attribute) =>
attribute.key === SemanticAttributes.HTTP_STATUS_CODE &&
attribute.value?.intValue === code,
);
});
const filteredSpans = filterByAttributeIntValue(
this.spans,
SemanticAttributes.HTTP_STATUS_CODE,
code,
);

if (filteredSpans.length === 0) {
throw new Error(
`No HTTP call with status code ${code} ${this.serviceErrorBySpanKind()}`,
);
}

return new HttpRequest(filteredSpans, this.serviceName, this.spanKind);
return new HttpRequest(filteredSpans, this.extra);
}

private serviceErrorBySpanKind() {
switch (this.spanKind) {
switch (this.extra.spanKind) {
case opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_CLIENT:
return `was sent by ${this.serviceName}`;
return `was sent by ${this.extra.serviceName}`;
case opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_SERVER:
return `was received by ${this.serviceName}`;
return `was received by ${this.extra.serviceName}`;
default:
return `was found for ${this.serviceName}`;
return `was found for ${this.extra.serviceName}`;
}
}
}

0 comments on commit e8c3193

Please sign in to comment.