Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 168f632

Browse files
committedJan 19, 2025··
fix: metrics
1 parent da25ef4 commit 168f632

File tree

5 files changed

+776
-65
lines changed

5 files changed

+776
-65
lines changed
 
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
CREATE TABLE `client_label` (
2+
`deployment_id` text NOT NULL,
3+
`client_id` text NOT NULL,
4+
`label` text NOT NULL,
5+
PRIMARY KEY(`client_id`, `deployment_id`)
6+
);
7+
--> statement-breakpoint
8+
CREATE TABLE `metric` (
9+
`deployment_id` text NOT NULL,
10+
`label` text NOT NULL,
11+
`type` text NOT NULL,
12+
`count` integer NOT NULL,
13+
PRIMARY KEY(`deployment_id`, `label`, `type`)
14+
);

‎apps/server/drizzle/migrations/meta/0001_snapshot.json

Lines changed: 627 additions & 0 deletions
Large diffs are not rendered by default.

‎apps/server/drizzle/migrations/meta/_journal.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88
"when": 1737180385012,
99
"tag": "0000_secret_thanos",
1010
"breakpoints": true
11+
},
12+
{
13+
"idx": 1,
14+
"version": "6",
15+
"when": 1737268122892,
16+
"tag": "0001_cuddly_ultimates",
17+
"breakpoints": true
1118
}
1219
]
1320
}

‎apps/server/src/db/schema.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,38 @@ export const accessKeyRelations = relations(accessKey, ({ one }) => ({
165165
references: [account.id],
166166
}),
167167
}));
168+
169+
export const metrics = sqliteTable(
170+
"metric",
171+
{
172+
deploymentId: text("deployment_id").notNull(),
173+
label: text("label").notNull(),
174+
type: text("type", {
175+
enum: [
176+
"active",
177+
"downloaded",
178+
"deployment_succeeded",
179+
"deployment_failed",
180+
],
181+
}).notNull(),
182+
count: integer("count").notNull(),
183+
},
184+
(t) => [
185+
primaryKey({ columns: [t.deploymentId, t.label, t.type] }),
186+
// unique().on(t.deploymentId, t.label, t.type),
187+
],
188+
);
189+
190+
// Client labels table (for active users)
191+
export const clientLabels = sqliteTable(
192+
"client_label",
193+
{
194+
deploymentId: text("deployment_id").notNull(),
195+
clientId: text("client_id").notNull(),
196+
label: text("label").notNull(),
197+
},
198+
(t) => [primaryKey({ columns: [t.clientId, t.deploymentId] })],
199+
// (t) => ({
200+
// pk: primaryKey({ columns: [t.clientId, t.deploymentId] }),
201+
// }),
202+
);

‎apps/server/src/utils/metrics.ts

Lines changed: 93 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import { and, eq, sql } from "drizzle-orm";
2+
import { type DrizzleD1Database, drizzle } from "drizzle-orm/d1";
13
import type { Context } from "hono";
2-
import type { Env } from "../types/env";
4+
import * as schema from "../db/schema";
35
import { getStorageProvider } from "../storage/factory";
46
import type { StorageProvider } from "../storage/storage";
7+
import type { Env } from "../types/env";
58

69
const METRICS_PREFIX = "metrics:" as const;
710

@@ -26,22 +29,12 @@ export interface DeploymentMetrics {
2629
}
2730

