1

I am trying to design an application that will show metrics about it's health and other custom metrics, like how long each function runs and how many times etc. I am on spring boot 2.7.1. I want to create metrics only by using annotations (@Counted, @Timed), but for some reason it is not working for me. It feels like I've tried almost every solution on the internet there is.

My yml file:

logging:
  level:
    root: "INFO"
server:
  port: 8083
spring:
  redis:
    host: "localhost"
    port: 6100
  task:
    scheduling:
      thread-name-prefix: "scheduling-"
      pool:
        size: 2

management:
  endpoint:
    health:
      show-details: always
      group:
        live:
          include: livenessState
        ready:
          include: readinessState, appReadinessIndicator
      probes:
        enabled: true
        livenessState: true
        readinessState: true
  endpoints:
    web:
      exposure:
        include: "*"

my main:

@SpringBootApplication
@ConfigurationPropertiesScan
@EnableScheduling
public class MyApp {

    public static void main(String[] args) {
       SpringApplication.run(MyApp.class, args);
    }

    @Bean
    RouterFunction<ServerResponse> routerFunction() {
       return route(GET("/health"), req ->
             ServerResponse.temporaryRedirect(URI.create("/actuator/health"))
                   .build());
    }

    @Bean
    RouterFunction<ServerResponse> routerFunction2() {
       return route(GET("/metrics"), req ->
             ServerResponse.temporaryRedirect(URI.create("/actuator/prometheus"))
                   .build());
    }
}

the class I am trying to get metrics from.

@Service
public class MyService {
    @Autowired
    private Template template;
    .........
    @Value 
    ....
    
    @Timed("MyTimer")
    @Counted("MyCounter")
    public void handleError() {
        ...
        code
        ...

    }

  

    public void handleMessage (param){
       if (message == null || message.getInfo() == null) {
           this.HandleError();
       }else if 
       ...


   }

   @PostConstruct
   public void setupSubscriber() {
    for (Map.Entry<String, NamespaceConfiguration> namespaceConfiguration : myConfig.getNamespaces().entrySet()) {
        if (!namespaceConfiguration.getKey().equals(MyConfig.NAMESPACE)) {
            this.template.listenToPattern(namespaceConfiguration.getValue().getRequest())
                    .doOnSubscribe(c -> logger.info("Successfully subscribed to {}", namespaceConfiguration.getValue().getRequest()))
                    .doOnNext(msg -> logger.info(msg.getChannel() + ": Received msg " + msg.getMessage()))
                    .onErrorContinue(this::handleError) 
                    .log("OUTER", Level.INFO)
                    .subscribe(this::handleMessage);
        }
    }
    myReadinessIndicator.markAsReady();
}

For it to work I've tried adding this. But it didn't work either.

@Configuration
@EnableAspectJAutoProxy
public class TimedConfiguration {
    @Bean
    public TimedAspect timedAspect(MeterRegistry registry){
        return new TimedAspect(registry);
    }
}

Only solution that worked is this one. But with a lot of methods its repetitive. I've also tried moving method to separate class. Only with this solution I've found my metrics in

http://localhost:port/actuator/prometheus

and also in

http://localhost:port/actuator/metrics
@Service
@Component
public class MyService {
    @Autowired
    private Template template;
    .........
    @Value 
    ....

    public MyService (MeterRegistry registry){

        Counter timeElapsedHandleError = Counter.builder("time.handleError").
                description("Time in ms of run fnc handleError").
                register(registry);
        Counter countRunHandleError = Counter.builder("count.handleError").
                description("Counts how many times fnc handleError was run").
                register(registry);

        ...
    }

    public void handleError() {
        long startTime = System.nanoTime();
        metrics.countErrors.increment();

        ...
        code
        ...

        metrics.timeErrors.increment(System.nanoTime()-startTime);

    }



    public void handleMessage (param){
       if (message == null || message.getInfo() == null) {
           this.HandleError();
       }

   }

   @PostConstruct
   public void setupSubscriber() {
    for (Map.Entry<String, NamespaceConfiguration> namespaceConfiguration : myConfig.getNamespaces().entrySet()) {
        if (!namespaceConfiguration.getKey().equals(MyConfig.NAMESPACE)) {
            this.template.listenToPattern(namespaceConfiguration.getValue().getRequest())
                    .doOnSubscribe(c -> logger.info("Successfully subscribed to {}", namespaceConfiguration.getValue().getRequest()))
                    .doOnNext(msg -> logger.info(msg.getChannel() + ": Received msg " + msg.getMessage()))
                    .onErrorContinue(this::handleError) 
                    .log("OUTER", Level.INFO)
                    .subscribe(this::handleMessage);
        }
    }
    myReadinessIndicator.markAsReady();
}

I've tried moving method to another class like this. Am I doing it wrong?

@Service
@Component
public class MyService {
    @Autowired
    private Template template;
    .........
    @Value 
    ....
    
    @Autowired
    ServiceHandler serviceHandler;

    @Autowire 
    MessageHandler messageHandler;
    

   @PostConstruct
   public void setupSubscriber() {
    for (Map.Entry<String, NamespaceConfiguration> namespaceConfiguration : myConfig.getNamespaces().entrySet()) {
        if (!namespaceConfiguration.getKey().equals(MyConfig.NAMESPACE)) {
            this.template.listenToPattern(namespaceConfiguration.getValue().getRequest())
                    .doOnSubscribe(c -> logger.info("Successfully subscribed to {}", namespaceConfiguration.getValue().getRequest()))
                    .doOnNext(msg -> logger.info(msg.getChannel() + ": Received msg " + msg.getMessage()))
                    .onErrorContinue(serviceHandler::handleError) 
                    .log("OUTER", Level.INFO)
                    .subscribe(messageHandler::handleMessage);
        }
    }
    myReadinessIndicator.markAsReady();
}

I need to call multiple methods from handleMessage(). I've tried moving it to separate class.

@Component
public class MessageHandler{

    @Autowire
    ServiceHanler serviceHanler;

    public void handleMessage (param){
       if (message == null || message.getInfo() == null) {
           serviceHanler.HandleError();
       }else if (something){
           serviceHandler.handleXYMessage();
       }

   }
}

and the second class:

@Component
public class ServiceHandler {

    @Timed("MyTimer")
    @Counted("MyCounter")
    public void handleError() {
        ...
        code
        ...

    }


    @Timed("MyTimer2")
    @Counted("MyCounter2")
    public void handleXYMessage(){
       ...
       code
       ...

    }


}

So I resolved the issue. The problem was with call of the function with the annotation. I was expecting to see metrics right away after application start up. But the metrics show up after the function was run.

12
  • It is an internal method call. Spring AOP uses proxies, so only calls INTO the object will be intercepted, you are calling it from the inside. Which is why it works if you move it to an external class. Commented Dec 18, 2023 at 7:57
  • That I've tried. But didn't work. I have a suspicion that it might not work because of the @PostConstruct. Commented Dec 18, 2023 at 8:07
  • The handleMessage needs to be in an external class, that you inject and then call the handleMessage. As loing as you keep calling it as an internal method it will simply not work. Commented Dec 18, 2023 at 8:18
  • Wait so for it to work I need to create special class for handleMessage, that calls another class with handleError? Commented Dec 18, 2023 at 8:25
  • The handleError needs to be in a separate class (sorry wrong method). The annotations for the metrics are on that, if you don't put that in an external class with the annotations on it it will not be passing through the proxy. Commented Dec 18, 2023 at 8:41

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.