관측 가능성
관측 가능성은 현대적인 웹 개발의 핵심 요소로, 애플리케이션의 성능과 오류를 모니터링하고 추적하며 디버깅할 수 있게 합니다. TanStack Start는 관측 가능성을 위한 기본 제공 패턴을 제공하고 외부 도구와 원활하게 통합되어 애플리케이션에 대한 포괄적인 인사이트를 제공합니다.
파트너 솔루션: Sentry
포괄적인 관측 가능성을 위해 오류 추적 및 성능 모니터링 분야의 신뢰할 수 있는 파트너인 Sentry를 권장합니다. Sentry는 다음 기능을 제공합니다:
- 실시간 오류 추적 - 전체 스택에서 오류를 포착하고 디버깅합니다
- 성능 모니터링 - 느린 트랜잭션을 추적하고 병목 지점을 최적화합니다
- 릴리스 상태 - 배포를 모니터링하고 시간 경과에 따른 오류율을 추적합니다
- 사용자 영향 분석 - 오류가 사용자에게 미치는 영향을 파악합니다
- TanStack Start 통합 - 서버 함수 및 클라이언트 코드와 원활하게 작동합니다
빠른 설정:
// Client-side (app.tsx)
import * as Sentry from '@sentry/react'
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.NODE_ENV,
})
// Server functions
import * as Sentry from '@sentry/node'
const serverFn = createServerFn().handler(async () => {
try {
return await riskyOperation()
} catch (error) {
Sentry.captureException(error)
throw error
}
})
기본 제공 관측 가능성 패턴
TanStack Start의 아키텍처는 외부 종속성 없이 관측 가능성을 구현할 수 있는 여러 방법을 제공합니다:
서버 함수 로깅
실행, 성능 및 오류를 추적할 수 있도록 서버 함수에 로깅을 추가합니다:
import { createServerFn } from '@tanstack/react-start'
const getUser = createServerFn({ method: 'GET' })
.validator((id: string) => id)
.handler(async ({ data: id }) => {
const startTime = Date.now()
try {
console.log(`[SERVER] Fetching user ${id}`)
const user = await db.users.findUnique({ where: { id } })
if (!user) {
console.log(`[SERVER] User ${id} not found`)
throw new Error('User not found')
}
const duration = Date.now() - startTime
console.log(`[SERVER] User ${id} fetched in ${duration}ms`)
return user
} catch (error) {
const duration = Date.now() - startTime
console.error(
`[SERVER] Error fetching user ${id} after ${duration}ms:`,
error,
)
throw error
}
})
요청/응답 미들웨어
모든 요청과 응답을 기록하는 미들웨어를 만듭니다:
import { createMiddleware } from '@tanstack/react-start'
const requestLogger = createMiddleware().server(async ({ request, next }) => {
const startTime = Date.now()
const timestamp = new Date().toISOString()
console.log(`[${timestamp}] ${request.method} ${request.url} - Starting`)
try {
const result = await next()
const duration = Date.now() - startTime
console.log(
`[${timestamp}] ${request.method} ${request.url} - ${result.response.status} (${duration}ms)`,
)
return result
} catch (error) {
const duration = Date.now() - startTime
console.error(
`[${timestamp}] ${request.method} ${request.url} - Error (${duration}ms):`,
error,
)
throw error
}
})
// Apply to all server routes
export const Route = createFileRoute('/api/users')({
server: {
middleware: [requestLogger],
handlers: {
GET: async () => {
return Response.json({ users: await getUsers() })
},
},
},
})
라우트 성능 모니터링
클라이언트와 서버 모두에서 라우트 로딩 성능을 추적합니다:
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard')({
loader: async ({ context }) => {
const startTime = Date.now()
try {
const data = await loadDashboardData()
const duration = Date.now() - startTime
// Log server-side performance
if (typeof window === 'undefined') {
console.log(`[SSR] Dashboard loaded in ${duration}ms`)
}
return data
} catch (error) {
const duration = Date.now() - startTime
console.error(`[LOADER] Dashboard error after ${duration}ms:`, error)
throw error
}
},
component: Dashboard,
})
function Dashboard() {
const data = Route.useLoaderData()
// Track client-side render time
React.useEffect(() => {
const renderTime = performance.now()
console.log(`[CLIENT] Dashboard rendered in ${renderTime}ms`)
}, [])
return <div>Dashboard content</div>
}
상태 확인 엔드포인트
상태 모니터링을 위한 서버 라우트를 만듭니다:
// routes/health.ts
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/health')({
server: {
handlers: {
GET: async () => {
const checks = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
database: await checkDatabase(),
version: process.env.npm_package_version,
}
return Response.json(checks)
},
},
},
})
async function checkDatabase() {
try {
await db.raw('SELECT 1')
return { status: 'connected', latency: 0 }
} catch (error) {
return { status: 'error', error: error.message }
}
}
오류 경계
포괄적인 오류 처리를 구현합니다:
// Client-side error boundary
import { ErrorBoundary } from 'react-error-boundary'
function ErrorFallback({ error, resetErrorBoundary }: any) {
// Log client errors
console.error('[CLIENT ERROR]:', error)
// Could also send to external service
// sendErrorToService(error)
return (
<div role="alert">
<h2>Something went wrong</h2>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)
}
export function App() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Router />
</ErrorBoundary>
)
}
// Server function error handling
const riskyOperation = createServerFn().handler(async () => {
try {
return await performOperation()
} catch (error) {
// Log server errors with context
console.error('[SERVER ERROR]:', {
error: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
// Add request context if available
})
// Return user-friendly error
throw new Error('Operation failed. Please try again.')
}
})
성능 메트릭 수집
기본 성능 메트릭을 수집하고 노출합니다:
// utils/metrics.ts
class MetricsCollector {
private metrics = new Map<string, number[]>()
recordTiming(name: string, duration: number) {
if (!this.metrics.has(name)) {
this.metrics.set(name, [])
}
this.metrics.get(name)!.push(duration)
}
getStats(name: string) {
const timings = this.metrics.get(name) || []
if (timings.length === 0) return null
const sorted = timings.sort((a, b) => a - b)
return {
count: timings.length,
avg: timings.reduce((a, b) => a + b, 0) / timings.length,
p50: sorted[Math.floor(sorted.length * 0.5)],
p95: sorted[Math.floor(sorted.length * 0.95)],
min: sorted[0],
max: sorted[sorted.length - 1],
}
}
getAllStats() {
const stats: Record<string, any> = {}
for (const [name] of this.metrics) {
stats[name] = this.getStats(name)
}
return stats
}
}
export const metrics = new MetricsCollector()
// Metrics endpoint
// routes/metrics.ts
export const Route = createFileRoute('/metrics')({
server: {
handlers: {
GET: async () => {
return Response.json({
system: {
uptime: process.uptime(),
memory: process.memoryUsage(),
timestamp: new Date().toISOString(),
},
application: metrics.getAllStats(),
})
},
},
},
})
개발용 디버그 헤더
응답에 유용한 디버그 정보를 추가합니다:
import { createMiddleware } from '@tanstack/react-start'
const debugMiddleware = createMiddleware().server(async ({ next }) => {
const result = await next()
if (process.env.NODE_ENV === 'development') {
result.response.headers.set('X-Debug-Timestamp', new Date().toISOString())
result.response.headers.set('X-Debug-Node-Version', process.version)
result.response.headers.set('X-Debug-Uptime', process.uptime().toString())
}
return result
})
환경별 로깅
개발 환경과 프로덕션 환경에 서로 다른 로깅 전략을 구성합니다:
// utils/logger.ts
import { createIsomorphicFn } from '@tanstack/react-start'
type LogLevel = 'debug' | 'info' | 'warn' | 'error'
const logger = createIsomorphicFn()
.server((level: LogLevel, message: string, data?: any) => {
const timestamp = new Date().toISOString()
if (process.env.NODE_ENV === 'development') {
// Development: Detailed console logging
console[level](`[${timestamp}] [${level.toUpperCase()}]`, message, data)
} else {
// Production: Structured JSON logging
console.log(
JSON.stringify({
timestamp,
level,
message,
data,
service: 'tanstack-start',
environment: process.env.NODE_ENV,
}),
)
}
})
.client((level: LogLevel, message: string, data?: any) => {
if (process.env.NODE_ENV === 'development') {
console[level](`[CLIENT] [${level.toUpperCase()}]`, message, data)
} else {
// Production: Send to analytics service
// analytics.track('client_log', { level, message, data })
}
})
// Usage anywhere in your app
export { logger }
// Example usage
const fetchUserData = createServerFn().handler(async ({ data: userId }) => {
logger('info', 'Fetching user data', { userId })
try {
const user = await db.users.findUnique({ where: { id: userId } })
logger('info', 'User data fetched successfully', { userId })
return user
} catch (error) {
logger('error', 'Failed to fetch user data', {
userId,
error: error.message,
})
throw error
}
})
간단한 오류 보고
외부 종속성 없이 기본적인 오류 보고를 구현합니다:
// utils/error-reporter.ts
const errorStore = new Map<
string,
{ count: number; lastSeen: Date; error: any }
>()
export function reportError(error: Error, context?: any) {
const key = `${error.name}:${error.message}`
const existing = errorStore.get(key)
if (existing) {
existing.count++
existing.lastSeen = new Date()
} else {
errorStore.set(key, {
count: 1,
lastSeen: new Date(),
error: {
name: error.name,
message: error.message,
stack: error.stack,
context,
},
})
}
// Log immediately
console.error('[ERROR REPORTED]:', {
error: error.message,
count: existing ? existing.count : 1,
context,
})
}
// Error reporting endpoint
// routes/errors.ts
export const Route = createFileRoute('/admin/errors')({
server: {
handlers: {
GET: async () => {
const errors = Array.from(errorStore.entries()).map(([key, data]) => ({
id: key,
...data,
}))
return Response.json({ errors })
},
},
},
})
외부 관측 가능성 도구
TanStack Start는 내장된 관측 가능성 패턴을 제공하지만, 외부 도구는 더 포괄적인 모니터링을 제공합니다:
기타 인기 도구
애플리케이션 성능 모니터링:
오류 추적:
분석 및 사용자 행동:
New Relic 통합
New Relic은 널리 사용되는 애플리케이션 성능 모니터링 도구입니다. TanStack Start와 통합하는 방법은 다음과 같습니다.
SSR
서버 측 렌더링에 New Relic을 활성화하려면 다음을 수행해야 합니다:
New Relic에서 Node 유형의 새 통합을 생성합니다. 아래에서 사용할 라이선스 키가 제공됩니다.
// newrelic.js - New Relic agent configuration
exports.config = {
app_name: ['YourTanStackApp'], // Your application name in New Relic
license_key: 'YOUR_NEW_RELIC_LICENSE_KEY', // Your New Relic license key
agent_enabled: true,
distributed_tracing: { enabled: true },
span_events: { enabled: true },
transaction_events: { enabled: true },
// Additional default settings
}
// server.tsx
import newrelic from 'newrelic' // Make sure this is the first import
import {
createStartHandler,
defaultStreamHandler,
defineHandlerCallback,
} from '@tanstack/react-start/server'
import type { ServerEntry } from '@tanstack/react-start/server-entry'
const customHandler = defineHandlerCallback(async (ctx) => {
// We do this so that transactions are grouped under the route ID instead of unique URLs
const matches = ctx.router?.state?.matches ?? []
const leaf = matches[matches.length - 1]
const routeId = leaf?.routeId ?? new URL(ctx.request.url).pathname
newrelic.setControllerName(routeId, ctx.request.method ?? 'GET')
newrelic.addCustomAttributes({
'route.id': routeId,
'http.method': ctx.request.method,
'http.path': new URL(ctx.request.url).pathname,
// Any other custom attributes you want to add
})
return defaultStreamHandler(ctx)
})
export default {
fetch(request) {
const handler = createStartHandler(customHandler)
return handler(request)
},
} satisfies ServerEntry
node -r newrelic .output/server/index.mjs
서버 함수 및 서버 라우트
서버 함수와 서버 라우트에 모니터링을 추가하려면 위 단계를 따른 후 다음을 추가해야 합니다:
// newrelic-middleware.ts
import newrelic from 'newrelic'
import { createMiddleware } from '@tanstack/react-start'
export const nrTransactionMiddleware = createMiddleware().server(
async ({ request, next }) => {
const reqPath = new URL(request.url).pathname
newrelic.setControllerName(reqPath, request.method ?? 'GET')
return await next()
},
)
// start.ts
import { createStart } from '@tanstack/react-start'
import { nrTransactionMiddleware } from './newrelic-middleware'
export const startInstance = createStart(() => {
return {
requestMiddleware: [nrTransactionMiddleware],
}
})
SPA 및 브라우저
New Relic에서 React 유형의 새 통합을 생성합니다.
설정을 완료한 후에는 New Relic에서 제공하는 통합 스크립트를 루트 라우트에 추가해야 합니다.
// __root.tsx
export const Route = createRootRoute({
head: () => ({
scripts: [
{
id: 'new-relic',
// either copy/paste your New Relic integration script here
children: `...`,
// or you can create it in your public folder and then reference it here
src: '/newrelic.js',
},
],
}),
})
OpenTelemetry 통합(실험적)
OpenTelemetry는 관측 가능성을 위한 업계 표준입니다. TanStack Start와 통합하는 실험적인 접근 방식은 다음과 같습니다:
// instrumentation.ts - Initialize before your app
import { NodeSDK } from '@opentelemetry/sdk-node'
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
import { Resource } from '@opentelemetry/resources'
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'tanstack-start-app',
[SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
}),
instrumentations: [getNodeAutoInstrumentations()],
})
// Initialize BEFORE importing your app
sdk.start()
// Server function tracing
import { trace, SpanStatusCode } from '@opentelemetry/api'
const tracer = trace.getTracer('tanstack-start')
const getUserWithTracing = createServerFn({ method: 'GET' })
.validator((id: string) => id)
.handler(async ({ data: id }) => {
return tracer.startActiveSpan('get-user', async (span) => {
span.setAttributes({
'user.id': id,
operation: 'database.query',
})
try {
const user = await db.users.findUnique({ where: { id } })
span.setStatus({ code: SpanStatusCode.OK })
return user
} catch (error) {
span.recordException(error)
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
})
throw error
} finally {
span.end()
}
})
})
// Middleware for automatic tracing
import { createMiddleware } from '@tanstack/react-start'
import { trace, SpanStatusCode } from '@opentelemetry/api'
const tracer = trace.getTracer('tanstack-start')
const tracingMiddleware = createMiddleware().server(
async ({ next, request }) => {
const url = new URL(request.url)
return tracer.startActiveSpan(
`${request.method} ${url.pathname}`,
async (span) => {
span.setAttributes({
'http.method': request.method,
'http.url': request.url,
'http.route': url.pathname,
})
try {
const result = await next()
span.setAttribute('http.status_code', result.response.status)
span.setStatus({ code: SpanStatusCode.OK })
return result
} catch (error) {
span.recordException(error)
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
})
throw error
} finally {
span.end()
}
},
)
},
)
참고: 위의 OpenTelemetry 통합은 실험적이며 수동 설정이 필요합니다. 서버 함수, 미들웨어, 라우트 로더에 자동 계측을 제공하는 OpenTelemetry의 기본 지원을 검토하고 있습니다.
빠른 통합 패턴
대부분의 관측성 도구는 TanStack Start와 유사한 통합 패턴을 따릅니다:
// Initialize in app entry point
import { initObservabilityTool } from 'your-tool'
initObservabilityTool({
dsn: import.meta.env.VITE_TOOL_DSN,
environment: import.meta.env.NODE_ENV,
})
// Server function middleware
const observabilityMiddleware = createMiddleware().handler(async ({ next }) => {
return yourTool.withTracing('server-function', async () => {
try {
return await next()
} catch (error) {
yourTool.captureException(error)
throw error
}
})
})
모범 사례
개발 환경과 프로덕션 환경
// Different strategies per environment
const observabilityConfig = {
development: {
logLevel: 'debug',
enableTracing: true,
enableMetrics: false, // Too noisy in dev
},
production: {
logLevel: 'warn',
enableTracing: true,
enableMetrics: true,
enableAlerting: true,
},
}
성능 모니터링 체크리스트
- 서버 함수 성능: 실행 시간을 추적합니다
- 라우트 로딩 시간: 로더 성능을 모니터링합니다
- 데이터베이스 쿼리 성능: 느린 쿼리를 기록합니다
- 외부 API 지연 시간: 서드 파티 서비스 호출을 모니터링합니다
- 메모리 사용량: 메모리 소비 패턴을 추적합니다
- 오류율: 오류 발생 빈도와 유형을 모니터링합니다
보안 고려 사항
- 민감한 데이터(비밀번호, 토큰, 개인 식별 정보)는 절대 기록하지 않습니다
- 더 나은 파싱을 위해 구조화된 로깅을 사용합니다
- 프로덕션 환경에서 로그 순환을 구현합니다
- 규정 준수 요구 사항(GDPR, CCPA)을 고려합니다
향후 OpenTelemetry 지원
TanStack Start에 OpenTelemetry 직접 지원이 추가될 예정이며, 이를 통해 위와 같은 수동 설정 없이 서버 함수, 미들웨어, 라우트 로더를 자동으로 계측할 수 있게 됩니다.
리소스
- Sentry 문서
- OpenTelemetry 문서 - 업계 표준 관측성
- 작동 예제 - 실제로 적용된 관측성 패턴을 확인합니다