Skip to content

Commit

Permalink
chore: replace more tinybird, add tests for clickhouse
Browse files Browse the repository at this point in the history
  • Loading branch information
chronark committed Nov 14, 2024
1 parent 0c99dfc commit f0fffb9
Show file tree
Hide file tree
Showing 20 changed files with 990 additions and 223 deletions.
1 change: 1 addition & 0 deletions .github/workflows/unit_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jobs:
- "./internal/id"
- "./internal/keys"
- "./internal/resend"
- "./internal/clickhouse"
- "./packages/api"
- "./packages/cache"
- "./packages/hono"
Expand Down
19 changes: 0 additions & 19 deletions apps/api/src/pkg/env.test.ts

This file was deleted.

5 changes: 1 addition & 4 deletions apps/api/src/pkg/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { MessageBody } from "./key_migration/message";

export const cloudflareRatelimiter = z.custom<{
limit: (opts: { key: string }) => Promise<{ success: boolean }>;
}>((r) => typeof r.limit === "function");
}>((r) => !!r && typeof r.limit === "function");

export const zEnv = z.object({
VERSION: z.string().default("unknown"),
Expand All @@ -18,9 +18,6 @@ export const zEnv = z.object({
CLOUDFLARE_API_KEY: z.string().optional(),
CLOUDFLARE_ZONE_ID: z.string().optional(),
ENVIRONMENT: z.enum(["development", "preview", "canary", "production"]).default("development"),
TINYBIRD_PROXY_URL: z.string().optional(),
TINYBIRD_PROXY_TOKEN: z.string().optional(),
TINYBIRD_TOKEN: z.string().optional(),
DO_USAGELIMIT: z.custom<DurableObjectNamespace>((ns) => typeof ns === "object"),
KEY_MIGRATIONS: z.custom<Queue<MessageBody>>((q) => typeof q === "object").optional(),
EMIT_METRICS_LOGS: z
Expand Down
11 changes: 0 additions & 11 deletions apps/api/src/pkg/middleware/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,6 @@ export function metrics(): MiddlewareHandler<HonoEnv> {
// time: Date.now(),
// };

// c.executionCtx.waitUntil(
// analytics.ingestSdkTelemetry(event).catch((err) => {
// logger.error("Error ingesting SDK telemetry into tinybird", {
// method: c.req.method,
// path: c.req.path,
// error: err.message,
// telemetry,
// event,
// });
// }),
// );
// c.executionCtx.waitUntil(
// analytics
// .insertSdkTelemetry({
Expand Down
1 change: 0 additions & 1 deletion apps/api/src/routes/v1_keys_createKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,6 @@ export const registerV1KeysCreateKey = (app: App) =>
];

await insertUnkeyAuditLog(c, undefined, auditLogs);
// TODO: emit event to tinybird
return c.json({
keyId: newKey.id,
key: newKey.secret,
Expand Down
3 changes: 2 additions & 1 deletion apps/dashboard/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ DATABASE_PASSWORD=
UNKEY_WORKSPACE_ID=
UNKEY_API_ID=

# ClickHouse
CLICKHOUSE_URL=

# Optional

TINYBIRD_TOKEN=

TRIGGER_API_KEY=
NEXT_PUBLIC_TRIGGER_PUBLIC_KEY=
2 changes: 0 additions & 2 deletions apps/dashboard/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ export const env = () =>
UPSTASH_REDIS_REST_URL: z.string().optional(),
UPSTASH_REDIS_REST_TOKEN: z.string().optional(),

TINYBIRD_TOKEN: z.string().optional(),

CLERK_WEBHOOK_SECRET: z.string().optional(),
CLERK_SECRET_KEY: z.string().optional(),
RESEND_API_KEY: z.string().optional(),
Expand Down
3 changes: 0 additions & 3 deletions apps/semantic-cache/src/pkg/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ export const zEnv = z.object({
DATABASE_USERNAME: z.string(),
DATABASE_PASSWORD: z.string(),
ENVIRONMENT: z.enum(["development", "preview", "canary", "production"]).default("development"),
TINYBIRD_PROXY_URL: z.string().optional(),
TINYBIRD_PROXY_TOKEN: z.string().optional(),
TINYBIRD_TOKEN: z.string().optional(),
APEX_DOMAIN: z.string().default("llm.unkey.io"),
EMIT_METRICS_LOGS: z
.string()
Expand Down
2 changes: 1 addition & 1 deletion apps/workflows/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ DATABASE_HOST=
DATABASE_USERNAME=
DATABASE_PASSWORD=

TINYBIRD_TOKEN=
CLICKHOUSE_URL=

STRIPE_SECRET_KEY=
STRIPE_PRODUCT_ID_KEY_VERIFICATIONS=
Expand Down
3 changes: 2 additions & 1 deletion apps/workflows/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export function env() {
DATABASE_HOST: z.string(),
DATABASE_USERNAME: z.string(),
DATABASE_PASSWORD: z.string(),
TINYBIRD_TOKEN: z.string(),
STRIPE_SECRET_KEY: z.string(),

STRIPE_PRODUCT_ID_KEY_VERIFICATIONS: z.string(),
Expand All @@ -16,6 +15,8 @@ export function env() {

CLERK_SECRET_KEY: z.string(),

CLICKHOUSE_URL: z.string(),

RESEND_API_KEY: z.string(),
TRIGGER_API_KEY: z.string(),
AGENT_TOKEN: z.string(),
Expand Down
2 changes: 1 addition & 1 deletion apps/www/content/jobs/software-engineer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Unkey currently uses the following tech:
- tRPC
- Clerk
- Planetscale with Drizzle ORM
- Tinybird
- ClickHouse
- Cloudflare Workers with Hono

## Who you are:
Expand Down
2 changes: 0 additions & 2 deletions deployment/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,6 @@ services:
"--var=AGENT_URL:http://agent_lb:8080",
"--var=AGENT_TOKEN:agent-auth-secret",
"--var=EMIT_METRICS_LOGS:false",
"--var=TINYBIRD_PROXY_URL:http://agent_lb:8080",
"--var=TINYBIRD_PROXY_TOKEN:agent-auth-secret",
"--var=SYNC_RATELIMIT_ON_NO_DATA:1.0",
"--var=CLICKHOUSE_URL:http://default:password@clickhouse:8123",

Expand Down
8 changes: 7 additions & 1 deletion internal/clickhouse/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@
"keywords": [],
"author": "Andreas Thomas",
"license": "AGPL-3.0",
"scripts": {
"test": "vitest run"
},
"devDependencies": {
"typescript": "^5.5.3"
"@dagger.io/dagger": "^0.14.0",
"testcontainers": "^10.14.0",
"typescript": "^5.5.3",
"vitest": "^1.6.0"
},
"dependencies": {
"@clickhouse/client-web": "^1.6.0",
Expand Down
39 changes: 39 additions & 0 deletions internal/clickhouse/src/insert_verifications.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { expect, test } from "vitest";
import { ClickHouse } from "./index";

import { ClickHouseContainer } from "./testutil";

test(
"inserts a single row",
{
timeout: 300_000,
},
async (t) => {
const container = await ClickHouseContainer.start(t);

const ch = new ClickHouse({ url: container.url() });

const verification = {
request_id: "1",
time: Date.now(),
workspace_id: "workspace_id",
key_space_id: "key_space_id",
key_id: "key_id",
outcome: "VALID",
region: "test",
} as const;

await ch.verifications.insert(verification);

const latestVerifications = await ch.verifications.logs({
workspaceId: verification.workspace_id,
keySpaceId: verification.key_space_id,
keyId: verification.key_id,
});

expect(latestVerifications.length).toBe(1);
expect(latestVerifications[0].time).toBe(verification.time);
expect(latestVerifications[0].outcome).toBe("VALID");
expect(latestVerifications[0].region).toBe(verification.region);
},
);
55 changes: 55 additions & 0 deletions internal/clickhouse/src/ratelimits_billing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { expect, test } from "vitest";
import { ClickHouse } from "./index";

import { randomUUID } from "node:crypto";
import { ClickHouseContainer } from "./testutil";

test(
"returns the correct amount of billable ratelimits",
{
timeout: 300_000,
},
async (t) => {
const container = await ClickHouseContainer.start(t);

const ch = new ClickHouse({ url: container.url() });

const workspaceId = randomUUID();
const namespaceId = randomUUID();
const now = new Date();
const year = now.getUTCFullYear();
const month = now.getUTCMonth() + 1; // 1 = January
const endTime = now.getTime();
const startTime = now.setUTCDate(1);

let billable = 0;
for (let i = 0; i < 100; i++) {
const ratelimits = new Array(10_000).fill(null).map(() => {
const passed = Math.random() > 0.2;
if (passed) {
billable++;
}
return {
workspace_id: workspaceId,
namespace_id: namespaceId,
identifier: randomUUID(),
passed,
time: Math.floor(startTime + Math.random() * (endTime - startTime)),
request_id: randomUUID(),
};
});

await ch.ratelimits.insert(ratelimits);
}
// give clickhouse time to process all writes and update the materialized views
await new Promise((r) => setTimeout(r, 10_000));

const billableRatelimits = await ch.billing.billableRatelimits({
workspaceId,
year,
month,
});

expect(billableRatelimits).toBe(billable);
},
);
68 changes: 68 additions & 0 deletions internal/clickhouse/src/testutil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
GenericContainer,
GenericContainerBuilder,
Network,
type StartedTestContainer,
Wait,
} from "testcontainers";
import type { TaskContext } from "vitest";

export class ClickHouseContainer {
static readonly username = "default";
static readonly password = "password";
private readonly container: StartedTestContainer;

private constructor(container: StartedTestContainer) {
this.container = container;
}

public url(): string {
return `http://${ClickHouseContainer.username}:${
ClickHouseContainer.password
}@${this.container.getHost()}:${this.container.getMappedPort(8123)}`;
}

// Stops the container
//
// You do not need to call this every time.
// The container is automatically stopped when the test ends
public async stop(): Promise<void> {
await this.container.stop();
}

static async start(t: TaskContext): Promise<ClickHouseContainer> {
const network = await new Network().start();

const container = await new GenericContainer("bitnami/clickhouse:latest")
.withEnvironment({
CLICKHOUSE_ADMIN_USER: ClickHouseContainer.username,
CLICKHOUSE_ADMIN_PASSWORD: ClickHouseContainer.password,
})
.withNetworkMode(network.getName())
.withExposedPorts(8123, 9000)
.start();
t.onTestFinished(async () => {
await container.stop();
});
const dsn = `tcp://${ClickHouseContainer.username}:${ClickHouseContainer.password}@${container
.getName()
.replace(/^\//, "")}:9000`;

const migratorImage = await new GenericContainerBuilder(".", "Dockerfile").build();

const migrator = await migratorImage
.withEnvironment({
GOOSE_DBSTRING: dsn,
})
.withNetworkMode(network.getName())
.withDefaultLogDriver()
.withWaitStrategy(Wait.forLogMessage("successfully migrated database"))
.start();

t.onTestFinished(async () => {
await migrator.stop();
});

return new ClickHouseContainer(container);
}
}
59 changes: 59 additions & 0 deletions internal/clickhouse/src/verifications_billing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { expect, test } from "vitest";
import { ClickHouse } from "./index";

import { randomUUID } from "node:crypto";
import { ClickHouseContainer } from "./testutil";

test(
"returns the correct amount of billable verifications",
{
timeout: 300_000,
},
async (t) => {
const container = await ClickHouseContainer.start(t);

const ch = new ClickHouse({ url: container.url() });

const workspaceId = randomUUID();
const keySpaceId = randomUUID();
const keyId = randomUUID();
const now = new Date();
const year = now.getUTCFullYear();
const month = now.getUTCMonth() + 1; // 1 = January
const endTime = now.getTime();
const startTime = now.setUTCDate(1);

const outcomes = ["VALID", "RATE_LIMITED", "DISABLED"] as const;
let valid = 0;
for (let i = 0; i < 100; i++) {
console.log({ i });
const verifications = new Array(10_000).fill(null).map(() => {
const outcome = outcomes[Math.floor(Math.random() * outcomes.length)];
if (outcome === "VALID") {
valid++;
}
return {
workspace_id: workspaceId,
key_space_id: keySpaceId,
key_id: keyId,
outcome,
time: Math.floor(startTime + Math.random() * (endTime - startTime)),
region: "test",
request_id: randomUUID(),
};
});

await ch.verifications.insert(verifications);
}
// give clickhouse time to process all writes and update the materialized views
await new Promise((r) => setTimeout(r, 10_000));

const billableVerifications = await ch.billing.billableVerifications({
workspaceId,
year,
month,
});

expect(billableVerifications).toBe(valid);
},
);
Loading

0 comments on commit f0fffb9

Please sign in to comment.