-
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
Merged
Merged
Changes from 10 commits
Commits
Show all changes
34 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 ad01c8f
fix eslint in files
pavelzotikov a2c6efe
merge master
pavelzotikov a4edc89
update package.json: new version
pavelzotikov 37cab1b
add redis-mock library and fix tests
pavelzotikov 82e0dc0
change version for redis-mock
pavelzotikov ea05c7d
add redis in integration.test
pavelzotikov 2bfe2b8
fix intergration.test
pavelzotikov cde06d2
Update src/redisHelper.ts
pavelzotikov 57b8de7
fix for pr comments
pavelzotikov 029fb48
Merge branch 'feature/redis-timeseries-helper' of https://github.com/…
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
Some comments aren't visible on the classic Files Changed page.
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
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,147 @@ | ||
| 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; | ||
|
|
||
| /** | ||
| * Singleton instance | ||
| */ | ||
| private static instance: RedisHelper | null = null; | ||
|
|
||
| /** | ||
| * Redis client instance | ||
| */ | ||
| private redisClient!: RedisClientType; | ||
|
|
||
| /** | ||
| * Flag to track if we're currently reconnecting | ||
| */ | ||
| private isReconnecting = false; | ||
|
|
||
| /** | ||
| * Constructor | ||
| * Initializes the Redis client and sets up error handling with auto-reconnect | ||
| */ | ||
| constructor() { | ||
| try { | ||
| this.redisClient = createClient({ | ||
| url: process.env.REDIS_URL, | ||
| socket: { | ||
| reconnectStrategy: (retries) => { | ||
| // Exponential backoff: wait longer between each retry | ||
| // Max wait time: 30 seconds | ||
| const delay = Math.min(retries * 1000, 30000); | ||
| console.log(`[Redis] Reconnecting... attempt ${retries}, waiting ${delay}ms`); | ||
| return delay; | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| // Handle connection errors | ||
| this.redisClient.on('error', (error) => { | ||
| console.error('[Redis] Client error:', error); | ||
| if (error) { | ||
| HawkCatcher.send(error); | ||
| } | ||
| }); | ||
|
|
||
| // Handle successful reconnection | ||
| this.redisClient.on('ready', () => { | ||
| console.log('[Redis] Client ready'); | ||
| this.isReconnecting = false; | ||
| }); | ||
|
|
||
| // Handle reconnecting event | ||
| this.redisClient.on('reconnecting', () => { | ||
| console.log('[Redis] Client reconnecting...'); | ||
| this.isReconnecting = true; | ||
| }); | ||
|
|
||
| // Handle connection end | ||
| this.redisClient.on('end', () => { | ||
| console.log('[Redis] Connection ended'); | ||
| }); | ||
| } catch (error) { | ||
| console.error('[Redis] Error creating client:', error); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Get singleton instance | ||
| */ | ||
| public static getInstance(): RedisHelper { | ||
| if (!RedisHelper.instance) { | ||
| RedisHelper.instance = new RedisHelper(); | ||
| } | ||
| return RedisHelper.instance; | ||
| } | ||
|
|
||
| /** | ||
| * Connect to Redis | ||
| */ | ||
| public async initialize(): Promise<void> { | ||
| try { | ||
| if (!this.redisClient.isOpen && !this.isReconnecting) { | ||
| await this.redisClient.connect(); | ||
| console.log('[Redis] Connected successfully'); | ||
| } | ||
| } catch (error) { | ||
| console.error('[Redis] Connection failed:', error); | ||
| HawkCatcher.send(error as Error); | ||
| // Don't throw - let reconnectStrategy handle it | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Close Redis client | ||
| */ | ||
| public async close(): Promise<void> { | ||
| if (this.redisClient.isOpen) { | ||
| await this.redisClient.quit(); | ||
| console.log('[Redis] Connection closed'); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check if Redis is connected | ||
| */ | ||
| public isConnected(): boolean { | ||
| return this.redisClient.isOpen; | ||
| } | ||
|
|
||
| /** | ||
| * Execute TS.RANGE command with aggregation | ||
| * | ||
| * @param key - Redis TimeSeries key | ||
| * @param start - start timestamp in milliseconds | ||
| * @param end - end timestamp in milliseconds | ||
| * @param aggregationType - aggregation type (sum, avg, min, max, etc.) | ||
| * @param bucketMs - bucket size in milliseconds | ||
| * @returns Array of [timestamp, value] tuples | ||
| */ | ||
| public async tsRange( | ||
| key: string, | ||
| start: string, | ||
| end: string, | ||
| aggregationType: string, | ||
| bucketMs: string | ||
| ): Promise<[string, string][]> { | ||
| return (await this.redisClient.sendCommand([ | ||
| 'TS.RANGE', | ||
| key, | ||
| start, | ||
| end, | ||
| 'AGGREGATION', | ||
| aggregationType, | ||
| bucketMs, | ||
| ])) as [string, string][]; | ||
| } | ||
| } | ||
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,92 @@ | ||
| import RedisHelper from '../redisHelper'; | ||
| import { composeTimeSeriesKey, getTimeSeriesSuffix } from '../utils/redisKeys'; | ||
|
|
||
| /** | ||
| * Service for fetching chart data from Redis TimeSeries | ||
| */ | ||
| export default class ChartDataService { | ||
| private redisHelper: RedisHelper; | ||
|
|
||
| constructor(redisHelper: RedisHelper) { | ||
| this.redisHelper = redisHelper; | ||
| } | ||
|
|
||
| /** | ||
| * Get project chart data from Redis TimeSeries | ||
| * | ||
| * @param projectId - project ID | ||
| * @param startDate - start date as ISO string (e.g., '2025-01-01T00:00:00Z') | ||
| * @param endDate - end date as ISO string (e.g., '2025-01-31T23:59:59Z') | ||
| * @param groupBy - grouping interval in minutes (1=minute, 60=hour, 1440=day) | ||
| * @param timezoneOffset - user's local timezone offset in minutes (default: 0) | ||
| * @returns Array of data points with timestamp and count | ||
| * @throws Error if Redis is not connected (caller should fallback to MongoDB) | ||
| */ | ||
| public async getProjectChartData( | ||
| projectId: string, | ||
| startDate: string, | ||
| endDate: string, | ||
| groupBy: number, | ||
| timezoneOffset = 0 | ||
| ): Promise<{ timestamp: number; count: number }[]> { | ||
| // Check if Redis is connected | ||
| if (!this.redisHelper.isConnected()) { | ||
| console.warn('[ChartDataService] Redis not connected, will fallback to MongoDB'); | ||
| throw new Error('Redis client not connected'); | ||
| } | ||
|
|
||
| // Determine suffix and compose key | ||
| const suffix = getTimeSeriesSuffix(groupBy); | ||
| const key = composeTimeSeriesKey(suffix, projectId); | ||
|
|
||
| // Parse ISO date strings to milliseconds | ||
| const start = new Date(startDate).getTime(); | ||
| const end = new Date(endDate).getTime(); | ||
| const bucketMs = groupBy * 60 * 1000; | ||
|
|
||
| // Fetch data from Redis | ||
| let result: [string, string][] = []; | ||
| try { | ||
| result = await this.redisHelper.tsRange( | ||
| key, | ||
| start.toString(), | ||
| end.toString(), | ||
| 'sum', | ||
| bucketMs.toString() | ||
| ); | ||
| } catch (err: any) { | ||
| if (err.message.includes('TSDB: the key does not exist')) { | ||
| console.warn(`[ChartDataService] 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); | ||
| } | ||
| } | ||
|
|
Oops, something went wrong.
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.