跳至主要内容

Monitor: Spring Boot Actuator & Prometheus (2)

Monitor: Spring Boot Actuator & Prometheus (2)

In the last blog of this topic, we have make a simple runnable example using Spring Boot 2.0 and Prometheus to do the monitoring of system state. But to make it accessible for old version Spring & Jersey, we need to do more works.

Metric in Actuator

In order to migrate the functionality to old versioned Spring & Jersey, we have to understand how actuator achieve it.

The actuator provides a serials of endpoints (like headdump, logfile, metrics, env) to monitor and get information from our applications and we focus only on metric related endpoints. The metric module has three main abstractions:

  • Metric: the class hold the metric in form of key & value with timestamp;
  • Counter: the service used to increment/decrease count for a metric, like visitor count;
  • Gauge: the service used to set a value for a metric, like CPU usage, Memory usage;

The auto configured default implementation is relative simple. With the following simple config:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    <version>1.5.8.RELEASE</version>
</dependency>
endpoints.enabled=true
# some endpoints are sensitive
endpoints.sensitive=false

We can get the following metric infos.

curl 'http://localhost:8080/metrics'
{"mem":374575,"mem.free":276095,"processors":4,"instance.uptime":612745,"uptime":620414,"systemload.average":7.50830078125,"heap.committed":321024,"heap.init":262144,"heap.used":44928,"heap":3728384,"nonheap.committed":55184,"nonheap.init":2496,"nonheap.used":53551,"nonheap":0,"threads.peak":25,"threads.daemon":21,"threads.totalStarted":28,"threads":23,"classes":6767,"classes.loaded":6767,"classes.unloaded":0,"gc.ps_scavenge.count":8,"gc.ps_scavenge.time":83,"gc.ps_marksweep.count":2,"gc.ps_marksweep.time":92,"httpsessions.max":-1,"httpsessions.active":0,"gauge.response.metrics":163972.0,"gauge.response.star-star.favicon.ico":2.0,"counter.api./favicon.ico":6,"counter.status.200.star-star.favicon.ico":6,"counter.status.200.metrics":6,"counter.api./metrics":6}

This default implementation use MetricFilter to intercept request and record request info into Counter & Gauge and accompany more metrics (like mem & uptime etc) from SystemPublicMetrics like metric collector.

In order to enrich the result of metric, we can use CounterService & GaugeService to customize. The following is an example to count login times.

@Service
public class LoginServiceImpl {
    private final CounterService counterService;   
    public LoginServiceImpl(CounterService counterService) {
        this.counterService = counterService;
    }
     
    public boolean login(String userName, char[] password) {
        boolean success;
        if (userName.equals("admin") && "secret".toCharArray().equals(password)) {
            counterService.increment("counter.login.success");
            success = true;
        } else {
            counterService.increment("counter.login.failure");
            success = false;
        }
        return success;
    }
}

Micrometer

We can customize endpoints and metrics, but there exists an easier way. Spring Boot 2.0 adopted the micrometer dependency as the implementation for metric endpoint and micrometer is back port to Spring 1.5. So we can use micrometer, which is powerful and easy to upgrade.

Comparing with the default implementations, Micrometer has more abstractions except Counter & Gauge.

MeterRegistry

Creates and manages our application’s meters. Exporters use the meter registry to iterate over the set of meters instrumenting your application, and then further iterate over each meter’s metrics, generally resulting in a time series in the metrics backend for each combination of metrics and dimensions.

If we have a dependency on micrometer-registry-{system} in our runtime classpath, Spring Boot will be able to auto configure the registry to use.

Meter Binder

Meter Binders register one or more metrics to Registry to provide information about the state of some aspect of the application or its container. We can find some default binders in MeterBindersConfiguration:

public JvmGcMetrics jvmGcMetrics() {  
    return new JvmGcMetrics();  
}

public JvmMemoryMetrics jvmMemoryMetrics() {  
    return new JvmMemoryMetrics();  
}

More in Metric

In Micrometer, metric has more rich infos than just key, value and timestamp, which is more like the data items in Prometheus (Remember the prometheus output? There exists some tags examples)

Id getId();
Iterable<Measurement> measure();
class Id {  
    private final String name;  
    private final List<Tag> tags;  
    private Type type;
}
public class Measurement {  
    private final Supplier<Double> f;  // measurement value
    private final Statistic statistic; // measurement types
}

Web Monitor

Micrometer contains built-in instrumentation for timings of requests made to Spring MVC and Spring WebFlux server endpoints and it is also implemented via Filters.

Server
# default to be true
management.metrics.web.server.auto-time-requests=true

And if we need customization, we can do it easily via annotations.

