From f2464053fe93e5c1150ed65d6fab2ee0fec2dd3b Mon Sep 17 00:00:00 2001 From: Bastian Krol Date: Thu, 13 Jun 2024 07:20:08 +0200 Subject: [PATCH] feat: collect logs --- package-lock.json | 40 ++++++++- package.json | 2 + src/init.ts | 11 +++ test/apps/express-typescript/app.ts | 9 ++ .../collector/CollectorChildProcessWrapper.ts | 10 +++ test/integration/.gitignore | 1 + test/integration/ChildProcessWrapper.ts | 6 +- test/integration/test.ts | 74 ++++++++++++---- test/util/expectAttribute.ts | 5 ++ test/util/expectMatchingLogRecord.ts | 46 ++++++++++ test/util/expectMatchingSpan.ts | 63 ++++++++------ ...dMatchingSpans.ts => findMatchingItems.ts} | 86 ++++++++++++------- 12 files changed, 270 insertions(+), 83 deletions(-) create mode 100644 test/integration/.gitignore create mode 100644 test/util/expectMatchingLogRecord.ts rename test/util/{findMatchingSpans.ts => findMatchingItems.ts} (63%) diff --git a/package-lock.json b/package-lock.json index 62a34f3..4b3bbae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,12 @@ "dependencies": { "@opentelemetry/api": "^1.8.0", "@opentelemetry/auto-instrumentations-node": "^0.44.0", + "@opentelemetry/exporter-logs-otlp-proto": "^0.51.0", "@opentelemetry/exporter-metrics-otlp-proto": "^0.51.0", "@opentelemetry/exporter-trace-otlp-proto": "^0.51.0", "@opentelemetry/resource-detector-container": "^0.3.11", "@opentelemetry/resources": "^1.24.0", + "@opentelemetry/sdk-logs": "^0.51.0", "@opentelemetry/sdk-metrics": "^1.24.0", "@opentelemetry/sdk-node": "^0.51.0", "@opentelemetry/sdk-trace-base": "^1.24.0", @@ -1375,6 +1377,38 @@ "@opentelemetry/api": ">=1.0.0 <1.9.0" } }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.51.0.tgz", + "integrity": "sha512-ylx1lDneOBjE2XoLsYSajhv4gL6RhL/0leie9KWv1SH+B+JbIAxOm152OFRs+oRQijkmWCuk1WS4uAZoFJTYZg==", + "dependencies": { + "@opentelemetry/api-logs": "0.51.0", + "@opentelemetry/core": "1.24.0", + "@opentelemetry/otlp-exporter-base": "0.51.0", + "@opentelemetry/otlp-proto-exporter-base": "0.51.0", + "@opentelemetry/otlp-transformer": "0.51.0", + "@opentelemetry/resources": "1.24.0", + "@opentelemetry/sdk-logs": "0.51.0", + "@opentelemetry/sdk-trace-base": "1.24.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/api-logs": { + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.51.0.tgz", + "integrity": "sha512-m/jtfBPEIXS1asltl8fPQtO3Sb1qMpuL61unQajUmM8zIxeMF1AlqzWXM3QedcYgTTFiJCew5uJjyhpmqhc0+g==", + "dependencies": { + "@opentelemetry/api": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-http": { "version": "0.51.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.51.0.tgz", @@ -5820,9 +5854,9 @@ } }, "node_modules/protobufjs": { - "version": "7.2.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", - "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.2.tgz", + "integrity": "sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", diff --git a/package.json b/package.json index 3f33c5a..07b48b3 100644 --- a/package.json +++ b/package.json @@ -43,10 +43,12 @@ "dependencies": { "@opentelemetry/api": "^1.8.0", "@opentelemetry/auto-instrumentations-node": "^0.44.0", + "@opentelemetry/exporter-logs-otlp-proto": "^0.51.0", "@opentelemetry/exporter-metrics-otlp-proto": "^0.51.0", "@opentelemetry/exporter-trace-otlp-proto": "^0.51.0", "@opentelemetry/resource-detector-container": "^0.3.11", "@opentelemetry/resources": "^1.24.0", + "@opentelemetry/sdk-logs": "^0.51.0", "@opentelemetry/sdk-metrics": "^1.24.0", "@opentelemetry/sdk-node": "^0.51.0", "@opentelemetry/sdk-trace-base": "^1.24.0", diff --git a/src/init.ts b/src/init.ts index c48082f..20c29ff 100644 --- a/src/init.ts +++ b/src/init.ts @@ -3,10 +3,12 @@ import { SpanKind, trace } from '@opentelemetry/api'; import { getNodeAutoInstrumentations, getResourceDetectors } from '@opentelemetry/auto-instrumentations-node'; +import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-proto'; import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'; import { containerDetector } from '@opentelemetry/resource-detector-container'; import { Detector, DetectorSync, envDetector, hostDetector, processDetector, Resource } from '@opentelemetry/resources'; +import { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs'; import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'; import { NodeSDK, NodeSDKConfiguration } from '@opentelemetry/sdk-node'; import { BatchSpanProcessor, SpanProcessor } from '@opentelemetry/sdk-trace-base'; @@ -47,6 +49,12 @@ const spanProcessors: SpanProcessor[] = [ ), ]; +const logRecordProcessor = new BatchLogRecordProcessor( + new OTLPLogExporter({ + url: `${baseUrl}/v1/logs`, + }), +); + if (process.env.DASH0_DEBUG_PRINT_SPANS != null) { if (process.env.DASH0_DEBUG_PRINT_SPANS.toLocaleLowerCase() === 'true') { spanProcessors.push(new BatchSpanProcessor(new ConsoleSpanExporter())); @@ -57,12 +65,15 @@ if (process.env.DASH0_DEBUG_PRINT_SPANS != null) { const configuration: Partial = { spanProcessors: spanProcessors, + metricReader: new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter({ url: `${baseUrl}/v1/metrics`, }), }), + logRecordProcessor, + instrumentations: [getNodeAutoInstrumentations(instrumentationConfig)], resource: new Resource({ diff --git a/test/apps/express-typescript/app.ts b/test/apps/express-typescript/app.ts index 9027b23..fd2465c 100644 --- a/test/apps/express-typescript/app.ts +++ b/test/apps/express-typescript/app.ts @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. // SPDX-License-Identifier: Apache-2.0 +import * as logsApi from '@opentelemetry/api-logs'; import express, { Express } from 'express'; import { sendReadyToParentProcess } from '../../util/sendToParentProcess'; @@ -8,7 +9,15 @@ import { sendReadyToParentProcess } from '../../util/sendToParentProcess'; const port: number = parseInt(process.env.PORT || '1302'); const app: Express = express(); +const logger = logsApi.logs.getLoggerProvider().getLogger('default'); + app.get('/ohai', (req, res) => { + logger.emit({ + severityNumber: logsApi.SeverityNumber.INFO, + severityText: 'INFO', + body: 'log body', + attributes: { 'log.type': 'LogRecord' }, + }); res.json({ message: 'We make Observability easy for every developer.' }); }); diff --git a/test/collector/CollectorChildProcessWrapper.ts b/test/collector/CollectorChildProcessWrapper.ts index d2dca3a..b626e99 100644 --- a/test/collector/CollectorChildProcessWrapper.ts +++ b/test/collector/CollectorChildProcessWrapper.ts @@ -19,6 +19,16 @@ export default class CollectorChildProcessWrapper extends ChildProcessWrapper { return stats.traces >= 1; } + async hasMetrics() { + const stats = await collector().fetchStats(); + return stats.metrics >= 1; + } + + async hasLogs() { + const stats = await collector().fetchStats(); + return stats.logs >= 1; + } + async hasTelemetry() { const stats = await collector().fetchStats(); return stats.traces >= 1 || stats.metrics >= 1 || stats.logs >= 1; diff --git a/test/integration/.gitignore b/test/integration/.gitignore new file mode 100644 index 0000000..c6bbc9a --- /dev/null +++ b/test/integration/.gitignore @@ -0,0 +1 @@ +spans.json diff --git a/test/integration/ChildProcessWrapper.ts b/test/integration/ChildProcessWrapper.ts index 491290e..a00b05d 100644 --- a/test/integration/ChildProcessWrapper.ts +++ b/test/integration/ChildProcessWrapper.ts @@ -172,8 +172,10 @@ export function defaultAppConfiguration(appPort: number): ChildProcessWrapperOpt env: { ...process.env, PORT: appPort.toString(), - // have the Node.js SDK send spans every 100 ms instead of every 5 seconds to speed up tests - OTEL_BSP_SCHEDULE_DELAY: '100', + // have the Node.js SDK send spans every 20 ms instead of every 5 seconds to speed up tests + OTEL_BSP_SCHEDULE_DELAY: '20', + // have the Node.js SDK send logs every 20 ms instead of every 5 seconds to speed up tests + OTEL_BLRP_SCHEDULE_DELAY: '20', DASH0_OTEL_COLLECTOR_BASE_URL: 'http://localhost:4318', }, }; diff --git a/test/integration/test.ts b/test/integration/test.ts index 0b1312d..8b5b9eb 100644 --- a/test/integration/test.ts +++ b/test/integration/test.ts @@ -7,9 +7,10 @@ import { FileHandle, open, readFile, unlink } from 'node:fs/promises'; import { join } from 'node:path'; import semver from 'semver'; +import { SeverityNumber } from '../collector/types/opentelemetry/proto/logs/v1/logs'; import delay from '../util/delay'; - -import { expectResourceAttribute, expectSpanAttribute } from '../util/expectAttribute'; +import { expectLogRecordAttribute, expectResourceAttribute, expectSpanAttribute } from '../util/expectAttribute'; +import { expectMatchingLogRecord } from '../util/expectMatchingLogRecord'; import { expectMatchingSpan, expectMatchingSpanInFileDump } from '../util/expectMatchingSpan'; import runCommand from '../util/runCommand'; import waitUntil from '../util/waitUntil'; @@ -38,7 +39,7 @@ describe('attach', () => { collector().clear(); }); - describe('basic tracing', () => { + describe('basic signals', () => { let appUnderTest: ChildProcessWrapper; before(async () => { @@ -53,9 +54,9 @@ describe('attach', () => { it('should attach via --require and capture spans', async () => { await waitUntil(async () => { - const telemetry = await sendRequestAndWaitForTraceData(); + const traces = await sendRequestAndWaitForTraceData(); expectMatchingSpan( - telemetry.traces, + traces, [ resource => expectResourceAttribute(resource, 'telemetry.sdk.name', 'opentelemetry'), resource => expectResourceAttribute(resource, 'telemetry.sdk.language', 'nodejs'), @@ -69,6 +70,31 @@ describe('attach', () => { ); }); }); + + it('should attach via --require and capture logs', async () => { + await waitUntil(async () => { + const logs = await sendRequestAndWaitForLogRecords(); + expectMatchingLogRecord( + logs, + [ + resource => expectResourceAttribute(resource, 'telemetry.sdk.name', 'opentelemetry'), + resource => expectResourceAttribute(resource, 'telemetry.sdk.language', 'nodejs'), + resource => expectResourceAttribute(resource, 'telemetry.distro.name', 'dash0-nodejs'), + resource => expectResourceAttribute(resource, 'telemetry.distro.version', expectedDistroVersion), + ], + [ + logRecord => expect(logRecord.body).to.deep.equal({ string_value: 'log body' }), + logRecord => + expect(logRecord.severity_number).to.equal( + SeverityNumber.SEVERITY_NUMBER_INFO, + 'severity number should be info', + ), + logRecord => expect(logRecord.severity_text).to.equal('INFO'), + logRecord => expectLogRecordAttribute(logRecord, 'log.type', 'LogRecord'), + ], + ); + }); + }); }); describe('pod uid detection', () => { @@ -87,9 +113,9 @@ describe('attach', () => { it('should attach via --require and detect the pod uid', async () => { await waitUntil(async () => { - const telemetry = await sendRequestAndWaitForTraceData(); + const traces = await sendRequestAndWaitForTraceData(); expectMatchingSpan( - telemetry.traces, + traces, [resource => expectResourceAttribute(resource, 'k8s.pod.uid', 'f57400dc-94ce-4806-a52e-d2726f448f15')], [ span => expect(span.kind).to.equal(SpanKind.SERVER, 'span kind should be server'), @@ -115,9 +141,9 @@ describe('attach', () => { it('should attach via --require and derive a service name from the package.json file', async () => { await waitUntil(async () => { - const telemetry = await sendRequestAndWaitForTraceData(); + const traces = await sendRequestAndWaitForTraceData(); expectMatchingSpan( - telemetry.traces, + traces, [ resource => expectResourceAttribute(resource, 'service.name', 'dash0-app-under-test-express-typescript@1.0.0'), @@ -150,9 +176,9 @@ describe('attach', () => { // (because the top level beforeHook is executed after this suite's before hook). await appUnderTest.start(); await waitUntil(async () => { - const telemetry = await waitForTraceData(); + const traces = await waitForTraceData(); expectMatchingSpan( - telemetry.traces, + traces, [ resource => expectResourceAttribute(resource, 'telemetry.sdk.name', 'opentelemetry'), resource => expectResourceAttribute(resource, 'telemetry.sdk.language', 'nodejs'), @@ -187,9 +213,9 @@ describe('attach', () => { await appUnderTest.start(); await appUnderTest.stop(); await waitUntil(async () => { - const telemetry = await waitForTraceData(); + const traces = await waitForTraceData(); expectMatchingSpan( - telemetry.traces, + traces, [ resource => expectResourceAttribute(resource, 'telemetry.sdk.name', 'opentelemetry'), resource => expectResourceAttribute(resource, 'telemetry.sdk.language', 'nodejs'), @@ -205,9 +231,9 @@ describe('attach', () => { await appUnderTest.start(); await appUnderTest.stop('SIGINT'); await waitUntil(async () => { - const telemetry = await waitForTraceData(); + const traces = await waitForTraceData(); expectMatchingSpan( - telemetry.traces, + traces, [ resource => expectResourceAttribute(resource, 'telemetry.sdk.name', 'opentelemetry'), resource => expectResourceAttribute(resource, 'telemetry.sdk.language', 'nodejs'), @@ -241,9 +267,9 @@ describe('attach', () => { it('should flush telemetry before process exit due to empty event loop', async () => { await appUnderTest.start(); await waitUntil(async () => { - const telemetry = await waitForTraceData(); + const traces = await waitForTraceData(); expectMatchingSpan( - telemetry.traces, + traces, [ resource => expectResourceAttribute(resource, 'telemetry.sdk.name', 'opentelemetry'), resource => expectResourceAttribute(resource, 'telemetry.sdk.language', 'nodejs'), @@ -351,6 +377,11 @@ describe('attach', () => { return waitForTraceData(); } + async function sendRequestAndWaitForLogRecords() { + await sendRequestAndVerifyResponse(); + return waitForLogRecords(); + } + async function sendRequestAndVerifyResponse() { const response = await fetch(`http://localhost:${appPort}/ohai`); expect(response.status).to.equal(200); @@ -362,7 +393,14 @@ describe('attach', () => { if (!(await collector().hasTraces())) { throw new Error('The collector never received any spans.'); } - return await collector().fetchTelemetry(); + return (await collector().fetchTelemetry()).traces; + } + + async function waitForLogRecords() { + if (!(await collector().hasLogs())) { + throw new Error('The collector never received any log records.'); + } + return (await collector().fetchTelemetry()).logs; } async function verifyFileHasBeenCreated(filename: string): Promise { diff --git a/test/util/expectAttribute.ts b/test/util/expectAttribute.ts index f914940..b8e2664 100644 --- a/test/util/expectAttribute.ts +++ b/test/util/expectAttribute.ts @@ -5,6 +5,7 @@ import { expect } from 'chai'; import { KeyValue } from '../collector/types/opentelemetry/proto/common/v1/common'; import { Resource } from '../collector/types/opentelemetry/proto/resource/v1/resource'; import { Span } from '../collector/types/opentelemetry/proto/trace/v1/trace'; +import { LogRecord } from '../collector/types/opentelemetry/proto/logs/v1/logs'; const { fail } = expect; @@ -41,6 +42,10 @@ export function expectSpanAttribute(span: Span, key: string, expectedValue: any) expectAttribute(span, key, expectedValue, 'span'); } +export function expectLogRecordAttribute(logRecord: LogRecord, key: string, expectedValue: any) { + expectAttribute(logRecord, key, expectedValue, 'log record'); +} + function getValue(attribute: KeyValue) { const v = attribute.value; if (v == null) { diff --git a/test/util/expectMatchingLogRecord.ts b/test/util/expectMatchingLogRecord.ts new file mode 100644 index 0000000..242670a --- /dev/null +++ b/test/util/expectMatchingLogRecord.ts @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { ExportLogsServiceRequest } from '../collector/types/opentelemetry/proto/collector/logs/v1/logs_service'; +import { LogRecord, ResourceLogs, ScopeLogs } from '../collector/types/opentelemetry/proto/logs/v1/logs'; +import { Resource } from '../collector/types/opentelemetry/proto/resource/v1/resource'; +import { + Expectation, + findMatchingItemsInServiceRequest, + processFindItemsResult, + ServiceRequestMapper, +} from './findMatchingItems'; + +class LogsServiceRequestMapper + implements ServiceRequestMapper +{ + getResourceItems(serviceRequest: ExportLogsServiceRequest): ResourceLogs[] { + return serviceRequest.resource_logs; + } + + getResource(resourceLogs: ResourceLogs): Resource | undefined { + return resourceLogs.resource; + } + + getScopeItems(resourceLogs: ResourceLogs): ScopeLogs[] { + return resourceLogs.scope_logs; + } + + getItems(scopeLogs: ScopeLogs): LogRecord[] { + return scopeLogs.log_records; + } +} + +export function expectMatchingLogRecord( + logRecords: ExportLogsServiceRequest[], + resourceExpectations: Expectation[], + logRecordExpectations: Expectation[], +): LogRecord { + const matchResult = findMatchingItemsInServiceRequest( + logRecords, + new LogsServiceRequestMapper(), + resourceExpectations, + logRecordExpectations, + ); + return processFindItemsResult(matchResult, 'log record'); +} diff --git a/test/util/expectMatchingSpan.ts b/test/util/expectMatchingSpan.ts index e1d1ede..b32a986 100644 --- a/test/util/expectMatchingSpan.ts +++ b/test/util/expectMatchingSpan.ts @@ -3,16 +3,47 @@ import { ExportTraceServiceRequest } from '../collector/types/opentelemetry/proto/collector/trace/v1/trace_service'; import { Resource } from '../collector/types/opentelemetry/proto/resource/v1/resource'; -import { Span } from '../collector/types/opentelemetry/proto/trace/v1/trace'; -import { Expectation, findMatchingSpans, findMatchingSpansInFileDump, MatchingSpansResult } from './findMatchingSpans'; +import { ResourceSpans, ScopeSpans, Span } from '../collector/types/opentelemetry/proto/trace/v1/trace'; +import { + Expectation, + findMatchingItemsInServiceRequest, + findMatchingSpansInFileDump, + processFindItemsResult, + ServiceRequestMapper, +} from './findMatchingItems'; + +class TraceDataServiceRequestMapper + implements ServiceRequestMapper +{ + getResourceItems(serviceRequest: ExportTraceServiceRequest): ResourceSpans[] { + return serviceRequest.resource_spans; + } + + getResource(resourceSpans: ResourceSpans): Resource | undefined { + return resourceSpans.resource; + } + + getScopeItems(resourceSpan: ResourceSpans): ScopeSpans[] { + return resourceSpan.scope_spans; + } + + getItems(scopeSpan: ScopeSpans): Span[] { + return scopeSpan.spans; + } +} export function expectMatchingSpan( traceDataItems: ExportTraceServiceRequest[], resourceExpectations: Expectation[], spanExpectations: Expectation[], ): Span { - const matchResult = findMatchingSpans(traceDataItems, resourceExpectations, spanExpectations); - return processFindSpanResult(matchResult); + const matchResult = findMatchingItemsInServiceRequest( + traceDataItems, + new TraceDataServiceRequestMapper(), + resourceExpectations, + spanExpectations, + ); + return processFindItemsResult(matchResult, 'span'); } export function expectMatchingSpanInFileDump( @@ -27,27 +58,5 @@ export function expectMatchingSpanInFileDump( spanExpectations, spanAttributeExpectations, ); - return processFindSpanResult(matchResult); -} - -function processFindSpanResult(matchResult: MatchingSpansResult): Span { - if (matchResult.matchingSpans) { - const matchingSpans = matchResult.matchingSpans; - if (matchingSpans.length === 1) { - return matchingSpans[0]; - } else if (matchingSpans.length > 1) { - throw new Error( - `Expected exactly one matching span, found ${matchingSpans.length}.\nMatches:\n${JSON.stringify(matchingSpans, null, 2)}`, - ); - } else { - throw new Error('Unexpected error while processing matching spans.'); - } - } else if (matchResult.bestCandidate) { - const bestCandidate = matchResult.bestCandidate; - throw new Error( - `No matching span has been found. The best candidate passed ${bestCandidate.passedChecks} and failed check ${bestCandidate.passedChecks + 1} with error ${bestCandidate.error}. This is the best candidate:\n${JSON.stringify(bestCandidate.spanLike, null, 2)}`, - ); - } else { - throw new Error('No matching span has been found.'); - } + return processFindItemsResult(matchResult, 'span'); } diff --git a/test/util/findMatchingSpans.ts b/test/util/findMatchingItems.ts similarity index 63% rename from test/util/findMatchingSpans.ts rename to test/util/findMatchingItems.ts index 43e7b9f..357b263 100644 --- a/test/util/findMatchingSpans.ts +++ b/test/util/findMatchingItems.ts @@ -2,45 +2,52 @@ // SPDX-License-Identifier: Apache-2.0 import { expect } from 'chai'; -import { ExportTraceServiceRequest } from '../collector/types/opentelemetry/proto/collector/trace/v1/trace_service'; import { Resource } from '../collector/types/opentelemetry/proto/resource/v1/resource'; import { Span } from '../collector/types/opentelemetry/proto/trace/v1/trace'; const { fail } = expect; +export interface ServiceRequestMapper { + getResourceItems(serviceRequest: SR): R[]; + getResource(resourceItem: R): Resource | undefined; + getScopeItems(resourceItem: R): S[]; + getItems(scopeItem: S): I[]; +} + export type Expectation = (span: T) => void; export type Candidate = { - spanLike?: any; + item?: any; passedChecks: number; error?: any; }; -export type MatchingSpansResult = { - matchingSpans?: Span[]; +export type MatchingItemsResult = { + matchingItems?: T[]; bestCandidate?: Candidate; }; -export function findMatchingSpans( - traceDataItems: ExportTraceServiceRequest[], +export function findMatchingItemsInServiceRequest( + serviceRequests: SR[], + mapper: ServiceRequestMapper, resourceExpectations: Expectation[], - spanExpectations: Expectation[], -): MatchingSpansResult { - if (traceDataItems.length === 0) { - fail('No trace data has been provided.'); + spanExpectations: Expectation[], +): MatchingItemsResult { + if (serviceRequests.length === 0) { + fail('No service requests has been provided.'); } - const matchingSpans: Span[] = []; + const matchingItems: I[] = []; let bestCandidate: Candidate = { passedChecks: 0, }; - traceDataItems.forEach(traceDataItem => { - traceDataItem.resource_spans.forEach(resourceSpan => { + serviceRequests.forEach(serviceRequest => { + mapper.getResourceItems(serviceRequest).forEach(resourceItem => { let passedResourceChecks = 0; if (resourceExpectations.length > 0) { - // verify that the resource attribtues match - const resource = resourceSpan.resource; + // verify that the resource attributes match + const resource = mapper.getResource(resourceItem); if (!resource) { - // This resource span has no resource information, try the next resource span. + // This resource span/log has no resource information, try the next one. return; } try { @@ -49,11 +56,11 @@ export function findMatchingSpans( passedResourceChecks++; } } catch (error) { - // This resource did not pass all checks, try the next resource span. Memorize the resource span if it has - // been the best match so far. + // This resource did not pass all checks, try the next one. Memorize the resource if it has been the best + // match so far. if (passedResourceChecks > bestCandidate.passedChecks) { bestCandidate = { - spanLike: resourceSpan, + item: resourceItem, passedChecks: passedResourceChecks, error, }; @@ -62,33 +69,33 @@ export function findMatchingSpans( } } - resourceSpan.scope_spans.forEach(scopeSpan => { - scopeSpan.spans.forEach(span => { + mapper.getScopeItems(resourceItem).forEach(scopeItem => { + mapper.getItems(scopeItem).forEach(item => { let passedChecks = passedResourceChecks; try { for (let j = 0; j < spanExpectations.length; j++) { - spanExpectations[j](span); + spanExpectations[j](item); passedChecks++; } - matchingSpans.push(span); + matchingItems.push(item); } catch (error) { + // This span/log record did not pass all checks, try the next one. Memorize it if it has been the best + // match so far. if (passedChecks > bestCandidate.passedChecks) { bestCandidate = { - spanLike: span, + item: item, passedChecks, error, }; } - // This span did not pass all checks, try the next span. Memorize the span if it has - // been the best match so far. return; } }); }); }); }); - if (matchingSpans.length > 0) { - return { matchingSpans }; + if (matchingItems.length > 0) { + return { matchingItems }; } else { return { bestCandidate }; } @@ -99,7 +106,7 @@ export function findMatchingSpansInFileDump( resourceAttributeExpectations: Expectation[], spanExpectations: Expectation[], spanAttributeExpectations: Expectation[], -): MatchingSpansResult { +): MatchingItemsResult { if (spans.length === 0) { fail('No trace data has been provided.'); } @@ -131,7 +138,7 @@ export function findMatchingSpansInFileDump( // The resource attributes of this span did not match, try the next span. if (passedChecks > bestCandidate.passedChecks) { bestCandidate = { - spanLike: span, + item: span, passedChecks: passedChecks, error, }; @@ -151,7 +158,7 @@ export function findMatchingSpansInFileDump( // This span has no attributes, try the next span. if (passedChecks > bestCandidate.passedChecks) { bestCandidate = { - spanLike: span, + item: span, passedChecks: passedChecks, }; } @@ -167,7 +174,7 @@ export function findMatchingSpansInFileDump( // This span did not match, try the next span. if (passedChecks > bestCandidate.passedChecks) { bestCandidate = { - spanLike: span, + item: span, passedChecks: passedChecks, error, }; @@ -175,8 +182,21 @@ export function findMatchingSpansInFileDump( } }); if (matchingSpans.length > 0) { - return { matchingSpans }; + return { matchingItems: matchingSpans }; } else { return { bestCandidate }; } } + +export function processFindItemsResult(matchResult: MatchingItemsResult, itemLabel: string): T { + if (matchResult.matchingItems && matchResult.matchingItems.length >= 1) { + return matchResult.matchingItems[0]; + } else if (matchResult.bestCandidate) { + const bestCandidate = matchResult.bestCandidate; + throw new Error( + `No matching ${itemLabel} has been found. The best candidate passed ${bestCandidate.passedChecks} checks and failed check ${bestCandidate.passedChecks + 1} with error ${bestCandidate.error}. This is the best candidate:\n${JSON.stringify(bestCandidate.item, null, 2)}`, + ); + } else { + throw new Error(`No matching ${itemLabel} has been found.`); + } +}