Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support uploading traces in OpenTelemetry format (OTLP JSON) #2145

Merged
merged 9 commits into from
Mar 4, 2024
3 changes: 3 additions & 0 deletions packages/jaeger-ui/src/api/jaeger.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ const JaegerAPI = {
fetchServiceOperations(serviceName) {
return getJSON(`${this.apiRoot}services/${encodeURIComponent(serviceName)}/operations`);
},
transformOTLP(traces) {
return getJSON(`${this.apiRoot}transform`, { method: 'POST', body: JSON.stringify(traces) });
},
fetchServiceServerOps(service) {
return getJSON(`${this.apiRoot}operations`, {
query: {
Expand Down
9 changes: 9 additions & 0 deletions packages/jaeger-ui/src/api/jaeger.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,15 @@ describe('fetchServiceServerOps', () => {
});
});

describe('transformOTLP', () => {
it('GETs the transformed trace of Jaeger kind when provided with OTLP', () => {
const trace = JSON.parse('{"test" : true}');
const body = { ...defaultOptions, body: JSON.stringify(trace), method: 'POST' };
JaegerAPI.transformOTLP(trace);
expect(fetchMock).toHaveBeenLastCalledWith(`${DEFAULT_API_ROOT}transform`, body);
});
});

describe('fetchTrace', () => {
const generatedTraces = traceGenerator.traces({ numberOfTraces: 5 });

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"resourceSpans": [
{
"resource": {
"attributes": []
},
"scopeSpans": [
{
"scope": {
"name": "telemetrygen"
},
"spans": [
{
"traceId": "83a9efd15c1c98a977e0711cc93ee28b",
"spanId": "e127af99e3b3e074",
"parentSpanId": "909541b92cf05311",
"name": "okey-dokey-0",
"kind": 2,
"startTimeUnixNano": "1706678909209712000",
"endTimeUnixNano": "1706678909209835000",
"attributes": [
{
"key": "net.peer.ip",
"value": {
"stringValue": "1.2.3.4"
}
},
{
"key": "peer.service",
"value": {
"stringValue": "telemetrygen-client"
}
}
],
"status": {}
}
]
}
],
"schemaUrl": "https://opentelemetry.io/schemas/1.4.0"
}
]
}
50 changes: 50 additions & 0 deletions packages/jaeger-ui/src/middlewares/fixtures/otlp2jaeger-in.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "telemetrygen"
}
}
]
},
"scopeSpans": [
{
"scope": {
"name": "telemetrygen"
},
"spans": [
{
"traceId": "83a9efd15c1c98a977e0711cc93ee28b",
"spanId": "e127af99e3b3e074",
"parentSpanId": "909541b92cf05311",
"name": "okey-dokey-0",
"kind": 2,
"startTimeUnixNano": "1706678909209712000",
"endTimeUnixNano": "1706678909209835000",
"attributes": [
{
"key": "net.peer.ip",
"value": {
"stringValue": "1.2.3.4"
}
},
{
"key": "peer.service",
"value": {
"stringValue": "telemetrygen-client"
}
}
],
"status": {}
}
]
}
],
"schemaUrl": "https://opentelemetry.io/schemas/1.4.0"
}
]
}
59 changes: 59 additions & 0 deletions packages/jaeger-ui/src/middlewares/fixtures/otlp2jaeger-out.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"data": [
{
"traceID": "83a9efd15c1c98a977e0711cc93ee28b",
"spans": [
{
"traceID": "83a9efd15c1c98a977e0711cc93ee28b",
"spanID": "e127af99e3b3e074",
"operationName": "okey-dokey-0",
"references": [
{
"refType": "CHILD_OF",
"traceID": "83a9efd15c1c98a977e0711cc93ee28b",
"spanID": "909541b92cf05311"
}
],
"startTime": 1706678909209712,
"duration": 123,
"tags": [
{
"key": "otel.library.name",
"type": "string",
"value": "telemetrygen"
},
{
"key": "net.peer.ip",
"type": "string",
"value": "1.2.3.4"
},
{
"key": "peer.service",
"type": "string",
"value": "telemetrygen-client"
},
{
"key": "span.kind",
"type": "string",
"value": "server"
}
],
"logs": [],
"processID": "p1",
"warnings": null
}
],
"processes": {
"p1": {
"serviceName": "telemetrygen",
"tags": []
}
},
"warnings": null
}
],
"total": 0,
"limit": 0,
"offset": 0,
"errors": null
}
28 changes: 28 additions & 0 deletions packages/jaeger-ui/src/middlewares/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import { change } from 'redux-form';
import { replace } from 'redux-first-history';

import { searchTraces, fetchServiceOperations } from '../actions/jaeger-api';
import { loadJsonTraces } from '../actions/file-reader-api';
import { getUrl as getSearchUrl } from '../components/SearchTracePage/url';
import JaegerAPI from '../api/jaeger';

