Skip to content

Commit 1833c6d

Browse files
authored
add relative timeframe support for querying workers observability data (#192)
1 parent bce5ff7 commit 1833c6d

File tree

4 files changed

+140
-5
lines changed

4 files changed

+140
-5
lines changed

.changeset/dirty-days-crash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@repo/mcp-common': minor
3+
---
4+
5+
add relative timeframe support for querying

packages/mcp-common/src/types/workers-logs.types.ts

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { z } from 'zod'
22

3+
import { nowISO, parseRelativeTime } from '../utils'
4+
35
export const numericalOperations = ['eq', 'neq', 'gt', 'gte', 'lt', 'lte'] as const
46

57
export const queryOperations = [
@@ -150,27 +152,80 @@ export const zStatistics = z.object({
150152
bytes_read: z.number(),
151153
})
152154

153-
export const zTimeframe = z
155+
export const zTimeframeAbsolute = z
154156
.object({
155157
to: z.string(),
156158
from: z.string(),
157159
})
158160
.describe(
159-
`Timeframe for your query (ISO-8601 format).
161+
`An absolute timeframe for your query (ISO-8601 format).
160162
161-
• Current server time: ${new Date()}
163+
• Current server time: ${nowISO()}
162164
• Default: Last hour from current time
163165
• Maximum range: Last 7 days
164166
• Format: "YYYY-MM-DDTHH:MM:SSZ" (e.g., "2025-04-29T14:30:00Z")
165167
166168
Examples:
167-
- Last 30 minutes: from="2025-04-29T14:00:00Z", to="2025-04-29T14:30:00Z"
168-
- Yesterday: from="2025-04-28T00:00:00Z", to="2025-04-29T00:00:00Z"
169+
- Between April 1st and 5th: from="2025-04-01T00:00:00Z", to="2025-04-05T23:59:59Z"
169170
170171
Note: Narrower timeframes provide faster responses and more specific results.
171172
Omit this parameter entirely to use the default (last hour).`
172173
)
173174

175+
export const zTimeframeRelative = z
176+
.object({
177+
reference: z.string(),
178+
offset: z.string(),
179+
})
180+
.describe(
181+
`Relative timeframe for your query, composed of a reference time and an offset.
182+
183+
• Current server time: ${nowISO()}
184+
• Default: Last hour from current time
185+
• Maximum range: Last 7 days
186+
• Reference time format: "YYYY-MM-DDTHH:MM:SSZ" (ISO-8601) (e.g., "2025-04-29T14:30:00Z")
187+
• Offset format: Must start with a '+' or '-' sign, which indicates whether the offset is in the past or future, followed by one or more time units (e.g., '+5d', '-2h', '+6h20m').
188+
Units: s (seconds), m (minutes), h (hours), d (days), w (weeks).
189+
• You should not use a future looking offset in combination with the current server time as the reference time, as this will yield no results. (e.g. "the next 20 minutes")
190+
191+
Examples:
192+
- Last 30 minutes: reference="${nowISO()}", offset="-30m"
193+
- Yesterday: reference="${nowISO()}", offset="-1d"
194+
195+
Note: Narrower timeframes provide faster responses and more specific results.
196+
Omit this parameter entirely to use the default (last hour).`
197+
)
198+
.transform((val) => {
199+
const referenceTime = new Date(val.reference).getTime() / 1000
200+
201+
if (isNaN(referenceTime)) {
202+
throw new Error(`Invalid reference time: ${val.reference}`)
203+
}
204+
205+
const offsetSeconds = parseRelativeTime(val.offset)
206+
207+
const from = new Date(Math.min(referenceTime + offsetSeconds, referenceTime) * 1000)
208+
const to = new Date(Math.max(referenceTime + offsetSeconds, referenceTime) * 1000)
209+
210+
return {
211+
from: from.toISOString(),
212+
to: to.toISOString(),
213+
}
214+
})
215+
216+
export const zTimeframe = z.union([zTimeframeAbsolute, zTimeframeRelative]).describe(
217+
`Timeframe for your query, which can be either absolute or relative.
218+
219+
• Absolute timeframe: Specify exact start and end times in ISO-8601 format (e.g., "2025-04-29T14:30:00Z").
220+
• Relative timeframe: Specify a reference time and an offset (e.g., reference="2025-04-29T14:30:00Z", offset="-30m").
221+
222+
Examples:
223+
- Absolute: from="2025-04-01T00:00:00Z", to="2025-04-05T23:59:59Z"
224+
- Relative: reference="2025-04-29T14:30:00Z", offset="-30m"
225+
226+
Note: Narrower timeframes provide faster responses and more specific results.`
227+
)
228+
174229
const zCloudflareMiniEventDetailsRequest = z.object({
175230
url: z.string().optional(),
176231
method: z.string().optional(),
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { nowISO, parseRelativeTime } from './utils'
4+
5+
describe('parseRelativeTime', () => {
6+
it('parses positive relative time correctly', () => {
7+
expect(parseRelativeTime('+1h')).toBe(3600)
8+
expect(parseRelativeTime('+2d')).toBe(172800)
9+
expect(parseRelativeTime('+3w')).toBe(1814400)
10+
})
11+
12+
it('parses negative relative time correctly', () => {
13+
expect(parseRelativeTime('-1h')).toBe(-3600)
14+
expect(parseRelativeTime('-2d')).toBe(-172800)
15+
expect(parseRelativeTime('-3w')).toBe(-1814400)
16+
})
17+
18+
it('parses mixed units correctly', () => {
19+
expect(parseRelativeTime('+1h30m')).toBe(5400)
20+
expect(parseRelativeTime('-2d6h')).toBe(-194400)
21+
})
22+
23+
it('throws an error for invalid formats', () => {
24+
expect(() => parseRelativeTime('1h')).toThrow()
25+
expect(() => parseRelativeTime('+')).toThrow()
26+
expect(() => parseRelativeTime('')).toThrow()
27+
})
28+
})
29+
30+
describe('nowISO', () => {
31+
it('returns the current time in ISO format without milliseconds', () => {
32+
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/
33+
expect(nowISO()).toMatch(isoRegex)
34+
})
35+
})

packages/mcp-common/src/utils.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Utility functions for common operations
3+
*/
4+
5+
/**
6+
* Parse a relative time string into seconds
7+
*/
8+
export function parseRelativeTime(input: string): number {
9+
const units = { s: 1, m: 60, h: 3600, d: 86400, w: 604800 } as const
10+
11+
const cleanedInput = input.replace(/\s+/g, '').toLowerCase()
12+
if (!/^[+-](?:\d+[smhdw]){1,}$/.test(cleanedInput)) {
13+
throw new Error(`Invalid relative time format: ${input}`)
14+
}
15+
16+
const sign = cleanedInput.startsWith('-') ? -1 : 1
17+
18+
const timeStr = cleanedInput.slice(1) // Remove the sign
19+
const matches = timeStr.match(/\d+[smhdw]/g)
20+
21+
if (!matches) {
22+
throw new Error(`No matches found while parsing relative time: ${timeStr}`)
23+
}
24+
25+
const seconds = matches.reduce((total, match) => {
26+
const value = parseInt(match)
27+
const unit = match.slice(-1) as keyof typeof units
28+
29+
return total + value * units[unit]
30+
}, 0)
31+
32+
return sign * seconds
33+
}
34+
35+
/**
36+
* Get the current time as an ISO string without milliseconds
37+
*/
38+
export function nowISO(): string {
39+
return new Date().toISOString().split('.')[0] + 'Z'
40+
}

0 commit comments

Comments
 (0)