// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. // Generates a random hex string of size "size" const genRanHex = (size) => [...Array(size)] .map(() => Math.floor(Math.random() * 16).toString(16)) .join(''); const traceID = genRanHex(32); const output = { data: [{ traceID: traceID, spans: [], processes: {} }] }; const processes = output['data'][0]['processes']; const processMap = new Map(); const data = output['data'][0]['spans']; let traceStartTimestamp = 0; let processID = 1; export default function transform(trace) { let temp = trace['trace']['children']; let message = temp[0]['message'].split(' '); processes.p0 = { serviceName: message[3], tags: [] }; let spans = digDownInTrace(temp); traceStartTimestamp = findTraceStartTime(spans); let firstSpan = createNewSpan(traceStartTimestamp, 0, 'p0', message[6]); const totalDuration = findTotalDuration(spans); firstSpan['duration'] = totalDuration; data.push(firstSpan); traverseSpans(spans, firstSpan); return output; } function traverseChildren(span, parent) { let logSpan; if (span.hasOwnProperty('children')) { let duration = (span['children'][span['children'].length - 1]['timestamp'] - span['children'][0]['timestamp']) * 1000; if (isNaN(duration) || duration <= 0) { duration = 1; } parent['duration'] = duration; for (let i = 0; i < span['children'].length; i++) { let x = span['children'][i]; if (x.hasOwnProperty('children')) { // Create a new parent span so that the timeline for the spans are correct let message = findProcessName(parent['operationName']); let processKey = message === '' ? 'p0' : getProcess(message); logSpan = createNewSpan( traceStartTimestamp + x['timestamp'] * 1000, duration, processKey, parent['operationName'], [{ refType: 'CHILD_OF', traceID: traceID, spanID: parent['spanID'] }] ); data.push(logSpan); traverseChildren(x, logSpan); } else if (Array.isArray(x['message'])) { createProtonSpans(x['message'], parent['spanID']); } else if (x.hasOwnProperty('message') && x.hasOwnProperty('timestamp')) { // only add logs with a timestamp let logPointDuration; if (i >= span['children'].length - 1) { logPointDuration = 1; } else { logPointDuration = findDuration(span['children'], i) - x['timestamp'] * 1000; } if (isNaN(logPointDuration) || logPointDuration <= 0) { logPointDuration = 1; } addLogSpan(data, x, logPointDuration, parent); } } } } function traverseSpans(spans, firstSpan) { for (let i = 0; i < spans.length; i++) { if (spans[i].hasOwnProperty('children')) { traverseChildren(spans[i], data[data.length - 1]); } else if ( spans[i].hasOwnProperty('message') && spans[i].hasOwnProperty('timestamp') ) { let duration; if (i >= spans.length - 1) { duration = 1; } else { duration = findDuration(spans, i) - spans[i]['timestamp'] * 1000; } if (isNaN(duration) || duration <= 0) { duration = 1; } addLogSpan(data, spans[i], duration, firstSpan); } } } function addLogSpan(data, span, duration, parent) { let logPointStart = traceStartTimestamp + span['timestamp'] * 1000; let message = findProcessName(span['message']); let processKey = message === '' ? 'p0' : getProcess(message); let logSpan = createNewSpan( logPointStart, duration, processKey, span['message'], [ { refType: 'CHILD_OF', traceID: traceID, spanID: parent['spanID'], }, ] ); data.push(logSpan); } function createProtonSpans(children, parentID) { let child = children[0]; let processKey = genRanHex(5); processes[processKey] = { serviceName: 'Proton:' + genRanHex(3), tags: [] }; let startTimestamp = Date.parse(child['start_time']) * 1000; let newSpan = createNewSpan( startTimestamp, child['duration_ms'] * 1000, processKey, 'Search Dispatch', [{ refType: 'CHILD_OF', traceID: traceID, spanID: parentID }] ); data.push(newSpan); // eslint-disable-next-line no-prototype-builtins if (!child.hasOwnProperty('traces')) { return; } let traces = child['traces']; for (let k = 0; k < traces.length; k++) { let trace = traces[k]; let traceTimestamp = trace['timestamp_ms']; let events; let firstEvent; let duration; processKey = getProcessID(); processes[processKey] = { serviceName: trace['tag'], tags: [] }; if (trace['tag'] === 'query_execution') { traverseQueryExecution( trace, traceTimestamp, startTimestamp, processKey, newSpan['spanID'] ); } else { if (trace['tag'] === 'query_execution_plan') { events = []; let nextTrace = traces[k + 1]; firstEvent = trace; // query execution plan has no events, duration must therefore be found using the next trace if (nextTrace['tag'] === 'query_execution') { duration = (nextTrace['threads'][0]['traces'][0]['timestamp_ms'] - traceTimestamp) * 1000; } else { duration = (nextTrace['timestamp_ms'] - traceTimestamp) * 1000; } } else { events = trace['traces']; firstEvent = events[0]; duration = (traceTimestamp - firstEvent['timestamp_ms']) * 1000; } let childSpan = createNewSpan( startTimestamp + firstEvent['timestamp_ms'] * 1000, duration, processKey, trace['tag'], [{ refType: 'CHILD_OF', traceID: traceID, spanID: newSpan['spanID'] }] ); data.push(childSpan); traverseEvents(events, childSpan, startTimestamp, traceTimestamp); } } } // the query execution tag might have several threads function traverseQueryExecution( trace, traceTimestamp, startTimestamp, processKey, spanID ) { let threads = trace['threads']; for (let i = 0; i < threads.length; i++) { let events = threads[i]['traces']; let firstEvent = events[0]; let duration = (traceTimestamp - firstEvent['timestamp_ms']) * 1000; let span = createNewSpan( startTimestamp + firstEvent['timestamp_ms'] * 1000, duration, processKey, trace['tag'], [{ refType: 'CHILD_OF', traceID: traceID, spanID: spanID }] ); data.push(span); traverseEvents(events, span, startTimestamp, traceTimestamp); } } function traverseEvents(events, parent, startTimestamp, traceTimestamp) { for (let i = 0; i < events.length; i++) { let event = events[i]; let eventStart = event['timestamp_ms']; let operationName; let duration; if (event.hasOwnProperty('event')) { operationName = event['event']; if ( operationName === 'Complete query setup' || operationName === 'MatchThread::run Done' ) { duration = (traceTimestamp - eventStart) * 1000; } else { duration = (events[i + 1]['timestamp_ms'] - eventStart) * 1000; } } else { operationName = event['tag']; duration = (events[i + 1]['timestamp_ms'] - eventStart) * 1000; } let eventSpan = createNewSpan( startTimestamp + eventStart * 1000, duration, parent['processID'], operationName, [ { refType: 'CHILD_OF', traceID: traceID, spanID: parent['spanID'], }, ] ); data.push(eventSpan); } } function digDownInTrace(traces) { for (let trace of traces) { if (trace.hasOwnProperty('children')) { return trace['children']; } } } // Get an estimated start time by using the start time of the query and subtracting the current run time function getTraceStartTime(trace) { if (Array.isArray(trace['message'])) { let timestamp = Date.parse(trace['message'][0]['start_time']) * 1000; let currentTimestamp = trace['timestamp'] * 1000; return timestamp - currentTimestamp; } } function findTraceStartTime(spans) { let startTime = 0; for (let span of spans) { if (span.hasOwnProperty('children')) { startTime = findTraceStartTime(span['children']); } else if (span.hasOwnProperty('message')) { if (Array.isArray(span['message'])) { return getTraceStartTime(span); } } if (startTime !== 0) { return startTime; } } return startTime; } // Finds the total duration of the entire trace function findTotalDuration(spans) { let i = spans.length - 1; while (i >= 0) { if (spans[i].hasOwnProperty('timestamp')) { return spans[i]['timestamp'] * 1000; } i--; } return 0; } // Finds the duration of a single span function findDuration(span, i) { i = i + 1; while (i < span.length) { if (span[i].hasOwnProperty('timestamp')) { return span[i]['timestamp'] * 1000; } i++; } return 0; } function createNewSpan( startTime = 0, duration = 1, processID = 'p0', operationName = 'Complete', references = [] ) { let spanID = genRanHex(16); let newSpan = { traceID: traceID, spanID: spanID, operationName: operationName, startTime: startTime, duration: duration, references: references, tags: [], logs: [], processID: processID, }; return newSpan; } function getProcess(key) { if (processMap.has(key)) { return processMap.get(key); } else { let id = 'p' + getProcessID(); processes[id] = { serviceName: key, tags: [] }; processMap.set(key, id); return id; } } function getProcessID() { processID += 1; return processID; } // find a name to use for a process using the operationName from a span function findProcessName(string) { let regex = /(?:[a-z]+\.)+[a-zA-Z]+/gm; let match = string.match(regex); if (match != null && match.length > 0) { let temp = match[0]; temp = temp.split('.'); return temp[temp.length - 1]; } else { return ''; } }