Skip to content

Commit 33753a8

Browse files
feat: otel
1 parent c236e10 commit 33753a8

File tree

10 files changed

+1561
-5
lines changed

10 files changed

+1561
-5
lines changed

.env.example

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# OpenTelemetry Configuration
2+
# Set the OTLP endpoint to enable tracing (omit to disable tracing)
3+
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
4+
5+
# Optional: Service identification
6+
OTEL_SERVICE_NAME=manifestsio
7+
OTEL_SERVICE_VERSION=0.1.0
8+
OTEL_DEPLOYMENT_ENVIRONMENT=development
9+
10+
# Optional: Additional headers for OTLP exporter (comma-separated key=value pairs)
11+
# Example for authentication: OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer token123,X-Custom-Header=value
12+
# OTEL_EXPORTER_OTLP_HEADERS=

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ yarn-error.log*
2626
.pnpm-debug.log*
2727

2828
# local env files
29+
.env
2930
.env*.local
3031

3132
# vercel

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,65 @@ Easy to use online kubernetes documentation.
1313

1414
You can support more kubernetes versions by dropping any k8s version's [Open API spec](https://github.com/kubernetes/kubernetes/blob/master/api/openapi-spec/swagger.json) into `oaspec/kubernetes` and updating `lib/oaspec.tsx`.
1515

16+
## OpenTelemetry Tracing
17+
18+
This project includes OpenTelemetry instrumentation for distributed tracing. Tracing is **disabled by default** and requires configuration to enable.
19+
20+
### Local Development
21+
22+
To enable tracing locally:
23+
24+
1. Copy `.env.example` to `.env`
25+
2. Set the `OTEL_EXPORTER_OTLP_ENDPOINT` to your OTLP collector endpoint:
26+
```bash
27+
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
28+
OTEL_SERVICE_NAME=manifestsio
29+
OTEL_DEPLOYMENT_ENVIRONMENT=development
30+
```
31+
3. Start your application with `yarn dev`
32+
33+
If `OTEL_EXPORTER_OTLP_ENDPOINT` is not set or empty, tracing will be disabled automatically.
34+
35+
### Production Deployment
36+
37+
For Kubernetes deployments, configure the environment variables in `deployment.yaml`:
38+
39+
```yaml
40+
env:
41+
- name: OTEL_EXPORTER_OTLP_ENDPOINT
42+
value: "http://otel-collector.observability.svc.cluster.local:4318"
43+
- name: OTEL_SERVICE_NAME
44+
value: "manifestsio"
45+
- name: OTEL_DEPLOYMENT_ENVIRONMENT
46+
value: "production"
47+
```
48+
49+
### What's Instrumented
50+
51+
The application automatically traces:
52+
- **HTTP requests** - All incoming requests to the Next.js server
53+
- **Spec loading** - OpenAPI spec fetch operations (`oaspecFetch`)
54+
- **Server-side rendering** - Page rendering with `getServerSideProps`
55+
56+
Custom spans include attributes like:
57+
- `manifestsio.item` - The product being viewed (e.g., "kubernetes")
58+
- `manifestsio.version` - The version being viewed (e.g., "1.34")
59+
- `manifestsio.resource` - The specific resource being viewed (e.g., "Pod")
60+
61+
### Testing with Jaeger
62+
63+
For local testing, you can run Jaeger all-in-one:
64+
65+
```bash
66+
docker run -d --name jaeger \
67+
-e COLLECTOR_OTLP_ENABLED=true \
68+
-p 16686:16686 \
69+
-p 4318:4318 \
70+
jaegertracing/all-in-one:latest
71+
```
72+
73+
Then set `OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318` in your `.env` file and access the Jaeger UI at http://localhost:16686.
74+
1675
# Supporting new k8s/product versions
1776

1877
### New kubernetes versions

