-
Notifications
You must be signed in to change notification settings - Fork 1
Add Redis TS helper and integrate chart data retrieval #576
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
pavelzotikov
wants to merge
24
commits into
master
Choose a base branch
from
feature/redis-timeseries-helper
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
dafb4cb
feat: add Redis TimeSeries helper with safe increment and auto-creation
pavelzotikov dbf6845
add grouping mode: 'hours' or 'days'
pavelzotikov 04278d2
feat(api): implement flexible chart data API with Redis TimeSeries
pavelzotikov 2a88509
change expires time for jwt access secret
pavelzotikov 6147a7a
Bump version up to 1.2.14
github-actions[bot] 0599da4
refactor: separate Event and Project chart data APIs
pavelzotikov a40d522
merge master
pavelzotikov 3af0959
Bump version up to 1.2.20
github-actions[bot] 56281b8
fix: Add Redis auto-reconnect mechanism for Kubernetes pod restarts
pavelzotikov db9d94c
refactor: Apply PR #576 review comments and improve architecture
pavelzotikov 067bd6a
refactor: Apply PR #576 review comments and improve architecture
pavelzotikov 14134f9
Merge branch 'feature/redis-timeseries-helper' of https://github.com/…
pavelzotikov 1fe3d01
Update redisKeys.ts
pavelzotikov 476378d
refactor: Rename redisKeys.ts to chartStorageKeys.ts
pavelzotikov e0bf8be
refactor: Rename composeTimeSeriesKey to composeProjectMetricsKey
pavelzotikov 2115a1d
Update chartStorageKeys.ts
pavelzotikov e8bed5c
Update eventsFactory.js
pavelzotikov ba0b275
linter
pavelzotikov 5abce3c
Update index.ts
pavelzotikov 9276156
Update eventsFactory.js
pavelzotikov dec8f36
Merge branch 'master' into feature/redis-timeseries-helper
pavelzotikov ac754bf
Bump version up to 1.2.21
github-actions[bot] 9a57f76
Update eventsFactory.js
pavelzotikov db93f62
Update api.env
pavelzotikov File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| import HawkCatcher from '@hawk.so/nodejs'; | ||
| import { createClient, RedisClientType } from 'redis'; | ||
| import { Effect, sgr } from './utils/ansi'; | ||
|
|
||
| /** | ||
| * Helper class for working with Redis | ||
| */ | ||
| export default class RedisHelper { | ||
| /** | ||
| * TTL for lock records in Redis (in seconds) | ||
| */ | ||
| private static readonly LOCK_TTL = 10; | ||
|
|
||
| /** | ||
| * Redis client instance | ||
| */ | ||
| private readonly redisClient!: RedisClientType; | ||
|
|
||
| /** | ||
| * Constructor | ||
| * Initializes the Redis client and sets up error handling | ||
| */ | ||
| constructor() { | ||
| try { | ||
| this.redisClient = createClient({ url: process.env.REDIS_URL }); | ||
|
|
||
| this.redisClient.on('error', (error) => { | ||
| console.error('[Redis] Client error:', error); | ||
| if (error) { | ||
| HawkCatcher.send(error); | ||
| } | ||
| }); | ||
| } catch (error) { | ||
| console.error('[Redis] Error creating client:', error); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Connect to Redis | ||
| */ | ||
| public async initialize(): Promise<void> { | ||
| try { | ||
| await this.redisClient.connect(); | ||
| console.log('[Redis] Connected successfully'); | ||
| } catch (error) { | ||
| console.error('[Redis] Connection failed:', error); | ||
| HawkCatcher.send(error as Error); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Close Redis client | ||
| */ | ||
| public async close(): Promise<void> { | ||
| if (this.redisClient.isOpen) { | ||
| await this.redisClient.quit(); | ||
| console.log('[Redis] Connection closed'); | ||
| } | ||
| } | ||
|
|
||
| public async getChartDataFromRedis( | ||
neSpecc marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| startDate: string, | ||
| endDate: string, | ||
| groupBy: number, // minutes: 1=minute, 60=hour, 1440=day | ||
| timezoneOffset = 0, | ||
| projectId = '', | ||
| groupHash = '' | ||
| ): Promise<{ timestamp: number; count: number }[]> { | ||
| if (!this.redisClient.isOpen) { | ||
| throw new Error('Redis client not connected'); | ||
| } | ||
|
|
||
| // Determine suffix based on groupBy | ||
| let suffix: string; | ||
| if (groupBy === 1) { | ||
| suffix = 'minutely'; | ||
| } else if (groupBy === 60) { | ||
| suffix = 'hourly'; | ||
| } else if (groupBy === 1440) { | ||
| suffix = 'daily'; | ||
| } else { | ||
| // For custom intervals, fallback to minutely with aggregation | ||
| suffix = 'minutely'; | ||
| } | ||
|
|
||
| const key = groupHash | ||
| ? `ts:events:${groupHash}:${suffix}` | ||
| : projectId | ||
| ? `ts:events:${projectId}:${suffix}` | ||
| : `ts:events:${suffix}`; | ||
neSpecc marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Parse dates (support ISO string or Unix timestamp in seconds) | ||
| const start = typeof startDate === 'string' && startDate.includes('-') | ||
| ? new Date(startDate).getTime() | ||
| : Number(startDate) * 1000; | ||
| const end = typeof endDate === 'string' && endDate.includes('-') | ||
| ? new Date(endDate).getTime() | ||
| : Number(endDate) * 1000; | ||
neSpecc marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| const bucketMs = groupBy * 60 * 1000; | ||
|
|
||
| let result: [string, string][] = []; | ||
| try { | ||
| // Use aggregation to sum events within each bucket | ||
| // Since we now use TS.ADD (not TS.INCRBY), each sample is 1, so SUM gives us count | ||
| result = (await this.redisClient.sendCommand([ | ||
| 'TS.RANGE', | ||
| key, | ||
| start.toString(), | ||
| end.toString(), | ||
| 'AGGREGATION', | ||
| 'sum', | ||
| bucketMs.toString(), | ||
| ])) as [string, string][] | []; | ||
| } catch (err: any) { | ||
| if (err.message.includes('TSDB: the key does not exist')) { | ||
| console.warn(`[Redis] Key ${key} does not exist, returning zeroed data`); | ||
| result = []; | ||
| } else { | ||
| throw err; | ||
| } | ||
| } | ||
|
|
||
| // Transform data from Redis | ||
| const dataPoints: { [ts: number]: number } = {}; | ||
| for (const [tsStr, valStr] of result) { | ||
| const tsMs = Number(tsStr); | ||
| dataPoints[tsMs] = Number(valStr) || 0; | ||
| } | ||
|
|
||
| // Fill missing intervals with zeros | ||
| const filled: { timestamp: number; count: number }[] = []; | ||
| let current = start; | ||
|
|
||
| // Round current to the nearest bucket boundary | ||
| current = Math.floor(current / bucketMs) * bucketMs; | ||
|
|
||
| while (current <= end) { | ||
| const count = dataPoints[current] || 0; | ||
| filled.push({ | ||
| timestamp: Math.floor((current + timezoneOffset * 60 * 1000) / 1000), | ||
| count, | ||
| }); | ||
| current += bucketMs; | ||
| } | ||
|
|
||
| return filled.sort((a, b) => a.timestamp - b.timestamp); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.