Thumbnail image

Table of Contents

[스프링인액션] 스프링 액추에이터 사용

스프링인액션 16장을 읽고 스프링 액추에이터 사용법에 대해 정리하였다.

  • 스프링 액추에이터 활성화
  • 액추에이터 엔드포인트
  • 액추에이터 커스터마이징
  • 액추에이터 보안 처리

에 대해 작성하였다.

액추에이터란?


엑추에이터는 스프링 부트 어플리케이션의 모니터링이나 메트릭(metric)과 같은 기능을 HTTP와 JMX 엔드포인트를 통해 제공한다. 즉, 실행 중인 어플리케이션의 내부를 볼 수 있고 어플리케이션의 작동 방법을 제어할 수 있다.

액추에이터가 노출하는 엔드포인트를 사용하여 얻을 수 있는 정보는 다음과 같다.

  • 어플리케이션 환경에서 사용할 수 있는 구성 속성
  • 패키지의 로깅 레벨
  • 어플리케이션이 사용 중인 메모리
  • 엔드포인트가 받은 요청 횟수
  • 어플리케이션 건강 상태 정보

액추에이터 활성화

액추에이터를 활성화하려면 액추에이터의 스타터 의존성을 추가해야 한다.

1implementation 'org.springframework.boot:spring-boot-starter-actuator'

액추에이터를 활성화하면 어플리케이션에서 액추에이터 엔드포인트를 사용할 수 있다.

액추에이터 엔드포인트 정보는 아래의 링크에서 확인할 수 있다.

https://docs.spring.io/spring-boot/docs/2.1.7.RELEASE/reference/html/production-ready-endpoints.html

액추에이터 설정

액추에이터 기본 경로 구성

모든 엔드포인트 경로에는 /actuator가 앞에 붙는데, 기본 경로를 변경한 예제이다.

1management:
2  endpoints:
3    web:
4      base-path: /management

기본 경로는 application.yml 파일의 management.endpoint.web.base-path 속성에서 변경할 수 있다. 이후 건강 상태 정보를 얻고 싶다면 /management/health GET 요청을 보내면 된다.

액추에이터 엔드포인트 활성화/비활성화

엔드포인트 노출 여부를 제어하는 코드이다.

1management:
2  endpoints:
3    web:
4      exposure:
5        include: health, info, beans, conditions

application.yml 파일의 management.endpoint.web.excludemanagement.endpoint.web.include 속성을 통해 노출을 원하는 엔드포인트를 지정할 수 있다. exclude는 제외할 엔드포인트, include는 노출할 엔드포인트를 의미한다.

액추에이터 엔드포인트 사용


엑추에이터의 HTTP 엔드포인트를 제공하는데, REST API 혹은 curl 명령어를 통해 엔드포인트를 사용할 수 있다. /actuator GET 요청을 하면 엔드포인트의 HATEOAS 링크를 응답으로 받을 수 있다.

어플리케이션 기본 정보 받기

/info 엔드포인트를 통해 어플리케이션의 기본 정보를 알 수 있다.

1info:
2  contact:
3    email: email@email.com
4    phone: 010-0000-0000

/info 엔드포인트가 반환하는 정보를 설정하는 방법은 여러가지가 있는데, 위에서 설정한 바와 같이 application.yml 에서 info로 시작하는 구성 속성을 설정하는 방법도 있다.

/health 엔드포인트를 통해 어플리케이션의 건강 상태 정보를 얻을 수 있다.

1$ curl localhost:8080/actuator/health
2{"status": "UP"}

기본적으로 종합된 건강 상태 정보를 가지는 JSON 응답을 받는다.

건강 상태를 나타내는 각 지표

  • 모든 건강 지표가 UP → 건강 상태 UP
  • 하나의 건강 지표가 DOWN → 건강 상태 DOWN
  • 하나의 건강 지표가 OUT_OF_SERVICE → 건강 상태 OUT_OF_SERVICE
  • 하나의 건강 지표가 UNKNOWN → 종합 건강 상태에서 고려되지 않음

