콘텐츠로 이동

사용자 지정 Trace 전파 | Next.js용 Sentry

Source URL: https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/distributed-tracing/custom-instrumentation

사용자 지정 Trace 전파 | Next.js용 Sentry

섹션 제목: “사용자 지정 Trace 전파 | Next.js용 Sentry”

이 페이지에서는 JavaScript 애플리케이션 안팎으로 trace 정보를 수동으로 전파하는 방법을 배웁니다.

이 SDK에서는 trace 전파가 자동으로 설정됩니다. 기본적으로 나가는 HTTP 요청은 자동으로 계측됩니다. trace를 계속 이어가고 싶은 다른 경우(예: 작업 큐 또는 websockets 사용 시)에는 tracing 정보를 수동으로 추출하고 주입할 수 있습니다.

분산 추적이란 무엇인가요?

분산 추적은 요청이 애플리케이션 아키텍처의 서로 다른 계층을 통과할 때 그 경로를 연결하고 기록합니다. 아키텍처가 서로 다른 서브도메인(예: fe.example.comapi.example.com)에 있는 여러 서비스로 구성되어 있다면, 분산 추적을 통해 이벤트가 한 서비스에서 다른 서비스로 이동하는 경로를 따라갈 수 있습니다.

이러한 엔드 투 엔드 가시성을 통해 개발자는 병목 지점을 식별하고, 오류의 근본 원인을 정확히 찾아내며, 컴포넌트 간 상호작용을 이해할 수 있습니다. 즉, 복잡한 디버깅 악몽을 시스템 신뢰성과 성능을 개선하는 관리 가능한 프로세스로 바꿔줍니다.

애플리케이션에 tracing 데이터를 수동으로 추출하고 주입할 수도 있습니다. 이를 위해서는 다음이 필요합니다.

  • 들어오는 요청 헤더 등에서 들어오는 tracing 정보를 추출해 저장합니다.
  • 나가는 모든 요청에 tracing 정보를 주입합니다.

분산 추적에 대해 더 알아보려면 Distributed Tracing 문서를 참고하세요.

나중에 사용할 수 있도록 들어오는 tracing 정보를 메모리에 추출해 저장해야 합니다. Sentry는 이를 돕기 위해 continueTrace() 함수를 제공합니다. 들어오는 tracing 정보는 다양한 위치에서 올 수 있습니다.

  • 웹 환경에서는 HTTP headers와 함께 전송되며, 예를 들어 프런트엔드 프로젝트에서 사용하는 다른 Sentry SDK가 보낼 수 있습니다.
  • 작업 큐에서는 meta 또는 header 변수에서 가져올 수 있습니다.
  • 환경 변수에서도 tracing 정보를 가져올 수 있습니다.

다음은 continueTrace()를 사용해 들어오는 tracing 정보를 추출하고 저장하는 예시입니다.

const http = require("http");
http.createServer((request, response) => {
const sentryTrace = request.headers["sentry-trace"];
const baggage = request.headers["baggage"];
Sentry.continueTrace({ sentryTrace, baggage }, () => {
Sentry.startSpan(
{,
name: "my request",
op: "http.server",
},
() => {
// Your API code...
}
);
});
});

이 예시에서는 sentry-tracebaggage headers에 지정된 trace에 연결된 새 transaction을 생성합니다.

서버 사이드에서 HTML을 렌더링하고 브라우저 애플리케이션에서 Sentry SDK를 사용하는 경우, 브라우저에 처음 전달되는 HTML에 서버의 tracing 정보를 <meta> tags로 주입해 백엔드와 프런트엔드 trace를 연결할 수 있습니다. 프런트엔드 SDK가 초기화되면 <meta> tags에서 tracing 정보를 자동으로 가져와 trace를 이어갑니다. 이 기능이 동작하려면 브라우저 SDK에서 browserTracingIntegration을 등록해야 합니다.

가장 쉽고 권장되는 방법은 Sentry.getTraceMetaTags()를 사용하는 것입니다.

index.js

function renderHtml() {
return `
<html>
<head>
${Sentry.getTraceMetaTags()}
</head>
<body>
<!-- Your HTML content -->
</body>
</html>
`;
}

또는 meta tags 생성 방식을 더 세밀하게 제어해야 한다면, Sentry.getTraceData()를 사용해 meta tag 값만 가져오고 meta tags는 직접 생성할 수 있습니다.

index.js

function renderHtml() {
const metaTagValues = Sentry.getTraceData();
return `
<html>
<head>
<meta name="sentry-trace" content="${metaTagValues["sentry-trace"]}">
<meta name="baggage" content="${metaTagValues.baggage}">
</head>
<body>
<!-- Your HTML content -->
</body>
</html>
`;
}

분산 추적이 동작하려면 활성 루트 span에 추출 및 저장해 둔 두 header인 sentry-tracebaggage를 나가는 HTTP 요청에 추가해야 합니다.

