Skip to content

Commit

Permalink
Task#33 (#39)
Browse files Browse the repository at this point in the history
* Add use cases for quantile type. Add vegteta endpoint for parsing

* add properties for success and test execution count

* Remove logging

* add changesets for next release

* wip
  • Loading branch information
diogoViana95 authored Jan 3, 2024
1 parent f7a946e commit b5cc872
Show file tree
Hide file tree
Showing 12 changed files with 125 additions and 24 deletions.
5 changes: 5 additions & 0 deletions .changeset/fair-pigs-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@deadcow-enterprises/junit-prometheus-exporter": minor
---

add quantile type and vegeta endpoints
8 changes: 8 additions & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mode": "pre",
"tag": "next",
"initialVersions": {
"@deadcow-enterprises/junit-prometheus-exporter": "0.2.0"
},
"changesets": []
}
5 changes: 2 additions & 3 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,5 @@
### Reviewers

<!-- Mention any specific team members or individuals you'd like to review this pull request. -->

@reviewer1
@reviewer2
<!-- @reviewer1 -->
<!-- @reviewer2 -->
5 changes: 5 additions & 0 deletions src/middlewares/slurp.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ export default function registerSlurpMiddleware(instance: Express) {
facadeService.parseJunit(body, q);
res.status(202).send();
});
instance.post("/slurp/vegeta", ({ body, query }, res) => {
const q = query as Record<string, string>;
facadeService.parseVegeta(body, q);
res.status(202).send();
});
}
3 changes: 2 additions & 1 deletion src/models/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ export interface ParsingSchema {
}

export interface PropertySchema {
type: "variable" | "counter";
type: "variable" | "counter" | "quantile";
quantiles?: string[];
description: string;
value: string;
validLabels?: string[];
Expand Down
27 changes: 27 additions & 0 deletions src/schemas/vegeta-schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"vegeta_load_test_latency_ms": {
"type": "quantile",
"description": "Load test latency in milliseconds",
"value": "{latencies.[quantile]} / 1000000",
"quantiles": ["mean", "max", "50th", "95th", "99th"],
"validLabels": ["project", "team", "version"],
"labelEquality": "{currentLabels.project} == {newLabels.project} && {newLabels.team} == {currentLabels.team}",
"labelEqualityResolution": "replace"
},
"vegeta_load_test_success_percent_ms": {
"type": "variable",
"description": "Total number of tests passing",
"value": "{success}",
"defaultValue": 0,
"validLabels": ["project", "team", "version"],
"labelEquality": "{currentLabels.project} == {newLabels.project} && {newLabels.team} == {currentLabels.team}",
"labelEqualityResolution": "replace"
},
"vegeta_load_test_executions": {
"type": "counter",
"description": "Total tests runs",
"validLabels": ["project", "team", "version"],
"labelEquality": "{currentLabels.project} == {newLabels.project} && {newLabels.team} == {currentLabels.team}",
"labelEqualityResolution": "replace"
}
}
9 changes: 7 additions & 2 deletions src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import { Property } from "./parsers";
import { Property, VegetaSlurper } from "./parsers";
import { JunitSlurper } from "./parsers/junit.slurper";
import { JsonSerializer, PrometheusSerializer } from "./serializers";

