diff --git a/README.md b/README.md index 76878eb..9452498 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ This is a spring boot starter for [Telegram Bot API](https://github.com/pengrad/ * [How to support a new one](#How-to-support-a-new-one) * [Configurations](#Configurations) * [Webhooks](#Webhooks) +* [Metrics](#Metrics) * [License](#License) * [Thanks](#Thanks) @@ -114,7 +115,7 @@ If you want to add additional arguments or result values types for your controll ## Configurations By default, you can configure only these properties: -| property | description | default | +| Property | Description | Default value | | -------- | ----------- | ------- | | telegram.bot.core-pool-size | Core pool size for default pool executor | 15 | | telegram.bot.max-pool-size | Max pool size for default pool executor | 50 | @@ -164,11 +165,23 @@ In this case the library * registers `{url}/{random_uuid}` webhook via Telegram API * adds `/{random_uuid}` endpoint to the local server +## Metrics +You can check the following metrics via jmx in the `bot.metrics` domain: + +| Metric | Description | +| ------ | ----------- | +| `updates` | A number of updates received from Telegram | +| `processing.errors` | A number of exceptions thrown during updates processing | +| `no.handlers.errors` | A number of updates for which no suitable handlers were found | +| `handler.{handler_method_name}.errors` | A number of exceptions thrown during handler method execution | +| `handler.{handler_method_name}.successes` | A number of successful executions of handler method | +| `handler.{handler_method_name}.execution.time` | A time spent on successful handler method execution | + ## License ``` MIT License -Copyright (c) 2019 Kirill Shashov +Copyright (c) 2020 Kirill Shashov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/pom.xml b/pom.xml index 6540aec..18b488b 100644 --- a/pom.xml +++ b/pom.xml @@ -91,6 +91,14 @@ javalin ${javalin.version} + + io.dropwizard.metrics + metrics-core + + + io.dropwizard.metrics + metrics-jmx + diff --git a/src/main/java/com/github/kshashov/telegram/TelegramAutoConfiguration.java b/src/main/java/com/github/kshashov/telegram/TelegramAutoConfiguration.java index c7aa3db..c22c9dc 100644 --- a/src/main/java/com/github/kshashov/telegram/TelegramAutoConfiguration.java +++ b/src/main/java/com/github/kshashov/telegram/TelegramAutoConfiguration.java @@ -11,6 +11,8 @@ import com.github.kshashov.telegram.handler.processor.arguments.BotHandlerMethodArgumentResolverComposite; import com.github.kshashov.telegram.handler.processor.response.BotHandlerMethodReturnValueHandler; import com.github.kshashov.telegram.handler.processor.response.BotHandlerMethodReturnValueHandlerComposite; +import com.github.kshashov.telegram.metrics.MetricsConfiguration; +import com.github.kshashov.telegram.metrics.MetricsService; import com.pengrad.telegrambot.Callback; import com.pengrad.telegrambot.request.BaseRequest; import com.pengrad.telegrambot.response.BaseResponse; @@ -46,7 +48,7 @@ */ @Slf4j @Configuration -@Import(MethodProcessorsConfiguration.class) +@Import({MethodProcessorsConfiguration.class, MetricsConfiguration.class}) @EnableConfigurationProperties(TelegramConfigurationProperties.class) public class TelegramAutoConfiguration implements BeanFactoryPostProcessor, EnvironmentAware { private Environment environment; @@ -68,8 +70,8 @@ Javalin javalinServer(@Qualifier("telegramBotPropertiesList") List telegramServices(@Qualifier("telegramBotPropertiesList") List botProperties, TelegramBotGlobalProperties globalProperties, RequestDispatcher requestDispatcher, Optional server) { - TelegramUpdatesHandler updatesHandler = new TelegramUpdatesHandler(requestDispatcher, globalProperties); + List telegramServices(@Qualifier("telegramBotPropertiesList") List botProperties, TelegramBotGlobalProperties globalProperties, RequestDispatcher requestDispatcher, MetricsService metricsService, Optional server) { + TelegramUpdatesHandler updatesHandler = new TelegramUpdatesHandler(requestDispatcher, globalProperties, metricsService); List services = botProperties.stream() .map(p -> { @@ -109,11 +111,12 @@ List telegramBotPropertiesList(List> nonAnnotatedClasses = - Collections.newSetFromMap(new ConcurrentHashMap<>(64)); - final private HandlerMethodContainer botHandlerMethodContainer; + private final Set> nonAnnotatedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>(64)); + private final HandlerMethodContainer botHandlerMethodContainer; + private final MetricsService metricsService; - public TelegramControllerBeanPostProcessor(@NotNull HandlerMethodContainer botHandlerMethodContainer) { + public TelegramControllerBeanPostProcessor(@NotNull HandlerMethodContainer botHandlerMethodContainer, @NotNull MetricsService metricsService) { this.botHandlerMethodContainer = botHandlerMethodContainer; + this.metricsService = metricsService; } @Override @@ -62,7 +65,8 @@ public Object postProcessAfterInitialization(@NonNull Object bean, @NonNull Stri // Non-empty set of methods annotatedMethods.forEach((method, mappingInfo) -> { Method invocableMethod = AopUtils.selectInvocableMethod(method, targetClass); - botHandlerMethodContainer.registerController(bean, invocableMethod, mappingInfo); + HandlerMethod handlerMethod = botHandlerMethodContainer.registerController(bean, invocableMethod, mappingInfo); + metricsService.registerHandlerMethod(handlerMethod); }); } } else { diff --git a/src/main/java/com/github/kshashov/telegram/config/TelegramBotProperties.java b/src/main/java/com/github/kshashov/telegram/config/TelegramBotProperties.java index c7d2c66..2a353c4 100644 --- a/src/main/java/com/github/kshashov/telegram/config/TelegramBotProperties.java +++ b/src/main/java/com/github/kshashov/telegram/config/TelegramBotProperties.java @@ -37,6 +37,7 @@ public static class Builder { * * @param builderConsumer builder consumer * @return current instance + * @since 0.21 */ public Builder configure(Consumer builderConsumer) { builderConsumer.accept(botBuilder); @@ -48,6 +49,8 @@ public Builder configure(Consumer builderConsumer) { * * @param webhook configured webhook request. See https://core.telegram.org/bots/faq * @return current instance + * + * @since 0.21 */ public Builder useWebhook(@NotNull SetWebhook webhook) { this.webhook = webhook; diff --git a/src/main/java/com/github/kshashov/telegram/handler/HandlerMethodContainer.java b/src/main/java/com/github/kshashov/telegram/handler/HandlerMethodContainer.java index c69c2d1..104cd56 100644 --- a/src/main/java/com/github/kshashov/telegram/handler/HandlerMethodContainer.java +++ b/src/main/java/com/github/kshashov/telegram/handler/HandlerMethodContainer.java @@ -62,10 +62,11 @@ public HandlerLookupResult lookupHandlerMethod(@NotNull TelegramEvent telegramEv return new HandlerLookupResult(); } - public void registerController(@NotNull Object bean, @NotNull Method method, @NotNull RequestMappingInfo mappingInfo) { + public HandlerMethod registerController(@NotNull Object bean, @NotNull Method method, @NotNull RequestMappingInfo mappingInfo) { HandlerMethod handlerMethod = new HandlerMethod(bean, method); Map botHandlers = handlers.computeIfAbsent(mappingInfo.getToken(), (k) -> new HashMap<>()); botHandlers.put(mappingInfo, handlerMethod); + return handlerMethod; } private List getMatchingPatterns(@NotNull RequestMappingInfo mappingInfo, @NotNull String lookupPath) { diff --git a/src/main/java/com/github/kshashov/telegram/handler/TelegramUpdatesHandler.java b/src/main/java/com/github/kshashov/telegram/handler/TelegramUpdatesHandler.java index a539700..713d748 100644 --- a/src/main/java/com/github/kshashov/telegram/handler/TelegramUpdatesHandler.java +++ b/src/main/java/com/github/kshashov/telegram/handler/TelegramUpdatesHandler.java @@ -3,6 +3,7 @@ import com.github.kshashov.telegram.config.TelegramBotGlobalProperties; import com.github.kshashov.telegram.handler.processor.RequestDispatcher; import com.github.kshashov.telegram.handler.processor.TelegramEvent; +import com.github.kshashov.telegram.metrics.MetricsService; import com.pengrad.telegrambot.Callback; import com.pengrad.telegrambot.TelegramBot; import com.pengrad.telegrambot.model.Update; @@ -24,11 +25,13 @@ public class TelegramUpdatesHandler { private final RequestDispatcher botRequestDispatcher; private final TelegramBotGlobalProperties globalProperties; + private final MetricsService metricsService; @Autowired - public TelegramUpdatesHandler(@NotNull RequestDispatcher botRequestDispatcher, @NotNull TelegramBotGlobalProperties globalProperties) { + public TelegramUpdatesHandler(@NotNull RequestDispatcher botRequestDispatcher, @NotNull TelegramBotGlobalProperties globalProperties, @NotNull MetricsService metricsService) { this.botRequestDispatcher = botRequestDispatcher; this.globalProperties = globalProperties; + this.metricsService = metricsService; } /** @@ -39,11 +42,13 @@ public TelegramUpdatesHandler(@NotNull RequestDispatcher botRequestDispatcher, @ * @param updates telegram updates */ public void processUpdates(@NotNull String token, @NotNull TelegramBot bot, @NotNull List updates) { + metricsService.onUpdatesReceived(updates.size()); try { for (Update update : updates) { globalProperties.getTaskExecutor().execute(() -> { try { TelegramEvent event = new TelegramEvent(token, update, bot); + BaseRequest executionResult = botRequestDispatcher.execute(event); if (executionResult != null) { // Execute telegram request from controller response @@ -51,6 +56,7 @@ public void processUpdates(@NotNull String token, @NotNull TelegramBot bot, @Not postExecute(executionResult, bot); } } catch (IllegalStateException e) { + metricsService.onUpdateError(); log.error("Execution error", e); } }); @@ -72,6 +78,7 @@ public void onResponse(BaseRequest request, BaseResponse response) { @Override public void onFailure(BaseRequest request, IOException e) { globalProperties.getResponseCallback().onFailure(request, e); + metricsService.onUpdateError(); log.error(baseRequest + " request was failed", e); } }); diff --git a/src/main/java/com/github/kshashov/telegram/handler/processor/RequestDispatcher.java b/src/main/java/com/github/kshashov/telegram/handler/processor/RequestDispatcher.java index da16934..59de47b 100644 --- a/src/main/java/com/github/kshashov/telegram/handler/processor/RequestDispatcher.java +++ b/src/main/java/com/github/kshashov/telegram/handler/processor/RequestDispatcher.java @@ -1,11 +1,13 @@ package com.github.kshashov.telegram.handler.processor; +import com.codahale.metrics.Timer; import com.github.kshashov.telegram.TelegramSessionResolver; import com.github.kshashov.telegram.api.TelegramRequest; import com.github.kshashov.telegram.api.TelegramSession; import com.github.kshashov.telegram.handler.HandlerMethodContainer; import com.github.kshashov.telegram.handler.processor.arguments.BotHandlerMethodArgumentResolver; import com.github.kshashov.telegram.handler.processor.response.BotHandlerMethodReturnValueHandler; +import com.github.kshashov.telegram.metrics.MetricsService; import com.pengrad.telegrambot.request.BaseRequest; import lombok.extern.slf4j.Slf4j; @@ -20,12 +22,14 @@ public class RequestDispatcher { private final TelegramSessionResolver sessionResolver; private final BotHandlerMethodArgumentResolver argumentResolver; private final BotHandlerMethodReturnValueHandler returnValueHandler; + private final MetricsService metricsService; - public RequestDispatcher(@NotNull HandlerMethodContainer handlerMethodContainer, @NotNull TelegramSessionResolver sessionResolver, @NotNull BotHandlerMethodArgumentResolver argumentResolver, @NotNull BotHandlerMethodReturnValueHandler returnValueHandler) { + public RequestDispatcher(@NotNull HandlerMethodContainer handlerMethodContainer, @NotNull TelegramSessionResolver sessionResolver, @NotNull BotHandlerMethodArgumentResolver argumentResolver, @NotNull BotHandlerMethodReturnValueHandler returnValueHandler, @NotNull MetricsService metricsService) { this.handlerMethodContainer = handlerMethodContainer; this.sessionResolver = sessionResolver; this.argumentResolver = argumentResolver; this.returnValueHandler = returnValueHandler; + this.metricsService = metricsService; } /** @@ -38,16 +42,29 @@ public RequestDispatcher(@NotNull HandlerMethodContainer handlerMethodContainer, public BaseRequest execute(@NotNull TelegramEvent event) throws IllegalStateException { TelegramSessionResolver.TelegramSessionHolder sessionHolder = null; + HandlerMethodContainer.HandlerLookupResult lookupResult = handlerMethodContainer.lookupHandlerMethod(event); + HandlerMethod method = lookupResult.getHandlerMethod(); try { // Start telegram session sessionHolder = sessionResolver.resolveTelegramSession(event); // Process telegram request by controller - HandlerMethodContainer.HandlerLookupResult lookupResult = handlerMethodContainer.lookupHandlerMethod(event); - if (lookupResult.getHandlerMethod() == null) { + if (method == null) { log.debug("Not found controller for {} (type {})", event.getText(), event.getMessageType()); + metricsService.onNoHandlersFound(); return null; } - return doExecute(event, lookupResult, sessionHolder.getSession()); + + // Save execution time to metrics + Timer.Context timerContext = metricsService.onMethodHandlerStarted(method); + BaseRequest result = doExecute(event, lookupResult, sessionHolder.getSession()); + metricsService.onUpdateSuccess(method, timerContext); + + return result; + } catch (Exception ex) { + if (method != null) { + metricsService.onUpdateError(method); + } + throw ex; } finally { // Clear session id from current scope if (sessionHolder != null) sessionHolder.releaseSessionId(); diff --git a/src/main/java/com/github/kshashov/telegram/metrics/MetricsConfiguration.java b/src/main/java/com/github/kshashov/telegram/metrics/MetricsConfiguration.java new file mode 100644 index 0000000..0381bd7 --- /dev/null +++ b/src/main/java/com/github/kshashov/telegram/metrics/MetricsConfiguration.java @@ -0,0 +1,27 @@ +package com.github.kshashov.telegram.metrics; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.jmx.JmxReporter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MetricsConfiguration { + + @Bean + public MetricsService metricsService(MetricRegistry metricRegistry) { + return new MetricsService(metricRegistry); + } + + @Bean + public MetricRegistry getMetricRegistry() { + MetricRegistry registry = new MetricRegistry(); + JmxReporter + .forRegistry(registry) + .inDomain("bot.metrics") + .build() + .start(); + + return registry; + } +} diff --git a/src/main/java/com/github/kshashov/telegram/metrics/MetricsService.java b/src/main/java/com/github/kshashov/telegram/metrics/MetricsService.java new file mode 100644 index 0000000..1e21271 --- /dev/null +++ b/src/main/java/com/github/kshashov/telegram/metrics/MetricsService.java @@ -0,0 +1,105 @@ +package com.github.kshashov.telegram.metrics; + +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.SlidingWindowReservoir; +import com.codahale.metrics.Timer; +import com.github.kshashov.telegram.handler.processor.HandlerMethod; + +import static java.lang.String.format; + +/** + * Manages application metrics using jmx. + * + * @since 0.22 + */ +public class MetricsService { + public static final String UPDATES_RECEIVED = "updates"; + public static final String UPDATE_ERRORS = "processing.errors"; + public static final String NO_HANDLERS_ERRORS = "no.handlers.errors"; + public static final String HANDLER_ERRORS = "handler.%s.errors"; + public static final String HANDLER_SUCCESSES = "handler.%s.successes"; + public static final String HANDLER_EXECUTION_TIME = "handler.%s.execution.time"; + private final MetricRegistry metricRegistry; + + public MetricsService(MetricRegistry metricRegistry) { + this.metricRegistry = metricRegistry; + metricRegistry.register(UPDATES_RECEIVED, new Meter()); + metricRegistry.register(UPDATE_ERRORS, new Meter()); + metricRegistry.register(NO_HANDLERS_ERRORS, new Meter()); + } + + /** + * Stores updates count into {@link #UPDATES_RECEIVED} metric. + * + * @param messages updates count + */ + public void onUpdatesReceived(int messages) { + metricRegistry.getMeters().get(UPDATES_RECEIVED).mark(messages); + } + + /** + * Updates {@link #NO_HANDLERS_ERRORS} metric. + */ + public void onNoHandlersFound() { + metricRegistry.getMeters().get(NO_HANDLERS_ERRORS).mark(); + } + + /** + * Updates {@link #UPDATE_ERRORS} metric. + */ + public void onUpdateError() { + metricRegistry.getMeters().get(UPDATE_ERRORS).mark(); + } + + /** + * Creates handler related metrics. + * + * @param method handler method + */ + public void registerHandlerMethod(HandlerMethod method) { + metricRegistry.register(format(HANDLER_ERRORS, getMethodName(method)), new Meter()); + metricRegistry.register(format(HANDLER_EXECUTION_TIME, getMethodName(method)), new Timer(new SlidingWindowReservoir(64))); + metricRegistry.register(format(HANDLER_SUCCESSES, getMethodName(method)), new Meter()); + } + + /** + * Started times associated with method. + * + * @param method handler method + * @return timer context that should be passed to {@link #onUpdateSuccess} when updated is processed + */ + public Timer.Context onMethodHandlerStarted(HandlerMethod method) { + return metricRegistry.getTimers().get(format(HANDLER_EXECUTION_TIME, getMethodName(method))).time(); + } + + /** + * Updates {@link #HANDLER_ERRORS} metric. + * + * @param method handler method + */ + public void onUpdateError(HandlerMethod method) { + metricRegistry.getMeters().get(format(HANDLER_ERRORS, getMethodName(method))).mark(); + } + + /** + * Updates {@link #HANDLER_SUCCESSES} and {@link #HANDLER_EXECUTION_TIME} metric. + * + * @param method handler method + * @param timerContext context created by {@link #onMethodHandlerStarted} + */ + public void onUpdateSuccess(HandlerMethod method, Timer.Context timerContext) { + metricRegistry.getMeters().get(format(HANDLER_SUCCESSES, getMethodName(method))).mark(); + timerContext.close(); + } + + /** + * Returns user-friendly method name. + * + * @param method handler method + * @return user-friendly method name + */ + private String getMethodName(HandlerMethod method) { + return method.getBeanType().getName() + "." + method.getBridgedMethod().getName(); + } +} diff --git a/src/test/java/com/github/kshashov/telegram/handler/processor/RequestDispatcherTest.java b/src/test/java/com/github/kshashov/telegram/handler/processor/RequestDispatcherTest.java index 2686ce4..c599329 100644 --- a/src/test/java/com/github/kshashov/telegram/handler/processor/RequestDispatcherTest.java +++ b/src/test/java/com/github/kshashov/telegram/handler/processor/RequestDispatcherTest.java @@ -8,6 +8,7 @@ import com.github.kshashov.telegram.handler.processor.arguments.BotRequestMethodArgumentResolver; import com.github.kshashov.telegram.handler.processor.response.BotBaseRequestMethodProcessor; import com.github.kshashov.telegram.handler.processor.response.BotHandlerMethodReturnValueHandler; +import com.github.kshashov.telegram.metrics.MetricsService; import com.pengrad.telegrambot.TelegramBot; import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.request.BaseRequest; @@ -29,11 +30,13 @@ public class RequestDispatcherTest { private TelegramEvent telegramEvent; private TelegramSessionResolver.TelegramSessionHolder sessionHolder; private SendMessage sendMessage = new SendMessage(12, "text"); + private MetricsService metricsService; @BeforeEach void init() { handlerMethodContainer = mock(HandlerMethodContainer.class); sessionResolver = mock(TelegramSessionResolver.class); + metricsService = mock(MetricsService.class); argumentResolver = new BotRequestMethodArgumentResolver(); returnValueHandler = new BotBaseRequestMethodProcessor(); @@ -110,7 +113,8 @@ BaseRequest doExecute() throws Exception { handlerMethodContainer, sessionResolver, argumentResolver, - returnValueHandler); + returnValueHandler, + metricsService); return dispatcher.execute(telegramEvent); }