@RestController
@Timed (1)
public class MyController {
    @GetMapping("/api/people")
    @Timed(extraTags = { "region", "us-east-1" }) (2)
    @Timed(value = "all.people", longTask = true) (3)
    public List<Person> listPeople() { ... }

Mis

Enabled Endpoint 404 Not Found

If you use the Jersey and set the @ApplicationPath("/") with root path, you will encounter this problem. The console output tells you the /Prometheus is registered, but access it only returns 404.

In order to fix this problem, we need understand that Spring MVC and Jersey all implemented by Filter. If the Jersey intercept the root path, Spring will not dispatch any url. And, our /prometheus endpoint registered in Spring, not in Jersey, so the 404 Not Found.

In order to fix the problem, simply setting the @ApplicationPath("/somePrefix") rather than using root path will fine (Spring dispatcher will handle all other things). If it is not easy to change the url, we have two options, one is to change the default implementation of Jersey to add Spring’s dispatcher, another is to register prometheus endpoint manually using Jersey:

@Path("/prometheus")  
public class PrometheusResource {   
  @Autowired  
  private PrometheusScrapeMvcEndpoint endpoint;  
  
  @GET  
  @Produces(MediaType.TEXT_PLAIN)  
  public Response getPrometheusData() {  
      Object o = endpoint.invoke();  
      ResponseEntity re = (ResponseEntity) o;  
      return Response.ok(re.getBody()).build();  
  }   
}

Aggregate URL with Path Parameter

In our cases, there exists some path which is like send/123/456, i.e. containing some path parameter. This kind of REST style should be aggregated together, otherwise, the number of different urls will be bloated very fast and will be impossible to view in Proemetheus.

As we have introduced, every metric has some tags attached. The url info is actually a tag for a request timing. So we can customize TagProvider to reduce those url.

private Pattern idMatcher = Pattern.compile("/[0-9]+");  
@Override  
public Iterable<Tag> httpRequestTags(HttpServletRequest request, HttpServletResponse response,  
    Object handler, Throwable ex) {  
  Tag tag = WebMvcTags.uri(request, response);  
  String uri = tag.getValue();  
  Matcher matcher = idMatcher.matcher(uri);  
  if (matcher.find()) {  
    String value = matcher.replaceAll("/{id}");  
    return Arrays.asList(WebMvcTags.method(request), Tag.of(tag.getKey(), value),  
        WebMvcTags.exception(ex), WebMvcTags.status(response));  
  }  
  return super.httpRequestTags(request, response, handler, ex);  
}

Full examples can be found here.

Ref

Written with StackEdit.

评论

此博客中的热门博文

Spring Boot: Customize Environment

Spring Boot: Customize Environment Environment variable is a very commonly used feature in daily programming: used in init script used in startup configuration used by logging etc In Spring Boot, all environment variables are a part of properties in Spring context and managed by Environment abstraction. Because Spring Boot can handle the parse of configuration files, when we want to implement a project which uses yml file as a separate config file, we choose the Spring Boot. The following is the problems we met when we implementing the parse of yml file and it is recorded for future reader. Bind to Class Property values can be injected directly into your beans using the @Value annotation, accessed via Spring’s Environment abstraction or bound to structured objects via @ConfigurationProperties. As the document says, there exists three ways to access properties in *.properties or *.yml : @Value : access single value Environment : can access multi

Elasticsearch: Join and SubQuery

Elasticsearch: Join and SubQuery Tony was bothered by the recent change of search engine requirement: they want the functionality of SQL-like join in Elasticsearch! “They are crazy! How can they think like that. Didn’t they understand that Elasticsearch is kind-of NoSQL 1 in which every index should be independent and self-contained? In this way, every index can work independently and scale as they like without considering other indexes, so the performance can boost. Following this design principle, Elasticsearch has little related supports.” Tony thought, after listening their requirements. Leader notice tony’s unwillingness and said, “Maybe it is hard to do, but the requirement is reasonable. We need to search person by his friends, didn’t we? What’s more, the harder to implement, the more you can learn from it, right?” Tony thought leader’s word does make sense so he set out to do the related implementations Application-Side Join “The first implementation

Implement isdigit

It is seems very easy to implement c library function isdigit , but for a library code, performance is very important. So we will try to implement it and make it faster. Function So, first we make it right. int isdigit ( char c) { return c >= '0' && c <= '9' ; } Improvements One – Macro When it comes to performance for c code, macro can always be tried. #define isdigit (c) c >= '0' && c <= '9' Two – Table Upper version use two comparison and one logical operation, but we can do better with more space: # define isdigit(c) table[c] This works and faster, but somewhat wasteful. We need only one bit to represent true or false, but we use a int. So what to do? There are many similar functions like isalpha(), isupper ... in c header file, so we can combine them into one int and get result by table[c]&SOME_BIT , which is what source do. Source code of ctype.h : # define _ISbit(bit) (1 << (