Skip to content

Commit 4d0990f

Browse files
authored
Merge pull request #1 from tshmieldev/prom
Prom
2 parents 0bece9b + 5def88a commit 4d0990f

19 files changed

+879
-394
lines changed

.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
API_KEY=1234
1+
API_KEY=1234
2+
METRICS_API_KEY=5678

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ Prerequisites:
66
- docker -> see `https://docs.docker.com/engine/install/`
77
- docker image - python:3.10-slim -> `docker pull python:3.10-slim`
88
- make sure the user running the code has docker permissions -> `sudo usermod -aG docker $USER`, then reload terminal or `newgrp docker`
9+
- to consume metrics, set METRICS_API_KEY in `.env` and `prometheus.yml`, then you can:
10+
```bash
11+
docker run -p 9090:9090 \
12+
-v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml \
13+
prom/prometheus
14+
```
15+
and open localhost:9090
916

1017
To install dependencies:
1118
```sh

bun.lock

Lines changed: 256 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bunfig.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
coveragePathIgnorePatterns = [
44
"src/lib/docker.ts",
55
"src/lib/files.ts"
6-
]
6+
]

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"start": "bun run src/index.ts"
66
},
77
"dependencies": {
8+
"@hono/prometheus": "^1.0.2",
89
"@hono/swagger-ui": "^0.5.2",
910
"@hono/zod-openapi": "^1.1.4",
1011
"@scalar/hono-api-reference": "^0.9.22",
@@ -13,6 +14,9 @@
1314
"zod": "^4.0.14"
1415
},
1516
"devDependencies": {
16-
"@types/bun": "^1.3.1"
17+
"@types/bun": "^1.3.1",
18+
"@typescript-eslint/eslint-plugin": "^8.46.2",
19+
"@typescript-eslint/parser": "^8.46.2",
20+
"eslint": "^9.38.0"
1721
}
1822
}

prometheus.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
scrape_configs:
2+
- job_name: "runalyzer"
3+
static_configs:
4+
- targets: ["host.docker.internal:3001"]
5+
metrics_path: /metrics
6+
bearer_token: "5678"
7+
scrape_interval: 1s

pyproject.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[tool.pyright]
2+
# Global settings
3+
reportIncompatibleMethodOverride = false
4+
5+
# Main configuration for src/lib/python
6+
include = ["src/lib/python/**/*.py"]
7+
reportOptionalMemberAccess = false
8+
reportMissingImports = false
9+
reportAny = false
10+
11+
[[tool.pyright.executionEnvironments]]
12+
# Here, pretty much all rules must be disabled
13+
# This code will be executed in a container
14+
# And so, the linter does not know the context, and is unhappy about it.
15+
root = "tests/data"
16+
reportIncompatibleMethodOverride = false
17+
reportMissingImports = false
18+
reportAny = false
19+
reportMissingTypeArgument = false

src/controllers/metrics.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {
2+
OpenAPIHono,
3+
type RouteConfigToTypedResponse,
4+
} from "@hono/zod-openapi";
5+
import { metricsRoute } from "../lib/openapi";
6+
import { printMetrics } from "../lib/prometheus";
7+
8+
const app = new OpenAPIHono();
9+
10+
// Path is overriden, as I prefer to have the
11+
// full path in lib/openapi.ts to keep the
12+
// semantics there and in index.ts
13+
app.openapi(
14+
{ ...metricsRoute, path: "/" },
15+
async (c): Promise<RouteConfigToTypedResponse<typeof metricsRoute>> => {
16+
if (
17+
c.req.header("authorization") !==
18+
`Bearer ${process.env.METRICS_API_KEY}`
19+
) {
20+
return c.json(
21+
{
22+
success: false,
23+
error: "Unauthorized",
24+
},
25+
401,
26+
);
27+
}
28+
return printMetrics(c) as unknown as RouteConfigToTypedResponse<
29+
typeof metricsRoute
30+
>;
31+
},
32+
);
33+
34+
export default app;

src/controllers/status.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
1-
import { OpenAPIHono, type RouteConfigToTypedResponse } from "@hono/zod-openapi";
1+
import {
2+
OpenAPIHono,
3+
type RouteConfigToTypedResponse,
4+
} from "@hono/zod-openapi";
25
import { statusRoute } from "../lib/openapi";
36
import { getActiveContainers } from "../lib/docker";
47

58
const app = new OpenAPIHono();
69

10+
// Path is overriden, as I prefer to have the
11+
// full path in lib/openapi.ts to keep the
12+
// semantics there and in index.ts
713
app.openapi(
814
{ ...statusRoute, path: "/" },
915
async (c): Promise<RouteConfigToTypedResponse<typeof statusRoute>> => {
1016
return c.json({ runningContainers: getActiveContainers().size }, 200);
1117
},
1218
);
1319

14-
export default app;
20+
export default app;

src/controllers/unit-tests.ts

Lines changed: 86 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,114 @@
11
import {
2-
OpenAPIHono,
3-
type RouteConfigToTypedResponse,
2+
OpenAPIHono,
3+
type RouteConfigToTypedResponse,
44
} from "@hono/zod-openapi";
55
import { runUnitTestRequestSchema } from "../lib/validation";
66
import { v4 } from "uuid";
77
import {
8-
cleanupSessionDirectory,
9-
createSessionDirectory,
10-
prepareUnitTestFiles,
8+
cleanupSessionDirectory,
9+
createSessionDirectory,
10+
prepareUnitTestFiles,
1111
} from "../lib/files";
1212
import {
13-
buildDockerCommand,
14-
getContainerName,
15-
trackContainer,
16-
untrackContainer,
13+
buildDockerCommand,
14+
getContainerName,
15+
trackContainer,
16+
untrackContainer,
1717
} from "../lib/docker";
1818
import { unitTestRoute } from "../lib/openapi";
1919

2020
const app = new OpenAPIHono();
2121

22+
// Path is overriden, as I prefer to have the
23+
// full path in lib/openapi.ts to keep the
24+
// semantics there and in index.ts
2225
app.openapi(
23-
{ ...unitTestRoute, path: "/" },
24-
async (c): Promise<RouteConfigToTypedResponse<typeof unitTestRoute>> => {
25-
let body = null;
26+
{ ...unitTestRoute, path: "/" },
27+
async (c): Promise<RouteConfigToTypedResponse<typeof unitTestRoute>> => {
28+
let body = null;
2629

27-
try {
28-
body = await c.req.json();
29-
} catch {
30-
return c.json({ success: false, error: "Invalid JSON" }, 400);
31-
}
30+
try {
31+
body = await c.req.json();
32+
} catch {
33+
return c.json({ success: false, error: "Invalid JSON" }, 400);
34+
}
3235

33-
const validatedData = runUnitTestRequestSchema.safeParse(body);
36+
const validatedData = runUnitTestRequestSchema.safeParse(body);
3437

35-
if (!validatedData.success) {
36-
return c.json(
37-
{ success: false, error: validatedData.error.message },
38-
400,
39-
);
40-
}
38+
if (!validatedData.success) {
39+
return c.json(
40+
{ success: false, error: validatedData.error.message },
41+
400,
42+
);
43+
}
4144

42-
const { user_code, unit_tests, api_key, config } = validatedData.data;
45+
const { user_code, unit_tests, api_key, config } = validatedData.data;
4346

44-
if (api_key !== Bun.env.API_KEY) {
45-
return c.json({ success: false, error: "Invalid API key" }, 401);
46-
}
47+
if (api_key !== Bun.env.API_KEY) {
48+
return c.json({ success: false, error: "Invalid API key" }, 401);
49+
}
4750

48-
// Create unique session
49-
const sessionId = v4();
50-
const sessionPath = await createSessionDirectory(sessionId);
51-
const containerName = getContainerName(sessionId);
51+
// Create unique session
52+
const sessionId = v4();
53+
const sessionPath = await createSessionDirectory(sessionId);
54+
const containerName = getContainerName(sessionId);
5255

53-
try {
54-
// Setup files and container
55-
await prepareUnitTestFiles(sessionPath, user_code, unit_tests);
56-
trackContainer(containerName);
56+
try {
57+
// Setup files and container
58+
await prepareUnitTestFiles(sessionPath, user_code, unit_tests);
59+
trackContainer(containerName);
5760

58-
// Execute in Docker
59-
const command = buildDockerCommand(
60-
sessionId,
61-
sessionPath,
62-
"unit-test",
63-
config,
64-
);
65-
const proc = Bun.spawn(command, {
66-
stdout: "pipe",
67-
stderr: "pipe",
68-
});
61+
// Execute in Docker
62+
const command = buildDockerCommand(sessionId, sessionPath, config);
63+
const proc = Bun.spawn(command, {
64+
stdout: "pipe",
65+
stderr: "pipe",
66+
});
6967

70-
const stdout = await new Response(proc.stdout).text();
71-
const stderr = await new Response(proc.stderr).text();
68+
const stdout = await new Response(proc.stdout).text();
69+
const stderr = await new Response(proc.stderr).text();
7270

73-
// Wait for process to complete and get exit code
74-
const exitCode = await proc.exited;
71+
// Wait for process to complete and get exit code
72+
const exitCode = await proc.exited;
7573

76-
const result = stdout ? JSON.parse(stdout) : undefined;
74+
const result = stdout ? JSON.parse(stdout) : undefined;
7775

78-
return c.json(
79-
{
80-
success: exitCode == 0,
81-
runalyzer_output: result,
82-
runalyzer_errors:
83-
stderr || (exitCode === 124 ? "Time limit exceeded" : ""),
84-
exit_code: exitCode,
85-
},
86-
200,
87-
);
88-
} catch (error) {
89-
console.error(error);
90-
return c.json({ success: false, error: "Internal server error" }, 500);
91-
} finally {
92-
untrackContainer(containerName);
93-
await cleanupSessionDirectory(sessionPath, sessionId);
94-
}
95-
},
76+
// Yes, this could be one return
77+
// But typescript wants it to be split
78+
// Because of the discriminatory union
79+
if (exitCode === 0) {
80+
return c.json(
81+
{
82+
success: true,
83+
runalyzer_output: result,
84+
runalyzer_errors: stderr,
85+
exit_code: exitCode,
86+
},
87+
200,
88+
);
89+
}
90+
return c.json(
91+
{
92+
success: false,
93+
runalyzer_output: result,
94+
runalyzer_errors:
95+
stderr ||
96+
(exitCode === 124 ? "Time limit exceeded" : ""),
97+
exit_code: exitCode,
98+
},
99+
200,
100+
);
101+
} catch (error) {
102+
console.error(error);
103+
return c.json(
104+
{ success: false, error: "Internal server error" },
105+
500,
106+
);
107+
} finally {
108+
untrackContainer(containerName);
109+
await cleanupSessionDirectory(sessionPath, sessionId);
110+
}
111+
},
96112
);
97113

98114
export default app;

0 commit comments

Comments
 (0)