오픈 트레이싱 맛보기 - NodeJS

오픈 트레이싱의 기본적인 내용을 테스트 해보았는데, 실제로 어떻게 적용할 수 있는지 확인해보려고 한다. 분산추적 Lab에서 오픈 트레이싱이 무엇인지 그리고 간단하게 코드를 수정해서 분산 추적을 테스트 해볼 수 있다.

HTTP 요청 트레이싱

예제코드가 담겨있는 코드를 다운 받고 node js코드가 있는 폴더로 이동한다.

git clone https://github.com/ibm-cloud-architecture/learning-distributed-tracing-101.git
cd learning-distributed-tracing-101/lab-jaeger-nodejs

service-a 수정

먼저 jaeger-client 라이브러리를 추가한다.

지금은 사용되지 않는 라이브러리임 공식적으로는 opentelementry-js 라이브러리가 있다

cd service-a
npm install --save jaeger-client

실행 후 package.json 파일을 확인해보면 jaeger-client가 추가된 것을 확인할 수 있다.

{
  "name": "service",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node app.js",
    "debug": "npm start",
    "vscode": "node --inspect-brk=9229 app.js"
  },
  "dependencies": {
    "bent": "^6.0.5",
    "express": "^4.17.1",
    "jaeger-client": "^3.18.1"
  },
  "devDependencies": {
    "standard": "^16.0.4"
  }
}

코드 수정

jaeger 트레이서를 초기화하는 코드를 app.js의 맨 아래에 삽입하자.

function initTracer (serviceName) {
  const initJaegerTracer = require('jaeger-client').initTracerFromEnv

  // Sampler set to const 1 to capture every request, do not do this for production
  const config = {
    serviceName: serviceName
  }
  // Only for DEV the sampler will report every span
  // Other sampler types are described here: https://www.jaegertracing.io/docs/1.7/sampling/
  config.sampler = { type: 'const', param: 1 }

  return initJaegerTracer(config)
}

함수를 하나씩 살펴보면 처음에 jaeger-client 모듈을 갖고온다.

트레이싱에 필요한 정보가 필요한데 initTracerFromEnv를 이용하면 환경변수로 설정되어있는 값을 가져올 수 있다.

이 예제에서는 config 값을 이용하여 서비스의 이름을 설정하고, 샘플러 설정을 1로 하여 모든 요청을 수집 한다. (실제 서비스 환경에서는 모든 서비스를 수집하지 않음)

이제 이 initTracer 함수를 이용해 Tracer를 초기화 하고 opentracing 모듈에 tracer를 설정 한다.

app.js의 7번 째 줄에 아래의 코드를 삽입하자

const tracer = initTracer(serviceName)
const opentracing = require('opentracing')
opentracing.initGlobalTracer(tracer)

들어오는 모든 HTTP 요청을 추적하기 위해 아래의 함수를 service-a/app.js 의 맨 아래에 추가한다.

function tracingMiddleWare (req, res, next) {
  const tracer = opentracing.globalTracer();
  // 들어온 http 요청으로 부터 트레이싱 헤더들을 추출
  const wireCtx = tracer.extract(opentracing.FORMAT_HTTP_HEADERS, req.headers)
  // 들어온 요청의 context 정보를 이용해 span을 생성 
  const span = tracer.startSpan(req.path, { childOf: wireCtx })
  // 로그 API를 이용하여 로그를 캡쳐
  span.log({ event: 'request_received' })

  // setTag API를 사용해 HTTP 추적을 위한 일반적인 스팬 태그를 캡쳐
  span.setTag(opentracing.Tags.HTTP_METHOD, req.method)
  span.setTag(opentracing.Tags.SPAN_KIND, opentracing.Tags.SPAN_KIND_RPC_SERVER)
  span.setTag(opentracing.Tags.HTTP_URL, req.path)

  // 응답 헤더에서 추적 ID를 찾아 
  // 브라우저에서 볼 수 있는 느린 요청을 디버깅 할 수 있도록 
  // trace ID를 포함시킨다. 
  const responseHeaders = {}
  tracer.inject(span, opentracing.FORMAT_HTTP_HEADERS, responseHeaders)
  res.set(responseHeaders)

  // 스팬 정보를 다른 곳에서도 사용할 수 있도록 요청 object에 스팬 추가 
  Object.assign(req, { span })

  // finalize the span when the response is completed
  const finishSpan = () => {
    if (res.statusCode >= 500) {
      // 샘플링 우선순위를 높여 무조건 HTTP 에러가 수집 되도록 함 
      span.setTag(opentracing.Tags.SAMPLING_PRIORITY, 1)
      // 에러가 발생한 경우 Span에 error 태그를 추가
      span.setTag(opentracing.Tags.ERROR, true)

      // 응답에는 트러블 슈팅을 위한 의미있는 정보가 포함되어야 함
      span.log({ event: 'error', message: res.statusMessage })
    }
    // 상태 코드 캡쳐 
    span.setTag(opentracing.Tags.HTTP_STATUS_CODE, res.statusCode)
    span.log({ event: 'request_end' })
    span.finish()
  }
  res.on('finish', finishSpan)
  next()
}

마지막으로 모든 HTTP 요청을 처리하기 위해 tracingMiddleWare 함수를 첫 미들웨어로 사용할 수 있도록 12번째 라인에 아래의 코드를 추가한다.

app.use(tracingMiddleWare)

앱 실행과 추적해보기

이제 서비스를 실행시켜 보자

docker-compose build
docker-compose up

이제 같은 API를 여러번 호출해 추적을 캡처해본다.

curl http://localhost:8080/sayHello/Carlos
Hello Carlos!

http://localhost:16686/jaeger로 접속해 Jaeger를 실행시키고 추적 정보를 살펴보자.

처음에 넣었던 request_received 로그와 마지막에 넣은 request_end 로그가 들어있는걸 확인할 수 있다.

이제 에러가 발생하도록 아래의 주소를 호출해보자

curl http://localhost:8080/error
some error (ノ ゜Д゜)ノ ︵ ┻━┻

에러 태그가 붙은걸 확인할 수 있다.

태그와 로그를 보면 응답 코드가 500인 것과 로그에 Internal Server Error를 확인할 수 있다.

좋은 웹페이지 즐겨찾기