Skip to content

Commit 80136c6

Browse files
Merge pull request #7 from nextinterfaces/otel
Adds Open Telemtry instrumentation and wires it to Jaeger
2 parents 85e747a + 6409d19 commit 80136c6

File tree

12 files changed

+780
-73
lines changed

12 files changed

+780
-73
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ POSTGRES_USER=your_postgres_user
66
POSTGRES_PASSWORD=your_secure_password_here
77
POSTGRES_DB=your_database_name
88

9+
# Jaeger Basic Auth Credentials
10+
# Generate with: htpasswd -nb admin yourpassword
11+
JAEGER_AUTH_HTPASSWD='admin:$apr1$xyz123$abc456def789...'

Taskfile.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,31 @@ tasks:
226226
cmds:
227227
- kubectl --kubeconfig={{.KUBECONFIG}} apply -f {{.K8S_DIR}}/cert-manager/cluster-issuer.yaml
228228

229+
deploy:jaeger:
230+
desc: Deploy Jaeger for distributed tracing
231+
dotenv: ['.env']
232+
cmds:
233+
- |
234+
echo "Creating Jaeger basic auth secret..."
235+
if [ -z "$JAEGER_AUTH_HTPASSWD" ]; then
236+
echo "Error: JAEGER_AUTH_HTPASSWD not set in .env file"
237+
echo ""
238+
echo "Generate with: htpasswd -nb admin yourpassword"
239+
echo "Then add to .env: JAEGER_AUTH_HTPASSWD='admin:\$apr1\$...'"
240+
exit 1
241+
fi
242+
# Delete existing secret if it exists
243+
kubectl --kubeconfig={{.KUBECONFIG}} delete secret jaeger-basic-auth -n default --ignore-not-found=true
244+
# Create new secret
245+
printf '%s' "$JAEGER_AUTH_HTPASSWD" | kubectl --kubeconfig={{.KUBECONFIG}} create secret generic jaeger-basic-auth --from-file=auth=/dev/stdin -n default
246+
echo "Secret created"
247+
- kubectl --kubeconfig={{.KUBECONFIG}} apply -f {{.K8S_DIR}}/observability/jaeger-deployment.yaml
248+
- echo "Jaeger deployed"
249+
- echo "Waiting for rollout to complete..."
250+
- kubectl --kubeconfig={{.KUBECONFIG}} rollout status deployment/jaeger --timeout=120s
251+
- echo ""
252+
- echo "Jaeger UI available at https://app.roussev.com/jaeger"
253+
229254
# Rollout tasks
230255
rollout:restart:items-service:
231256
desc: Restart items-service deployment

Tiltfile

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,24 @@ k8s_resource(
4545
resource_deps=[]
4646
)
4747

48+
# ============================================================================
49+
# Jaeger (OpenTelemetry Collector)
50+
# ============================================================================
51+
52+
# Deploy Jaeger for distributed tracing
53+
k8s_yaml('infra/k8s/local/jaeger-local.yaml')
54+
55+
# Configure Jaeger resource
56+
k8s_resource(
57+
'jaeger',
58+
port_forwards='16686:16686',
59+
labels=['observability'],
60+
resource_deps=[],
61+
links=[
62+
link('http://localhost:16686', 'Jaeger UI'),
63+
]
64+
)
65+
4866
# ============================================================================
4967
# Items Service
5068
# ============================================================================
@@ -69,7 +87,7 @@ k8s_resource(
6987
'items-service',
7088
port_forwards='8081:8080',
7189
labels=['apps'],
72-
resource_deps=['postgres'],
90+
resource_deps=['postgres', 'jaeger'],
7391
links=[
7492
link('http://localhost:8081/v1/health', 'Health Check'),
7593
link('http://localhost:8081/docs', 'API Docs'),
@@ -125,6 +143,7 @@ Services will be available at:
125143
- API Docs: http://localhost:8081/docs
126144
🌐 Website App: http://localhost:8082
127145
- Health: http://localhost:8082/health
146+
📊 Jaeger UI: http://localhost:16686
128147
129148
Press 'space' to open Tilt UI in your browser
130149
Press 'q' to quit

apps/items-service/bun.lock

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

apps/items-service/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
"start": "bun run src/index.ts"
99
},
1010
"dependencies": {
11+
"@opentelemetry/api": "^1.9.0",
12+
"@opentelemetry/auto-instrumentations-node": "^0.66.0",
13+
"@opentelemetry/exporter-trace-otlp-http": "^0.207.0",
14+
"@opentelemetry/resources": "^2.2.0",
15+
"@opentelemetry/sdk-node": "^0.207.0",
16+
"@opentelemetry/semantic-conventions": "^1.37.0",
1117
"postgres": "^3.4.4"
1218
}
1319
}

