diff --git a/README.md b/README.md
index ff706319..0f34e7a3 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/qa-guru/allure-notifications/blob/master/README.en.md)
# Allure notifications
-**Allure notifications** - это библиотека, позволяющая выполнять автоматическое оповещение о результатах прохождения автотестов, которое направляется в нужный вам мессенджер (Telegram, Slack, ~~Skype~~, Email, Mattermost, Discord, Loop, Rocket.Chat, Zoho Cliq).
+**Allure notifications** - это библиотека, позволяющая выполнять автоматическое оповещение о результатах прохождения автотестов, которое направляется в нужный вам мессенджер (Telegram, Slack, ~~Skype~~, Email, Mattermost, Discord, Loop, Rocket.Chat, Zoho Cliq) или TSDB InfluxDB.
Languages: 🇬🇧 🇫🇷 🇷🇺 🇺🇦 🇧🇾 🇨🇳
@@ -141,6 +141,18 @@ Languages: 🇬🇧 🇫🇷 🇷🇺 🇺🇦 🇧🇾 🇨🇳
"port": 0,
"username": "",
"password": ""
+ },
+ "influxdb": {
+ "url": "",
+ "org": "",
+ "bucket": "",
+ "token": "",
+ "measurement": "",
+ "tags": {
+ "tag1": "val1",
+ "tag2": "val2",
+ "tag3": "val3"
+ }
}
}
```
@@ -351,3 +363,18 @@ java "-DconfigFile=notifications/config.json" -jar ../allure-notifications-4.2.1
Для получения дополнительной информации об API Zoho Cliq посетите официальную документацию.
++
+ Influxdb config
+ Для включения отправки в InfluxDB необходимо предоставить следующие параметры конфигурации:
+
+ enabled
- Признак отправки в InfluxDB
+ url
- адрес инстанса InfluxDB
+ org
- имя организации в InfluxDB
+ token
- Ваш API токен в InfluxDB
+ bucket
- Имя корзины (InfluxDB2) / базы данных (InfluxDB3)
+ measurement
- имя метрики (InfluxDB2) / таблицы (InfluxDB3)
+ tags
- набор тегов метрики
+
+ Fields, которые отправляются в InfluxDB, включают поля из класса Statistic.
+ Timestamp берется из поля stop класса Time.
+
diff --git a/allure-notifications-api/build.gradle b/allure-notifications-api/build.gradle
index ff018b23..7e478236 100644
--- a/allure-notifications-api/build.gradle
+++ b/allure-notifications-api/build.gradle
@@ -18,6 +18,8 @@ dependencies {
implementation('com.fasterxml.jackson.core:jackson-databind')
implementation('com.fasterxml.jackson.dataformat:jackson-dataformat-properties')
+ implementation('com.influxdb:influxdb-client-java:7.3.0')
+
testImplementation('org.junit.jupiter:junit-jupiter:5.13.4')
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
testImplementation('net.bytebuddy:byte-buddy:1.17.7')
diff --git a/allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/ClientFactory.java b/allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/ClientFactory.java
index bc37a9a4..d90d95a5 100644
--- a/allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/ClientFactory.java
+++ b/allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/ClientFactory.java
@@ -1,5 +1,6 @@
package guru.qa.allure.notifications.clients;
+import guru.qa.allure.notifications.clients.influxdb.InfluxdbClient;
import guru.qa.allure.notifications.clients.rocket.RocketChatClient;
import java.util.ArrayList;
import java.util.List;
@@ -41,6 +42,9 @@ public static List from(Config config) {
if (config.getCliq() != null) {
notifiers.add(new CliqClient(config.getCliq(), config.getProxy()));
}
+ if (config.getInfluxdb() != null && Boolean.TRUE.equals(config.getInfluxdb().getEnabled())) {
+ notifiers.add(new InfluxdbClient(config.getInfluxdb()));
+ }
return notifiers;
}
}
diff --git a/allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/influxdb/InfluxdbClient.java b/allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/influxdb/InfluxdbClient.java
new file mode 100644
index 00000000..74dfd9ef
--- /dev/null
+++ b/allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/influxdb/InfluxdbClient.java
@@ -0,0 +1,71 @@
+package guru.qa.allure.notifications.clients.influxdb;
+
+import com.influxdb.client.InfluxDBClient;
+import com.influxdb.client.InfluxDBClientFactory;
+import com.influxdb.client.WriteApiBlocking;
+import com.influxdb.client.domain.WritePrecision;
+import com.influxdb.client.write.Point;
+import guru.qa.allure.notifications.clients.Notifier;
+import guru.qa.allure.notifications.config.influxdb.Influxdb;
+import guru.qa.allure.notifications.exceptions.MessagingException;
+import guru.qa.allure.notifications.model.summary.Statistic;
+import guru.qa.allure.notifications.model.summary.Time;
+import guru.qa.allure.notifications.template.data.MessageData;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Arrays;
+
+@Slf4j
+public class InfluxdbClient implements Notifier {
+ private final Influxdb influxdb;
+
+ public InfluxdbClient(Influxdb influxdb) {
+ this.influxdb = influxdb;
+ }
+
+ @Override
+ public void sendText(MessageData messageData) throws MessagingException {
+ send(messageData);
+ }
+
+ @Override
+ public void sendPhoto(MessageData messageData, byte[] chartImage) throws MessagingException {
+ send(messageData);
+ }
+
+ @SneakyThrows
+ private void send(MessageData messageData) {
+ try (InfluxDBClient influxDBClient = InfluxDBClientFactory.create(influxdb.getUrl(), influxdb.getToken().toCharArray(), influxdb.getOrg(), influxdb.getBucket());){
+ WriteApiBlocking writeApi = influxDBClient.getWriteApiBlocking();
+ Point point = buildPoint(messageData);
+ log.info("Sending data to InfluxDB {}", influxdb.getUrl());
+ writeApi.writePoint(point);
+ } catch (Exception e) {
+ log.error("error", e);
+ }
+ }
+
+ private Point buildPoint(MessageData messageData) {
+ Point point = Point.measurement(influxdb.getMeasurement());
+ influxdb.getTags().forEach(point::addTag);
+ Statistic stat = (Statistic) messageData.getValues().get("statistic");
+ Time time = (Time) messageData.getValues().get("timeData");
+
+ Arrays.stream(stat.getClass().getDeclaredFields()).forEach(field -> {
+ field.setAccessible(true);
+ try {
+ point.addField(field.getName(), (Integer) field.get(stat));
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ point.addField("duration", time.getDuration());
+
+ if (time.getStop() != null) point.time(time.getStop(), WritePrecision.MS);
+
+ return point;
+ }
+
+}
diff --git a/allure-notifications-api/src/main/java/guru/qa/allure/notifications/config/Config.java b/allure-notifications-api/src/main/java/guru/qa/allure/notifications/config/Config.java
index 8805e716..b4a5eab2 100644
--- a/allure-notifications-api/src/main/java/guru/qa/allure/notifications/config/Config.java
+++ b/allure-notifications-api/src/main/java/guru/qa/allure/notifications/config/Config.java
@@ -3,6 +3,7 @@
import guru.qa.allure.notifications.config.base.Base;
import guru.qa.allure.notifications.config.cliq.Cliq;
import guru.qa.allure.notifications.config.discord.Discord;
+import guru.qa.allure.notifications.config.influxdb.Influxdb;
import guru.qa.allure.notifications.config.loop.Loop;
import guru.qa.allure.notifications.config.mail.Mail;
import guru.qa.allure.notifications.config.mattermost.Mattermost;
@@ -29,4 +30,5 @@ public class Config {
private RocketChat rocketChat;
private Cliq cliq;
private Proxy proxy;
+ private Influxdb influxdb;
}
diff --git a/allure-notifications-api/src/main/java/guru/qa/allure/notifications/config/influxdb/Influxdb.java b/allure-notifications-api/src/main/java/guru/qa/allure/notifications/config/influxdb/Influxdb.java
new file mode 100644
index 00000000..0bb16dab
--- /dev/null
+++ b/allure-notifications-api/src/main/java/guru/qa/allure/notifications/config/influxdb/Influxdb.java
@@ -0,0 +1,16 @@
+package guru.qa.allure.notifications.config.influxdb;
+
+import lombok.Data;
+
+import java.util.Map;
+
+@Data
+public class Influxdb {
+ private Boolean enabled = true;
+ private String url;
+ private String token;
+ private String org;
+ private String bucket;
+ private String measurement;
+ private Map tags;
+}
diff --git a/allure-notifications-api/src/main/java/guru/qa/allure/notifications/model/summary/Time.java b/allure-notifications-api/src/main/java/guru/qa/allure/notifications/model/summary/Time.java
index c0960b3b..77ec629b 100644
--- a/allure-notifications-api/src/main/java/guru/qa/allure/notifications/model/summary/Time.java
+++ b/allure-notifications-api/src/main/java/guru/qa/allure/notifications/model/summary/Time.java
@@ -12,4 +12,6 @@
public class Time {
@SerializedName("duration")
private Long duration;
+ @SerializedName("stop")
+ private Long stop;
}
diff --git a/allure-notifications-api/src/main/java/guru/qa/allure/notifications/template/data/MessageData.java b/allure-notifications-api/src/main/java/guru/qa/allure/notifications/template/data/MessageData.java
index ba176793..b242fc67 100644
--- a/allure-notifications-api/src/main/java/guru/qa/allure/notifications/template/data/MessageData.java
+++ b/allure-notifications-api/src/main/java/guru/qa/allure/notifications/template/data/MessageData.java
@@ -44,6 +44,7 @@ public Map getValues() {
data.put("customData", base.getCustomData());
data.put("time", Formatters.formatDuration(summary.getTime().getDuration(), base.getDurationFormat()));
+ data.put("timeData", summary.getTime());
data.put("statistic", summary.getStatistic());
data.put("suitesSummaryJson", suitesSummaryJson);
diff --git a/allure-notifications-api/src/test/java/guru/qa/allure/notifications/clients/influxdb/ClientFactoryInfluxdbTest.java b/allure-notifications-api/src/test/java/guru/qa/allure/notifications/clients/influxdb/ClientFactoryInfluxdbTest.java
new file mode 100644
index 00000000..33c3e2af
--- /dev/null
+++ b/allure-notifications-api/src/test/java/guru/qa/allure/notifications/clients/influxdb/ClientFactoryInfluxdbTest.java
@@ -0,0 +1,65 @@
+package guru.qa.allure.notifications.clients.influxdb;
+
+import guru.qa.allure.notifications.clients.ClientFactory;
+import guru.qa.allure.notifications.clients.Notifier;
+import guru.qa.allure.notifications.config.Config;
+import guru.qa.allure.notifications.config.influxdb.Influxdb;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class ClientFactoryInfluxdbTest {
+
+ @Test
+ void shouldCreateInfluxdbClientWhenEnabled() {
+ Config config = new Config();
+ Influxdb influxdb = buildInfluxConfig(true);
+ config.setInfluxdb(influxdb);
+
+ List notifiers = ClientFactory.from(config);
+
+ assertEquals(1, notifiers.size(), "Only InfluxdbClient expected");
+ assertInstanceOf(InfluxdbClient.class, notifiers.get(0));
+ }
+
+ @Test
+ void shouldNotCreateInfluxdbClientWhenDisabled() {
+ Config config = new Config();
+ Influxdb influxdb = buildInfluxConfig(false);
+ influxdb.setEnabled(false);
+ config.setInfluxdb(influxdb);
+
+ List notifiers = ClientFactory.from(config);
+
+ assertTrue(notifiers.isEmpty(), "No clients expected when Influxdb disabled");
+ }
+
+ @Test
+ void shouldNotCreateInfluxdbClientWhenConfigNull() {
+ Config config = new Config();
+
+ List notifiers = ClientFactory.from(config);
+
+ assertTrue(notifiers.isEmpty(), "No clients expected when Influxdb config absent");
+ }
+
+ private Influxdb buildInfluxConfig(boolean enabled) {
+ Map tags = new HashMap<>();
+ tags.put("env", "test");
+
+ Influxdb influxdb = new Influxdb();
+ influxdb.setEnabled(enabled);
+ influxdb.setUrl("http://localhost:8086");
+ influxdb.setToken("test-token");
+ influxdb.setOrg("test-org");
+ influxdb.setBucket("test-bucket");
+ influxdb.setMeasurement("test-measurement");
+ influxdb.setTags(tags);
+ return influxdb;
+ }
+}
+
diff --git a/allure-notifications-api/src/test/java/guru/qa/allure/notifications/clients/influxdb/InfluxdbClientTest.java b/allure-notifications-api/src/test/java/guru/qa/allure/notifications/clients/influxdb/InfluxdbClientTest.java
new file mode 100644
index 00000000..c8d94828
--- /dev/null
+++ b/allure-notifications-api/src/test/java/guru/qa/allure/notifications/clients/influxdb/InfluxdbClientTest.java
@@ -0,0 +1,144 @@
+package guru.qa.allure.notifications.clients.influxdb;
+
+import com.influxdb.client.InfluxDBClient;
+import com.influxdb.client.InfluxDBClientFactory;
+import com.influxdb.client.WriteApiBlocking;
+import com.influxdb.client.write.Point;
+import guru.qa.allure.notifications.config.influxdb.Influxdb;
+import guru.qa.allure.notifications.template.data.MessageData;
+import guru.qa.allure.notifications.model.summary.Statistic;
+import guru.qa.allure.notifications.model.summary.Time;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class InfluxdbClientTest {
+
+ @Mock private InfluxDBClient influxDBClient;
+ @Mock private WriteApiBlocking writeApi;
+ @Mock private MessageData messageData;
+
+ @Test
+ void sendText_shouldWritePoint() throws Exception {
+ Influxdb cfg = buildInfluxConfig();
+ Map values = prepareMessageValues(true); // with stop
+ when(messageData.getValues()).thenReturn(values);
+ when(influxDBClient.getWriteApiBlocking()).thenReturn(writeApi);
+
+ try (MockedStatic factoryMock = mockStatic(InfluxDBClientFactory.class)) {
+ factoryMock.when(() -> InfluxDBClientFactory.create(cfg.getUrl(), cfg.getToken().toCharArray(), cfg.getOrg(), cfg.getBucket()))
+ .thenReturn(influxDBClient);
+
+ InfluxdbClient client = new InfluxdbClient(cfg);
+ assertDoesNotThrow(() -> client.sendText(messageData));
+
+ verify(influxDBClient).getWriteApiBlocking();
+ ArgumentCaptor pointCaptor = ArgumentCaptor.forClass(Point.class);
+ verify(writeApi).writePoint(pointCaptor.capture());
+ verify(influxDBClient).close();
+
+ Point p = pointCaptor.getValue();
+ String lp = p.toLineProtocol();
+ assertTrue(lp.startsWith(cfg.getMeasurement()+","));
+ assertTrue(lp.contains("env=test"));
+ assertTrue(lp.contains("passed=10i"));
+ assertTrue(lp.contains("failed=2i"));
+ assertTrue(lp.contains("broken=1i"));
+ assertTrue(lp.contains("skipped=3i"));
+ assertTrue(lp.contains("unknown=0i"));
+ assertTrue(lp.contains("total=16i"));
+ assertTrue(lp.contains("duration=5000i"));
+ // timestamp (stop) should be present after a space
+ assertTrue(lp.matches(".* \\d+"), "Expected timestamp at end when stop present");
+ }
+ }
+
+ @Test
+ void sendPhoto_shouldWritePoint() throws Exception {
+ Influxdb cfg = buildInfluxConfig();
+ Map values = prepareMessageValues(false); // stop null
+ when(messageData.getValues()).thenReturn(values);
+ when(influxDBClient.getWriteApiBlocking()).thenReturn(writeApi);
+
+ try (MockedStatic factoryMock = mockStatic(InfluxDBClientFactory.class)) {
+ factoryMock.when(() -> InfluxDBClientFactory.create(cfg.getUrl(), cfg.getToken().toCharArray(), cfg.getOrg(), cfg.getBucket()))
+ .thenReturn(influxDBClient);
+
+ InfluxdbClient client = new InfluxdbClient(cfg);
+ assertDoesNotThrow(() -> client.sendPhoto(messageData, new byte[]{}));
+
+ ArgumentCaptor pointCaptor = ArgumentCaptor.forClass(Point.class);
+ verify(writeApi).writePoint(pointCaptor.capture());
+ Point p = pointCaptor.getValue();
+ String lp = p.toLineProtocol();
+ assertFalse(lp.matches(".* \\d+"), "Expected no timestamp when stop is null");
+ }
+ }
+
+ @Test
+ void sendText_shouldSwallowExceptions() throws Exception {
+ Influxdb cfg = buildInfluxConfig();
+
+ RuntimeException failure = new RuntimeException("boom");
+
+ try (MockedStatic factoryMock = mockStatic(InfluxDBClientFactory.class)) {
+ when(influxDBClient.getWriteApiBlocking()).thenThrow(failure);
+ factoryMock.when(() -> InfluxDBClientFactory.create(cfg.getUrl(), cfg.getToken().toCharArray(), cfg.getOrg(), cfg.getBucket()))
+ .thenReturn(influxDBClient);
+ InfluxdbClient client = new InfluxdbClient(cfg);
+ assertDoesNotThrow(() -> client.sendText(messageData));
+ }
+ }
+
+ private Map prepareMessageValues(boolean withStop) throws Exception {
+ Statistic stat = new Statistic();
+ setField(stat, "passed", 10);
+ setField(stat, "failed", 2);
+ setField(stat, "broken", 1);
+ setField(stat, "skipped", 3);
+ setField(stat, "unknown", 0);
+ setField(stat, "total", 16);
+
+ Time time = new Time();
+ setField(time, "duration", 5000L);
+ if (withStop) setField(time, "stop", 1700000000000L);
+
+ Map map = new HashMap<>();
+ map.put("statistic", stat);
+ map.put("timeData", time);
+ return map;
+ }
+
+ private void setField(Object target, String name, Object value) throws Exception {
+ Field f = target.getClass().getDeclaredField(name);
+ f.setAccessible(true);
+ f.set(target, value);
+ }
+
+ private Influxdb buildInfluxConfig() {
+ Map tags = new HashMap<>();
+ tags.put("env", "test");
+
+ Influxdb influxdb = new Influxdb();
+ influxdb.setEnabled(true);
+ influxdb.setUrl("http://localhost:8086");
+ influxdb.setToken("token");
+ influxdb.setOrg("org");
+ influxdb.setBucket("bucket");
+ influxdb.setMeasurement("allure_stats");
+ influxdb.setTags(tags);
+ return influxdb;
+ }
+}