Skip to content

Commit b82da70

Browse files
Merge pull request #14 from nextinterfaces/adds-prometheus
Adds Prometheus to items-service
2 parents e9124ac + f7fb98f commit b82da70

File tree

8 files changed

+215
-1
lines changed

8 files changed

+215
-1
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,23 @@ task local:start
2626
🔧 Items Service: http://localhost:8081
2727
- Health: http://localhost:8081/v1/health
2828
- API Docs: http://localhost:8081/docs
29+
- Metrics: http://localhost:8081/metrics
2930
🌐 Website App: http://localhost:8082
3031
- Health: http://localhost:8082/health
3132
📊 Jaeger UI: http://localhost:16686
3233
📦 PostgreSQL: localhost:5432
3334
- Connection: psql postgresql://{.env.POSTGRES_USER}:{.env.POSTGRES_PASSWORD}@localhost:5432/
3435
```
3536

37+
## Production URLs
38+
39+
- **Website**: https://roussev.com
40+
- **Items API**: https://app.roussev.com/items
41+
- Health: https://app.roussev.com/items/v1/health
42+
- Docs: https://app.roussev.com/items/docs
43+
- Metrics: https://app.roussev.com/items/metrics
44+
- **Jaeger Tracing**: https://app.roussev.com/jaeger
45+
3646
## Next Steps
3747

3848
- Set up monitoring with Prometheus/Grafana

Tiltfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ k8s_resource(
9191
links=[
9292
link('http://localhost:8081/v1/health', 'Health Check'),
9393
link('http://localhost:8081/docs', 'API Docs'),
94+
link('http://localhost:8081/metrics', 'Prometheus Metrics'),
9495
]
9596
)
9697

apps/items-service/README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ A simple REST API service for managing items, built with Bun and PostgreSQL.
99
- **Health check** endpoint with database connectivity status
1010
- **OpenAPI/Swagger** documentation
1111
- **OpenTelemetry** distributed tracing support
12+
- **Prometheus** metrics collection and export
1213
- **Structured Logging** with Pino for high-performance logging
1314
- **Hot reload** in development mode
1415
- **Docker** support with development and production configurations
@@ -21,6 +22,7 @@ A simple REST API service for managing items, built with Bun and PostgreSQL.
2122
- `POST /v1/items` - Create a new item
2223
- `GET /docs` - Swagger UI for interactive API documentation
2324
- `GET /v1/openapi.json` - OpenAPI specification
25+
- `GET /metrics` - Prometheus metrics endpoint
2426

2527
## Local Development
2628

@@ -34,6 +36,7 @@ Access the service at:
3436
- Service: http://localhost:8081
3537
- Health Check: http://localhost:8081/v1/health
3638
- API Docs: http://localhost:8081/docs
39+
- Metrics: http://localhost:8081/metrics
3740

3841

3942
#### PostgreSQL Database (Port 5432)
@@ -61,6 +64,9 @@ curl -X POST http://localhost:8081/v1/items \
6164
-H "Content-Type: application/json" \
6265
-d '{"name":"test-item"}'
6366

67+
# View Prometheus metrics
68+
curl http://localhost:8081/metrics
69+
6470
# Connect to PostgreSQL
6571
psql postgresql://{.env.POSTGRES_USER}:{.env.POSTGRES_PASSWORD}@localhost:5432/{.env.POSTGRES_DB}
6672

@@ -73,7 +79,9 @@ open http://localhost:16686
7379
- **Runtime**: [Bun](https://bun.sh/)
7480
- **Database**: PostgreSQL
7581
- **Logging**: [Pino](https://getpino.io/) - structured logging
76-
- **Observability**: OpenTelemetry - distributed tracing
82+
- **Observability**:
83+
- OpenTelemetry - distributed tracing
84+
- Prometheus - metrics collection and export
7785
- **Container**: Docker with Alpine Linux
7886
- **Orchestration**: Kubernetes (k3s)
7987
- **Local Development**: Task and Tilt with k3d

apps/items-service/bun.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"dependencies": {
77
"@opentelemetry/api": "^1.9.0",
88
"@opentelemetry/auto-instrumentations-node": "^0.66.0",
9+
"@opentelemetry/exporter-prometheus": "^0.207.0",
910
"@opentelemetry/exporter-trace-otlp-http": "^0.207.0",
1011
"@opentelemetry/resources": "^2.2.0",
1112
"@opentelemetry/sdk-node": "^0.207.0",

apps/items-service/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"dependencies": {
1111
"@opentelemetry/api": "^1.9.0",
1212
"@opentelemetry/auto-instrumentations-node": "^0.66.0",
13+
"@opentelemetry/exporter-prometheus": "^0.207.0",
1314
"@opentelemetry/exporter-trace-otlp-http": "^0.207.0",
1415
"@opentelemetry/resources": "^2.2.0",
1516
"@opentelemetry/sdk-node": "^0.207.0",

apps/items-service/src/metrics.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* Prometheus Metrics Module
3+
* Provides Prometheus metrics collection using OpenTelemetry
4+
*
5+
* Features:
6+
* - Prometheus metrics exporter
7+
* - HTTP metrics endpoint at /metrics
8+
* - Integration with OpenTelemetry SDK
9+
* - Custom metrics support
10+
*/
11+
12+
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
13+
import { MeterProvider } from "@opentelemetry/sdk-metrics";
14+
import { resourceFromAttributes } from "@opentelemetry/resources";
15+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
16+
import type { Histogram, Counter, Meter } from "@opentelemetry/api";
17+
18+
const PROMETHEUS_PORT = parseInt(process.env.PROMETHEUS_PORT || "9464", 10);
19+
const OTEL_SERVICE_NAME = process.env.OTEL_SERVICE_NAME || "items-service";
20+
const OTEL_SERVICE_VERSION = process.env.OTEL_SERVICE_VERSION || "1.0.0";
21+
22+
let prometheusExporter: PrometheusExporter | null = null;
23+
let meterProvider: MeterProvider | null = null;
24+
let meter: Meter | null = null;
25+
26+
// HTTP metrics
27+
let httpRequestDuration: Histogram | null = null;
28+
let httpRequestCount: Counter | null = null;
29+
30+
/**
31+
* Initialize Prometheus metrics exporter
32+
* This sets up the Prometheus exporter and meter provider
33+
*/
34+
export function initPrometheusMetrics() {
35+
try {
36+
// Configure resource with service information
37+
const resource = resourceFromAttributes({
38+
[ATTR_SERVICE_NAME]: OTEL_SERVICE_NAME,
39+
[ATTR_SERVICE_VERSION]: OTEL_SERVICE_VERSION,
40+
});
41+
42+
// Create Prometheus exporter
43+
// Note: The exporter will NOT start its own HTTP server
44+
// We'll expose metrics through our existing HTTP server
45+
prometheusExporter = new PrometheusExporter({
46+
// Don't start a separate HTTP server
47+
preventServerStart: true,
48+
});
49+
50+
// Create meter provider with Prometheus exporter
51+
meterProvider = new MeterProvider({
52+
resource,
53+
readers: [prometheusExporter],
54+
});
55+
56+
// Create meter for recording metrics
57+
meter = meterProvider.getMeter(OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION);
58+
59+
// Create HTTP metrics instruments
60+
httpRequestDuration = meter.createHistogram("http_server_duration", {
61+
description: "HTTP request duration in milliseconds",
62+
unit: "ms",
63+
});
64+
65+
httpRequestCount = meter.createCounter("http_server_requests_total", {
66+
description: "Total number of HTTP requests",
67+
unit: "1",
68+
});
69+
70+
console.log("Prometheus metrics initialized");
71+
console.log(`Service: ${OTEL_SERVICE_NAME} v${OTEL_SERVICE_VERSION}`);
72+
console.log("Metrics will be available at /metrics endpoint");
73+
74+
return prometheusExporter;
75+
} catch (error) {
76+
console.error("Failed to initialize Prometheus metrics:", error);
77+
return null;
78+
}
79+
}
80+
81+
/**
82+
* Get the Prometheus exporter instance
83+
*/
84+
export function getPrometheusExporter(): PrometheusExporter | null {
85+
return prometheusExporter;
86+
}
87+
88+
/**
89+
* Get the meter provider instance
90+
*/
91+
export function getMeterProvider(): MeterProvider | null {
92+
return meterProvider;
93+
}
94+
95+
/**
96+
* Record HTTP request metrics
97+
*/
98+
export function recordHttpRequest(
99+
method: string,
100+
path: string,
101+
statusCode: number,
102+
durationMs: number
103+
) {
104+
if (!httpRequestDuration || !httpRequestCount) {
105+
return;
106+
}
107+
108+
const attributes = {
109+
method,
110+
route: path,
111+
status_code: statusCode.toString(),
112+
};
113+
114+
httpRequestDuration.record(durationMs, attributes);
115+
httpRequestCount.add(1, attributes);
116+
}
117+
118+
/**
119+
* Shutdown Prometheus metrics
120+
*/
121+
export async function shutdownPrometheusMetrics() {
122+
if (meterProvider) {
123+
try {
124+
await meterProvider.shutdown();
125+
console.log("Prometheus metrics shut down successfully");
126+
} catch (error) {
127+
console.error("Error shutting down Prometheus metrics:", error);
128+
}
129+
}
130+
}
131+

apps/items-service/src/router.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { openapi } from "./openapi.js";
1010
import { json, notFound, html } from "./http-utils.js";
1111
import type { ServerConfig } from "./config.js";
1212
import { logRequest, logResponse, logError } from "./logger.js";
13+
import { getPrometheusExporter, recordHttpRequest } from "./metrics.js";
1314

1415
type RouteHandler = (req: Request) => Promise<Response> | Response;
1516

@@ -59,6 +60,13 @@ export class Router {
5960
handler: () => this.healthController.check(),
6061
});
6162

63+
// Prometheus metrics endpoint
64+
this.routes.push({
65+
method: "GET",
66+
path: "/metrics",
67+
handler: () => this.handleMetrics(),
68+
});
69+
6270
// List items
6371
this.routes.push({
6472
method: "GET",
@@ -78,6 +86,47 @@ export class Router {
7886
return this.routes.find((route) => route.method === method && route.path === path);
7987
}
8088

89+
/**
90+
* Handle Prometheus metrics endpoint
91+
* Returns metrics in Prometheus text format
92+
*/
93+
private async handleMetrics(): Promise<Response> {
94+
const exporter = getPrometheusExporter();
95+
96+
if (!exporter) {
97+
return new Response("Prometheus metrics not initialized", {
98+
status: 503,
99+
headers: { "Content-Type": "text/plain" }
100+
});
101+
}
102+
103+
try {
104+
// The PrometheusExporter has a getMetricsRequestHandler method
105+
// that returns a promise with the metrics in Prometheus text format
106+
return new Promise((resolve) => {
107+
const mockReq = {} as any;
108+
const mockRes = {
109+
statusCode: 200,
110+
setHeader: () => {},
111+
end: (data: string) => {
112+
resolve(new Response(data, {
113+
status: 200,
114+
headers: { "Content-Type": "text/plain; version=0.0.4; charset=utf-8" }
115+
}));
116+
}
117+
} as any;
118+
119+
exporter.getMetricsRequestHandler(mockReq, mockRes);
120+
});
121+
} catch (error) {
122+
console.error("Error generating metrics:", error);
123+
return new Response("Error generating metrics", {
124+
status: 500,
125+
headers: { "Content-Type": "text/plain" }
126+
});
127+
}
128+
}
129+
81130
async handle(req: Request): Promise<Response> {
82131
const tracer = trace.getTracer("items-service");
83132
const url = new URL(req.url);
@@ -113,6 +162,11 @@ export class Router {
113162

114163
logResponse(req, response, duration);
115164

165+
// Record metrics
166+
if (path !== "/metrics") {
167+
recordHttpRequest(req.method, path, response.status, duration);
168+
}
169+
116170
return response;
117171
} catch (error) {
118172
logError(error, "Request handler error");

apps/items-service/src/telemetry.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
55
import { resourceFromAttributes } from "@opentelemetry/resources";
66
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
77
import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
8+
import { initPrometheusMetrics, shutdownPrometheusMetrics } from "./metrics.js";
89

910
// Configuration from environment variables
1011
const OTEL_ENABLED = process.env.OTEL_ENABLED !== "false"; // Default to true
@@ -67,6 +68,9 @@ export function initTelemetry() {
6768
console.log(`Service: ${OTEL_SERVICE_NAME} v${OTEL_SERVICE_VERSION}`);
6869
console.log(`Endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT}`);
6970

71+
// Initialize Prometheus metrics
72+
initPrometheusMetrics();
73+
7074
// Handle graceful shutdown
7175
process.on("SIGTERM", async () => {
7276
await shutdownTelemetry();
@@ -83,6 +87,10 @@ export function initTelemetry() {
8387
}
8488

8589
export async function shutdownTelemetry() {
90+
// Shutdown Prometheus metrics
91+
await shutdownPrometheusMetrics();
92+
93+
// Shutdown OpenTelemetry SDK
8694
if (sdk) {
8795
try {
8896
await sdk.shutdown();

0 commit comments

Comments
 (0)