feat: Get ClickHouse client from AlertProvider (#1183)

HDX-2078

This PR shifts the creation of the ClickHouse client used by alerts to the AlertProvider interface, to support other auth methods in other AlertProvider implementations.

Running locally, the default provider creates the client and successfully queries with it:
<img width="1716" height="206" alt="Screenshot 2025-09-18 at 3 58 31 PM" src="https://github.com/user-attachments/assets/971a633f-6ddd-42ca-be70-19e303573938" />
This commit is contained in:
Drew Davis 2025-09-18 16:34:22 -04:00 committed by GitHub
parent 21f1aa7567
commit 140e4d2f23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 54 additions and 15 deletions

View file

@ -0,0 +1,5 @@
---
"@hyperdx/api": patch
---
feat: Get ClickHouse client from AlertProvider

View file

@ -41,6 +41,9 @@ describe('CheckAlertTask', () => {
asyncDispose: jest.fn(),
buildChartLink: jest.fn(),
buildLogSearchLink: jest.fn(),
getClickHouseClient: jest
.fn()
.mockResolvedValue({} as ClickhouseClient),
};
jest.mocked(loadProvider).mockResolvedValue(mockAlertProvider);
@ -150,6 +153,9 @@ describe('CheckAlertTask', () => {
mockAlertProvider.getAlertTasks.mockResolvedValue([mockAlertTask]);
mockAlertProvider.getWebhooks.mockResolvedValue(teamWebhooksById);
mockAlertProvider.getClickHouseClient.mockResolvedValue(
new ClickhouseClient({}),
);
await task.execute();
@ -272,6 +278,10 @@ describe('CheckAlertTask', () => {
mockAlertTask2,
]);
mockAlertProvider.getClickHouseClient.mockResolvedValue(
new ClickhouseClient({}),
);
// Mock getWebhooks to return team-specific webhooks
mockAlertProvider.getWebhooks.mockImplementation(
(teamId: string | ObjectId): Promise<Map<string, IWebhook>> => {

View file

@ -438,20 +438,7 @@ export default class CheckAlertTask implements HdxTask<CheckAlertsTaskArgs> {
alertCount: alerts.length,
});
if (!conn.password && conn.password !== '') {
const providerName = this.provider.constructor.name;
logger.info({
message: `alert provider did not fetch connection password`,
providerName,
connectionId: conn.id,
});
}
const clickhouseClient = new ClickhouseClient({
host: conn.host,
username: conn.username,
password: conn.password,
});
const clickhouseClient = await this.provider.getClickHouseClient(conn);
for (const alert of alerts) {
await this.task_queue.add(() =>

View file

@ -1,3 +1,5 @@
import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node';
import { AlertProvider, isValidProvider } from '../index';
describe('isValidProvider', () => {
@ -10,6 +12,7 @@ describe('isValidProvider', () => {
buildChartLink: () => 'http://example.com/chart',
updateAlertState: () => Promise.resolve(),
getWebhooks: () => Promise.resolve(new Map()),
getClickHouseClient: () => Promise.resolve({} as ClickhouseClient),
};
expect(isValidProvider(validProvider)).toBe(true);
@ -36,6 +39,7 @@ describe('isValidProvider', () => {
buildChartLink: () => 'http://example.com/chart',
updateAlertState: () => Promise.resolve(),
getWebhooks: () => Promise.resolve(new Map()),
getClickHouseClient: () => Promise.resolve({} as ClickhouseClient),
};
expect(isValidProvider(invalidProvider)).toBe(false);
@ -49,6 +53,7 @@ describe('isValidProvider', () => {
buildChartLink: () => 'http://example.com/chart',
updateAlertState: () => Promise.resolve(),
getWebhooks: () => Promise.resolve(new Map()),
getClickHouseClient: () => Promise.resolve({} as ClickhouseClient),
};
expect(isValidProvider(invalidProvider)).toBe(false);
@ -62,6 +67,7 @@ describe('isValidProvider', () => {
buildChartLink: () => 'http://example.com/chart',
updateAlertState: () => Promise.resolve(),
getWebhooks: () => Promise.resolve(new Map()),
getClickHouseClient: () => Promise.resolve({} as ClickhouseClient),
};
expect(isValidProvider(invalidProvider)).toBe(false);
@ -75,6 +81,7 @@ describe('isValidProvider', () => {
buildChartLink: () => 'http://example.com/chart',
updateAlertState: () => Promise.resolve(),
getWebhooks: () => Promise.resolve(new Map()),
getClickHouseClient: () => Promise.resolve({} as ClickhouseClient),
};
expect(isValidProvider(invalidProvider)).toBe(false);
@ -88,6 +95,7 @@ describe('isValidProvider', () => {
buildLogSearchLink: () => 'http://example.com/search',
updateAlertState: () => Promise.resolve(),
getWebhooks: () => Promise.resolve(new Map()),
getClickHouseClient: () => Promise.resolve({} as ClickhouseClient),
};
expect(isValidProvider(invalidProvider)).toBe(false);
@ -102,6 +110,7 @@ describe('isValidProvider', () => {
buildChartLink: () => 'http://example.com/chart',
updateAlertState: () => Promise.resolve(),
getWebhooks: () => Promise.resolve(new Map()),
getClickHouseClient: () => Promise.resolve({} as ClickhouseClient),
};
expect(isValidProvider(invalidProvider)).toBe(false);
@ -116,6 +125,7 @@ describe('isValidProvider', () => {
buildChartLink: () => 'http://example.com/chart',
updateAlertState: () => Promise.resolve(),
getWebhooks: () => Promise.resolve(new Map()),
getClickHouseClient: () => Promise.resolve({} as ClickhouseClient),
};
expect(isValidProvider(invalidProvider)).toBe(false);
@ -130,6 +140,7 @@ describe('isValidProvider', () => {
buildChartLink: () => 'http://example.com/chart',
updateAlertState: () => Promise.resolve(),
getWebhooks: () => Promise.resolve(new Map()),
getClickHouseClient: () => Promise.resolve({} as ClickhouseClient),
extraProperty: 'should not affect validation',
anotherMethod: () => {},
};

View file

@ -1,3 +1,4 @@
import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node';
import { Tile } from '@hyperdx/common-utils/dist/types';
import mongoose from 'mongoose';
import ms from 'ms';
@ -294,4 +295,24 @@ export default class DefaultAlertProvider implements AlertProvider {
});
return new Map<string, IWebhook>(webhooks.map(w => [w.id, w]));
}
async getClickHouseClient({
host,
username,
password,
id,
}: IConnection): Promise<ClickhouseClient> {
if (!password && password !== '') {
logger.info({
message: `connection password not found`,
connectionId: id,
});
}
return new ClickhouseClient({
host,
username,
password,
});
}
}

View file

@ -1,3 +1,4 @@
import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node';
import { Tile } from '@hyperdx/common-utils/dist/types';
import { ObjectId } from '@/models';
@ -76,6 +77,9 @@ export interface AlertProvider {
/** Fetch all webhooks for the given team, returning a map of webhook ID to webhook */
getWebhooks(teamId: string | ObjectId): Promise<Map<string, IWebhook>>;
/** Create and return an authenticated ClickHouse client */
getClickHouseClient(connection: IConnection): Promise<ClickhouseClient>;
}
export function isValidProvider(obj: any): obj is AlertProvider {
@ -87,7 +91,8 @@ export function isValidProvider(obj: any): obj is AlertProvider {
typeof obj.buildLogSearchLink === 'function' &&
typeof obj.buildChartLink === 'function' &&
typeof obj.updateAlertState === 'function' &&
typeof obj.getWebhooks === 'function'
typeof obj.getWebhooks === 'function' &&
typeof obj.getClickHouseClient === 'function'
);
}