export class FacadeService {
constructor(
private readonly junit: JunitSlurper,
private readonly vegeta: VegetaSlurper,
private readonly prometheus: PrometheusSerializer,
private readonly json: JsonSerializer
) {}

private get properties(): Property[] {
return [...this.junit.properties];
return [...this.junit.properties, ...this.vegeta.properties];
}

parseJunit(content: any, labels: Record<string, string>) {
this.junit.parse(content, labels);
}
parseVegeta(content: any, labels: Record<string, string>) {
this.vegeta.parse(content, labels);
}

toPrometheus(): string {
return this.prometheus.serialize(...this.properties);
Expand All @@ -33,6 +37,7 @@ export const facadeService =
globalForServices.facadeService ??
new FacadeService(
new JunitSlurper(),
new VegetaSlurper(),
new PrometheusSerializer(),
new JsonSerializer()
);
Expand Down
5 changes: 3 additions & 2 deletions src/services/parsers/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './junit.slurper';
export { type Property } from './slurper';
export * from "./junit.slurper";
export { type Property } from "./slurper";
export * from "./vegeta.slurper";
40 changes: 37 additions & 3 deletions src/services/parsers/slurper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type PropertyParser<T> = {
type: PropertySchema["type"];
name: string;
pattern: Array<string | ((content: T, values: Labels, oldValue: any) => any)>;
extraLabels?: Labels;
validLabels?: PropertySchema["validLabels"];
equalityResolution?: PropertySchema["labelEqualityResolution"];
equality: Array<string | ((currentLabels: Labels, newLabels: Labels) => any)>;
Expand Down Expand Up @@ -44,6 +45,29 @@ export abstract class Slurper<T = any> {
// if property parser already exists then we don't need to generate it again
if (this.parsers.findIndex((g) => g.name === name) >= 0) return;
const curr = this.schema[name];
if (curr.type === "quantile") {
if (!curr.quantiles)
throw new Error(`Property ${name} is missing quantiles`);
for (const quantile of curr.quantiles) {
this.generatePropertyParserForSingle(
name,
{
...curr,
value: curr.value.replaceAll("[quantile]", quantile),
},
{ quantile }
);
}
} else {
this.generatePropertyParserForSingle(name, curr);
}
}

private generatePropertyParserForSingle(
name: string,
curr: PropertySchema,
extraLabels?: Labels
) {
const pattern = this.getValuePattern(curr);
const labelPattern = this.getLabelPattern(curr);

Expand All @@ -53,6 +77,7 @@ export abstract class Slurper<T = any> {
pattern,
validLabels: curr.validLabels,
equality: labelPattern,
extraLabels,
equalityResolution: curr.labelEqualityResolution,
});
}
Expand All @@ -67,6 +92,7 @@ export abstract class Slurper<T = any> {
);
break;
case "variable":
case "quantile":
// normalize instructions by splitting them by space and removing empty strings
const instructions = curr.value
.split(" ")
Expand Down Expand Up @@ -128,9 +154,10 @@ export abstract class Slurper<T = any> {
return labelPattern;
}

parse(content: T, labels: Labels): void {
parse(content: T, _labels: Labels): void {
const values: Labels = {};
this.parsers.forEach((g) => {
const labels = { ..._labels, ...g.extraLabels };
const curr = this._properties.find((p) => p.name === g.name)!;
const valueIndex = this.getLabelIndex(curr, g, labels);

Expand All @@ -150,15 +177,20 @@ export abstract class Slurper<T = any> {
if (g.equalityResolution === "replace") {
curr.values[valueIndex].labels = this.getValidLabels(
labels,
g.validLabels
g.validLabels?.concat(
g.extraLabels ? Object.keys(g.extraLabels) : []
)
);
curr.values[valueIndex].value = result;
return;
}
}

curr.values.push({
labels: this.getValidLabels(labels, g.validLabels),
labels: this.getValidLabels(
labels,
g.validLabels?.concat(g.extraLabels ? Object.keys(g.extraLabels) : [])
),
value: result,
});
});
Expand Down Expand Up @@ -186,6 +218,8 @@ export abstract class Slurper<T = any> {
if (!equal) {
continue;
}
if (p.type === "quantile" && oldLabels.quantile !== labels.quantile)
continue;
return index;
}
return -1;
Expand Down
15 changes: 15 additions & 0 deletions src/services/parsers/vegeta.slurper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as fs from "fs";
import * as path from "path";
import { ParsingSchema } from "../../models/schema";
import { Slurper } from "./slurper";

export class VegetaSlurper extends Slurper {
constructor(filePath?: string) {
const fileLocation =
filePath || path.join(__dirname, "../../schemas/vegeta-schema.json");
const fileStr = fs.readFileSync(fileLocation);

const schema = JSON.parse(fileStr.toString());
super(schema as ParsingSchema);
}
}
25 changes: 13 additions & 12 deletions src/services/serializers/prometheus.serializer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Property } from '../parsers';
import { Serializer } from './serializer';
import { Property } from "../parsers";
import { Serializer } from "./serializer";

export class PrometheusSerializer extends Serializer {
serialize(...data: Property[]): string {
Expand All @@ -12,28 +12,29 @@ export class PrometheusSerializer extends Serializer {
} ${type}\n${p.values
.map(
({ labels, value }) =>
`${p.name}${this.parseLabels(labels)} ${value}`,
`${p.name}${this.parseLabels(labels)} ${value}`
)
.join('\n')}`;
.join("\n")}`;
})
.join('\n\n');
.join("\n\n");
}

parseLabels(labels: Record<string, string>) {
if (Object.keys(labels).length === 0) {
return '';
return "";
}
return `{${Object.keys(labels)
.map((l) => `${l}="${labels[l]}"`)
.join(', ')}}`;
.join(", ")}}`;
}

propertyMapToPrometheusType(p: Property['type']): string {
propertyMapToPrometheusType(p: Property["type"]): string {
switch (p) {
case 'counter':
return 'counter';
case 'variable':
return 'gauge';
case "counter":
return "counter";
case "variable":
case "quantile":
return "gauge";
}
}
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"require": ["tsconfig-paths/register"]
},
"compilerOptions": {
"lib": ["es5", "es6", "es7"],
"lib": ["es5", "es6", "es7", "es2021"],
"target": "es2017",
"module": "commonjs",
"moduleResolution": "node",
Expand Down

0 comments on commit b5cc872

Please sign in to comment.