Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c5f62a6
Add throughput forecasting APIs and types
jamby77 Mar 27, 2026
5d8dde4
Refactor throughput forecasting settings to use a reusable section co…
jamby77 Mar 27, 2026
36f392f
Modularize throughput forecasting components and utilities
jamby77 Mar 27, 2026
5cb015e
Update SettingsPanel heading and adjust component placement in Throug…
jamby77 Mar 27, 2026
6b2c15e
Update SettingsPanel heading and adjust component placement in Throug…
jamby77 Mar 27, 2026
72f6985
Migrate forecasting hooks to TanStack Query (#70)
jamby77 Mar 31, 2026
10f5bfd
Generic metric forecasting (#63)
jamby77 Mar 31, 2026
f469f8b
bugfix: import WebhookEventType as value from @betterdb/shared
jamby77 Mar 31, 2026
a1e99dd
bugfix: update e2e test ports to match docker-compose.test.yml
jamby77 Mar 31, 2026
8b449d6
bugfix: address PR review feedback for metric forecasting
jamby77 Mar 31, 2026
18f2904
bugfix: persist default ceilings, log prometheus errors, cleanup timers
jamby77 Mar 31, 2026
67205e5
bugfix: address second round of PR review feedback
jamby77 Mar 31, 2026
f968168
chore: add parseInt radix and cpu extractor clarification comment
jamby77 Mar 31, 2026
36dfef9
chore: remove Prometheus Infinity sentinel, prune cache in non-Pro
jamby77 Mar 31, 2026
d39507d
chore: tighten dispatch interface types, fix usePolling memoization, …
jamby77 Mar 31, 2026
aee363f
fix: add void operator to suppress ignored promise in MetricForecasting
jamby77 Apr 1, 2026
0281d63
bugfix: accumulate debounced updates, fix zero-start trend detection,…
jamby77 Apr 1, 2026
98ccfad
bugfix: pass real dataPointCount in insufficient forecast, allow frac…
jamby77 Apr 1, 2026
9a33fca
chore: add re-entry guard on checkAlerts, use ENV_DEFAULT_ID constant
jamby77 Apr 1, 2026
97cc8d3
fixed failing to save settings
KIvanow Apr 1, 2026
641cae0
Merge branch 'feature/61-throughput-forecasting' of https://github.co…
KIvanow Apr 1, 2026
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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ DB_TYPE=auto

# Storage Configuration
STORAGE_TYPE=sqlite
STORAGE_SQLITE_PATH=./data/audit.db
STORAGE_SQLITE_FILEPATH=./data/audit.db

# Audit Trail Configuration
AUDIT_POLL_INTERVAL_MS=60000
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/api-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ jobs:
if: failure()
run: |
echo "=== Valkey Logs ==="
docker logs betterdb-monitor-valkey 2>&1 | tail -50 || echo "Container not found"
docker logs betterdb-test-valkey 2>&1 | tail -50 || echo "Container not found"
echo "=== Redis Logs ==="
docker logs betterdb-monitor-redis 2>&1 | tail -50 || echo "Container not found"
docker logs betterdb-test-redis 2>&1 | tail -50 || echo "Container not found"
echo "=== PostgreSQL Logs ==="
docker logs betterdb-monitor-postgres 2>&1 | tail -50 || echo "Container not found"
docker logs betterdb-test-postgres 2>&1 | tail -50 || echo "Container not found"
4 changes: 2 additions & 2 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
"test:integration:audit": "jest test/api-audit.e2e-spec.ts",
"test:integration:client-analytics": "jest test/api-client-analytics.e2e-spec.ts",
"test:integration:full": "jest test/api-full-flow.e2e-spec.ts",
"test:integration:redis": "TEST_DB_PORT=6382 jest test/database-compatibility.e2e-spec.ts",
"test:integration:valkey": "TEST_DB_PORT=6380 jest --testRegex='.e2e-spec.ts$'",
"test:integration:redis": "TEST_DB_PORT=6392 jest test/database-compatibility.e2e-spec.ts",
"test:integration:valkey": "TEST_DB_PORT=6390 jest --testRegex='.e2e-spec.ts$'",
"test:cluster": "jest test/api-cluster.e2e-spec.ts",
"test:cluster:unit": "jest src/cluster/*.spec.ts",
"test:integration:cluster": "TEST_DB_HOST=localhost TEST_DB_PORT=7001 jest test/api-cluster.e2e-spec.ts",
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { TelemetryModule } from './telemetry/telemetry.module';
import { VectorSearchModule } from './vector-search/vector-search.module';
import { CloudAuthModule } from './auth/cloud-auth.module';
import { McpModule } from './mcp/mcp.module';
import { MetricForecastingModule } from './metric-forecasting/metric-forecasting.module';

let AiModule: any = null;
let LicenseModule: any = null;
Expand Down Expand Up @@ -119,6 +120,7 @@ const baseImports = [
WebhooksModule,
McpModule,
VectorSearchModule,
MetricForecastingModule,
];

const proprietaryImports = [
Expand Down
198 changes: 137 additions & 61 deletions apps/api/src/common/interfaces/storage-port.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,36 +29,30 @@ export type {
VectorIndexSnapshot,
VectorIndexSnapshotQueryOptions,
} from '@betterdb/shared';
import type { StoredAclEntry, AuditQueryOptions, AuditStats } from '@betterdb/shared';
export type { MetricForecastSettings, MetricKind } from '@betterdb/shared';
import type {
StoredClientSnapshot,
AppSettings,
AuditQueryOptions,
AuditStats,
ClientAnalyticsStats,
ClientSnapshotQueryOptions,
ClientTimeSeriesPoint,
ClientAnalyticsStats,
CommandDistributionParams,
CommandDistributionResponse,
IdleConnectionsParams,
IdleConnectionsResponse,
BufferAnomaliesParams,
BufferAnomaliesResponse,
ActivityTimelineParams,
ActivityTimelineResponse,
SpikeDetectionParams,
SpikeDetectionResponse,
AppSettings,
SettingsUpdateRequest,
KeyPatternSnapshot,
KeyPatternQueryOptions,
KeyAnalyticsSummary,
DatabaseConnectionConfig,
HotKeyEntry,
HotKeyQueryOptions,
KeyAnalyticsSummary,
KeyPatternQueryOptions,
KeyPatternSnapshot,
SettingsUpdateRequest,
StoredAclEntry,
StoredClientSnapshot,
MetricForecastSettings,
MetricKind,
VectorIndexSnapshot,
VectorIndexSnapshotQueryOptions,
Webhook,
WebhookDelivery,
WebhookEventType,
DeliveryStatus,
DatabaseConnectionConfig,
VectorIndexSnapshot,
VectorIndexSnapshotQueryOptions,
} from '@betterdb/shared';

// Anomaly Event Types
Expand Down Expand Up @@ -120,24 +114,24 @@ export interface AnomalyStats {

// Slow Log Entry Types
export interface StoredSlowLogEntry {
id: number; // Original slowlog ID from Valkey/Redis
timestamp: number; // Unix timestamp in seconds
duration: number; // Microseconds
command: string[]; // Command name + args (e.g., ['GET', 'key1'])
id: number; // Original slowlog ID from Valkey/Redis
timestamp: number; // Unix timestamp in seconds
duration: number; // Microseconds
command: string[]; // Command name + args (e.g., ['GET', 'key1'])
clientAddress: string;
clientName: string;
capturedAt: number; // When we captured this entry (ms)
capturedAt: number; // When we captured this entry (ms)
sourceHost: string;
sourcePort: number;
connectionId?: string;
}

export interface SlowLogQueryOptions {
startTime?: number; // Unix timestamp in seconds
startTime?: number; // Unix timestamp in seconds
endTime?: number;
command?: string;
clientName?: string;
minDuration?: number; // Microseconds
minDuration?: number; // Microseconds
limit?: number;
offset?: number;
connectionId?: string;
Expand All @@ -147,38 +141,38 @@ export interface SlowLogQueryOptions {
export type CommandLogType = 'slow' | 'large-request' | 'large-reply';

export interface StoredCommandLogEntry {
id: number; // Original commandlog ID from Valkey
timestamp: number; // Unix timestamp in seconds
duration: number; // Microseconds
command: string[]; // Command name + args
id: number; // Original commandlog ID from Valkey
timestamp: number; // Unix timestamp in seconds
duration: number; // Microseconds
command: string[]; // Command name + args
clientAddress: string;
clientName: string;
type: CommandLogType; // slow, large-request, or large-reply
capturedAt: number; // When we captured this entry (ms)
type: CommandLogType; // slow, large-request, or large-reply
capturedAt: number; // When we captured this entry (ms)
sourceHost: string;
sourcePort: number;
connectionId?: string;
}

export interface CommandLogQueryOptions {
startTime?: number; // Unix timestamp in seconds
startTime?: number; // Unix timestamp in seconds
endTime?: number;
command?: string;
clientName?: string;
type?: CommandLogType;
minDuration?: number; // Microseconds
minDuration?: number; // Microseconds
limit?: number;
offset?: number;
connectionId?: string;
}

// Latency Snapshot Types
export interface StoredLatencySnapshot {
id: string; // UUID
timestamp: number; // When we captured this snapshot (ms)
id: string; // UUID
timestamp: number; // When we captured this snapshot (ms)
eventName: string;
latestEventTimestamp: number; // Unix timestamp from LATENCY LATEST
maxLatency: number; // Microseconds
latestEventTimestamp: number; // Unix timestamp from LATENCY LATEST
maxLatency: number; // Microseconds
connectionId?: string;
}

Expand All @@ -199,8 +193,8 @@ export interface StoredLatencyHistogram {

// Memory Snapshot Types
export interface StoredMemorySnapshot {
id: string; // UUID
timestamp: number; // When we captured this snapshot (ms)
id: string; // UUID
timestamp: number; // When we captured this snapshot (ms)
usedMemory: number;
usedMemoryRss: number;
usedMemoryPeak: number;
Expand Down Expand Up @@ -272,16 +266,34 @@ export interface StoragePort {
// Client Analytics Methods - connectionId required for writes, optional filter for reads
saveClientSnapshot(clients: StoredClientSnapshot[], connectionId: string): Promise<number>;
getClientSnapshots(options?: ClientSnapshotQueryOptions): Promise<StoredClientSnapshot[]>;
getClientTimeSeries(startTime: number, endTime: number, bucketSizeMs?: number, connectionId?: string): Promise<ClientTimeSeriesPoint[]>;
getClientAnalyticsStats(startTime?: number, endTime?: number, connectionId?: string): Promise<ClientAnalyticsStats>;
getClientConnectionHistory(identifier: { name?: string; user?: string; addr?: string }, startTime?: number, endTime?: number, connectionId?: string): Promise<StoredClientSnapshot[]>;
getClientTimeSeries(
startTime: number,
endTime: number,
bucketSizeMs?: number,
connectionId?: string,
): Promise<ClientTimeSeriesPoint[]>;
getClientAnalyticsStats(
startTime?: number,
endTime?: number,
connectionId?: string,
): Promise<ClientAnalyticsStats>;
getClientConnectionHistory(
identifier: { name?: string; user?: string; addr?: string },
startTime?: number,
endTime?: number,
connectionId?: string,
): Promise<StoredClientSnapshot[]>;
pruneOldClientSnapshots(olderThanTimestamp: number, connectionId?: string): Promise<number>;

// Anomaly Methods - connectionId required for writes, optional filter for reads
saveAnomalyEvent(event: StoredAnomalyEvent, connectionId: string): Promise<string>;
saveAnomalyEvents(events: StoredAnomalyEvent[], connectionId: string): Promise<number>;
getAnomalyEvents(options?: AnomalyQueryOptions): Promise<StoredAnomalyEvent[]>;
getAnomalyStats(startTime?: number, endTime?: number, connectionId?: string): Promise<AnomalyStats>;
getAnomalyStats(
startTime?: number,
endTime?: number,
connectionId?: string,
): Promise<AnomalyStats>;
resolveAnomaly(id: string, resolvedAt: number): Promise<boolean>;
pruneOldAnomalyEvents(cutoffTimestamp: number, connectionId?: string): Promise<number>;

Expand All @@ -292,13 +304,24 @@ export interface StoragePort {
// Key Analytics Methods - connectionId required for writes, optional filter for reads
saveKeyPatternSnapshots(snapshots: KeyPatternSnapshot[], connectionId: string): Promise<number>;
getKeyPatternSnapshots(options?: KeyPatternQueryOptions): Promise<KeyPatternSnapshot[]>;
getKeyAnalyticsSummary(startTime?: number, endTime?: number, connectionId?: string): Promise<KeyAnalyticsSummary | null>;
getKeyPatternTrends(pattern: string, startTime: number, endTime: number, connectionId?: string): Promise<Array<{
timestamp: number;
keyCount: number;
memoryBytes: number;
staleCount: number;
}>>;
getKeyAnalyticsSummary(
startTime?: number,
endTime?: number,
connectionId?: string,
): Promise<KeyAnalyticsSummary | null>;
getKeyPatternTrends(
pattern: string,
startTime: number,
endTime: number,
connectionId?: string,
): Promise<
Array<{
timestamp: number;
keyCount: number;
memoryBytes: number;
staleCount: number;
}>
>;
pruneOldKeyPatternSnapshots(cutoffTimestamp: number, connectionId?: string): Promise<number>;

// Hot Key Stats Methods - connectionId required for writes, optional filter for reads
Expand All @@ -316,14 +339,24 @@ export interface StoragePort {
getWebhook(id: string): Promise<Webhook | null>;
getWebhooksByInstance(connectionId?: string): Promise<Webhook[]>;
getWebhooksByEvent(event: WebhookEventType, connectionId?: string): Promise<Webhook[]>;
updateWebhook(id: string, updates: Partial<Omit<Webhook, 'id' | 'createdAt' | 'updatedAt'>>): Promise<Webhook | null>;
updateWebhook(
id: string,
updates: Partial<Omit<Webhook, 'id' | 'createdAt' | 'updatedAt'>>,
): Promise<Webhook | null>;
deleteWebhook(id: string): Promise<boolean>;

// Webhook Delivery Methods - connectionId optional filter
createDelivery(delivery: Omit<WebhookDelivery, 'id' | 'createdAt'>): Promise<WebhookDelivery>;
getDelivery(id: string): Promise<WebhookDelivery | null>;
getDeliveriesByWebhook(webhookId: string, limit?: number, offset?: number): Promise<WebhookDelivery[]>;
updateDelivery(id: string, updates: Partial<Omit<WebhookDelivery, 'id' | 'webhookId' | 'createdAt'>>): Promise<boolean>;
getDeliveriesByWebhook(
webhookId: string,
limit?: number,
offset?: number,
): Promise<WebhookDelivery[]>;
updateDelivery(
id: string,
updates: Partial<Omit<WebhookDelivery, 'id' | 'webhookId' | 'createdAt'>>,
): Promise<boolean>;
getRetriableDeliveries(limit?: number, connectionId?: string): Promise<WebhookDelivery[]>;
pruneOldDeliveries(cutoffTimestamp: number, connectionId?: string): Promise<number>;

Expand All @@ -346,7 +379,12 @@ export interface StoragePort {

// Latency Histogram Methods
saveLatencyHistogram(histogram: StoredLatencyHistogram, connectionId: string): Promise<number>;
getLatencyHistograms(options?: { connectionId?: string; startTime?: number; endTime?: number; limit?: number }): Promise<StoredLatencyHistogram[]>;
getLatencyHistograms(options?: {
connectionId?: string;
startTime?: number;
endTime?: number;
limit?: number;
}): Promise<StoredLatencyHistogram[]>;
pruneOldLatencyHistograms(cutoffTimestamp: number, connectionId?: string): Promise<number>;

// Memory Snapshot Methods - connectionId required for writes, optional filter for reads
Expand All @@ -367,9 +405,47 @@ export interface StoragePort {
updateConnection(id: string, updates: Partial<DatabaseConnectionConfig>): Promise<void>;

// Agent/MCP Token Methods (cloud-only, optional — implementations may no-op)
saveAgentToken(token: { id: string; name: string; type: 'agent' | 'mcp'; tokenHash: string; createdAt: number; expiresAt: number; revokedAt: number | null; lastUsedAt: number | null }): Promise<void>;
getAgentTokens(type?: 'agent' | 'mcp'): Promise<Array<{ id: string; name: string; type: 'agent' | 'mcp'; tokenHash: string; createdAt: number; expiresAt: number; revokedAt: number | null; lastUsedAt: number | null }>>;
getAgentTokenByHash(hash: string): Promise<{ id: string; name: string; type: 'agent' | 'mcp'; tokenHash: string; createdAt: number; expiresAt: number; revokedAt: number | null; lastUsedAt: number | null } | null>;
saveAgentToken(token: {
id: string;
name: string;
type: 'agent' | 'mcp';
tokenHash: string;
createdAt: number;
expiresAt: number;
revokedAt: number | null;
lastUsedAt: number | null;
}): Promise<void>;
getAgentTokens(type?: 'agent' | 'mcp'): Promise<
Array<{
id: string;
name: string;
type: 'agent' | 'mcp';
tokenHash: string;
createdAt: number;
expiresAt: number;
revokedAt: number | null;
lastUsedAt: number | null;
}>
>;
getAgentTokenByHash(hash: string): Promise<{
id: string;
name: string;
type: 'agent' | 'mcp';
tokenHash: string;
createdAt: number;
expiresAt: number;
revokedAt: number | null;
lastUsedAt: number | null;
} | null>;
revokeAgentToken(id: string): Promise<void>;
updateAgentTokenLastUsed(id: string): Promise<void>;

// Metric Forecasting Settings
getMetricForecastSettings(
connectionId: string,
metricKind: MetricKind,
): Promise<MetricForecastSettings | null>;
saveMetricForecastSettings(settings: MetricForecastSettings): Promise<MetricForecastSettings>;
deleteMetricForecastSettings(connectionId: string, metricKind: MetricKind): Promise<boolean>;
getActiveMetricForecastSettings(): Promise<MetricForecastSettings[]>;
}
3 changes: 2 additions & 1 deletion apps/api/src/connections/connection-registry.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { EnvelopeEncryptionService, getEncryptionService } from '../common/utils
import { RuntimeCapabilityTracker } from './runtime-capability-tracker.service';
import { UsageTelemetryService } from '../telemetry/usage-telemetry.service';

const ENV_DEFAULT_ID = 'env-default';
// TODO: Export and use across the codebase instead of hardcoded 'env-default' strings
export const ENV_DEFAULT_ID = 'env-default';

@Injectable()
export class ConnectionRegistry implements OnModuleInit, OnModuleDestroy {
Expand Down
Loading
Loading