Skip to content

Commit 115d993

Browse files
committed
Implement OpenMetrics API endpoint
1 parent 1f9cdca commit 115d993

File tree

9 files changed

+136
-27
lines changed

9 files changed

+136
-27
lines changed

.env-example

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,13 @@ LOGGER_LEVEL=3
5151
# "gcp" (Google Cloud), "nginx" (Nginx), "vercel" (Vercel),
5252
# "direct" (no proxy/development), "development" (dev with proxy headers)
5353
# Default: "direct"
54-
PROXY=direct
54+
PROXY=direct
55+
56+
# Enable or disable OpenMetrics endpoint
57+
# Default: false
58+
OPEN_METRICS_ENABLED=false
59+
60+
# Protect OpenMetrics API endpoint with Bearer authentication
61+
# Set to "none" to disable authentication (not recommended for production)
62+
# Default: none
63+
OPEN_METRICS_AUTH_TOKEN=none

README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,15 @@ LOGGER_LEVEL=3
8383
# "direct" (no proxy/development), "development" (dev with proxy headers)
8484
# Default: "direct"
8585
PROXY=direct
86+
87+
# Enable or disable OpenMetrics endpoint
88+
# Default: false
89+
OPEN_METRICS_ENABLED=false
90+
91+
# Protect OpenMetrics API endpoint with Bearer authentication
92+
# Set to "none" to disable authentication (not recommended for production)
93+
# Default: none
94+
OPEN_METRICS_AUTH_TOKEN=none
8695
```
8796

8897
### Running with Docker Compose
@@ -118,7 +127,7 @@ Health Check and Statistics
118127
```json
119128
{
120129
"program": "RabbitForexAPI",
121-
"version": "4.0.3",
130+
"version": "4.1.0",
122131
"sourceCode": "https://github.com/Rabbit-Company/RabbitForexAPI",
123132
"monitorStats": {
124133
"currencyCount": 162,
@@ -135,6 +144,28 @@ Health Check and Statistics
135144
}
136145
```
137146

147+
### GET `/metrics`
148+
149+
Returns metrics in OpenMetrics format for Prometheus monitoring.
150+
151+
**Authentication**: Protected with Bearer token (configure via `OPEN_METRICS_AUTH_TOKEN` environment variable)
152+
153+
**Response**: OpenMetrics text format
154+
155+
**Example**:
156+
157+
```text
158+
# TYPE rabbitforex_http_requests counter
159+
# HELP rabbitforex_http_requests Total HTTP requests
160+
rabbitforex_http_requests_total 0 1764012335.777
161+
rabbitforex_http_requests_total{endpoint="/metrics"} 52 1764012466.32
162+
rabbitforex_http_requests_total{endpoint="/"} 18 1764012414.1
163+
rabbitforex_http_requests_created 1764012335.777 1764012335.777
164+
rabbitforex_http_requests_created{endpoint="/metrics"} 1764012396.269 1764012466.32
165+
rabbitforex_http_requests_created{endpoint="/"} 1764012410.614 1764012414.1
166+
# EOF
167+
```
168+
138169
### GET `/openapi.json`
139170

140171
OpenAPI specification

bun.lock

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

docker-compose.override.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ services:
2424
- ENABLED_CRYPTOS
2525
- LOGGER_LEVEL
2626
- PROXY
27+
- OPEN_METRICS_ENABLED
28+
- OPEN_METRICS_AUTH_TOKEN
2729
healthcheck:
2830
test: ["CMD", "curl", "-f", "http://localhost:3000"]
2931
interval: 10s

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ services:
2323
- ENABLED_CRYPTOS
2424
- LOGGER_LEVEL
2525
- PROXY
26+
- OPEN_METRICS_ENABLED
27+
- OPEN_METRICS_AUTH_TOKEN
2628
healthcheck:
2729
test: ["CMD", "curl", "-f", "http://localhost:3000"]
2830
interval: 10s

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "rabbitforexapi",
3-
"version": "4.0.3",
3+
"version": "4.1.0",
44
"module": "src/index.ts",
55
"type": "module",
66
"private": true,
@@ -14,6 +14,7 @@
1414
"typescript": "^5"
1515
},
1616
"dependencies": {
17+
"@rabbit-company/openmetrics-client": "^2.0.1",
1718
"@rabbit-company/web": "^0.16.0",
1819
"@rabbit-company/web-middleware": "^0.16.0"
1920
}

src/index.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ import { logger } from "@rabbit-company/web-middleware/logger";
77
import { cors } from "@rabbit-company/web-middleware/cors";
88
import pkg from "../package.json";
99
import { openapi } from "./openapi";
10+
import { httpRequests, registry } from "./metrics";
11+
import { bearerAuth } from "@rabbit-company/web-middleware/bearer-auth";
1012

1113
const host = process.env.SERVER_HOST || "0.0.0.0";
1214
const port = parseInt(process.env.SERVER_PORT || "3000") || 3000;
1315
const proxy = Object.keys(IP_EXTRACTION_PRESETS).includes(process.env.PROXY || "direct") ? (process.env.PROXY as CloudProvider) : "direct";
1416
const updateInterval = parseInt(process.env.UPDATE_INTERVAL || "30") || 30;
17+
const openMetricsEnabled = process.env.OPEN_METRICS_ENABLED === "true";
1518

1619
const cacheControl = [
1720
"public",
@@ -50,6 +53,8 @@ app.use(
5053
);
5154

5255
app.get("/", (c) => {
56+
httpRequests.labels({ endpoint: "/" }).inc();
57+
5358
return c.json(
5459
{
5560
program: "RabbitForexAPI",
@@ -73,11 +78,37 @@ app.get("/", (c) => {
7378
);
7479
});
7580

81+
if (openMetricsEnabled) {
82+
app.get(
83+
"/metrics",
84+
bearerAuth({
85+
validate(token) {
86+
return token === process.env.OPEN_METRICS_AUTH_TOKEN;
87+
},
88+
skip() {
89+
return process.env.OPEN_METRICS_AUTH_TOKEN === "none";
90+
},
91+
}),
92+
(c) => {
93+
httpRequests.labels({ endpoint: "/metrics" }).inc();
94+
95+
return c.text(registry.metricsText(), 200, {
96+
"Content-Type": registry.contentType,
97+
"Cache-Control": "no-store",
98+
});
99+
}
100+
);
101+
}
102+
76103
app.get("/openapi.json", (c) => {
104+
httpRequests.labels({ endpoint: "/openapi.json" }).inc();
105+
77106
return c.json(openapi, 200, { "Cache-Control": "public, max-age=3600 s-maxage=3600 stale-while-revalidate=36000 stale-if-error=31536000" });
78107
});
79108

80109
app.get("/v1/assets", (c) => {
110+
httpRequests.labels({ endpoint: "/v1/assets" }).inc();
111+
81112
return c.json(
82113
{
83114
currencies: exchange.getSupportedCurrencies(),
@@ -97,6 +128,8 @@ app.get("/v1/assets", (c) => {
97128
});
98129

99130
app.get("/v1/rates", (c) => {
131+
httpRequests.labels({ endpoint: "/v1/rates" }).inc();
132+
100133
return c.json(
101134
{
102135
base: "USD",
@@ -112,6 +145,9 @@ app.get("/v1/rates", (c) => {
112145

113146
app.get("/v1/rates/:base", (c) => {
114147
const base = c.params["base"]!.toUpperCase();
148+
149+
httpRequests.labels({ endpoint: "/v1/rates/:base" }).inc();
150+
115151
return c.json(
116152
{
117153
base: base,
@@ -126,6 +162,8 @@ app.get("/v1/rates/:base", (c) => {
126162
});
127163

128164
app.get("/v1/metals/rates", (c) => {
165+
httpRequests.labels({ endpoint: "/v1/metals/rates" }).inc();
166+
129167
return c.json(
130168
{
131169
base: "USD",
@@ -143,6 +181,8 @@ app.get("/v1/metals/rates", (c) => {
143181
app.get("/v1/metals/rates/:base", (c) => {
144182
const base = c.params["base"]!.toUpperCase();
145183

184+
httpRequests.labels({ endpoint: "/v1/metals/rates/:base" }).inc();
185+
146186
return c.json(
147187
{
148188
base: base,
@@ -158,6 +198,8 @@ app.get("/v1/metals/rates/:base", (c) => {
158198
});
159199

160200
app.get("/v1/crypto/rates", (c) => {
201+
httpRequests.labels({ endpoint: "/v1/crypto/rates" }).inc();
202+
161203
return c.json(
162204
{
163205
base: "USD",
@@ -175,6 +217,8 @@ app.get("/v1/crypto/rates", (c) => {
175217
app.get("/v1/crypto/rates/:base", (c) => {
176218
const base = c.params["base"]!.toUpperCase();
177219

220+
httpRequests.labels({ endpoint: "/v1/crypto/rates/:base" }).inc();
221+
178222
return c.json(
179223
{
180224
base: base,
@@ -190,6 +234,8 @@ app.get("/v1/crypto/rates/:base", (c) => {
190234
});
191235

192236
app.get("/v1/stocks/rates", (c) => {
237+
httpRequests.labels({ endpoint: "/v1/stocks/rates" }).inc();
238+
193239
return c.json(
194240
{
195241
base: "USD",
@@ -207,6 +253,8 @@ app.get("/v1/stocks/rates", (c) => {
207253
app.get("/v1/stocks/rates/:base", (c) => {
208254
const base = c.params["base"]!.toUpperCase();
209255

256+
httpRequests.labels({ endpoint: "/v1/stocks/rates/:base" }).inc();
257+
210258
return c.json(
211259
{
212260
base: base,
@@ -231,6 +279,7 @@ Logger.info(`Server running on http://${host}:${port}`);
231279
Logger.info(`Exchange rates updates every ${updateInterval}s`);
232280
Logger.info("Available endpoints:");
233281
Logger.info(" GET / - Health check and stats");
282+
Logger.info(" GET /metrics - OpenMetrics format");
234283
Logger.info(" GET /openapi.json - OpenAPI specification");
235284
Logger.info(" GET /v1/assets - List all supported currencies, metals, stocks and cryptocurrencies");
236285
Logger.info(" GET /v1/rates - Exchange rates for USD (default)");

src/metrics.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Counter, Registry } from "@rabbit-company/openmetrics-client";
2+
3+
export const registry = new Registry({
4+
prefix: "rabbitforex",
5+
});
6+
7+
export const httpRequests = new Counter({
8+
name: "http_requests",
9+
help: "Total HTTP requests",
10+
labelNames: ["endpoint"],
11+
registry,
12+
});

