clickdetect 1.2.0__tar.gz → 1.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {clickdetect-1.2.0 → clickdetect-1.3.0}/PKG-INFO +1 -1
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/__init__.py +2 -2
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/datasource/__init__.py +2 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/datasource/base.py +8 -3
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/datasource/loki.py +24 -29
- clickdetect-1.3.0/clickdetect/detector/datasource/victorialogs.py +101 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/detector.py +1 -1
- {clickdetect-1.2.0 → clickdetect-1.3.0}/pyproject.toml +1 -1
- {clickdetect-1.2.0 → clickdetect-1.3.0}/README.md +0 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/api/detector.py +0 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/api/health.py +0 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/api/rules.py +0 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/__init__.py +0 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/config.py +0 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/datasource/clickhouse.py +0 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/datasource/elasticsearch.py +0 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/datasource/postgresql.py +0 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/manager.py +0 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/rules.py +0 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/runner.py +0 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/utils.py +0 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/watcher.py +0 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/webhooks/__init__.py +0 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/webhooks/base.py +0 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/webhooks/email.py +0 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/webhooks/forgejo.py +0 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/webhooks/generic.py +0 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/webhooks/iris.py +0 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/webhooks/matrix.py +0 -0
- {clickdetect-1.2.0 → clickdetect-1.3.0}/clickdetect/detector/webhooks/teams.py +0 -0
|
@@ -20,7 +20,7 @@ logger = getLogger(__name__)
|
|
|
20
20
|
|
|
21
21
|
def print_webhooks():
|
|
22
22
|
for w in webhooks:
|
|
23
|
-
print(f"
|
|
23
|
+
print(f"Webhook: {w._name()}")
|
|
24
24
|
for param in w._params():
|
|
25
25
|
print(
|
|
26
26
|
f"\tName: {param.name}({param.type.__name__}) {'Required' if param.required else 'Optional'}. {f'Help: {param.help}' if param.help else ''}", end=' '
|
|
@@ -34,7 +34,7 @@ def print_webhooks():
|
|
|
34
34
|
|
|
35
35
|
def print_datasources():
|
|
36
36
|
for ds in datasources:
|
|
37
|
-
print(f"
|
|
37
|
+
print(f"Datasources: {ds._name()}")
|
|
38
38
|
for param in ds._params():
|
|
39
39
|
print(
|
|
40
40
|
f"\tName: {param.name}({param.type.__name__}) {'Required' if param.required else 'Optional'}. {f'Help: {param.help}' if param.help else ''}", end=' '
|
|
@@ -4,6 +4,7 @@ from .clickhouse import ClickhouseDataSource
|
|
|
4
4
|
from .loki import LokiDataSource
|
|
5
5
|
from .elasticsearch import ElasticsearchDataSource
|
|
6
6
|
from .postgresql import PostgreSQLDataSource
|
|
7
|
+
from .victorialogs import VictoriaLogsDataSource
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
datasources: List[Type[BaseDataSource]] = [
|
|
@@ -11,4 +12,5 @@ datasources: List[Type[BaseDataSource]] = [
|
|
|
11
12
|
LokiDataSource,
|
|
12
13
|
ElasticsearchDataSource,
|
|
13
14
|
PostgreSQLDataSource,
|
|
15
|
+
VictoriaLogsDataSource
|
|
14
16
|
]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from ..rules import Rule
|
|
1
2
|
from ..utils import Parameters
|
|
2
3
|
from dataclasses import dataclass
|
|
3
4
|
from typing import Any, Dict, List
|
|
@@ -19,6 +20,9 @@ class BaseDataSource:
|
|
|
19
20
|
async def connect(self):
|
|
20
21
|
raise NotImplementedError()
|
|
21
22
|
|
|
23
|
+
async def _query(self, data: str, rule: Rule) -> DataSourceQueryResult | None:
|
|
24
|
+
return await self.query(data)
|
|
25
|
+
|
|
22
26
|
async def query(self, data: str) -> DataSourceQueryResult | None:
|
|
23
27
|
raise NotImplementedError()
|
|
24
28
|
|
|
@@ -26,6 +30,10 @@ class BaseDataSource:
|
|
|
26
30
|
def _name(cls) -> str:
|
|
27
31
|
raise NotImplementedError()
|
|
28
32
|
|
|
33
|
+
@classmethod
|
|
34
|
+
def _params(cls) -> List[Parameters]:
|
|
35
|
+
raise NotImplementedError()
|
|
36
|
+
|
|
29
37
|
async def _parse(self, data: Any):
|
|
30
38
|
self.params = self._params()
|
|
31
39
|
|
|
@@ -54,6 +62,3 @@ class BaseDataSource:
|
|
|
54
62
|
result[param.name] = getattr(self, attr, param.default)
|
|
55
63
|
return result
|
|
56
64
|
|
|
57
|
-
@classmethod
|
|
58
|
-
def _params(cls) -> List[Parameters]:
|
|
59
|
-
raise NotImplementedError()
|
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
from typing import Any, List
|
|
2
2
|
from logging import getLogger
|
|
3
3
|
from .base import BaseDataSource, DataSourceQueryResult
|
|
4
|
-
import aiohttp
|
|
5
|
-
|
|
6
4
|
from ..utils import Parameters
|
|
5
|
+
import aiohttp
|
|
7
6
|
|
|
8
7
|
logger = getLogger(__name__)
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
class LokiDataSource(BaseDataSource):
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
org_id: str | None = None
|
|
11
|
+
url: str
|
|
12
|
+
username: str
|
|
13
|
+
password: str
|
|
14
|
+
verify: bool
|
|
15
|
+
org_id: str
|
|
18
16
|
_session: aiohttp.ClientSession | None = None
|
|
19
|
-
|
|
20
|
-
def _base_url(self) -> str:
|
|
21
|
-
scheme = "https" if self.verify else "http"
|
|
22
|
-
return f"{scheme}://{self.host}:{self.port}"
|
|
17
|
+
_base_url: str
|
|
23
18
|
|
|
24
19
|
def _headers(self) -> dict:
|
|
25
20
|
headers = {"Content-Type": "application/json"}
|
|
@@ -33,16 +28,17 @@ class LokiDataSource(BaseDataSource):
|
|
|
33
28
|
return None
|
|
34
29
|
|
|
35
30
|
async def connect(self):
|
|
31
|
+
logger.debug(f"Connecting to {self.url}")
|
|
32
|
+
self._base_url = self.url.rstrip("/")
|
|
36
33
|
try:
|
|
37
|
-
connector = aiohttp.TCPConnector(
|
|
34
|
+
connector = aiohttp.TCPConnector(verify_ssl=self.verify)
|
|
38
35
|
self._session = aiohttp.ClientSession(
|
|
39
36
|
connector=connector, auth=self._auth(), headers=self._headers()
|
|
40
37
|
)
|
|
41
|
-
async with self._session.get(f"{self._base_url
|
|
42
|
-
|
|
43
|
-
raise Exception(f"Loki not ready, status: {resp.status}")
|
|
38
|
+
async with self._session.get(f"{self._base_url}/ready") as resp:
|
|
39
|
+
resp.raise_for_status()
|
|
44
40
|
except Exception as ex:
|
|
45
|
-
logger.error(f"
|
|
41
|
+
logger.error(f"Datasource connect exception. url: {self.url} | {ex}")
|
|
46
42
|
if self._session:
|
|
47
43
|
await self._session.close()
|
|
48
44
|
self._session = None
|
|
@@ -53,13 +49,12 @@ class LokiDataSource(BaseDataSource):
|
|
|
53
49
|
if not self._session:
|
|
54
50
|
return None
|
|
55
51
|
try:
|
|
52
|
+
logger.debug(f"Sending query to {self.url}")
|
|
56
53
|
params = {"query": data, "limit": 5000}
|
|
57
54
|
async with self._session.get(
|
|
58
|
-
f"{self._base_url
|
|
55
|
+
f"{self._base_url}/loki/api/v1/query_range", params=params
|
|
59
56
|
) as resp:
|
|
60
|
-
|
|
61
|
-
body = await resp.text()
|
|
62
|
-
raise Exception(f"HTTP {resp.status}: {body}")
|
|
57
|
+
resp.raise_for_status()
|
|
63
58
|
payload = await resp.json()
|
|
64
59
|
return await self._parse_result(payload)
|
|
65
60
|
except Exception as ex:
|
|
@@ -72,9 +67,9 @@ class LokiDataSource(BaseDataSource):
|
|
|
72
67
|
async def _parse_result(self, payload: Any) -> DataSourceQueryResult:
|
|
73
68
|
result_type = payload.get("data", {}).get("resultType", "streams")
|
|
74
69
|
results = payload.get("data", {}).get("result", [])
|
|
70
|
+
rows = []
|
|
75
71
|
|
|
76
72
|
if result_type == "matrix":
|
|
77
|
-
rows = []
|
|
78
73
|
total = 0
|
|
79
74
|
for series in results:
|
|
80
75
|
metric = series.get("metric", {})
|
|
@@ -85,7 +80,6 @@ class LokiDataSource(BaseDataSource):
|
|
|
85
80
|
total += float(values[-1][1])
|
|
86
81
|
return DataSourceQueryResult(int(total), rows, self._name())
|
|
87
82
|
|
|
88
|
-
rows = []
|
|
89
83
|
for stream in results:
|
|
90
84
|
labels = stream.get("stream", {})
|
|
91
85
|
for ts, line in stream.get("values", []):
|
|
@@ -99,10 +93,11 @@ class LokiDataSource(BaseDataSource):
|
|
|
99
93
|
@classmethod
|
|
100
94
|
def _params(cls) -> List[Parameters]:
|
|
101
95
|
return [
|
|
102
|
-
Parameters(
|
|
103
|
-
Parameters(
|
|
104
|
-
Parameters(
|
|
105
|
-
Parameters(
|
|
106
|
-
Parameters(
|
|
107
|
-
|
|
96
|
+
Parameters("url", str, True, "Loki url: http://localhost:3100"),
|
|
97
|
+
Parameters("username", str, False, "Username", is_sensive_field=True),
|
|
98
|
+
Parameters("password", str, False, "Password", is_sensive_field=True),
|
|
99
|
+
Parameters("verify", bool, False, "Verify SSL", False),
|
|
100
|
+
Parameters(
|
|
101
|
+
"org_id", str, False, "Loki org ID (X-Scope-OrgID)", default="fake"
|
|
102
|
+
),
|
|
108
103
|
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from logging import getLogger
|
|
3
|
+
from .base import BaseDataSource, DataSourceQueryResult
|
|
4
|
+
from ..utils import Parameters
|
|
5
|
+
|
|
6
|
+
import aiohttp
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
logger = getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class VictoriaLogsDataSource(BaseDataSource):
|
|
13
|
+
url: str
|
|
14
|
+
username: str
|
|
15
|
+
password: str
|
|
16
|
+
verify: bool
|
|
17
|
+
account_id: str
|
|
18
|
+
project_id: str
|
|
19
|
+
_session: aiohttp.ClientSession | None = None
|
|
20
|
+
_base_url: str
|
|
21
|
+
|
|
22
|
+
def _headers(self) -> dict:
|
|
23
|
+
headers = {"Content-Type": "application/json"}
|
|
24
|
+
if self.account_id:
|
|
25
|
+
headers["X-VictoriaMetrics-Account-Id"] = self.account_id
|
|
26
|
+
if self.project_id:
|
|
27
|
+
headers["X-VictoriaMetrics-Project-Id"] = self.project_id
|
|
28
|
+
return headers
|
|
29
|
+
|
|
30
|
+
def _auth(self) -> aiohttp.BasicAuth | None:
|
|
31
|
+
if self.username and self.password:
|
|
32
|
+
return aiohttp.BasicAuth(self.username, self.password)
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
async def connect(self):
|
|
36
|
+
logger.debug(f"Connecting to {self.url}")
|
|
37
|
+
self._base_url = self.url.rstrip("/")
|
|
38
|
+
try:
|
|
39
|
+
connector = aiohttp.TCPConnector(ssl=False)
|
|
40
|
+
self._session = aiohttp.ClientSession(
|
|
41
|
+
connector=connector, auth=self._auth(), headers=self._headers()
|
|
42
|
+
)
|
|
43
|
+
async with self._session.get(f"{self._base_url}/health") as resp:
|
|
44
|
+
if resp.status != 200:
|
|
45
|
+
raise Exception(f"VictoriaLogs not ready, status: {resp.status}")
|
|
46
|
+
except Exception as ex:
|
|
47
|
+
logger.error(f"Failed to connect to VictoriaLogs at {self.url} | {ex}")
|
|
48
|
+
if self._session:
|
|
49
|
+
await self._session.close()
|
|
50
|
+
self._session = None
|
|
51
|
+
|
|
52
|
+
async def query(self, data: str) -> DataSourceQueryResult | None:
|
|
53
|
+
if not self._session:
|
|
54
|
+
await self.connect()
|
|
55
|
+
if not self._session:
|
|
56
|
+
return None
|
|
57
|
+
try:
|
|
58
|
+
logger.debug(f"Sending query to {self.url}")
|
|
59
|
+
params = {"query": data, "limit": 5000}
|
|
60
|
+
async with self._session.get(
|
|
61
|
+
f"{self._base_url}/select/logsql/query", params=params
|
|
62
|
+
) as resp:
|
|
63
|
+
resp.raise_for_status()
|
|
64
|
+
body = await resp.text()
|
|
65
|
+
return self._parse_result(body)
|
|
66
|
+
except Exception as ex:
|
|
67
|
+
logger.error(f"Query failed, resetting session | {ex}")
|
|
68
|
+
if self._session:
|
|
69
|
+
await self._session.close()
|
|
70
|
+
self._session = None
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
def _parse_result(self, body: str) -> DataSourceQueryResult:
|
|
74
|
+
rows = []
|
|
75
|
+
for line in body.splitlines():
|
|
76
|
+
line = line.strip()
|
|
77
|
+
if line:
|
|
78
|
+
rows.append(json.loads(line))
|
|
79
|
+
return DataSourceQueryResult(len(rows), rows, self._name())
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def _name(cls) -> str:
|
|
83
|
+
logger.warning("This datasource integration hasn't been tested yet")
|
|
84
|
+
logger.warning("I only did the integration based on victorialogs api documentation")
|
|
85
|
+
logger.warning("Please open an issue if you can validate this datasource")
|
|
86
|
+
return "victorialogs"
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def _params(cls) -> List[Parameters]:
|
|
90
|
+
return [
|
|
91
|
+
Parameters("url", str, True, "VictoriaLogs url"),
|
|
92
|
+
Parameters("username", str, False, "Username", is_sensive_field=True),
|
|
93
|
+
Parameters("password", str, False, "Password", is_sensive_field=True),
|
|
94
|
+
Parameters("verify", bool, False, "Verify SSL", False),
|
|
95
|
+
Parameters(
|
|
96
|
+
"account_id", str, False, "X-VictoriaMetrics-Account-Id header", "0"
|
|
97
|
+
),
|
|
98
|
+
Parameters(
|
|
99
|
+
"project_id", str, False, "X-VictoriaMetrics-Project-Id header", "0"
|
|
100
|
+
),
|
|
101
|
+
]
|
|
@@ -120,7 +120,7 @@ class Detector:
|
|
|
120
120
|
logger.debug(f"Rule id: {rule.id}")
|
|
121
121
|
logger.debug(f"Rule name: {rule.name}")
|
|
122
122
|
logger.debug(f"rule query:\n {query}\n")
|
|
123
|
-
result = await self.datasource.
|
|
123
|
+
result = await self.datasource._query(query, rule)
|
|
124
124
|
|
|
125
125
|
if result is None:
|
|
126
126
|
logger.warning("Datasource unavailable, skipping rule evaluation")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|