토리맘의 한글라이즈 프로젝트 logo 토리맘의 한글라이즈 프로젝트

스프링 클라우드 슬루스 공식 레퍼런스를 한글로 번역한 문서입니다.

전체 목차는 여기에 있습니다.

이번 섹션에선 Spring Cloud Sleuth 사용법에 대해 좀 더 자세히 설명한다. 여기에서 다루는 주제들 중에는 Spring Cloud Sleuth API나 애노테이션을 이용해 span의 수명 주기를 제어하는 것과 같은 주제도 있다. 또한 Spring Cloud Sleuth와 관련된 몇 가지 모범 사례best practice도 함께 다룬다.

Spring Cloud Sleuth가 처음이라면, 이번 섹션을 시작하기 전에 Getting Started 가이드를 먼저 읽어보는 것을 추천한다.

목차


3.1. Span Lifecycle with Spring Cloud Sleuth’s API

Spring Cloud Sleuth의 핵심 코드는 api 모듈에 있는데, 여기에는 트레이서tracer가 구현해야 하는 모든 인터페이스가 들어 있다. Spring Cloud Sleuth 프로젝트엔 OpenZipkin Brave 구현체가 포함되어 있다. 트레이서tracer가 Sleuth의 API에 어떻게 연결되는지는 org.springframework.cloud.sleuth.brave.bridge를 보면 알 수 있다.

가장 많이 사용하는 인터페이스는 다음과 같다:

물론 트레이서tracer 구현체의 API를 직접 사용할 수도 있다.

이어서 Span 수명 주기에 따른 작업을 살펴보자.

Tracer 인스턴스는 Spring Cloud Sleuth가 하나 생성해줄 거다. 이 인스턴스를 사용하고 싶다면, 자동 주입autowire을 이용하면 된다.

3.1.1. Creating and Ending Spans

span은 아래 예제와 같이 Tracer를 사용해 직접 생성할 수 있다:

// Start a span. If there was a span present in this thread it will become
// the `newSpan`'s parent.
Span newSpan = this.tracer.nextSpan().name("calculateTax");
try (Tracer.SpanInScope ws = this.tracer.withSpan(newSpan.start())) {
    // ...
    // You can tag a span
    newSpan.tag("taxValue", taxValue);
    // ...
    // You can log an event on a span
    newSpan.event("taxCalculated");
}
finally {
    // Once done remember to end the span. This will allow collecting
    // the span to send it to a distributed tracing system e.g. Zipkin
    newSpan.end();
}

앞의 예제에서는 span의 인스턴스를 새로 만드는 방법을 확인할 수 있었다. 현재 스레드에 이미 span이 있는 경우, 기존 span은 새 span의 부모가 된다.

span을 생성한 다음에는 반드시 정리해줘야 한다.

span에 50자를 초과하는 이름을 지정하면 50자로 잘리게된다. 이름은 명시적이면서 구체적이어야 한다. 하지만 이름이 너무 길어지면 지연 이슈가 생겨나고 때에 따라 예외가 발생하기도 한다.

3.1.2. Continuing Spans

때에 따라서 span을 새로 만들기보단 기존 span을 계속 사용하길 바랄 수도 있다. 예를 들면 다음과 같은 상황이 있을 수 있다:

span을 계속 이어가려면 아래 예제에서처럼 특정 스레드에 저장한 span을 다른 스레드로 넘겨주면 된다.

Span spanFromThreadX = this.tracer.nextSpan().name("calculateTax");
try (Tracer.SpanInScope ws = this.tracer.withSpan(spanFromThreadX.start())) {
    executorService.submit(() -> {
        // Pass the span from thread X
        Span continuedSpan = spanFromThreadX;
        // ...
        // You can tag a span
        continuedSpan.tag("taxValue", taxValue);
        // ...
        // You can log an event on a span
        continuedSpan.event("taxCalculated");
    }).get();
}
finally {
    spanFromThreadX.end();
}

3.1.3. Creating a Span with an explicit Parent

span을 새로 시작면서 이 span의 부모를 명시하고 싶을 수도 있다. 예를 들어서 어떤 스레드에서 새 span을 시작하고 싶은데, 다른 스레드에 이 span의 부모가 있다고 가정해 보자. Tracer.nextSpan()을 호출하면 언제나 현재 스코프 내에 있는 span을 참조해서 span을 만든다. 다음 예제와 같이 스코프 안에 span을 집어넣은 다음 Tracer.nextSpan()을 호출하면 된다:

// let's assume that we're in a thread Y and we've received
// the `initialSpan` from thread X. `initialSpan` will be the parent
// of the `newSpan`
Span newSpan = null;
try (Tracer.SpanInScope ws = this.tracer.withSpan(initialSpan)) {
    newSpan = this.tracer.nextSpan().name("calculateCommission");
    // ...
    // You can tag a span
    newSpan.tag("commissionValue", commissionValue);
    // ...
    // You can log an event on a span
    newSpan.event("commissionCalculated");
}
finally {
    // Once done remember to end the span. This will allow collecting
    // the span to send it to e.g. Zipkin. The tags and events set on the
    // newSpan will not be present on the parent
    if (newSpan != null) {
        newSpan.end();
    }
}

이렇게 span을 만든 후에는 반드시 완료해줘야 한다. 그렇지 않으면 리포트되지 않는다 (e.g. Zipkin에).

Tracer.nextSpan(Span parentSpan)과 같이 사용하면 부모 span을 직접 명시해줄 수도 있다.


3.2. Naming Spans

span 이름을 잘 고르는 것은 쉽지만은 않다. span의 이름을 보면 어떤 연산operation인지 알 수 있어야 한다. 이름은 카디널리티가 높으면 안 되기 때문에, 식별자를 포함해선 안 된다.

Sleuth에선 굉장히 많은 것들을 계측instrumentation하기 때문에, 다소 인위적인 span 이름도 존재한다:

다행히도 비동기 처리의 경우 이름을 직접 명시할 수 있다.

3.2.1. @SpanName Annotation

아래 예제와 같이 @SpanName 애노테이션을 통해 span의 이름을 직접 지정할 수 있다:

@SpanName("calculateTax")
class TaxCountingRunnable implements Runnable {

    @Override
    public void run() {
        // perform logic
    }

}

이 경우, 다음과 같은 방식으로 코드를 실행하면 span의 이름은 calculateTax가 된다:

Runnable runnable = new TraceRunnable(this.tracer, spanNamer, new TaxCountingRunnable());
Future<?> future = executorService.submit(runnable);
// ... some additional logic ...
future.get();

3.2.2. toString() Method

Runnable이나 Callable을 직접 상속해서 별도 클래스를 정의하는 경우는 많지 않다. 보통 필요하면 익명 클래스를 만들어 사용한다. 하지만 익명 클래스에는 애노테이션을 달 수 없다. Sleuth는 이렇게 @SpanName 애노테이션이 없을 때는, 해당 클래스가 toString() 메소드를 재정의했는지를 확인하기 때문에, 이럴 땐 toString() 메소드를 활용하면 된다.

다음과 같은 코드를 실행하면 calculateTax라는 이름의 span이 만들어진다:

Runnable runnable = new TraceRunnable(this.tracer, spanNamer, new Runnable() {
    @Override
    public void run() {
        // perform logic
    }

    @Override
    public String toString() {
        return "calculateTax";
    }
});
Future<?> future = executorService.submit(runnable);
// ... some additional logic ...
future.get();

3.3. Managing Spans with Annotations

span을 애노테이션으로 관리하면 좋은 점들이 몇 가지 있다. 예를 들어:

3.3.1. Creating New Spans

로컬 span을 직접 생성하고 싶지 않다면 @NewSpan 애노테이션을 사용하면 된다. 또한 @SpanTag 애노테이션도 제공하고 있으므로, 태그 추가도 자동화할 수 있다.

이제 몇 가지 사용 예시를 살펴보자.

@NewSpan
void testMethod();

파라미터가 없는 메소드에 애노테이션을 달면, 해당 메소드와 이름이 동일한 새 span이 생성된다.

@NewSpan("customNameOnTestMethod4")
void testMethod4();

애노테이션에 값을 설정해주면 (name 파라미터를 명시해도 되고, 생략해도 된다) 이 값을 이름으로 가진 span을 생성한다.

// method declaration
@NewSpan(name = "customNameOnTestMethod5")
void testMethod5(@SpanTag("testTag") String param);

// and method execution
this.testBean.testMethod5("test");