tsconfig.json

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
11
{
2-
"compilerOptions": {
3-
// Environment setup & latest features
4-
"lib": ["ESNext"],
5-
"target": "ESNext",
6-
"module": "Preserve",
7-
"moduleDetection": "force",
8-
"jsx": "react-jsx",
9-
"allowJs": true,
2+
"compilerOptions": {
3+
// Environment setup & latest features
4+
"lib": ["ESNext", "DOM"],
5+
"target": "ESNext",
6+
"module": "Preserve",
7+
"moduleDetection": "force",
8+
"jsx": "react-jsx",
9+
"allowJs": true,
1010

11-
// Bundler mode
12-
"moduleResolution": "bundler",
13-
"allowImportingTsExtensions": true,
14-
"verbatimModuleSyntax": true,
15-
"noEmit": true,
11+
// Bundler mode
12+
"moduleResolution": "bundler",
13+
"allowImportingTsExtensions": true,
14+
"verbatimModuleSyntax": true,
15+
"noEmit": true,
1616

17-
// Best practices
18-
"strict": true,
19-
"skipLibCheck": true,
20-
"noFallthroughCasesInSwitch": true,
21-
"noUncheckedIndexedAccess": true,
22-
"noImplicitOverride": true,
17+
// Best practices
18+
"strict": true,
19+
"skipLibCheck": true,
20+
"noFallthroughCasesInSwitch": true,
21+
"noUncheckedIndexedAccess": true,
22+
"noImplicitOverride": true,
2323

24-
// Some stricter flags (disabled by default)
25-
"noUnusedLocals": false,
26-
"noUnusedParameters": false,
27-
"noPropertyAccessFromIndexSignature": false
28-
}
24+
// Some stricter flags (disabled by default)
25+
"noUnusedLocals": false,
26+
"noUnusedParameters": false,
27+
"noPropertyAccessFromIndexSignature": false
28+
}
2929
}

0 commit comments

Comments
 (0)