|
| 1 | +import { MutableDataFrame, FieldType, LogLevel } from '@grafana/data'; |
| 2 | +import { LightstepQuery, QueryLogsRes } from 'types'; |
| 3 | + |
| 4 | +/** |
| 5 | + * Response pre-processor that converts the LS response data into Grafana wide |
| 6 | + * data frames, eg: |
| 7 | + * |
| 8 | + * **Lightstep API response shape** |
| 9 | + * ```json |
| 10 | + * { |
| 11 | + * "query": { |
| 12 | + * "data": { |
| 13 | + * "attributes": { |
| 14 | + * "logs": [ |
| 15 | + * [1691409972788, { event: "one", severity: "ErrorSeverity", tags: { ... } }], |
| 16 | + * [1691409971908, { event: "two", severity: "InfoSeverity", tags: { ... } }] |
| 17 | + * ] |
| 18 | + * } |
| 19 | + * } |
| 20 | + * } |
| 21 | + * } |
| 22 | + * ``` |
| 23 | + * |
| 24 | + * **Grafana DataFrame shape** |
| 25 | + * ```js |
| 26 | + * [ |
| 27 | + * { name: 'time', type: FieldType.time, values: [0, 1, 2] }, |
| 28 | + * { name: 'content', type: FieldType.string, values: ["one", "two"] }, |
| 29 | + * { name: 'level', type: FieldType.string, values: ["error", "info"] } |
| 30 | + * ] |
| 31 | + * ``` |
| 32 | + */ |
| 33 | +export function preprocessLogs(res: QueryLogsRes, query: LightstepQuery) { |
| 34 | + /** Map of "detected fields" aka log tags that have been added to the data frame */ |
| 35 | + const detectedFields = new Map<string, boolean>(); |
| 36 | + |
| 37 | + const frame = new MutableDataFrame({ |
| 38 | + refId: query.refId, |
| 39 | + meta: { |
| 40 | + preferredVisualisationType: 'logs', |
| 41 | + }, |
| 42 | + fields: [ |
| 43 | + { name: 'time', type: FieldType.time }, |
| 44 | + { name: 'content', type: FieldType.string }, |
| 45 | + { name: 'level', type: FieldType.string }, |
| 46 | + { name: 'severity', type: FieldType.string }, |
| 47 | + ], |
| 48 | + }); |
| 49 | + |
| 50 | + res.data.attributes.logs.forEach(([timestamp, log]) => { |
| 51 | + let tags = {}; |
| 52 | + if ('tags' in log && log.tags !== null && typeof log.tags === 'object') { |
| 53 | + tags = log.tags; |
| 54 | + Object.entries(log.tags).forEach(([key, value]) => { |
| 55 | + // Add every log tag to the set of detected fields once |
| 56 | + if (!detectedFields.has(key)) { |
| 57 | + detectedFields.set(key, true); |
| 58 | + frame.addField({ |
| 59 | + name: key, |
| 60 | + type: getFieldTypeForValue(value), |
| 61 | + }); |
| 62 | + } |
| 63 | + }); |
| 64 | + } |
| 65 | + |
| 66 | + frame.add({ |
| 67 | + time: timestamp, |
| 68 | + content: log.event, |
| 69 | + level: getLevel(log), |
| 70 | + severity: log.severity, |
| 71 | + ...tags, |
| 72 | + }); |
| 73 | + }); |
| 74 | + |
| 75 | + return frame; |
| 76 | +} |
| 77 | + |
| 78 | +// -------------------------------------------------------- |
| 79 | +// INTERNAL |
| 80 | + |
| 81 | +/** Best effort mapping fn to detect the correct "level" for a log. This |
| 82 | + * determines the color coding of the log line in the panel. */ |
| 83 | +function getLevel(log: Record<string, unknown>): LogLevel { |
| 84 | + let logLevel: unknown = log.severityNumber ?? log.severityText ?? log.level ?? log.severity ?? 0; |
| 85 | + |
| 86 | + if (typeof logLevel === 'string') { |
| 87 | + logLevel = logLevel.toLowerCase(); |
| 88 | + } |
| 89 | + |
| 90 | + // --- Grafana natively support levels |
| 91 | + if (String(logLevel) in LogLevel) { |
| 92 | + return LogLevel[logLevel as LogLevel]; |
| 93 | + } |
| 94 | + |
| 95 | + switch (logLevel) { |
| 96 | + // --- OTel SeverityNumber |
| 97 | + case 1: |
| 98 | + case 2: |
| 99 | + case 3: |
| 100 | + case 4: |
| 101 | + return LogLevel.trace; |
| 102 | + case 5: |
| 103 | + case 6: |
| 104 | + case 7: |
| 105 | + case 8: |
| 106 | + return LogLevel.debug; |
| 107 | + case 9: |
| 108 | + case 10: |
| 109 | + case 11: |
| 110 | + case 12: |
| 111 | + return LogLevel.info; |
| 112 | + case 13: |
| 113 | + case 14: |
| 114 | + case 15: |
| 115 | + case 16: |
| 116 | + return LogLevel.warning; |
| 117 | + case 17: |
| 118 | + case 18: |
| 119 | + case 19: |
| 120 | + case 20: |
| 121 | + return LogLevel.error; |
| 122 | + case 21: |
| 123 | + case 22: |
| 124 | + case 23: |
| 125 | + case 24: |
| 126 | + return LogLevel.critical; |
| 127 | + |
| 128 | + // --- OTel SeverityText |
| 129 | + case 'trace': |
| 130 | + return LogLevel.trace; |
| 131 | + case 'debug': |
| 132 | + return LogLevel.debug; |
| 133 | + case 'info': |
| 134 | + return LogLevel.info; |
| 135 | + case 'warn': |
| 136 | + return LogLevel.warning; |
| 137 | + case 'error': |
| 138 | + return LogLevel.error; |
| 139 | + case 'fatal': |
| 140 | + return LogLevel.critical; |
| 141 | + |
| 142 | + // --- Misc common levels |
| 143 | + case 'verboseseverity': |
| 144 | + return LogLevel.debug; |
| 145 | + case 'infoseverity': |
| 146 | + return LogLevel.info; |
| 147 | + case 'warningseverity': |
| 148 | + return LogLevel.warning; |
| 149 | + case 'errorseverity': |
| 150 | + return LogLevel.error; |
| 151 | + case 'immediateseverity': |
| 152 | + return LogLevel.critical; |
| 153 | + case 'fatalseverity': |
| 154 | + return LogLevel.critical; |
| 155 | + default: |
| 156 | + return LogLevel.unknown; |
| 157 | + } |
| 158 | +} |
| 159 | + |
| 160 | +/** Detects the correct FieldType enum for log fields */ |
| 161 | +function getFieldTypeForValue(value: unknown): FieldType { |
| 162 | + switch (typeof value) { |
| 163 | + case 'string': |
| 164 | + return FieldType.string; |
| 165 | + case 'number': |
| 166 | + return FieldType.number; |
| 167 | + case 'boolean': |
| 168 | + return FieldType.boolean; |
| 169 | + default: |
| 170 | + return FieldType.other; |
| 171 | + } |
| 172 | +} |
0 commit comments