OpenTelemetry κ°μ΄λ π
π OpenTelemetryλ?
OpenTelemetry(μ€μ¬μ OTel)λ μ±μ μ±λ₯κ³Ό λμμ μΆμ νλ μ€νμμ€ κ΄μΈ‘μ±(Observability) νλ μμν¬ μ
λλ€
μ¦, "λ΄ μ±μ΄ μ΄λ»κ² λμκ°κ³ μλμ§" μ€μκ°μΌλ‘ λ€μ¬λ€λ³΄λ λꡬμ
λλ€!
π― ν΅μ¬ κ°λ : 3λ Pillar
1. Traces (μΆμ )
μ¬μ©μ μμ²μ΄ μμ€ν μ ν΅κ³Όνλ μ 체 μ¬μ μ μΆμ
// μ: μ¬μ©μκ° μν ꡬ맀 λ²νΌ ν΄λ¦
[λΈλΌμ°μ ] 2ms → [API Gateway] 10ms → [μ£Όλ¬Έ μλΉμ€] 50ms → [κ²°μ μλΉμ€] 200ms → [DB] 30ms
μ΄ μμ μκ°: 292ms
2. Metrics (μ§ν)
μμ€ν μνλ₯Ό μ«μλ‘ μΈ‘μ
// μμ μ§νλ€
- μ΄λΉ μμ² μ: 1,234 req/s
- νκ· μλ΅ μκ°: 145ms
- CPU μ¬μ©λ₯ : 67%
- λ©λͺ¨λ¦¬ μ¬μ©λ: 2.3GB
- μλ¬μ¨: 0.02%
3. Logs (λ‘κ·Έ)
μ΄λ²€νΈ κΈ°λ‘ (νμ§λ§ ꡬ쑰νλ λ°©μμΌλ‘)
{
timestamp: "2024-01-20T10:30:45Z",
level: "ERROR",
traceId: "abc123", // Traceμ μ°κ²°!
spanId: "def456",
message: "Payment failed",
userId: "user_789",
amount: 50000
}
π‘ μ€μ μ¬μ© μμ
Next.jsμμ OpenTelemetry μ€μ
// instrumentation.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
const sdk = new NodeSDK({
// μλΉμ€ μ 보
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'my-shop-frontend',
[SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
}),
// μλ κ³μΈ‘ (μλμΌλ‘ μΆμ !)
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': {
enabled: false, // νμΌ μμ€ν
μ μ μΈ
},
}),
],
});
sdk.start();
console.log('OpenTelemetry μμ!');
}
}
π μ€μ νμ©: μ μμκ±°λ μλ리μ€
1. Distributed Tracing (λΆμ° μΆμ )
// API Route: app/api/checkout/route.ts
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('checkout-service');
export async function POST(request: Request) {
// μ 체 체ν¬μμ νλ‘μΈμ€ μΆμ μμ
return tracer.startActiveSpan('checkout', async (span) => {
try {
// 1. μ¬κ³ νμΈ
await tracer.startActiveSpan('check-inventory', async (inventorySpan) => {
const hasStock = await checkInventory(items);
inventorySpan.setAttribute('items.count', items.length);
inventorySpan.setAttribute('has.stock', hasStock);
inventorySpan.end();
});
// 2. κ²°μ μ²λ¦¬
await tracer.startActiveSpan('process-payment', async (paymentSpan) => {
const result = await processPayment(amount);
paymentSpan.setAttribute('payment.amount', amount);
paymentSpan.setAttribute('payment.status', result.status);
paymentSpan.end();
});
span.setStatus({ code: SpanStatusCode.OK });
return NextResponse.json({ success: true });
} catch (error) {
span.recordException(error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
});
}
2. Custom Metrics (컀μ€ν μ§ν)
import { metrics } from '@opentelemetry/api';
// λ―Έν° μμ±
const meter = metrics.getMeter('ecommerce-metrics');
// μΉ΄μ΄ν°: νλ§€ μλ
const salesCounter = meter.createCounter('sales_total', {
description: 'μ΄ νλ§€ μλ',
unit: 'items',
});
// νμ€ν κ·Έλ¨: κ²°μ μκ°
const paymentDuration = meter.createHistogram('payment_duration', {
description: 'κ²°μ μ²λ¦¬ μκ°',
unit: 'ms',
});
// μ¬μ© μ
export async function processOrder(order: Order) {
const startTime = Date.now();
try {
await processPayment(order);
// μ§ν κΈ°λ‘
salesCounter.add(order.items.length, {
category: order.category,
paymentMethod: order.paymentMethod,
});
paymentDuration.record(Date.now() - startTime, {
status: 'success',
});
} catch (error) {
paymentDuration.record(Date.now() - startTime, {
status: 'failed',
});
throw error;
}
}
π μκ°ν: μ€μ λ‘ λ³΄μ΄λ κ²
OpenTelemetry λ°μ΄ν°λ λ€μν λ°±μλλ‘ μ μ‘λμ΄ μκ°νλ©λλ€:
Jaeger UIμμ 보λ Trace
[GET /api/products] ββββββββββββββββββββ 245ms
ββ[DB Query: products] βββββ 89ms
ββ[Cache Check] βββ 5ms
ββ[Image CDN] ββββββββββ 120ms
ββ[Response Format] ββ 31ms
Grafanaμμ 보λ Metrics
βββββββββββββββββββββββββββββββββββ
β Response Time (p99)
β π 145ms → 189ms → 134ms
βββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββ
β Error Rate
β π 0.1% βββββββββββ
βββββββββββββββββββββββββββββββββββ
π Next.js μ μ© μ€μ μμ
// lib/telemetry.ts
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { registerOTel } from '@vercel/otel';
export function initTelemetry() {
// Vercelμ OpenTelemetry ν¬νΌ μ¬μ©
registerOTel({
serviceName: 'my-nextjs-app',
// Traceλ₯Ό μ΄λλ‘ λ³΄λΌμ§
traceExporter: new OTLPTraceExporter({
url: 'https://api.honeycomb.io/v1/traces',
headers: {
'x-honeycomb-team': process.env.HONEYCOMB_API_KEY,
},
}),
// μνλ§ (λͺ¨λ μμ² μΆμ νλ©΄ λΉμ©↑)
tracesSampleRate: process.env.NODE_ENV === 'production'
? 0.1 // νλ‘λμ
: 10%λ§
: 1.0, // κ°λ°: μ λΆ
});
}
π― μ OpenTelemetryλ₯Ό μ°λκ°?
λ¬Έμ μν©
// μ¬μ©μ: "κ²°μ κ° λ무 λλ €μ!" π€
// κ°λ°μ: "μ΄λκ° λλ¦°κ±°μ§?" π€
// - Next.js API Route?
// - μΈλΆ κ²°μ API?
// - λ°μ΄ν°λ² μ΄μ€ 쿼리?
// - Redis μΊμ?
OpenTelemetryλ‘ ν΄κ²°
// Trace κ²°κ³Ό:
checkout-process (μ΄ 3.2μ΄) π±
ββ validate-cart: 50ms β
ββ check-inventory: 200ms β
ββ payment-api-call: 2,800ms β (μ¬κΈ°κ° λ¬Έμ !)
ββ send-confirmation: 150ms β
π° μΈκΈ° μλ λ°±μλ μλΉμ€
- μ€νμμ€ (무λ£)
- Jaeger
- Zipkin
- Grafana Tempo
- μμ© μλΉμ€
- Datadog
- New Relic
- Honeycomb
- AWS X-Ray
π₯ Sentry vs OpenTelemetry
// Sentry: μλ¬ μ€μ¬
"μ±μ΄ ν°μ‘μ΄μ!" → μλ¬ μΆμ , μ€ννΈλ μ΄μ€
// OpenTelemetry: μ±λ₯ μ€μ¬
"μ±μ΄ λλ €μ!" → λ³λͺ© κ΅¬κ° μ°ΎκΈ°, μ±λ₯ μ΅μ ν
// ν¨κ» μ¬μ©νλ©΄ μ΅κ³ !
Sentry + OpenTelemetry = μλ²½ν λͺ¨λν°λ§
Vercelμ OpenTelemetry ν΅ν©
Vercelμ΄ @vercel/otel ν¨ν€μ§λ‘ Next.js μ μ© OpenTelemetry μ€μ μ μ½κ² ν΄μ€
Vercelμ΄ μ΄λ κ² κ°νΈν ν΅ν©μ μ 곡νλ μ΄μ λ μλ²λ¦¬μ€ νκ²½μ 볡μ‘μ±μ μ¨κΈ°κ³ , κ°λ°μκ° λΉμ¦λμ€ λ‘μ§μ μ§μ€ν μ μκ² νκΈ° μν΄μμ΄λ€.
νΉν Edge Functions, ISR, λμ λ λλ§ λ± Next.jsμ λ€μν κΈ°λ₯μ λͺ¨λ μΆμ ν μ μλ κ²μ΄ μ₯μ !
π¦ @vercel/otel ν¨ν€μ§
κΈ°λ³Έ μ€μ (곡μ λ¬Έμ μμ)
// instrumentation.ts
import { registerOTel } from '@vercel/otel';
export function register() {
registerOTel({
serviceName: 'my-nextjs-app'
});
}
μ΄ ν μ€μ΄λ©΄ μλμΌλ‘:
- β Next.js λΌμ°νΈ μΆμ
- β fetch μμ² μΆμ
- β λ°μ΄ν°λ² μ΄μ€ 쿼리 μΆμ
- β μλ²/ν΄λΌμ΄μΈνΈ μ»΄ν¬λνΈ κ΅¬λΆ
π μ€μ νλ‘λμ μ€μ
// instrumentation.ts
import { registerOTel } from '@vercel/otel';
export function register() {
registerOTel({
serviceName: 'ecommerce-frontend',
// 1. μ΄λλ‘ λ°μ΄ν°λ₯Ό 보λΌκΉ?
traceExporter: process.env.VERCEL_ENV === 'production'
? 'auto' // Vercel μλ κ°μ§ (Datadog, New Relic λ±)
: 'console', // κ°λ° νκ²½: μ½μ μΆλ ₯
// 2. μΌλ§λ μΆμ ν κΉ? (λΉμ© κ΄λ¦¬!)
tracesSampleRate: process.env.VERCEL_ENV === 'production'
? 0.1 // νλ‘λμ
: 10%λ§ (λΉμ© μ κ°)
: 1.0, // κ°λ°/ν리뷰: 100%
// 3. μΆκ° μ 보 ν¬ν¨
resourceAttributes: {
'environment': process.env.VERCEL_ENV || 'development',
'region': process.env.VERCEL_REGION || 'unknown',
'deployment.id': process.env.VERCEL_DEPLOYMENT_ID,
},
});
}
π― Vercel νλ«νΌ νΉν κΈ°λ₯
1. μλ νκ²½ κ°μ§
registerOTel({
serviceName: 'my-app',
// Vercelμ΄ μλμΌλ‘ κ°μ§!
// - Development: μ½μ μΆλ ₯
// - Preview: Vercel λμ보λ
// - Production: μ°κ²°λ APM μλΉμ€
});
2. Edge Runtime μ§μ
export function register() {
// Edgeμ Node.js λͺ¨λ μ§μ!
if (process.env.NEXT_RUNTIME === 'edge') {
registerOTel({
serviceName: 'edge-api',
// Edge Runtimeμμλ μλ!
});
}
}
π‘ μ€μ νμ© μμ
1. App Router μ±λ₯ μΆμ
// app/products/[id]/page.tsx
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('product-page');
export default async function ProductPage({ params }) {
return tracer.startActiveSpan('render-product-page', async (span) => {
try {
// μλμΌλ‘ μΆμ λλ κ²λ€:
const product = await fetch(`/api/products/${params.id}`); // ← μλ μΆμ !
// 컀μ€ν
μμ± μΆκ°
span.setAttribute('product.id', params.id);
span.setAttribute('product.category', product.category);
return <ProductView product={product} />;
} finally {
span.end();
}
});
}
2. API Route λͺ¨λν°λ§
// app/api/checkout/route.ts
export async function POST(request: Request) {
// @vercel/otelμ΄ μλμΌλ‘:
// - μμ² μμ/μ’
λ£ μκ° κΈ°λ‘
// - HTTP μν μ½λ μΆμ
// - μλ¬ μλ μΊ‘μ²
const data = await request.json();
// 컀μ€ν
μ΄λ²€νΈ μΆκ°
const span = trace.getActiveSpan();
span?.addEvent('checkout-started', {
userId: data.userId,
cartValue: data.total,
});
// DB, μΈλΆ API νΈμΆλ μλ μΆμ λ¨
const result = await processCheckout(data);
return NextResponse.json(result);
}
π Vercel λμ보λ ν΅ν©
// Vercel νλ‘μ νΈ μ€μ μμ μ°κ²° κ°λ₯ν μλΉμ€λ€:
registerOTel({
serviceName: 'my-app',
// VERCEL_OBSERVABILITY_PROVIDER νκ²½ λ³μλ‘ μλ μ€μ
});
// μ§μνλ Providers:
// - Datadog (μλ μ°λ!)
// - New Relic
// - Axiom
// - Honeycomb
// - Grafana Cloud
π κ³ κΈ μ€μ : λ©ν° μλΉμ€ μΆμ
// instrumentation.ts - λ§μ΄ν¬λ‘μλΉμ€ μν€ν
μ²
import { registerOTel } from '@vercel/otel';
import { W3CTraceContextPropagator } from '@opentelemetry/core';
export function register() {
registerOTel({
serviceName: 'frontend-gateway',
// λ€λ₯Έ μλΉμ€λ‘ μ ν
propagators: [new W3CTraceContextPropagator()],
// μλΉμ€ κ° μΆμ μ μν ν€λ
instrumentationConfig: {
fetch: {
propagateTraceHeaderCorsUrls: [
'https://api.myapp.com/*',
'https://auth.myapp.com/*',
],
},
},
});
}
// API νΈμΆ μ μλμΌλ‘ trace μ ν
const response = await fetch('https://api.myapp.com/orders', {
// traceparent ν€λκ° μλ μΆκ°λ¨!
});
π¨ μ€μ Trace μκ°ν μμ
[Vercel Dashboard / Datadog APMμμ 보μ΄λ λͺ¨μ΅]
GET /products/123 ββββββββββββββββββββββ 312ms
ββ getServerSideProps ββββββββββββββββ 287ms
β ββ fetch: GET /api/products/123 ββ 145ms
β β ββ Prisma: findUnique ββββββββ 89ms
β ββ fetch: GET /api/reviews βββββββ 98ms
β ββ generateMetadata ββββββββββββββ 44ms
ββ React SSR βββββββββββββββββββββββββ 25ms
π° λΉμ© μ΅μ ν ν
registerOTel({
serviceName: 'my-app',
// 1. μ€λ§νΈ μνλ§
tracesSampler: (samplingContext) => {
// μλ¬λ νμ μΆμ
if (samplingContext.attributes?.['http.status_code'] >= 500) {
return 1.0;
}
// λλ¦° μμ² μΆμ
if (samplingContext.attributes?.['http.duration'] > 1000) {
return 0.5;
}
// λλ¨Έμ§λ 1%λ§
return 0.01;
},
// 2. λΆνμν span μ μΈ
instrumentationConfig: {
'@opentelemetry/instrumentation-fs': {
enabled: false, // νμΌ μμ€ν
μ μΈ
},
},
});
π₯ Vercel + OpenTelemetry λ² μ€νΈ νλν°μ€
// instrumentation.ts - μλ²½ν μ€μ
import { registerOTel } from '@vercel/otel';
export function register() {
// κ°λ°/νλ‘λμ
μλ ꡬλΆ
const isDev = process.env.NODE_ENV === 'development';
registerOTel({
serviceName: `${process.env.VERCEL_PROJECT_NAME || 'nextjs-app'}`,
// νκ²½λ³ μ€μ
traceExporter: isDev ? 'console' : 'auto',
metricsExporter: isDev ? 'console' : 'auto',
// μνλ§ μ λ΅
tracesSampleRate: isDev ? 1.0 :
parseFloat(process.env.OTEL_SAMPLE_RATE || '0.1'),
// Vercel λ©νλ°μ΄ν° μλ ν¬ν¨
resourceAttributes: {
'vercel.env': process.env.VERCEL_ENV,
'vercel.region': process.env.VERCEL_REGION,
'vercel.git.commit': process.env.VERCEL_GIT_COMMIT_SHA,
'vercel.git.branch': process.env.VERCEL_GIT_COMMIT_REF,
},
});
console.log('π‘ OpenTelemetry μ΄κΈ°ν μλ£');
}'TIL' μΉ΄ν κ³ λ¦¬μ λ€λ₯Έ κΈ
| [251124 TIL] κ°μ μμ‘΄μ± κ΄λ ¨ μ λ°μ΄νΈ (0) | 2025.11.24 |
|---|---|
| [251123 TIL] Turbopack κ°μ μμ‘΄μ± κ²½κ³ μν© (0) | 2025.11.23 |
| [251123 TIL] Next.js instrumentation.ts μ 리 (0) | 2025.11.23 |
| [251102 TIL] PostgREST vs GraphQL(Hasura)κΈ°μ μ€ν λΉκ΅ (0) | 2025.11.02 |
| [251031 TIL] naver λ‘κ·ΈμΈ κ΅¬ν with Supabase 3νΈ (1) | 2025.10.31 |