export { default as trackMiddleware } from './track';

Expand Down Expand Up @@ -45,4 +47,30 @@ export const historyUpdateMiddleware = store => next => action => {
return next(action);
};

export const loadJsonTracesMiddleware = store => next => action => {
if (action.type === String([`${loadJsonTraces}_FULFILLED`])) {
yurishkuro marked this conversation as resolved.
Show resolved Hide resolved
// Check if action.payload is OTLP and make API call if so
// We are allowed to change the action.payload here
//
if ('resourceSpans' in action.payload) {
yurishkuro marked this conversation as resolved.
Show resolved Hide resolved
JaegerAPI.transformOTLP(action.payload)
.then(result => {
const transformedAction = {
...action,
payload: result,
};
return next(transformedAction);
})
.catch(() => {
return next(action);
});
} else {
return next(action);
}
} else {
return next(action);
}
return undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this the right behavior?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume calling the next action is a pattern that needs to be maintained, it looks like a dangerous shortcut when you change it just to work around another problem. The right fix is to control the order of these middlewares, so that OTLP translator is invoked before the rest of the handlers.

};

export const promise = promiseMiddleware;
54 changes: 53 additions & 1 deletion packages/jaeger-ui/src/middlewares/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,22 @@ jest.mock(
);

import { change } from 'redux-form';

import fs from 'fs';
import _ from 'lodash';
import * as jaegerMiddlewares from './index';
import { fetchServiceOperations } from '../actions/jaeger-api';
import { loadJsonTraces } from '../actions/file-reader-api';
import JaegerAPI from '../api/jaeger';

jest.spyOn(JaegerAPI, 'transformOTLP').mockImplementation(APICallRequest => {
const OTLPTrace = JSON.parse(fs.readFileSync('src/middlewares/fixtures/otlp2jaeger-in.json', 'utf-8'));
const jaegerTrace = JSON.parse(fs.readFileSync('src/middlewares/fixtures/otlp2jaeger-out.json', 'utf-8'));
if (_.isEqual(APICallRequest, OTLPTrace)) {
return Promise.resolve(jaegerTrace);
}
// This defines case where API call errors out even after detecting a `resourceSpan` in the request
return Promise.reject();
});

it('jaegerMiddlewares should contain the promise middleware', () => {
expect(typeof jaegerMiddlewares.promise).toBe('function');
Expand All @@ -41,3 +54,42 @@ it('loadOperationsForServiceMiddleware fetches operations for services', () => {
expect(dispatch).toHaveBeenCalledWith(fetchServiceOperations('yo'));
expect(dispatch).toHaveBeenCalledWith(change('searchSideBar', 'operation', 'all'));
});

it('loadJsonTracesMiddleware transformes traces from OTLP to jaeger by making an API call', async () => {
// Testing 3 paths in all :
// OTLP traces
// Jaeger traces
// Ivalid traces
const { loadJsonTracesMiddleware } = jaegerMiddlewares;
const dispatch = jest.fn();
const next = jest.fn();

const OTLPFileAction = {
type: String([`${loadJsonTraces}_FULFILLED`]),
payload: JSON.parse(fs.readFileSync('src/middlewares/fixtures/otlp2jaeger-in.json', 'utf-8')),
};

const OTLPFileActionErrored = {
type: String([`${loadJsonTraces}_FULFILLED`]),
payload: JSON.parse(fs.readFileSync('src/middlewares/fixtures/otlp2jaeger-in-error.json', 'utf-8')),
};
// We are moking an API failure by take a pseudo input body with no service name
// But for testing we are delibertaley sending back a rejected promise

const JaegerAction = {
type: String([`${loadJsonTraces}_FULFILLED`]),
payload: JSON.parse(fs.readFileSync('src/middlewares/fixtures/otlp2jaeger-out.json', 'utf-8')),
};

await loadJsonTracesMiddleware({ dispatch })(next)(OTLPFileAction);
expect(JaegerAPI.transformOTLP).toHaveBeenCalledWith(OTLPFileAction.payload);
expect(next).toHaveBeenCalledWith(JaegerAction);
jest.clearAllMocks();
await loadJsonTracesMiddleware({ dispatch })(next)(JaegerAction);
expect(JaegerAPI.transformOTLP).not.toHaveBeenCalled();
expect(next).toHaveBeenCalledWith(JaegerAction);
jest.clearAllMocks();
await loadJsonTracesMiddleware({ dispatch })(next)(OTLPFileActionErrored);
expect(JaegerAPI.transformOTLP).toHaveBeenCalledWith(OTLPFileActionErrored.payload);
// Errored JSON (malformed JSON) are caught before any redux action is invoked
});