Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/promql-ast/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ The deparser inserts parentheses based on operator precedence, so ASTs built by
| `metric('up', { job: 'api' })` | `up{job="api"}` |
| `selector({ __name__: 'up' })` | `{__name__="up"}` |
| `eq / neq / re / nre` | `=` `!=` `=~` `!~` matchers |
| `range(sel, '5m')` | `sel[5m]` |
| `subquery(expr, '1h', '1m')` | `expr[1h:1m]` |
| `seconds/minutes/hours/days/... / duration({h:1,m:30})` | typed durations (`60s`, `1h30m`) |
| `range(sel, minutes(5))` | `sel[5m]` |
| `subquery(expr, hours(1), minutes(1))` | `expr[1h:1m]` |
| `rate / irate / increase / *_over_time` | function calls |
| `sum / min / max / avg / count / ...` | aggregations |
| `topk / bottomk / quantile / countValues` | aggregations with a parameter |
Expand Down
17 changes: 17 additions & 0 deletions packages/promql-ast/__tests__/builders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,23 @@ describe('promql builders', () => {
expect(reparsed.matchers[0].value).toBe('a"b\\c');
});

it('composes durations without string interpolation', () => {
expect(b.seconds(60)).toBe('60s');
expect(b.minutes(5)).toBe('5m');
expect(b.hours(1)).toBe('1h');
expect(b.days(2)).toBe('2d');
expect(b.milliseconds(500)).toBe('500ms');
expect(b.duration({ h: 1, m: 30 })).toBe('1h30m');
});

it('duration() rejects an empty spec', () => {
expect(() => b.duration({})).toThrow();
});

it('range accepts a built duration', () => {
expect(deparse(b.rate(b.range(b.metric('x'), b.seconds(60))))).toBe('rate(x[60s])');
});

it('at() accepts start/end/number', () => {
expect(deparse(b.at(b.metric('x'), 'start'))).toBe('x @ start()');
expect(deparse(b.at(b.metric('x'), 'end'))).toBe('x @ end()');
Expand Down
2 changes: 1 addition & 1 deletion packages/promql-ast/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "promql-ast",
"version": "0.2.1",
"version": "0.3.0",
"author": "Constructive <developers@constructive.io>",
"description": "TypeScript PromQL parser, AST builders, and deparser",
"main": "index.js",
Expand Down
57 changes: 49 additions & 8 deletions packages/promql-ast/src/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,47 @@ export function nre(name: string, value: string): LabelMatcher {
return { name, op: '!~', value };
}

/**
* A PromQL duration string (e.g. `5m`, `1h30m`, `500ms`). Produced by the
* duration builders so callers never assemble the syntax by hand.
*/
export type Duration = string & { readonly __promqlDuration?: unique symbol };

/** Compose a PromQL duration from unit components, e.g. `duration({ h: 1, m: 30 })` → `1h30m`. */
export function duration(parts: {
y?: number;
w?: number;
d?: number;
h?: number;
m?: number;
s?: number;
ms?: number;
}): Duration {
const order: Array<[keyof typeof parts, string]> = [
['y', 'y'],
['w', 'w'],
['d', 'd'],
['h', 'h'],
['m', 'm'],
['s', 's'],
['ms', 'ms'],
];
const out = order
.filter(([k]) => parts[k] != null)
.map(([k, unit]) => `${parts[k]}${unit}`)
.join('');
if (out === '') throw new Error('duration() requires at least one non-null unit');
return out as Duration;
}

export const milliseconds = (n: number): Duration => duration({ ms: n });
export const seconds = (n: number): Duration => duration({ s: n });
export const minutes = (n: number): Duration => duration({ m: n });
export const hours = (n: number): Duration => duration({ h: n });
export const days = (n: number): Duration => duration({ d: n });
export const weeks = (n: number): Duration => duration({ w: n });
export const years = (n: number): Duration => duration({ y: n });

/** Instant vector selector with a metric name, e.g. `metric('up', { job: 'api' })`. */
export function metric(name: string, matchers?: MatchersInput): VectorSelector {
return { type: 'VectorSelector', name, matchers: toMatchers(matchers) };
Expand All @@ -61,13 +102,13 @@ export function selector(matchers: MatchersInput): VectorSelector {
return { type: 'VectorSelector', matchers: toMatchers(matchers) };
}

/** Range vector, e.g. `range(metric('x'), '5m')` → `x[5m]`. */
export function range(vectorSelector: VectorSelector, window: string): MatrixSelector {
/** Range vector, e.g. `range(metric('x'), minutes(5))` → `x[5m]`. */
export function range(vectorSelector: VectorSelector, window: Duration): MatrixSelector {
return { type: 'MatrixSelector', vectorSelector, range: window };
}

/** Subquery, e.g. `subquery(rate(range(metric('x'), '5m')), '1h', '1m')`. */
export function subquery(expr: Expr, window: string, step?: string): SubqueryExpr {
/** Subquery, e.g. `subquery(rate(range(metric('x'), minutes(5))), hours(1), minutes(1))`. */
export function subquery(expr: Expr, window: Duration, step?: Duration): SubqueryExpr {
return { type: 'SubqueryExpr', expr, range: window, ...(step ? { step } : {}) };
}

Expand All @@ -84,12 +125,12 @@ export function pos(expr: Expr): UnaryExpr {
return { type: 'UnaryExpr', op: '+', expr };
}

/** Attach an `offset` modifier (accepts a signed duration string, e.g. `-5m`). */
/** Attach an `offset` modifier, e.g. `offset(sel, minutes(5))` (accepts signed durations). */
export function offset<T extends VectorSelector | MatrixSelector | SubqueryExpr>(
expr: T,
duration: string
window: Duration
): T {
return { ...expr, offset: duration };
return { ...expr, offset: window };
}

/** Attach an `@` modifier. */
Expand All @@ -105,7 +146,7 @@ export function at<T extends VectorSelector | MatrixSelector | SubqueryExpr>(
return { ...expr, at: mod };
}

/** Generic function call, e.g. `call('rate', range(metric('x'), '5m'))`. */
/** Generic function call, e.g. `call('rate', range(metric('x'), minutes(5)))`. */
export function call(func: string, ...args: Expr[]): Call {
return { type: 'Call', func, args };
}
Expand Down
Loading