Armeria & Spring Boot에서 Prometheus 지표 수집하기
Spring 연동하고 Prometheus 지표 수집까지
이전 글인 Armeria와 Spring Boot 연동하기를 통해 기존 Spring Boot에 Armeria를 쉽게 도입하는 방법에 대해 알아보았습니다. Armeria와 Spring Boot를 연동할 때 적용할 수 있는 것들이 꽤 많은데요. 그 중 하나가 지표 수집입니다. Armeria는 간단한 코드를 추가하여 Prometheus 지표를 수집할 수 있는데 Spring Boot와 함께 사용하는 경우 Armeria를 단독으로 사용할 때와 다른 설정을 사용해야 합니다. 이 글에서는 직접 Spring Boot와 연동한 Armeria에서 지표를 수집하는 코드를 작성했던 과정과 주의해야 할 점들에 대해 이야기해보겠습니다.
Armeria만 쓸 때는 어떻게 하는가
먼저 라인 기술 블로그를 보면 Armeria에서 Prometheus 지표를 사용하는 방법을 자세히 소개합니다. 그 과정을 코드 한 블록으로 간단하게 요약했습니다.
import com.linecorp.armeria.common.metric.MeterIdPrefixFunction; | |
import com.linecorp.armeria.server.metric.MetricCollectingService; | |
import com.linecorp.armeria.server.metric.PrometheusExpositionService; | |
import com.linecorp.armeria.server.Server; | |
import io.micrometer.prometheus.PrometheusConfig; | |
import io.micrometer.prometheus.PrometheusMeterRegistry; | |
PrometheusMeterRegistry meterRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); | |
Server server = | |
Server.builder() | |
.meterRegistry(meterRegistry) | |
.decorator(MetricCollectingService.builder(MeterIdPrefixFunction.ofDefault("my.metric")).newDecorator()) | |
.service("/metrics", PrometheusExpositionService.of(meterRegistry.getPrometheusRegistry())) | |
.build(); |
먼저 기본 설정값으로 PrometheusMeterRegistry
인스턴스를 만듭니다. 그 다음 ServerBuilder#meterRegistry()
메소드를 통해 서버 빌더에 MeterRegistry
인스턴스를 주입합니다. 그리고 MetricCollectingService
를 데코레이터로 추가합니다. 이 때 MeterIdPrefixFunction
을 사용하는데 위 코드에서는 Armeria에서 수집하는 지표의 prefix에 my.metric
을 붙입니다. (실제로는 my_metric_
이 붙습니다.) 마지막으로 Armeria가 제공하는 PrometheusExpositionService
를 사용해 메트릭 정보를 조회할 수 있는 엔드포인트를 생성합니다. 이 간단한 설정을 통해 Armeria에서 Prometheus 지표를 수집할 수 있습니다.
Spring에서 사용하기
그럼 Spring과 Armeria를 함께 사용할 때는 어떻게 Prometheus와 연결할까요? 우선 이전 글인 Armeria와 Spring 연동하기가 완료되었다고 해봅시다. 연동을 위해 ArmeriaServerConfigurator
빈을 만들었던 것과 마찬가지로 빈 설정을 통해 MeterRegistry
, MetricCollectingServiceConfigurator
, MeterIdPrefixFunction
을 주입할 수 있습니다. 참고로 추가적인 빈 설정은 AbstractArmeriaAutoConfiguration#armeriaServer()
에서 보실 수 있습니다.
그럼 하나씩 빈을 만들어보겠습니다. 먼저 MeterRegistry
인스턴스로 사용할 PrometheusMeterRegistry
빈을 만들어줍니다.
@Bean | |
public PrometheusMeterRegistry prometheusMeterRegistry() { | |
return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); | |
} |
다음으로 MetricCollectingServiceConfigurator
빈을 만들어줍니다. 그런데 위 Armeria-Prometheus 예시를 보면 MetricCollectingService
를 사용하였지만 MetricCollectingServiceConfigurator
는 처음 보실 텐데요. 해당 코드를 열어보면 그 역할을 쉽게 알 수 있습니다.
@FunctionalInterface | |
public interface MetricCollectingServiceConfigurator extends Ordered { | |
void configure(MetricCollectingServiceBuilder metricCollectingServiceBuilder); | |
@Override | |
default int getOrder() { | |
return 0; | |
} | |
} |
코드를 보면 @FunctionalInterface
어노테이션을 통해 단순한 함수임을 알 수 있습니다. 이는 MetricServiceCollectingServiceBuilder
를 받아 void
리턴하므로 빌더에 추가적인 설정을 주입해주는 함수임을 쉽게 알 수 있습니다. 또한 getOrder()
메소드를 오버라이드하여 Configurator
간의 우선순위도 설정할 수 있음을 알 수 있네요.
그럼 그 정체를 알았으니 실제 빈을 만들겠습니다. 우선 위 Armeria-Prometheus 예시에서는 MetricCollectingService
에 추가적인 설정없이 기본값으로 사용하였습니다. 따라서 우리가 빈을 만들지 않고 기본 설정으로 사용할 수 있지만 여기서는 어떻게 사용하는지 보기 위해 404 status code도 응답 성공 기준에 포함시키도록 successFunction()
을 사용합니다.
@Bean | |
public MetricCollectingServiceConfigurator metricCollectingServiceConfigurator() { | |
return builder -> builder | |
.successFunction((context, log) -> { | |
final int statusCode = log.responseHeaders().status().code(); | |
// 404 응답도 성공으로 간주한다. | |
return statusCode >= 200 && statusCode < 400 || statusCode == 404; | |
}); | |
} |
MeterIdPrefixFunction
빈 역시 쉽게 만들 수 있습니다.
@Bean | |
public MeterIdPrefixFunction meterIdPrefixFunction() { | |
return MeterIdPrefixFunction.ofDefault("my.armeria.service"); | |
} |
모든 빈 설정이 끝났으니 마지막으로 해당 지표를 조회할 수 있는 엔드포인트를 만들어야 합니다. 이 부분도 Armeria에서 쉽게 사용할 수 있도록 application.yml
(또는 application.properties
) 설정을 통해 엔드포인트를 사용할 수 있습니다. 추가적인 설정은 ArmeriaSettings
에서 보실 수 있습니다.
armeria: | |
internal-services: | |
include: metrics | |
port: 8080 | |
enable-metrics: true # 기본값 true | |
metrics-path: /internal/metrics # 기본값 /internal/metrics |
Armeria는 Spring과 함께 사용할 수 있는 docs, metrics, health, actuator
네 가지 내부 서비스들을 제공합니다. 따라서 armeria.internal-services.include
에 metrics
서비스를 추가합니다. 이 때 armeria.internal-services.port
값을 설정해주지 않으면 사용 가능한 랜덤 포트를 할당합니다. 다음으로 armeria.enable-metrics
, armeria.metric-path
설정을 변경할 수 있습니다. 기본값이 모두 설정되어 있기 때문에 따로 변경하지 않아도 무방합니다.
이제 모든 준비가 끝났습니다. 애플리케이션을 재시작한 뒤 /internal/metrics
에 접속하면 Prometheus 지표를 조회할 수 있습니다. 다음으로 /internal/metrics
엔드포인트를 Prometheus 서비스와 연결하여 사용하시면 됩니다. (이후 연결 내용은 라인 기술 블로그에 자세히 나와있습니다.)
주의할 점
추가적인 팁으로 Spring Boot Actuator를 함께 사용하는 경우 주의할 점이 있습니다. Actuator 역시 설정을 통해 Prometheus 지표를 /actuator/prometheus
엔드포인트로 조회할 수 있습니다. 만약 레거시 Spring Boot가 이미 해당 기능을 사용하고 있다면 위의 코드에서 PrometheusMeterRegistry
빈을 만들면 안됩니다. Actuator가 자동으로 해당 빈을 생성하기 때문에 명시적으로 생성한 빈을 지우지 않으면 지표 수집이 되지 않습니다.
// Spring Boot Acutuator가 자동으로 생성하므로 지워야 한다. | |
@Bean | |
public PrometheusMeterRegistry prometheusMeterRegistry() { | |
return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); | |
} |
또한 Actuator를 함께 사용하는 경우 /actuator/prometheus
, /internal/metrics
두 개의 엔드포인트에서 모두 지표를 수집할 수 있는데요. 만약 둘 중의 하나만 사용하고 싶다면 application.yml
설정에서 armeria.metric-path=""
로 설정하면 됩니다. 반대로 Actuator의 엔드포인트를 닫는 것도 괜찮습니다.
다음 주의할 점은 지표 설정 관련 빈을 만들지 않고 ArmeriaServerConfigurator
빈으로 모든 것을 설정하면 안됩니다. 예를 들어 지표 설정에 대해 잘 모른다면 다음과 같은 코드를 만들 수 있습니다.
@Bean | |
public ArmeriaServerConfigurator armeriaServerConfigurator( | |
TomcatService tomcatService | |
) { | |
PrometheusMeterRegistry meterRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); | |
return serverBuilder -> { | |
serverBuilder | |
.serviceUnder("/", tomcatService) | |
.meterRegistry(meterRegistry) | |
.decorator(MetricCollectingService.builder(MeterIdPrefixFunction.ofDefault("my.metric")).newDecorator()) | |
.service("/my-metric", PrometheusExpositionService.of(meterRegistry.getPrometheusRegistry())) | |
}; | |
} |
Armeria를 단독으로 사용할 때 코드와 유사하게 ArmeriaServerConfigurator
빈의 서버 빌더 설정이 괜찮아보이지만 실제 Armeria 내부 연동 과정 코드를 살펴보면 몇 가지 설정들이 기본값으로 덮어써질 수 있습니다. 다음은 우리가 주입하는 Armeria 설정 빈을 주입받는 빈의 코드입니다.
@Bean | |
@ConditionalOnMissingBean(Server.class) | |
public Server armeriaServer( | |
ArmeriaSettings armeriaSettings, | |
InternalServices internalService, | |
Optional<MeterRegistry> meterRegistry, | |
Optional<List<MetricCollectingServiceConfigurator>> metricCollectingServiceConfigurators, | |
Optional<MeterIdPrefixFunction> meterIdPrefixFunction, | |
Optional<List<ArmeriaServerConfigurator>> armeriaServerConfigurators, | |
Optional<List<Consumer<ServerBuilder>>> armeriaServerBuilderConsumers, | |
Optional<List<DependencyInjector>> dependencyInjectors, | |
BeanFactory beanFactory) { | |
// 코드 생략 | |
configureServerWithArmeriaSettings(serverBuilder, armeriaSettings, internalService, | |
armeriaServerConfigurators.orElse(ImmutableList.of()), | |
armeriaServerBuilderConsumers.orElse(ImmutableList.of()), | |
// 빈으로 주입하지 않는 경우 글로벌 설정의 기본값 사용 | |
meterRegistry.orElse(Flags.meterRegistry()), | |
// 빈으로 주입하지 않는 경우 "armeria.server"를 기본 prefix로 사용 | |
meterIdPrefixFunction.orElse(MeterIdPrefixFunction.ofDefault("armeria.server")), | |
metricCollectingServiceConfigurators.orElse(ImmutableList.of()), | |
dependencyInjectors.orElse(ImmutableList.of()), | |
beanFactory); | |
return serverBuilder.build(); | |
} |
코드 주석에 표시했듯이 MeterRegistry
, MeterIdPrefixFunction
빈 같은 경우 빈 주입으로 사용하지 않으면 기본값으로 덮어써질 위험이 있습니다. 따라서 반드시 직접 ServerBuilder
를 사용하는 대신 빈을 사용하여야 합니다. 참고로 해당 주의사항은 시간이 지나면 Armeria의 코드가 변경될 수 있기 때문에 직접 사용하기 전에 확인해보시기 바랍니다.
Armeria를 연동하고 공유하자
지구에서 자바로 서버 개발을 하고 있다면 십중팔구 Spring 프레임워크를 사용하고 있을 겁니다. Spring은 편리하고 강력하지만 Armeria를 함께 사용하면 더 쉽게 해결할 수 있는 요구사항들이 있습니다. 예를 들어 gRPC, Thrift 서비스를 같은 port를 사용하는 엔드포인트에 추가할 수 있고 이벤트 루프를 사용하는 멀티플렉싱 서버이기 때문에 트래픽 부하를 줄일 수 있습니다. (이전 글인 Armeria Spring 연동하기에서 보실 수 있습니다.)
그리고 Armeria는 단순한 연동 외에 추가적인 내부 서비스로 지표 수집, 문서화, 헬스체크, Actuator 연동(metrics
, docs
, health
, actuator
)을 제공합니다. 지표 수집의 경우 Dropwizard를 사용하는 것도 가능합니다. 이 글에서는 Prometheus 지표 수집 서비스를 빈 설정을 통해 사용하는 방법에 대해 알아보았습니다.
아직 Armeria에는 해당 과정이 문서화 되어있지 않아 직접 코드를 읽고 테스트하면서 시스템을 구축했습니다.(현재 제가 문서화 진행 중입니다.) 그 과정에서 더 자세한 내부 동작을 탐구하게 되어 얻은 지식도 많았습니다. 저는 Armeria를 알게 된 시간이 길지 않지만 장점이 많고 계속해서 발전할 수 있다고 생각합니다. 만약 이 글을 읽고 Armeria에 관심이 생겼다면 직접 시도해보고 그 지식을 공유해주세요. Armeria와 개발자들에게 많은 도움이 되리라 생각합니다!