건강 지표를 자세히 보려면 다음과 같이 구성한다.
1management:
2  endpoint:
3    health:
4      show-details: always

management.endpoint.health.show-details 속성을 always로 설정하면 모든 건강 지표를 자세히 볼 수 있다. 기본값은 never이고, when-authorized로 설정 시 클라이언트가 인가된 경우에 한해 상세한 내역을 보여준다.

구성 상세 정보 보기

빈 연결 정보를 얻으려면 /beans 엔드포인트를 사용한다.

 1{
 2  "contexts" : {
 3    "application-1": {
 4      "beans": {
 5        "ingredientsController": {
 6          "aliases": [],
 7          "scope": "singleton",
 8          "type": "tacos.ingredients.IngredientsController",
 9          "resource": "file [...ingredients/IngredientsController.class]",
10          "dependencies": ["ingredientRepository"]
11        },
12      },
13      "parentId": null
14    }
15  }
16}

해당 어플리케이션 컨텍스트에 있는 모든 빈의 상세 정보를 포함한다.

자동-구성 내역을 알기 위해서는 /conditions 엔드포인트를 사용한다.

 1{
 2  "contexts" : {
 3    "application-1": {
 4      "positiveMatches": {
 5        "MongoDataAutoConfiguration#mongoTemplate": [
 6          {
 7            "condition": "OnBeanCondition",
 8            "message": "..."
 9          }
10        ],
11      },
12      "negativeMatches": {
13        "DispatcherServletAutoConfiguration": {
14          "notMatched": [],
15          "matched": []
16        },
17      },
18      "unconditionalClasses": []  
19    }
20  }
21}

positiveMatches에서는 자동 구성이 된 빈들을 보여주고, negativeMatches에서는 자동 구성이 어떠한 이유때문에 실패했다는 것을 보여준다. unconditionalClassess는 조건 없이 자동 구성된 빈을 보여준다.

환경 속성과 구성 속성을 보기 위해서는 /env 엔드포인트를 사용한다.

 1{
 2  "activeProfiles": [
 3    "development"
 4  ],
 5  "propertySources": [
 6    {
 7      "name": "systemEnvironment",
 8      "properties": {
 9        "PATH": {
10          "value": "/usr/bin:/bin:/usr/sbin:/sbin",
11          "origin": "System Environment Property \"PATH\""
12        }
13      }
14    },
15    {
16      "name": "applicationConfig: [classpath:/application.yml]",
17      "properties": {
18        "spring.application.name": {
19          "value": "ingredient-service",
20          "origin": "class path resource [application.yml]:3:11"
21        },
22        "server.port": {
23          "value": 8081,
24          "origin": "class path resource [application.yml]:9:9"
25        },
26      }
27    },
28  ]
29}  

activeProfiles 필드를 통해 활성화된 프로파일을 알 수 있고, propertySources 필드에서는 스프링 어플리케이션 환경의 속성 근원들을 포함한다.


모든 HTTP 요청 핸들러 내역을 확인하려면 /mappings 엔드포인트를 사용한다.

 1{
 2  "contexts" : {
 3    "application-1": {
 4      "mappings": {
 5        "dispatcherHandlers": {
 6          "webHandler": [
 7            {
 8              "predicate": "{[/ingredients],methods=[GET]}",
 9              "handler": "...",
10              "details": {
11                "handlerMethod": {
12                  "className": "tacos.ingredients.IngredientsController",
13                  "name": "allIngredients",
14                  "descriptor": "()Lreactor/core/publisher/Flux;"
15                },
16                "handlerFunction": null,
17                "requestMappingConditions": {
18                  "consumes": [],
19                  "headers": [],
20                  "methods": ["GET"],
21                  "params": [],
22                  "patterns": ["/ingredients"],
23                  "produces": []
24                }
25              }  
26            },
27          ]
28        }
29      },
30      "parentId": "application-1"
31    }
32  }
33}