apps/items-service/src/index.ts

Lines changed: 120 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
// src/index.ts
2+
// Initialize OpenTelemetry first, before any other imports
3+
import { initTelemetry } from "./telemetry";
4+
initTelemetry();
5+
26
import postgres from "postgres";
37
import { getSwaggerHtml, getRootPageHtml } from "./html";
48
import { openapi } from "./openapi";
9+
import { trace, context, SpanStatusCode } from "@opentelemetry/api";
510

611
type Item = { id: number; name: string };
712

@@ -31,19 +36,27 @@ const sql = postgres({
3136

3237
// Initialize database schema
3338
async function initDatabase() {
34-
try {
35-
await sql`
36-
CREATE TABLE IF NOT EXISTS items (
37-
id SERIAL PRIMARY KEY,
38-
name VARCHAR(255) NOT NULL,
39-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
40-
)
41-
`;
42-
console.log("✅ Database initialized successfully");
43-
} catch (error) {
44-
console.error("❌ Failed to initialize database:", error);
45-
throw error;
46-
}
39+
const tracer = trace.getTracer("items-service");
40+
return await tracer.startActiveSpan("initDatabase", async (span) => {
41+
try {
42+
await sql`
43+
CREATE TABLE IF NOT EXISTS items (
44+
id SERIAL PRIMARY KEY,
45+
name VARCHAR(255) NOT NULL,
46+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
47+
)
48+
`;
49+
console.log("✅ Database initialized successfully");
50+
span.setStatus({ code: SpanStatusCode.OK });
51+
} catch (error) {
52+
console.error("❌ Failed to initialize database:", error);
53+
span.recordException(error as Error);
54+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
55+
throw error;
56+
} finally {
57+
span.end();
58+
}
59+
});
4760
}
4861

4962
function json(data: unknown, init: ResponseInit = {}) {
@@ -58,63 +71,109 @@ function notFound() {
5871
}
5972

6073
async function handle(req: Request): Promise<Response> {
74+
const tracer = trace.getTracer("items-service");
6175
const url = new URL(req.url);
6276
let path = url.pathname;
6377

64-
// Root page
65-
if (path === "/" && req.method === "GET") return new Response(getRootPageHtml(API_PREFIX), { headers: { "content-type": "text/html; charset=utf-8" } });
66-
67-
// Swagger UI & OpenAPI JSON
68-
if (path === "/docs" && req.method === "GET") return new Response(getSwaggerHtml(APP_PREFIX), { headers: { "content-type": "text/html; charset=utf-8" } });
69-
if (path === `${API_PREFIX}/openapi.json` && req.method === "GET") return json(openapi);
70-
71-
// if (!path.startsWith(API_PREFIX)) return notFound();
72-
path = path.slice(API_PREFIX.length) || "/";
73-
74-
if (path === "/health" && req.method === "GET") {
75-
try {
76-
// Check database connection
77-
await sql`SELECT 1`;
78-
return json({ status: "ok", commit: COMMIT_SHA, database: "connected" });
79-
} catch (error) {
80-
return json({ status: "degraded", commit: COMMIT_SHA, database: "disconnected", error: String(error) }, { status: 503 });
81-
}
82-
}
83-
84-
if (path === "/items" && req.method === "GET") {
78+
// Create a span for the HTTP request
79+
return await tracer.startActiveSpan(`${req.method} ${path}`, async (span) => {
8580
try {
86-
const items = await sql<Item[]>`SELECT id, name FROM items ORDER BY id`;
87-
return json({ items });
88-
} catch (error) {
89-
console.error("Error fetching items:", error);
90-
return json({ error: "Failed to fetch items" }, { status: 500 });
91-
}
92-
}
81+
span.setAttributes({
82+
"http.method": req.method,
83+
"http.url": url.toString(),
84+
"http.target": path,
85+
"http.scheme": url.protocol.replace(":", ""),
86+
"http.host": url.host,
87+
});
88+
89+
let response: Response;
90+
91+
// Root page
92+
if (path === "/" && req.method === "GET") {
9393
response = new Response(getRootPageHtml(), { headers: { "content-type": "text/html; charset=utf-8" } });
94+
}
95+
// Swagger UI & OpenAPI JSON
96+
else if (path === "/docs" && req.method === "GET") {
97+
response = new Response(getSwaggerHtml(APP_PREFIX), { headers: { "content-type": "text/html; charset=utf-8" } });
98+
}
99+
else if (path === `${API_PREFIX}/openapi.json` && req.method === "GET") {
100+
response = json(openapi);
101+
}
102+
else {
103+
// if (!path.startsWith(API_PREFIX)) return notFound();
104+
path = path.slice(API_PREFIX.length) || "/";
105+
106+
if (path === "/health" && req.method === "GET") {
107+
try {
108+
// Check database connection
109+
await sql`SELECT 1`;
110+
response = json({ status: "ok", commit: COMMIT_SHA, database: "connected" });
111+
} catch (error) {
112+
span.recordException(error as Error);
113+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
114+
response = json({ status: "degraded", commit: COMMIT_SHA, database: "disconnected", error: String(error) }, { status: 503 });
115+
}
116+
}
117+
else if (path === "/items" && req.method === "GET") {
118+
try {
119+
const items = await sql<Item[]>`SELECT id, name FROM items ORDER BY id`;
120+
span.setAttribute("items.count", items.length);
121+
response = json({ items });
122+
} catch (error) {
123+
console.error("Error fetching items:", error);
124+
span.recordException(error as Error);
125+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
126+
response = json({ error: "Failed to fetch items" }, { status: 500 });
127+
}
128+
}
129+
else if (path === "/items" && req.method === "POST") {
130+
try {
131+
const body = (await req.json()) as Partial<Item>;
132+
if (!body?.name || typeof body.name !== "string") {
133+
response = json({ error: "name required" }, { status: 400 });
134+
} else {
135+
const [item] = await sql<Item[]>`
136+
INSERT INTO items (name)
137+
VALUES (${body.name})
138+
RETURNING id, name
139+
`;
140+
span.setAttribute("item.id", item.id);
141+
span.setAttribute("item.name", item.name);
142+
response = json(item, { status: 201 });
143+
}
144+
} catch (error) {
145+
console.error("Error creating item:", error);
146+
span.recordException(error as Error);
147+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
148+
if (error instanceof SyntaxError) {
149+
response = json({ error: "invalid json" }, { status: 400 });
150+
} else {
151+
response = json({ error: "Failed to create item" }, { status: 500 });
152+
}
153+
}
154+
}
155+
else {
156+
response = notFound();
157+
}
158+
}
94159

95-
if (path === "/items" && req.method === "POST") {
96-
try {
97-
const body = (await req.json()) as Partial<Item>;
98-
if (!body?.name || typeof body.name !== "string") return json({ error: "name required" }, { status: 400 });
99-
response = new Response(getRootPageHtml(APP_PREFIX, API_PREFIX), { headers: { "content-type": "text/html; charset=utf-8" } });
100-
101-
const [item] = await sql<Item[]>`
102-
INSERT INTO items (name)
103-
VALUES (${body.name})
104-
RETURNING id, name
105-
`;
160+
// Set response status on span
161+
span.setAttribute("http.status_code", response.status);
162+
if (response.status >= 400) {
163+
span.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${response.status}` });
164+
} else {
165+
span.setStatus({ code: SpanStatusCode.OK });
166+
}
106167

107-
return json(item, { status: 201 });
168+
return response;
108169
} catch (error) {
109-
console.error("Error creating item:", error);
110-
if (error instanceof SyntaxError) {
111-
return json({ error: "invalid json" }, { status: 400 });
112-
}
113-
return json({ error: "Failed to create item" }, { status: 500 });
170+
span.recordException(error as Error);
171+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
172+
throw error;
173+
} finally {
174+
span.end();
114175
}
115-
}
116-
117-
return notFound();
176+
});
118177
}
119178

120179
// Initialize database and start server

apps/items-service/src/openapi.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,6 @@ export const openapi = {
3838
},
3939
},
4040
},
41-
"/test1": {
42-
get: {
43-
summary: "Test endpoint",
44-
responses: {
45-
"200": {
46-
description: "Test response",
47-
content: { "application/json": { schema: { type: "object", properties: { status: { type: "string" } } } } },
48-
},
49-
},
50-
},
51-
},
5241
},
5342
components: {
5443
schemas: { Item: { type: "object", properties: { id: { type: "integer" }, name: { type: "string" } }, required: ["id", "name"] } },

0 commit comments

Comments
 (0)