다음은 이 tracing 정보를 수집해 나가는 요청에 주입하는 예시입니다.

const traceData = Sentry.getTraceData();
const sentryTraceHeader = traceData["sentry-trace"];
const sentryBaggageHeader = traceData["baggage"];
// Make outgoing request
fetch("https://example.com", {
method: "GET",
headers: {
baggage: sentryBaggageHeader,
"sentry-trace": sentryTraceHeader,
},
}).then((response) => {
// ...
});

이 예시에서는 tracing 정보가 https://example.com에서 실행 중인 프로젝트로 전파됩니다. 이 프로젝트가 Sentry SDK를 사용한다면 tracing 정보를 추출해 나중에 사용할 수 있도록 저장합니다.

이제 두 서비스가 사용자 지정 분산 추적 구현으로 연결되었습니다.

사용 가능 버전: v8.5.0

trace 지속 시간에 대한 SDK의 기본 동작이 요구 사항에 맞지 않는 경우, 현재 (분산) trace와 더 이상 연결되지 않는 새 trace를 수동으로 시작할 수 있습니다. 즉, 이 새 trace 동안 SDK가 수집한 spans 또는 errors는 이 trace의 이전이나 이후에 수집된 spans 또는 errors와 연결되지 않습니다.

콜백 실행 시간 동안 유효한 새 trace를 시작하려면 startNewTrace를 사용하세요.

myButton.addEventListener("click", async () => {
Sentry.startNewTrace(() => {
Sentry.startSpan(
{ op: "ui.interaction.click", name: "fetch click" },
async () => {
await fetch("http://example.com");
},
);
});
});

콜백이 끝나면 SDK는 이전 trace(가능한 경우)를 계속 이어갑니다.

프로젝트에서 다른 서비스로 나가는 요청을 보낸다면 요청에 sentry-tracebaggage headers가 있는지 확인하세요. 있다면 분산 추적이 정상 동작하는 것입니다.

// gRPC server interceptor with Sentry instrumentation
function sentryInterceptor(methodDescriptor, nextCall) {
// Extract Sentry trace headers from the incoming metadata
const metadata = nextCall.metadata.getMap();
const sentryTrace = metadata["sentry-trace"];
const baggage = metadata["baggage"];
return new grpc.ServerInterceptingCall(nextCall, {
start: (next) => {
// Continue the trace using the extracted context
Sentry.continueTrace({ sentryTrace, baggage }, () => {
// Create a manual span that won't auto-close until we end it
Sentry.startSpanManual(
{
name: methodDescriptor.path,
op: "grpc.server",
forceTransaction: true, // Make this a transaction in the Sentry UI
attributes: {
"grpc.method": methodDescriptor.path,
"grpc.service": methodDescriptor.service.serviceName,
"grpc.status_code": grpc.status.OK,
},
},
(span) => {
// Store the span for later use
nextCall.sentrySpan = span;
next();
},
);
});
},
sendStatus: (status, next) => {
const span = nextCall.sentrySpan;
if (span) {
// Update status based on the gRPC result
if (status.code !== grpc.status.OK) {
span.setStatus({ code: 2, message: "error" });
span.setAttribute("grpc.status_code", status.code);
span.setAttribute("grpc.status_description", status.details);
}
// End the span when the call completes
span.end();
}
next(status);
},
});
}
// Add the interceptor to your gRPC server
const server = new grpc.Server({
interceptors: [sentryInterceptor],
});
// In your service implementation, use the active span
const serviceImplementation = {
myMethod: async (call, callback) => {
try {
const span = call.call?.nextCall?.sentrySpan;
// Use withActiveSpan to make the span active during service execution
await Sentry.withActiveSpan(span, async () => {
// Create child spans for operations within the service
await Sentry.startSpan(
{ name: "database.query", op: "db" },
async (childSpan) => {
// Database operations here
const result = await database.query("SELECT * FROM table");
childSpan.setAttribute("db.rows_affected", result.rowCount);
},
);
callback(null, { result: "success" });
});
} catch (error) {
// Capture the error with the current span as context
Sentry.captureException(error);
callback(error);
}
},
};
function createGrpcClient() {
// Create client with interceptor
return new MyServiceClient(address, grpc.credentials.createInsecure(), {
interceptors: [
(options, nextCall) => {
return new grpc.InterceptingCall(nextCall(options), {
start: (callMetadata, listener, next) => {
// `callMetadata` is the metadata object for the outgoing gRPC call.
// We will add our Sentry tracing headers to this object.
// Get current trace information from Sentry
const traceData = Sentry.getTraceData();
// Add Sentry trace and baggage headers to the call's metadata
if (traceData) {
callMetadata.set("sentry-trace", traceData["sentry-trace"]);
callMetadata.set("baggage", traceData["baggage"]);
}
// Proceed with the call, now including the Sentry headers in its metadata
next(callMetadata, listener);
},
});
},
],
});
}