특정 API의 요청이 어떤 메서드에서 처리되는지 확인할 수 있다.

로깅 레벨을 관리하려면 /loggers 엔드포인트를 사용한다.

 1{
 2  "levels": [ "OFF", "ERROR", "WARN", "INFO", "DEBUG", "TRACE" ],
 3  "loggers": {
 4    "ROOT": {
 5      "configuredLevel": "INFO", "effectiveLevel": "INFO"
 6    },
 7    "org.springframework.web": {
 8      "configuredLevel": null, "effectiveLevel": "INFO"
 9    }
10  }
11}

응답의 맨 앞에는 모든 로깅 레벨 내역이 노출되고, 각 패키지에 대한 로깅 상세 내역을 볼 수 있다. configuredLevel 속성은 명시적으로 구성된 로깅 레벨이고, effectiveLevel 속성은 상속받을 수 있는 유효 로깅 레벨이다.

어플리케이션 모니터링

메모리나 스레드 문제를 찾는 gzip 형태의 HPROF 힙 덤프 파일을 다운로드하려면 /heapdump 엔드포인트를 사용한다.

어플리케이션이 처리한 최근 100개의 요청을 알기 위해서는 /httptrace 엔드포인트를 사용한다.

 1{
 2  "traces": [
 3    {
 4      "timestamp": "2018-06-04T23:41:24.494Z",
 5      "principal": null,
 6      "session": null,
 7      "request": {
 8        "method": "GET",
 9        "uri": "http://localhost:8081/ingredients",
10        "headers": {
11          "Host": ["localhost:8081"],
12          "User-Agent": ["curl/7.54.0"],
13          "Accept": ["*/*"]
14        },
15        "remoteAddress": null
16      },
17      "response": {
18        "status": 200,
19        "headers": {
20          "Content-Type": ["application/json;charset=UTF-8"]
21        }
22      },
23      "timeTaken": 4
24    },
25  ]
26} 

스프링 부트 Admin에서는 이런 HTTP 추적 정보를 실시간으로 캡쳐하여 그래프로 보여준다.

현재 실행 중인 스레드에 관한 스냅샷을 확인하려면 /threaddump 엔드포인트를 사용한다.

 1{
 2  "threadName": "reactor-http-nio-8",
 3  "threadId": 338,
 4  "blockedTime": -1,
 5  "blockedCount": 0,
 6  //...
 7  "stackTrace": [
 8    {
 9      "methodName": "kevent0",
10      "fileName": "KQueueArrayWrapper.java",
11      "lineNumber": -2,
12      "className": "sun.nio.ch.KQueueArrayWrapper",
13      "nativeMethod": true
14     },
15   ],
16   "lockedMonitors": [],
17   "lockedSynchronizers": [],
18   "lockInfo": null
19}

요청 시점의 스레드 활동에 대한 스냅샷을 확인할 수 있다. 스레드 덤프에는 스레드의 블로킹과 록킹 상태의 관련 상세 정보와 스택 기록 등을 보여준다.