이름과 태그를 조합해서 사용할 수도 있다. 태그의 경우, 애노테이션을 선언한 메소드가 런타임에 받은 파라미터 값이 바로 태그의 값이 된다. 위 예제에선 태그의 키는 testTag, 태그 값은 test다.

@NewSpan(name = "customNameOnTestMethod3")
@Override
public void testMethod3() {
}

@NewSpan 애노테이션은 클래스 위에도, 인터페이스 위에도 선언할 수 있다. 인터페이스에 있는 메소드를 재정의하고 @NewSpan 애노테이션으로 다른 값을 지정했다면, 상속한 쪽에 있는 값을 우선시한다 (위 케이스에선 customNameOnTestMethod3로 설정된다).

3.3.2. Continuing Spans

기존 span에 태그와 애노테이션을 추가하고 싶다면, 다음과 같이 @ContinueSpan 애노테이션을 사용하면 된다:

// method declaration
@ContinueSpan(log = "testMethod11")
void testMethod11(@SpanTag("testTag11") String param);

// method execution
this.testBean.testMethod11("test");
this.testBean.testMethod13();

(@NewSpan 애노테이션과는 다르게, log 파라미터를 이용해 로그를 추가할 수도 있다.)

그러면 기존 span을 계속해서 이어가고,

3.3.3. Advanced Tag Setting

span은 세 가지 방법으로 태그를 추가할 수 있다. 세 가지 방법 모두 SpanTag 애노테이션으로 제어하며, 태그 값은 다음과 같은 우선순위를 따른다:

  1. TagValueResolver 타입 빈이 있다면 TagValueResolver를 사용한다.
  2. TagValueResolver 타입 클래스명을 지정하지 않은 경우 표현식을 평가해 본다. 이땐 TagValueExpressionResolver 빈을 찾아본다. 디폴트 구현체는 SPEL 표현식을 해석한다. (주의) 이때 SPEL 표현식에서는 프로퍼티 참조만 가능하다. 보안 상의 이유로 메소드 실행은 허용하지 않고있다.
  3. 평가할 표현식을 찾지 못하면 파라미터 값으로 toString()을 호출한다.

Custom Extractor

아래 있는 메소드의 태그 값은 TagValueResolver 인터페이스의 구현체로 계산한다. 이 클래스명은 resolver 속성 값으로 전달해야 한다.

다음과 같은 메소드에 애노테이션이 선언되어 있다:

@NewSpan
public void getAnnotationForTagValueResolver(
        @SpanTag(key = "test", resolver = TagValueResolver.class) String test) {
}

이제 아래 TagValueResolver 빈의 구현체를 자세히 살펴보자:

@Bean(name = "myCustomTagValueResolver")
public TagValueResolver tagValueResolver() {
    return parameter -> "Value from myCustomTagValueResolver";
}

위 두 코드에서는 태그 값을 Value from myCustomTagValueResolver와 같이 세팅하고 있다.

Resolving Expressions for a Value

이번엔 메소드에 위에 다음과 같이 애노테이션이 선언되어 있다:

@NewSpan
public void getAnnotationForTagValueExpression(
        @SpanTag(key = "test", expression = "'hello' + ' characters'") String test) {
}

별도로 TagValueExpressionResolver 구현체를 제공하지 않았다면 SPEL 표현식으로 평가하며, span에는 hello characters를 값으로 가진 태그가 세팅된다. 다른 메커니즘으로 표현식을 처리하고 싶다면 빈을 자체적으로 구현하면 된다.

Using The toString() Method

이제 아래 메소드와 애노테이션을 살펴보자:

@NewSpan
public void getAnnotationForArgumentToString(@SpanTag("test") Long param) {
}

위 메소드에 파라미터로 15를 넘겨 실행하면 문자열 "15"를 값으로 가진 태그가 설정된다.


여기까지 따라왔다면 Spring Cloud Sleuth를 어떻게 사용해야 하는지, 따라야 할 모범 사례best practice는 어떤 게 있는지 이해했을 거다. 이제 원하는 Spring Cloud Sleuth 기능에 대해 알아보거나, 앞부분은 건너뛰고 Spring Cloud Sleuth에서 지원하는 여러 가지 통합 기능들에 관해 읽어봐도 좋다.


Next :
Spring Cloud Sleuth Features
스프링 클라우드 슬루스 기능들

전체 목차는 여기에 있습니다.

<< >>

TOP