Skip to content

Commit 1faadb0

Browse files
authored
Improve Reason parsing for Grafana alerts (#178)
The old Grafana reason looked like this: > [ var=max_queue_size labels={runner=linux.aws.h100} value=12 ], [ var=max_queue_time_mins labels={runner=linux.aws.h100} value=122 ], [ var=queue_size_threshold labels={runner=linux.aws.h100} value=0 ], [ var=queue_time_threshold labels={runner=linux.aws.h100} value=1 ], [ var=threshold_breached labels={runner=linux.aws.h100} value=1 ] Now it'll look like: > [runner=linux.aws.h100] max_queue_size=12, max_queue_time_mins=122, queue_size_threshold=0, queue_time_threshold=1, threshold_breached=1
1 parent 6c6577a commit 1faadb0

File tree

2 files changed

+176
-1
lines changed

2 files changed

+176
-1
lines changed

lambdas/collector/__tests__/transformers/grafana.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,4 +397,110 @@ describe("GrafanaTransformer", () => {
397397
});
398398
});
399399
});
400+
401+
describe("parseValueString", () => {
402+
it("should parse valueString with no labels", () => {
403+
const valueString =
404+
"[ var=max_queue_size labels={} value=104 ], [ var=queue_size_threshold labels={} value=1 ]";
405+
406+
// Access private method through type assertion
407+
const result = (transformer as any).parseValueString(valueString);
408+
409+
expect(result).toBe("max_queue_size=104, queue_size_threshold=1");
410+
});
411+
412+
it("should parse valueString with labels and group by labels", () => {
413+
const valueString =
414+
"[ var=cpu_usage labels={instance=server1,env=prod} value=85 ], [ var=memory_usage labels={instance=server1,env=prod} value=70 ], [ var=disk_usage labels={instance=server2,env=dev} value=45 ]";
415+
416+
const result = (transformer as any).parseValueString(valueString);
417+
418+
// Should group by labels
419+
expect(result).toContain(
420+
"[instance=server1,env=prod] cpu_usage=85, memory_usage=70",
421+
);
422+
expect(result).toContain("[instance=server2,env=dev] disk_usage=45");
423+
});
424+
425+
it("should handle mixed scenarios with some items having labels and others not", () => {
426+
const valueString =
427+
"[ var=max_queue_size labels={} value=104 ], [ var=cpu_usage labels={instance=server1} value=85 ], [ var=disk_usage labels={instance=server1} value=70 ], [ var=threshold_breached labels={} value=1 ]";
428+
429+
const result = (transformer as any).parseValueString(valueString);
430+
431+
// No labels group
432+
expect(result).toContain("max_queue_size=104, threshold_breached=1");
433+
// Labeled group
434+
expect(result).toContain(
435+
"[instance=server1] cpu_usage=85, disk_usage=70",
436+
);
437+
});
438+
439+
it("should handle the example from the requirements", () => {
440+
const valueString =
441+
"[ var=max_queue_size labels={} value=104 ], [ var=max_queue_time_mins labels={} value=7 ], [ var=queue_size_threshold labels={} value=1 ], [ var=queue_time_threshold labels={} value=0 ], [ var=threshold_breached labels={} value=1 ]";
442+
443+
const result = (transformer as any).parseValueString(valueString);
444+
445+
expect(result).toBe(
446+
"max_queue_size=104, max_queue_time_mins=7, queue_size_threshold=1, queue_time_threshold=0, threshold_breached=1",
447+
);
448+
});
449+
450+
it("should handle empty or invalid valueString", () => {
451+
expect((transformer as any).parseValueString("")).toBe("");
452+
expect((transformer as any).parseValueString(null)).toBe("");
453+
expect((transformer as any).parseValueString(undefined)).toBe("");
454+
expect((transformer as any).parseValueString("invalid format")).toBe(
455+
"invalid format",
456+
);
457+
});
458+
459+
it("should handle valueString with complex label values", () => {
460+
const valueString =
461+
"[ var=response_time labels={service=api,version=v1.2.3,region=us-east-1} value=250 ], [ var=error_rate labels={service=api,version=v1.2.3,region=us-east-1} value=0.5 ]";
462+
463+
const result = (transformer as any).parseValueString(valueString);
464+
465+
expect(result).toBe(
466+
"[service=api,version=v1.2.3,region=us-east-1] response_time=250, error_rate=0.5",
467+
);
468+
});
469+
470+
it("should preserve original string if parsing fails", () => {
471+
const malformedString = "malformed [ var=test";
472+
473+
const result = (transformer as any).parseValueString(malformedString);
474+
475+
expect(result).toBe(malformedString);
476+
});
477+
478+
it("should handle single entry valueString", () => {
479+
const valueString = "[ var=single_metric labels={env=prod} value=42 ]";
480+
481+
const result = (transformer as any).parseValueString(valueString);
482+
483+
expect(result).toBe("[env=prod] single_metric=42");
484+
});
485+
486+
it("should handle valueString without labels parameter (future-proofing)", () => {
487+
const valueString =
488+
"[ var=max_queue_size value=104 ], [ var=queue_size_threshold value=1 ]";
489+
490+
const result = (transformer as any).parseValueString(valueString);
491+
492+
expect(result).toBe("max_queue_size=104, queue_size_threshold=1");
493+
});
494+
495+
it("should handle mixed format with some having labels parameter and others not", () => {
496+
const valueString =
497+
"[ var=max_queue_size labels={} value=104 ], [ var=cpu_usage value=85 ], [ var=memory_usage labels={instance=server1} value=70 ]";
498+
499+
const result = (transformer as any).parseValueString(valueString);
500+
501+
// Should group no-labels items together and labeled items separately
502+
expect(result).toContain("max_queue_size=104, cpu_usage=85");
503+
expect(result).toContain("[instance=server1] memory_usage=70");
504+
});
505+
});
400506
});