2831
export class MetricsManager {
29-
private readonly ctx: Context<Env>;
30-
private readonly kv: KVNamespace;
3132
private readonly storage: StorageProvider;
33+
private readonly db: DrizzleD1Database<typeof schema>;
3234

33-
constructor(ctx: Context<Env>) {
34-
this.ctx = ctx;
35-
this.kv = ctx.env.CODE_PUSH_KV;
35+
constructor(private readonly ctx: Context<Env>) {
3636
this.storage = getStorageProvider(ctx);
37-
}
38-
39-
private async increment(key: string): Promise<void> {
40-
const currentValue = await this.kv.get(key);
41-
const newValue = (
42-
(Number.parseInt(currentValue ?? "0", 10) || 0) + 1
43-
).toString();
44-
await this.kv.put(key, newValue);
37+
this.db = drizzle(ctx.env.DB, { schema });
4538
}
4639

4740
private getMetricKey(
@@ -56,6 +49,31 @@ export class MetricsManager {
5649
return `${METRICS_PREFIX}client:${deploymentKey}:${clientId}`;
5750
}
5851

52+
private async increment(
53+
deploymentKey: string,
54+
label: string,
55+
type: MetricType,
56+
): Promise<void> {
57+
await this.db
58+
.insert(schema.metrics)
59+
.values({
60+
deploymentId: deploymentKey,
61+
label,
62+
type,
63+
count: 1,
64+
})
65+
.onConflictDoUpdate({
66+
target: [
67+
schema.metrics.deploymentId,
68+
schema.metrics.label,
69+
schema.metrics.type,
70+
],
71+
set: {
72+
count: sql`${schema.metrics.count} + 1`,
73+
},
74+
});
75+
}
76+
5977
async recordDeploymentStatus(
6078
deploymentKey: string,
6179
label: string,
@@ -67,20 +85,33 @@ export class MetricsManager {
6785
? MetricType.DEPLOYMENT_SUCCEEDED
6886
: MetricType.DEPLOYMENT_FAILED;
6987

70-
await this.increment(this.getMetricKey(deploymentKey, label, type));
88+
await this.increment(deploymentKey, label, type);
7189

7290
if (status === "DeploymentSucceeded") {
7391
const clientKey = this.getClientKey(deploymentKey, clientId);
74-
await this.kv.put(clientKey, label);
75-
await this.increment(
76-
this.getMetricKey(deploymentKey, label, MetricType.ACTIVE),
77-
);
92+
await this.db
93+
.insert(schema.clientLabels)
94+
.values({
95+
deploymentId: deploymentKey,
96+
clientId,
97+
label,
98+
})
99+
.onConflictDoUpdate({
100+
target: [
101+
schema.clientLabels.clientId,
102+
schema.clientLabels.deploymentId,
103+
],
104+
set: {
105+
label,
106+
},
107+
});
108+
await this.increment(deploymentKey, label, MetricType.ACTIVE);
78109
}
79110
}
80111

81112
async recordDeployment(
82113
deploymentKey: string,
83-
appVersion: string,
114+
label: string,
84115
clientId: string,
85116
previousDeploymentKey?: string,
86117
previousLabel?: string,
@@ -91,87 +122,84 @@ export class MetricsManager {
91122
previousLabel,
92123
MetricType.ACTIVE,
93124
);
94-
const currentActive = Number.parseInt(
95-
(await this.kv.get(prevActiveKey)) ?? "0",
96-
10,
97-
);
98-
if (currentActive > 0) {
99-
await this.kv.put(prevActiveKey, (currentActive - 1).toString());
100-
}
125+
126+
await this.db.run(sql`
127+
UPDATE metrics
128+
SET count = count - 1
129+
WHERE deploymentId = ${previousDeploymentKey} AND label = ${previousLabel} AND type = ${MetricType.ACTIVE} AND count > 0
130+
`);
101131
}
102132

103133
const clientKey = this.getClientKey(deploymentKey, clientId);
104-
await this.kv.put(clientKey, appVersion);
105-
await this.increment(
106-
this.getMetricKey(deploymentKey, appVersion, MetricType.ACTIVE),
107-
);
134+
await this.db
135+
.insert(schema.clientLabels)
136+
.values({
137+
deploymentId: deploymentKey,
138+
clientId,
139+
label,
140+
})
141+
.onConflictDoUpdate({
142+
target: [
143+
schema.clientLabels.clientId,
144+
schema.clientLabels.deploymentId,
145+
],
146+
set: {
147+
label,
148+
},
149+
});
150+
await this.increment(deploymentKey, label, MetricType.ACTIVE);
108151
}
109152

110153
async recordDownload(
111154
deploymentKey: string,
112155
label: string,
113156
clientId: string,
114157
): Promise<void> {
115-
await this.increment(
116-
this.getMetricKey(deploymentKey, label, MetricType.DOWNLOADED),
117-
);
158+
await this.increment(deploymentKey, label, MetricType.DOWNLOADED);
118159
}
119160

120161
async getMetrics(deploymentKey: string): Promise<DeploymentMetrics> {
121-
const list = await this.kv.list({
122-
prefix: `${METRICS_PREFIX}${deploymentKey}:`,
162+
const results = await this.db.query.metrics.findMany({
163+
where: eq(schema.metrics.deploymentId, deploymentKey),
123164
});
165+
124166
const metrics: DeploymentMetrics = {};
125167

126-
for (const { name } of list.keys) {
127-
const [, , label, type] = name.split(":");
128-
if (!metrics[label]) {
129-
metrics[label] = {
168+
for (const result of results) {
169+
if (!metrics[result.label]) {
170+
metrics[result.label] = {
130171
active: 0,
131172
downloads: 0,
132173
installed: 0,
133174
failed: 0,
134175
};
135176
}
136177

137-
const value = Number.parseInt((await this.kv.get(name)) ?? "0", 10);
138-
139-
switch (type as MetricType) {
178+
switch (result.type as MetricType) {
140179
case MetricType.ACTIVE:
141-
metrics[label].active = value;
180+
metrics[result.label].active = result.count;
142181
break;
143182
case MetricType.DOWNLOADED:
144-
metrics[label].downloads = value;
183+
metrics[result.label].downloads = result.count;
145184
break;
146185
case MetricType.DEPLOYMENT_SUCCEEDED:
147-
metrics[label].installed = value;
186+
metrics[result.label].installed = result.count;
148187
break;
149188
case MetricType.DEPLOYMENT_FAILED:
150-
metrics[label].failed = value;
189+
metrics[result.label].failed = result.count;
151190
break;
152191
}
153192
}
154193

155194
return metrics;
156195
}
157196

158-
async clearMetrics(deploymentKey: string): Promise<void> {
159-
const list = await this.kv.list({
160-
prefix: `${METRICS_PREFIX}${deploymentKey}:`,
161-
});
162-
163-
// Delete in batches of 100
164-
const keys = list.keys.map((k) => k.name);
165-
for (let i = 0; i < keys.length; i += 100) {
166-
const batch = keys.slice(i, i + 100);
167-
await Promise.all(batch.map((key) => this.kv.delete(key)));
168-
}
169-
}
170-
171-
async getActiveDevices(deploymentKey: string): Promise<number> {
172-
const list = await this.kv.list({
173-
prefix: `${METRICS_PREFIX}client:${deploymentKey}:`,
174-
});
175-
return list.keys.length;
197+
async clearDeploymentMetrics(deploymentKey: string): Promise<void> {
198+
await this.db
199+
.delete(schema.metrics)
200+
.where(eq(schema.metrics.deploymentId, deploymentKey));
201+
await this.db
202+
.delete(schema.clientLabels)
203+
.where(eq(schema.clientLabels.deploymentId, deploymentKey));
176204
}
177205
}

0 commit comments

Comments
 (0)
Please sign in to comment.