메모리, 프로세스, 가비지 컬렉션, HTTP 요청 관련 메트릭을 확인하려면 /metrics 엔드포인트를 사용한다.
/metrics/{메트릭 종류} 형식으로 사용한다면 해당 종류의 메트릭에 대한 상세한 정보를 알 수 있다.

 1$ curl localhost:8081/actuator/metrics/http.server.requests?tag=status:404&tag=uri:/**
 2{ 
 3  "name": "http.server.requests",
 4  "measurements": [
 5    { "statistic": "COUNT", "value": 30 },
 6    { "statistic": "TOTAL_TIME", "value": 0.519791548 },
 7    { "statistic": "MAX", "value": 0 }, 
 8  ],
 9  "availableTags": [
10    { "tag": "exception", "values": [ "ResponseStatusException" ] },
11    { "tag": "method", "values": [ "GET" ] }
12  ]
13}

tag에 stauts, url 태그 이름과 값을 지정하면 HTTP 404 응답을 받은 /** 경로에 대한 메트릭을 볼 수 있다.

액추에이터 커스터마이징


어플리케이션의 특정 요구를 충족하기 위해 커스터마이징을 할 수 있다.

/info 엔드포인트 커스터마이징

InfoContributor라는 인터페이스를 사용하여 /info 엔드포인트 응답에 커스텀 데이터를 추가할 수 있다.

 1@Component
 2public class TacoCountInfoContributor implements InfoContributor {
 3  private TacoRepository tacoRepo;
 4  
 5  public TacoCountInfoContributor(TacoRepository tacoRepo) {
 6    this.tacoRepo = tacoRepo;
 7  }
 8
 9  @Override
10  public void contribute(Builder builder) {
11    long tacoCount = tacoRepo.count();
12    Map<String, Object> tacoMap = new HashMap<>();
13    tacoMap.put("count", tacoCount);
14    builder.withDetail("taco-stats", tacoMap);
15  }
16}

InfoContributor 인터페이스 구현체인 contribute() 메서드 내부에 인자로 받은 Builder 객체의 withDetails() 를 호출하여 /info 엔드포인트에 정보를 추가할 수 있다.

빌드 정보를 주입하려면 build.gradle 파일에 다음과 같이 추가한다.

1springBoot {
2  buildInfo()
3}

Git 커밋 정보를 주입하려면 build.gradle 파일에 다음과 같이 추가한다.

1plugins {
2  id "com.gorylenko.gradle-git-properties" version "1.4.17"
3}

이 정보는 프로젝트가 빌드된 시점의 간단한 코드 상태만을 나타낸다.
자세한 정보를 얻기 위해서는 management.info.git.mode 속성을 full로 설정하면 된다. - application.yml

건강 지표 커스터마이징

HealthIndicator 인터페이스를 사용하여 스프링 부트에서 지원하지 않는 외부 시스템의 건강 상태 정보를 제공할 수 있다.

 1@Component
 2public class WackoHealthIndicator implements HealthIndicator {
 3  @Override
 4  public Health health() {
 5    int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
 6    if (hour > 12) {
 7      return Health.outOfService().withDetail("reason", "out of service")
 8                   .withDetail("hour", hour).build();
 9    }
10    
11    if (Math.random() < 0.1) {
12      return Health.outOfService().withDetail("reason", "break 10% of the time")
13                   .build();
14    }    
15    
16    return Health.up().withDetail("reason", "good").build();
17  }
18}

외부 시스템에 원격 호출을 한 후 받은 응답 기준으로 건강 상태를 결정하는 방식으로 유용하게 사용할 수 있다.

커스텀 메트릭 등록

Micrometer의 MeterRegistry를 사용하여 스프링 부트 어플리케이션에서 메트릭을 발행할 수 있다. 메트릭을 발행할 때는 필요한 곳에 MeterRegistry를 주입하면 된다.

 1@Component
 2public class TacoMetrics extends AbstractRepositoryEventListener<Taco> {
 3  private MeterRegistry meterRegistry;
 4  public TacoMetrics(MeterRegistry meterRegistry) {
 5    this.meterRegistry = meterRegistry;
 6  }
 7
 8  @Override
 9  protected void onAfterCreate(Taco taco) {
10    List<Ingredient> ingredients = taco.getIngredients();
11    for (Ingredient ingredient : ingredients) {
12      meterRegistry.counter("tacocloud", "ingredient", ingredient.getId()).increment();
13    }
14  }
15}

스프링 데이터 클래스인 AbstractRepositoryEventListener의 서브 클래스를 생성하고 새로운 객체가 저장될 때마다 호출되도록 onAfterCreate() 메서드를 오버라이딩한다.

커스텀 엔드포인트 생성

액추에이터 앤드포인트를 지정하려면 @EndPoint 어노테이션을 사용한다. 또한 @GetMapping, @PostMapping 대신 @ReadOperation, @WriteOperation, @DeleteOperation 어노테이션이 지정된 메서드로 정의된다.

 1@Component
 2@Endpoint(id="notes", enableByDefault=true)
 3public class NotesEndpoint {
 4
 5  private List<Note> notes = new ArrayList<>();
 6  
 7  @ReadOperation
 8  public List<Note> notes() {
 9    return notes;
10  }
11  
12  @WriteOperation
13  public List<Note> addNote(String text) {
14    notes.add(new Note(text));
15    return notes;
16  }
17  
18  @DeleteOperation
19  public List<Note> deleteNote(int index) {
20    if (index < notes.size()) {
21      notes.remove(index);
22    }
23    return notes;
24  }
25  
26  @RequiredArgsConstructor
27  private class Note {
28    @Getter
29    private Date time = new Date();
30
31    @Getter
32    private final String text;
33  }
34}

쓰기 오퍼레이션으로 데이터를 제출하고, 읽기 오퍼레이션으로 데이터를 읽을 수 있으며, 삭제 오퍼레이션으로 데이터를 삭제할 수 있다. enableByDefault=true로 지정했기 때문에 해당 엔드포인트를 구성 속성에서 활성화하지 않아도 된다.

엔드포인트를 HTTP 엔드포인트로만 제한하고 싶다면 @WebEndPoint 어노테이션을 사용한다.

1@Component
2@WebEndpoint(id="notes", enableByDefault=true)
3public class NotesEndpoint {
4  ...
5}

액추에이터 보안 처리


권한을 갖는 클라이언트만 엔드포인트를 사용하게 하기 위해서는 스프링 시큐리티를 사용해서 액추에이터의 보안을 처리해야 한다.

액추에이터 엔드포인트의 공통 기본 경로인 /actuator 경로에 대한 보안을 추가하려면 다음과 같이 설정한다.

1@Override
2protected void configure(HttpSecurity http) throws Exception {
3  http.authorizeRequests()
4        .antMatchers("/actuator/**").hasRole("ADMIN")
5      .and()
6      .httpBasic();
7}

ROLE_ADMIN 권한을 가지는 사용자만 액추에이터 엔드포인트를 사용할 수 있다.

엔드포인트 경로를 구성 속성에서 변경했다면 EndpointRequest 클래스를 통해 변경된 경로를 알 수 있다.

1@Override
2protected void configure(HttpSecurity http) throws Exception {
3  http.requestMatcher(EndpointRequest.toAnyEndpoint())
4        .authorizeRequests()
5          .anyRequest().hasRole("ADMIN")
6      .and()
7      .httpBasic();
8}

EndpointRequest.toAnyEndpoint() 메서드는 액추에이터 엔드포인트와 일치하는 요청 경로 모음을 반환한다.
일부 엔드포인트를 제외하고 싶으면 excluding() 메서드를 사용한다.

1@Override
2protected void configure(HttpSecurity http) throws Exception {
3  http.requestMatcher(EndpointRequest.toAnyEndpoint().excluding("health", "info"))
4        .authorizeRequests()
5          .anyRequest().hasRole("ADMIN")
6      .and()
7      .httpBasic();
8}

일부 액추에이터 엔드포인트에만 보안을 적용하고 싶으면 to() 메서드를 호출한다.

1@Override
2protected void configure(HttpSecurity http) throws Exception {
3  http.requestMatcher(EndpointRequest.to("health", "info"))
4        .authorizeRequests()
5          .anyRequest().hasRole("ADMIN")
6      .and()
7      .httpBasic();
8}

Posts in this Series