lambdas/collector/src/transformers/grafana.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,10 @@ export class GrafanaTransformer extends BaseTransformer {
7878
title,
7979
description: this.sanitizeString(annotations.description || "", 1500),
8080
summary: this.sanitizeString(annotations.summary || "", 1500),
81-
reason: this.sanitizeString(alert.valueString || "", 500),
81+
reason: this.sanitizeString(
82+
this.parseValueString(alert.valueString || ""),
83+
500,
84+
),
8285
priority,
8386
occurred_at: occurredAt,
8487
team,
@@ -155,6 +158,72 @@ export class GrafanaTransformer extends BaseTransformer {
155158
return new Date().toISOString();
156159
}
157160

161+
private parseValueString(valueString: string): string {
162+
if (!valueString || typeof valueString !== "string") {
163+
return "";
164+
}
165+
166+
try {
167+
// Parse the valueString format: "[ var=name labels={key=value} value=123 ]" or "[ var=name value=123 ]"
168+
const matches = valueString.match(
169+
/\[\s*var=([^,\s]+)(?:\s+labels=\{([^}]*)\})?\s+value=([^,\s\]]+)\s*\]/g,
170+
);
171+
172+
if (!matches) {
173+
return valueString; // Return original if parsing fails
174+
}
175+
176+
const labelGroups = new Map<
177+
string,
178+
Array<{ var: string; value: string }>
179+
>();
180+
181+
// Parse each match
182+
for (const match of matches) {
183+
const innerMatch = match.match(
184+
/\[\s*var=([^,\s]+)(?:\s+labels=\{([^}]*)\})?\s+value=([^,\s\]]+)\s*\]/,
185+
);
186+
187+
if (!innerMatch) continue;
188+
189+
const varName = innerMatch[1].trim();
190+
const labels = innerMatch[2] ? innerMatch[2].trim() : ""; // Handle optional labels
191+
const value = innerMatch[3].trim();
192+
193+
// Use labels as the grouping key (empty string for no labels)
194+
const groupKey = labels;
195+
196+
if (!labelGroups.has(groupKey)) {
197+
labelGroups.set(groupKey, []);
198+
}
199+
200+
labelGroups.get(groupKey)!.push({ var: varName, value });
201+
}
202+
203+
const result: string[] = [];
204+
205+
// Process each label group
206+
for (const [labels, items] of labelGroups) {
207+
const varValuePairs = items
208+
.map((item) => `${item.var}=${item.value}`)
209+
.join(", ");
210+
211+
if (labels) {
212+
// Group has labels - include them in the output
213+
result.push(`[${labels}] ${varValuePairs}`);
214+
} else {
215+
// No labels - just output the var=value pairs
216+
result.push(varValuePairs);
217+
}
218+
}
219+
220+
return result.join("; ");
221+
} catch (error) {
222+
// If parsing fails, return the original string
223+
return valueString;
224+
}
225+
}
226+
158227
// Extract debugging context for error messages
159228
private extractDebugContext(rawPayload: any, envelope: Envelope): string {
160229
const context: string[] = [];

0 commit comments

Comments
 (0)