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);
}