deployment.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,21 @@ spec:
2323
image: <IMAGE>
2424
ports:
2525
- containerPort: 3000
26+
env:
27+
# OpenTelemetry Configuration
28+
# Set OTEL_EXPORTER_OTLP_ENDPOINT to enable tracing
29+
# Example: http://otel-collector.observability.svc.cluster.local:4318
30+
- name: OTEL_EXPORTER_OTLP_ENDPOINT
31+
value: "" # Set to your OTLP endpoint or leave empty to disable tracing
32+
- name: OTEL_SERVICE_NAME
33+
value: "manifestsio"
34+
- name: OTEL_SERVICE_VERSION
35+
value: "0.1.0"
36+
- name: OTEL_DEPLOYMENT_ENVIRONMENT
37+
value: "production"
38+
# Optional: Add authentication headers if required
39+
# - name: OTEL_EXPORTER_OTLP_HEADERS
40+
# value: "Authorization=Bearer token123"
2641
---
2742
apiVersion: v1
2843
kind: Service

instrumentation.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
export async function register() {
2+
if (process.env.NEXT_RUNTIME === 'nodejs') {
3+
const { NodeSDK } = await import('@opentelemetry/sdk-node');
4+
const { OTLPTraceExporter } = await import('@opentelemetry/exporter-trace-otlp-http');
5+
const { Resource } = await import('@opentelemetry/resources');
6+
const {
7+
ATTR_SERVICE_NAME,
8+
ATTR_SERVICE_VERSION,
9+
ATTR_DEPLOYMENT_ENVIRONMENT,
10+
} = await import('@opentelemetry/semantic-conventions');
11+
const { getNodeAutoInstrumentations } = await import('@opentelemetry/auto-instrumentations-node');
12+
13+
// Only initialize if OTLP endpoint is configured
14+
const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
15+
if (!otlpEndpoint) {
16+
console.log('OpenTelemetry: OTEL_EXPORTER_OTLP_ENDPOINT not set, tracing disabled');
17+
return;
18+
}
19+
20+
const sdk = new NodeSDK({
21+
resource: new Resource({
22+
[ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || 'manifestsio',
23+
[ATTR_SERVICE_VERSION]: process.env.OTEL_SERVICE_VERSION || '0.1.0',
24+
[ATTR_DEPLOYMENT_ENVIRONMENT]: process.env.OTEL_DEPLOYMENT_ENVIRONMENT || 'development',
25+
}),
26+
traceExporter: new OTLPTraceExporter({
27+
url: `${otlpEndpoint}/v1/traces`,
28+
headers: process.env.OTEL_EXPORTER_OTLP_HEADERS
29+
? Object.fromEntries(
30+
process.env.OTEL_EXPORTER_OTLP_HEADERS.split(',').map(header => {
31+
const [key, value] = header.split('=');
32+
return [key.trim(), value.trim()];
33+
})
34+
)
35+
: {},
36+
}),
37+
instrumentations: [
38+
getNodeAutoInstrumentations({
39+
// Disable fs instrumentation to reduce noise from Next.js file system operations
40+
'@opentelemetry/instrumentation-fs': {
41+
enabled: false,
42+
},
43+
// Configure HTTP instrumentation to capture useful headers
44+
'@opentelemetry/instrumentation-http': {
45+
requestHook: (span, request) => {
46+
span.setAttribute('http.user_agent', request.headers['user-agent'] || 'unknown');
47+
},
48+
},
49+
}),
50+
],
51+
});
52+
53+
sdk.start();
54+
console.log('OpenTelemetry: Tracing initialized successfully');
55+
56+
// Graceful shutdown
57+
process.on('SIGTERM', () => {
58+
sdk
59+
.shutdown()
60+
.then(() => console.log('OpenTelemetry: Tracing terminated'))
61+
.catch(error => console.error('OpenTelemetry: Error terminating tracing', error))
62+
.finally(() => process.exit(0));
63+
});
64+
}
65+
}

lib/oaspec.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {K8sDefinitions} from "@/typings/KubernetesSpec";
2+
import { trace, SpanStatusCode } from '@opentelemetry/api';
23

34
import kubernetes134 from "../oaspec/kubernetes/1.34.json";
45
import kubernetes133 from "../oaspec/kubernetes/1.33.json";
@@ -39,7 +40,31 @@ import spaceliftoperator010 from "../oaspec/spaceliftoperator/0.1.0.json";
3940

4041
import spaceliftworkerpool0021 from "../oaspec/spaceliftworkerpool/0.0.21.json";
4142

43+
const tracer = trace.getTracer('manifestsio-oaspec');
44+
4245
export function oaspecFetch(item: string, version: string): K8sDefinitions {
46+
return tracer.startActiveSpan('oaspec.fetch', (span) => {
47+
span.setAttribute('oaspec.item', item);
48+
span.setAttribute('oaspec.version', version);
49+
50+
try {
51+
const result = fetchSpec(item, version);
52+
span.setStatus({ code: SpanStatusCode.OK });
53+
return result;
54+
} catch (error) {
55+
span.setStatus({
56+
code: SpanStatusCode.ERROR,
57+
message: error instanceof Error ? error.message : 'Unknown error'
58+
});
59+
span.recordException(error as Error);
60+
throw error;
61+
} finally {
62+
span.end();
63+
}
64+
});
65+
}
66+
67+
function fetchSpec(item: string, version: string): K8sDefinitions {
4368
if (item === "kubernetes") {
4469
switch (version) {
4570
case "1.34":

next.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
const nextConfig = {
33
output: "standalone",
44
reactStrictMode: true,
5+
experimental: {
6+
instrumentationHook: true,
7+
},
58
headers: async () => {
69
return [
710
{

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313
"@emotion/react": "^11.13.3",
1414
"@emotion/styled": "^11.13.0",
1515
"@next/font": "14.2.15",
16+
"@opentelemetry/api": "^1.9.0",
17+
"@opentelemetry/auto-instrumentations-node": "^0.52.1",
18+
"@opentelemetry/exporter-trace-otlp-http": "^0.56.0",
19+
"@opentelemetry/resources": "^1.29.0",
20+
"@opentelemetry/sdk-node": "^0.56.0",
21+
"@opentelemetry/semantic-conventions": "^1.29.0",
1622
"@types/node": "22.9.1",
1723
"@types/react": "18.3.12",
1824
"@types/react-dom": "18.3.1",

pages/[item]/[version]/[resource]/index.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import ManifestsHeading from "@/components/ManifestsHeading";
88
import {NextParsedUrlQuery} from "next/dist/server/request-meta";
99
import {K8sDefinitions, K8sProperty, K8sPropertyArray} from "@/typings/KubernetesSpec";
1010
import {Resource} from "@/typings/Resource";
11+
import { trace, SpanStatusCode } from '@opentelemetry/api';
1112

1213
type OtherVersions = {
1314
keys: Array<string>;
@@ -111,7 +112,11 @@ export default function Home({
111112
)
112113
}
113114

115+
const tracer = trace.getTracer('manifestsio-pages');
116+
114117
export const getServerSideProps: GetServerSideProps = async ({query}) => {
118+
return tracer.startActiveSpan('getServerSideProps.resourceDetail', async (span) => {
119+
try {
115120
function parseQuery(query: NextParsedUrlQuery) {
116121
let {item, version, resource, linked, oneOf, key} = query;
117122
if (Array.isArray(item) || !item) {
@@ -260,6 +265,10 @@ export const getServerSideProps: GetServerSideProps = async ({query}) => {
260265

261266
const {item, version, resource, linked, oneOf, key} = parseQuery(query);
262267

268+
span.setAttribute('manifestsio.item', item);
269+
span.setAttribute('manifestsio.version', version);
270+
span.setAttribute('manifestsio.resource', resource);
271+
263272
let spec = oaspecFetch(item, version);
264273

265274
let linkedResource = defaultString(linked, popString(resource));
@@ -300,7 +309,19 @@ export const getServerSideProps: GetServerSideProps = async ({query}) => {
300309
props["oneOf"] = oneOfClicked;
301310
}
302311

312+
span.setStatus({ code: SpanStatusCode.OK });
303313
return {
304314
props
305315
}
316+
} catch (error) {
317+
span.setStatus({
318+
code: SpanStatusCode.ERROR,
319+
message: error instanceof Error ? error.message : 'Unknown error'
320+
});
321+
span.recordException(error as Error);
322+
throw error;
323+
} finally {
324+
span.end();
325+
}
326+
});
306327
}

0 commit comments

Comments
 (0)