vectorwave 0.1.4__tar.gz → 0.1.5__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.
- {vectorwave-0.1.4/src/vectorwave.egg-info → vectorwave-0.1.5}/PKG-INFO +30 -2
- {vectorwave-0.1.4 → vectorwave-0.1.5}/Readme.md +28 -1
- {vectorwave-0.1.4 → vectorwave-0.1.5}/pyproject.toml +3 -2
- vectorwave-0.1.5/src/tests/monitoring/alert/test_alerter.py +70 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/monitoring/test_async_trace.py +24 -1
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/monitoring/test_tracer.py +22 -1
- vectorwave-0.1.5/src/tests/search/test_execution_search.py +120 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/core/decorator.py +2 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/models/db_config.py +4 -0
- vectorwave-0.1.5/src/vectorwave/monitoring/alert/base.py +8 -0
- vectorwave-0.1.5/src/vectorwave/monitoring/alert/factory.py +19 -0
- vectorwave-0.1.5/src/vectorwave/monitoring/alert/null_alerter.py +7 -0
- vectorwave-0.1.5/src/vectorwave/monitoring/alert/webhook_alerter.py +69 -0
- vectorwave-0.1.5/src/vectorwave/monitoring/tracer.py +273 -0
- vectorwave-0.1.5/src/vectorwave/prediction/__init__.py +0 -0
- vectorwave-0.1.5/src/vectorwave/prediction/predictor.py +0 -0
- vectorwave-0.1.5/src/vectorwave/search/__init__.py +0 -0
- vectorwave-0.1.5/src/vectorwave/search/execution_search.py +148 -0
- vectorwave-0.1.5/src/vectorwave/search/extended_search.py +0 -0
- vectorwave-0.1.5/src/vectorwave/search/rag_search.py +0 -0
- vectorwave-0.1.5/src/vectorwave/vectorizer/__init__.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5/src/vectorwave.egg-info}/PKG-INFO +30 -2
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave.egg-info/SOURCES.txt +13 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave.egg-info/requires.txt +1 -0
- vectorwave-0.1.4/src/vectorwave/monitoring/tracer.py +0 -246
- {vectorwave-0.1.4 → vectorwave-0.1.5}/LICENSE +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/MANIFEST.in +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/NOTICE +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/setup.cfg +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/__init__.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/batch/__init__.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/batch/test_batch.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/core/__init__.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/core/test_decorator.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/database/__init__.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/database/test_db.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/database/test_db_search.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/exception/__init__.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/models/__init__.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/models/test_db_config.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/monitoring/__init__.py +0 -0
- {vectorwave-0.1.4/src/tests/prediction → vectorwave-0.1.5/src/tests/monitoring/alert}/__init__.py +0 -0
- {vectorwave-0.1.4/src/tests/vectorizer → vectorwave-0.1.5/src/tests/prediction}/__init__.py +0 -0
- {vectorwave-0.1.4/src/vectorwave/batch → vectorwave-0.1.5/src/tests/search}/__init__.py +0 -0
- {vectorwave-0.1.4/src/vectorwave/core → vectorwave-0.1.5/src/tests/vectorizer}/__init__.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/__init__.py +0 -0
- {vectorwave-0.1.4/src/vectorwave/database → vectorwave-0.1.5/src/vectorwave/batch}/__init__.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/batch/batch.py +0 -0
- {vectorwave-0.1.4/src/vectorwave/exception → vectorwave-0.1.5/src/vectorwave/core}/__init__.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/core/core.py +0 -0
- {vectorwave-0.1.4/src/vectorwave/models → vectorwave-0.1.5/src/vectorwave/database}/__init__.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/database/db.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/database/db_search.py +0 -0
- {vectorwave-0.1.4/src/vectorwave/monitoring → vectorwave-0.1.5/src/vectorwave/exception}/__init__.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/exception/exceptions.py +0 -0
- {vectorwave-0.1.4/src/vectorwave/prediction → vectorwave-0.1.5/src/vectorwave/models}/__init__.py +0 -0
- {vectorwave-0.1.4/src/vectorwave/vectorizer → vectorwave-0.1.5/src/vectorwave/monitoring}/__init__.py +0 -0
- /vectorwave-0.1.4/src/vectorwave/monitoring/monitoring.py → /vectorwave-0.1.5/src/vectorwave/monitoring/alert/__init__.py +0 -0
- /vectorwave-0.1.4/src/vectorwave/prediction/predictor.py → /vectorwave-0.1.5/src/vectorwave/monitoring/monitoring.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/vectorizer/base.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/vectorizer/factory.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/vectorizer/huggingface_vectorizer.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/vectorizer/openai_vectorizer.py +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave.egg-info/dependency_links.txt +0 -0
- {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vectorwave
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: VectorWave: Seamless Auto-Vectorization Framework
|
|
5
5
|
Author-email: junyeonggim <junyeonggim5@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -20,6 +20,7 @@ License-File: NOTICE
|
|
|
20
20
|
Requires-Dist: weaviate-client>=4.0.0
|
|
21
21
|
Requires-Dist: pydantic-settings>=2.0.0
|
|
22
22
|
Requires-Dist: sentence-transformers
|
|
23
|
+
Requires-Dist: requests
|
|
23
24
|
Dynamic: license-file
|
|
24
25
|
|
|
25
26
|
|
|
@@ -395,7 +396,6 @@ def other_function():
|
|
|
395
396
|
pass
|
|
396
397
|
```
|
|
397
398
|
|
|
398
|
-
**Tag Merging and Validation Rules**
|
|
399
399
|
|
|
400
400
|
1. **Validation (Important):** Tags (global or function-specific) will **only** be saved to Weaviate if their key (e.g., `run_id`, `team`, `priority`) was first defined in the `.weaviate_properties` file (Step 1). Tags not defined in the schema are **ignored**, and a warning is logged at startup.
|
|
401
401
|
|
|
@@ -408,6 +408,34 @@ def other_function():
|
|
|
408
408
|
|
|
409
409
|
-----
|
|
410
410
|
|
|
411
|
+
### 🚀 Real-time Error Alerting (Webhook)
|
|
412
|
+
|
|
413
|
+
Beyond just logging, `VectorWave` can send **real-time notifications via webhook** the instant an error occurs. This functionality is built directly into the tracer and can be activated simply by updating your `.env` file.
|
|
414
|
+
|
|
415
|
+
**How it Works:**
|
|
416
|
+
1. An exception is raised within a function decorated by `@trace_span` or `@vectorize`.
|
|
417
|
+
2. The tracer catches the exception in its `except` block and immediately calls the `alerter` object.
|
|
418
|
+
3. The alerter reads the `.env` configuration and uses the `WebhookAlerter` to dispatch the error details to your specified URL.
|
|
419
|
+
4. The notification is optimized for **Discord Embeds**, sending a rich report including the error code, trace ID, captured attributes (`user_id`, etc.), and the full stack trace.
|
|
420
|
+
|
|
421
|
+
**How to Enable:**
|
|
422
|
+
Add the following two variables to your `test_ex/.env` file (or environment variables):
|
|
423
|
+
|
|
424
|
+
```ini
|
|
425
|
+
# .env file
|
|
426
|
+
|
|
427
|
+
# 1. Set the alerter strategy to 'webhook'. (Default: "none")
|
|
428
|
+
ALERTER_STRATEGY="webhook"
|
|
429
|
+
|
|
430
|
+
# 2. Provide your webhook URL from Discord, Slack, etc.
|
|
431
|
+
ALERTER_WEBHOOK_URL="[https://discord.com/api/webhooks/YOUR_HOOK_ID/](https://discord.com/api/webhooks/YOUR_HOOK_ID/)..."
|
|
432
|
+
With just these two lines, running test_ex/example.py will now instantly send a Discord alert when the CustomValueError is raised.
|
|
433
|
+
|
|
434
|
+
Extensibility (Strategy Pattern): The alerting system is built on a Strategy Pattern. You can easily extend it by implementing the BaseAlerter interface to support other channels like email, PagerDuty, or more.
|
|
435
|
+
|
|
436
|
+
**Tag Merging and Validation Rules**
|
|
437
|
+
```
|
|
438
|
+
|
|
411
439
|
## 🤝 Contributing
|
|
412
440
|
|
|
413
441
|
Bug reports, feature requests, and code contributions are all welcome. For details, please see [CONTRIBUTING.md](https://www.google.com/search?q=httpsS://www.google.com/search%3Fq%3DCONTRIBUTING.md).
|
|
@@ -371,7 +371,6 @@ def other_function():
|
|
|
371
371
|
pass
|
|
372
372
|
```
|
|
373
373
|
|
|
374
|
-
**Tag Merging and Validation Rules**
|
|
375
374
|
|
|
376
375
|
1. **Validation (Important):** Tags (global or function-specific) will **only** be saved to Weaviate if their key (e.g., `run_id`, `team`, `priority`) was first defined in the `.weaviate_properties` file (Step 1). Tags not defined in the schema are **ignored**, and a warning is logged at startup.
|
|
377
376
|
|
|
@@ -384,6 +383,34 @@ def other_function():
|
|
|
384
383
|
|
|
385
384
|
-----
|
|
386
385
|
|
|
386
|
+
### 🚀 Real-time Error Alerting (Webhook)
|
|
387
|
+
|
|
388
|
+
Beyond just logging, `VectorWave` can send **real-time notifications via webhook** the instant an error occurs. This functionality is built directly into the tracer and can be activated simply by updating your `.env` file.
|
|
389
|
+
|
|
390
|
+
**How it Works:**
|
|
391
|
+
1. An exception is raised within a function decorated by `@trace_span` or `@vectorize`.
|
|
392
|
+
2. The tracer catches the exception in its `except` block and immediately calls the `alerter` object.
|
|
393
|
+
3. The alerter reads the `.env` configuration and uses the `WebhookAlerter` to dispatch the error details to your specified URL.
|
|
394
|
+
4. The notification is optimized for **Discord Embeds**, sending a rich report including the error code, trace ID, captured attributes (`user_id`, etc.), and the full stack trace.
|
|
395
|
+
|
|
396
|
+
**How to Enable:**
|
|
397
|
+
Add the following two variables to your `test_ex/.env` file (or environment variables):
|
|
398
|
+
|
|
399
|
+
```ini
|
|
400
|
+
# .env file
|
|
401
|
+
|
|
402
|
+
# 1. Set the alerter strategy to 'webhook'. (Default: "none")
|
|
403
|
+
ALERTER_STRATEGY="webhook"
|
|
404
|
+
|
|
405
|
+
# 2. Provide your webhook URL from Discord, Slack, etc.
|
|
406
|
+
ALERTER_WEBHOOK_URL="[https://discord.com/api/webhooks/YOUR_HOOK_ID/](https://discord.com/api/webhooks/YOUR_HOOK_ID/)..."
|
|
407
|
+
With just these two lines, running test_ex/example.py will now instantly send a Discord alert when the CustomValueError is raised.
|
|
408
|
+
|
|
409
|
+
Extensibility (Strategy Pattern): The alerting system is built on a Strategy Pattern. You can easily extend it by implementing the BaseAlerter interface to support other channels like email, PagerDuty, or more.
|
|
410
|
+
|
|
411
|
+
**Tag Merging and Validation Rules**
|
|
412
|
+
```
|
|
413
|
+
|
|
387
414
|
## 🤝 Contributing
|
|
388
415
|
|
|
389
416
|
Bug reports, feature requests, and code contributions are all welcome. For details, please see [CONTRIBUTING.md](https://www.google.com/search?q=httpsS://www.google.com/search%3Fq%3DCONTRIBUTING.md).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vectorwave"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.5"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name = "junyeonggim", email = "junyeonggim5@gmail.com" },
|
|
10
10
|
]
|
|
@@ -26,7 +26,8 @@ classifiers = [
|
|
|
26
26
|
dependencies = [
|
|
27
27
|
"weaviate-client>=4.0.0",
|
|
28
28
|
"pydantic-settings>=2.0.0",
|
|
29
|
-
"sentence-transformers"
|
|
29
|
+
"sentence-transformers",
|
|
30
|
+
"requests"
|
|
30
31
|
]
|
|
31
32
|
|
|
32
33
|
[project.urls]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from unittest.mock import MagicMock, patch
|
|
2
|
+
|
|
3
|
+
from vectorwave.monitoring.alert.factory import get_alerter
|
|
4
|
+
from vectorwave.monitoring.alert.null_alerter import NullAlerter
|
|
5
|
+
from vectorwave.monitoring.alert.webhook_alerter import WebhookAlerter
|
|
6
|
+
from vectorwave.models.db_config import WeaviateSettings
|
|
7
|
+
|
|
8
|
+
@patch('vectorwave.monitoring.alert.factory.get_weaviate_settings')
|
|
9
|
+
def test_get_alerter_returns_null_alerter_by_default(mock_get_settings):
|
|
10
|
+
"""
|
|
11
|
+
Case 1: When .env settings are missing or ALERTER_STRATEGY="none"
|
|
12
|
+
- Should return NullAlerter.
|
|
13
|
+
"""
|
|
14
|
+
# 1. Arrange
|
|
15
|
+
# Clear lru_cache to bypass caching
|
|
16
|
+
get_alerter.cache_clear()
|
|
17
|
+
|
|
18
|
+
# Mock default settings (strategy="none")
|
|
19
|
+
mock_get_settings.return_value = WeaviateSettings()
|
|
20
|
+
|
|
21
|
+
# 2. Act
|
|
22
|
+
alerter = get_alerter()
|
|
23
|
+
|
|
24
|
+
# 3. Assert
|
|
25
|
+
assert isinstance(alerter, NullAlerter)
|
|
26
|
+
|
|
27
|
+
@patch('vectorwave.monitoring.alert.factory.get_weaviate_settings')
|
|
28
|
+
def test_get_alerter_returns_webhook_alerter_when_configured(mock_get_settings):
|
|
29
|
+
"""
|
|
30
|
+
Case 2: When ALERTER_STRATEGY="webhook" and URL is provided
|
|
31
|
+
- Should return WebhookAlerter.
|
|
32
|
+
"""
|
|
33
|
+
# 1. Arrange
|
|
34
|
+
get_alerter.cache_clear()
|
|
35
|
+
|
|
36
|
+
# Mock settings object with webhook strategy and URL
|
|
37
|
+
settings = WeaviateSettings(
|
|
38
|
+
ALERTER_STRATEGY="webhook",
|
|
39
|
+
ALERTER_WEBHOOK_URL="https://test.webhook.url/..."
|
|
40
|
+
)
|
|
41
|
+
mock_get_settings.return_value = settings
|
|
42
|
+
|
|
43
|
+
# 2. Act
|
|
44
|
+
alerter = get_alerter()
|
|
45
|
+
|
|
46
|
+
# 3. Assert
|
|
47
|
+
assert isinstance(alerter, WebhookAlerter)
|
|
48
|
+
assert alerter.url == "https://test.webhook.url/..."
|
|
49
|
+
|
|
50
|
+
@patch('vectorwave.monitoring.alert.factory.get_weaviate_settings')
|
|
51
|
+
def test_get_alerter_returns_null_alerter_if_webhook_url_missing(mock_get_settings):
|
|
52
|
+
"""
|
|
53
|
+
Case 3: When ALERTER_STRATEGY="webhook" but URL is missing
|
|
54
|
+
- Should log a warning and return NullAlerter.
|
|
55
|
+
"""
|
|
56
|
+
# 1. Arrange
|
|
57
|
+
get_alerter.cache_clear()
|
|
58
|
+
|
|
59
|
+
# Mock settings with missing URL
|
|
60
|
+
settings = WeaviateSettings(
|
|
61
|
+
ALERTER_STRATEGY="webhook",
|
|
62
|
+
ALERTER_WEBHOOK_URL=None # URL is missing
|
|
63
|
+
)
|
|
64
|
+
mock_get_settings.return_value = settings
|
|
65
|
+
|
|
66
|
+
# 2. Act
|
|
67
|
+
alerter = get_alerter()
|
|
68
|
+
|
|
69
|
+
# 3. Assert
|
|
70
|
+
assert isinstance(alerter, NullAlerter)
|
|
@@ -23,6 +23,7 @@ async def test_trace_root_and_span_async_success(mock_tracer_deps):
|
|
|
23
23
|
- ContextVar (trace_id) must be correctly propagated across 'await' boundaries
|
|
24
24
|
'''
|
|
25
25
|
mock_batch = mock_tracer_deps["batch"]
|
|
26
|
+
mock_alerter = mock_tracer_deps["alerter"]
|
|
26
27
|
|
|
27
28
|
@trace_span(attributes_to_capture=['x'])
|
|
28
29
|
async def my_async_inner_span(x):
|
|
@@ -37,6 +38,7 @@ async def test_trace_root_and_span_async_success(mock_tracer_deps):
|
|
|
37
38
|
return result
|
|
38
39
|
|
|
39
40
|
mock_batch.reset_mock()
|
|
41
|
+
mock_alerter.reset_mock()
|
|
40
42
|
|
|
41
43
|
# --- Execution ---
|
|
42
44
|
result = await my_async_workflow_root()
|
|
@@ -47,6 +49,8 @@ async def test_trace_root_and_span_async_success(mock_tracer_deps):
|
|
|
47
49
|
# 2 calls expected: 1 (inner_span) + 1 (workflow_root)
|
|
48
50
|
assert mock_batch.add_object.call_count == 2
|
|
49
51
|
|
|
52
|
+
mock_alerter.notify.assert_not_called()
|
|
53
|
+
|
|
50
54
|
call_args_list = mock_batch.add_object.call_args_list
|
|
51
55
|
props_map = {
|
|
52
56
|
call.kwargs["properties"]["function_name"]: call.kwargs["properties"]
|
|
@@ -80,9 +84,12 @@ async def test_trace_span_async_failure(mock_tracer_deps):
|
|
|
80
84
|
Case 2: Async span failure test
|
|
81
85
|
'''
|
|
82
86
|
mock_batch = mock_tracer_deps["batch"]
|
|
87
|
+
mock_alerter = mock_tracer_deps["alerter"]
|
|
83
88
|
|
|
84
89
|
class AsyncTestError(Exception):
|
|
85
|
-
|
|
90
|
+
@property
|
|
91
|
+
def error_code(self):
|
|
92
|
+
return "ASYNC_TEST_FAILURE"
|
|
86
93
|
|
|
87
94
|
@trace_span
|
|
88
95
|
async def my_failing_async_span():
|
|
@@ -97,6 +104,20 @@ async def test_trace_span_async_failure(mock_tracer_deps):
|
|
|
97
104
|
with pytest.raises(AsyncTestError, match="Async failure test"):
|
|
98
105
|
await my_async_workflow_fail()
|
|
99
106
|
|
|
107
|
+
mock_alerter.notify.assert_called_once()
|
|
108
|
+
alert_props = mock_alerter.notify.call_args.args[0]
|
|
109
|
+
|
|
110
|
+
assert alert_props["status"] == "ERROR"
|
|
111
|
+
assert "AsyncTestError: Async failure test" in alert_props["error_message"]
|
|
112
|
+
assert alert_props["function_name"] == "my_failing_async_span"
|
|
113
|
+
assert alert_props["error_code"] == "ASYNC_TEST_FAILURE"
|
|
114
|
+
|
|
115
|
+
mock_batch.add_object.assert_called_once()
|
|
116
|
+
db_props = mock_batch.add_object.call_args.kwargs["properties"]
|
|
117
|
+
|
|
118
|
+
assert db_props == alert_props
|
|
119
|
+
assert db_props["span_id"] == alert_props["span_id"]
|
|
120
|
+
|
|
100
121
|
# --- Verification (Log) ---
|
|
101
122
|
# Check if the failed span log was recorded
|
|
102
123
|
failing_span_props = None
|
|
@@ -116,6 +137,7 @@ async def test_span_without_root_async_does_nothing(mock_tracer_deps):
|
|
|
116
137
|
Case 3: Async span called without a root (@trace_root) should not be logged
|
|
117
138
|
'''
|
|
118
139
|
mock_batch = mock_tracer_deps["batch"]
|
|
140
|
+
mock_alerter = mock_tracer_deps["alerter"]
|
|
119
141
|
|
|
120
142
|
@trace_span
|
|
121
143
|
async def my_lonely_async_span():
|
|
@@ -126,6 +148,7 @@ async def test_span_without_root_async_does_nothing(mock_tracer_deps):
|
|
|
126
148
|
|
|
127
149
|
assert result == "lonely_result"
|
|
128
150
|
mock_batch.add_object.assert_not_called()
|
|
151
|
+
mock_alerter.notify.assert_not_called()
|
|
129
152
|
|
|
130
153
|
|
|
131
154
|
# ==================================
|
|
@@ -10,6 +10,7 @@ from vectorwave.batch.batch import get_batch_manager as real_get_batch_manager
|
|
|
10
10
|
from vectorwave.database.db import get_cached_client as real_get_cached_client
|
|
11
11
|
from vectorwave.models.db_config import get_weaviate_settings as real_get_settings
|
|
12
12
|
from vectorwave.monitoring.tracer import TraceCollector, current_tracer_var
|
|
13
|
+
from vectorwave.monitoring.alert.base import BaseAlerter
|
|
13
14
|
|
|
14
15
|
# Module paths to mock (adjust to your project structure if needed)
|
|
15
16
|
TRACER_MODULE_PATH = "vectorwave.monitoring.tracer"
|
|
@@ -42,12 +43,17 @@ def mock_tracer_deps(monkeypatch):
|
|
|
42
43
|
)
|
|
43
44
|
mock_get_settings = MagicMock(return_value=mock_settings)
|
|
44
45
|
|
|
46
|
+
mock_alerter_instance = MagicMock(spec=BaseAlerter)
|
|
47
|
+
mock_alerter_instance.notify = MagicMock()
|
|
48
|
+
mock_get_alerter = MagicMock(return_value=mock_alerter_instance)
|
|
49
|
+
|
|
45
50
|
mock_client = MagicMock()
|
|
46
51
|
mock_get_client = MagicMock(return_value=mock_client)
|
|
47
52
|
|
|
48
53
|
# --- Patch dependencies for tracer.py ---
|
|
49
54
|
monkeypatch.setattr(f"{TRACER_MODULE_PATH}.get_batch_manager", mock_get_batch_manager)
|
|
50
55
|
monkeypatch.setattr(f"{TRACER_MODULE_PATH}.get_weaviate_settings", mock_get_settings)
|
|
56
|
+
monkeypatch.setattr(f"{TRACER_MODULE_PATH}.get_alerter", mock_get_alerter)
|
|
51
57
|
|
|
52
58
|
# Patch dependencies inside batch.py to prevent BatchManager init failure
|
|
53
59
|
monkeypatch.setattr(f"{BATCH_MODULE_PATH}.get_weaviate_client", mock_get_client)
|
|
@@ -60,7 +66,8 @@ def mock_tracer_deps(monkeypatch):
|
|
|
60
66
|
|
|
61
67
|
return {
|
|
62
68
|
"batch": mock_batch_instance,
|
|
63
|
-
"settings": mock_settings
|
|
69
|
+
"settings": mock_settings,
|
|
70
|
+
"alerter": mock_alerter_instance
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
|
|
@@ -69,6 +76,7 @@ def test_trace_root_and_span_success(mock_tracer_deps):
|
|
|
69
76
|
Case 1: Success (Root + Span) - The span should be recorded successfully.
|
|
70
77
|
"""
|
|
71
78
|
mock_batch = mock_tracer_deps["batch"]
|
|
79
|
+
mock_alerter = mock_tracer_deps["alerter"]
|
|
72
80
|
|
|
73
81
|
@trace_span
|
|
74
82
|
def my_inner_span(x):
|
|
@@ -84,6 +92,7 @@ def test_trace_root_and_span_success(mock_tracer_deps):
|
|
|
84
92
|
# --- Assert ---
|
|
85
93
|
assert result == "result: 10"
|
|
86
94
|
mock_batch.add_object.assert_called_once()
|
|
95
|
+
mock_alerter.notify.assert_not_called()
|
|
87
96
|
|
|
88
97
|
args, kwargs = mock_batch.add_object.call_args
|
|
89
98
|
props = kwargs["properties"]
|
|
@@ -103,6 +112,7 @@ def test_trace_span_failure(mock_tracer_deps):
|
|
|
103
112
|
Case 2: Failure (Root + Failing Span) - The span should be recorded with an ERROR status.
|
|
104
113
|
"""
|
|
105
114
|
mock_batch = mock_tracer_deps["batch"]
|
|
115
|
+
mock_alerter = mock_tracer_deps["alerter"]
|
|
106
116
|
|
|
107
117
|
@trace_span
|
|
108
118
|
def my_failing_span():
|
|
@@ -118,10 +128,21 @@ def test_trace_span_failure(mock_tracer_deps):
|
|
|
118
128
|
|
|
119
129
|
# --- Assert (Log) ---
|
|
120
130
|
mock_batch.add_object.assert_called_once()
|
|
131
|
+
db_props = mock_batch.add_object.call_args.kwargs["properties"]
|
|
132
|
+
assert db_props["status"] == "ERROR"
|
|
133
|
+
assert "ValueError: This is a test error" in db_props["error_message"]
|
|
134
|
+
assert db_props["error_code"] == "INVALID_INPUT"
|
|
135
|
+
|
|
136
|
+
mock_alerter.notify.assert_called_once()
|
|
137
|
+
alert_props = mock_alerter.notify.call_args.args[0]
|
|
121
138
|
|
|
122
139
|
args, kwargs = mock_batch.add_object.call_args
|
|
123
140
|
props = kwargs["properties"]
|
|
124
141
|
|
|
142
|
+
|
|
143
|
+
assert alert_props == db_props
|
|
144
|
+
assert alert_props["status"] == "ERROR"
|
|
145
|
+
assert alert_props["error_code"] == "INVALID_INPUT"
|
|
125
146
|
assert props["status"] == "ERROR"
|
|
126
147
|
assert "ValueError: This is a test error" in props["error_message"]
|
|
127
148
|
assert kwargs["properties"]["error_code"] == "INVALID_INPUT"
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from datetime import datetime, timezone, timedelta
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from vectorwave.search.execution_search import (
|
|
6
|
+
find_executions,
|
|
7
|
+
find_recent_errors,
|
|
8
|
+
find_slowest_executions,
|
|
9
|
+
find_by_trace_id
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def mock_low_level_search(monkeypatch):
|
|
15
|
+
"""
|
|
16
|
+
Mocks 'vectorwave.database.db_search.search_executions'
|
|
17
|
+
Used when testing the find_executions function.
|
|
18
|
+
"""
|
|
19
|
+
mock = MagicMock(return_value=["dummy_log_data"])
|
|
20
|
+
monkeypatch.setattr("vectorwave.search.execution_search.search_executions", mock)
|
|
21
|
+
return mock
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_find_executions(mock_low_level_search):
|
|
25
|
+
"""
|
|
26
|
+
Tests if find_executions correctly passes arguments (dict) to the low-level function.
|
|
27
|
+
"""
|
|
28
|
+
filters_dict = {"status": "OK"} # Use dict
|
|
29
|
+
|
|
30
|
+
find_executions(
|
|
31
|
+
filters=filters_dict,
|
|
32
|
+
limit=5,
|
|
33
|
+
sort_by="duration_ms",
|
|
34
|
+
sort_ascending=True
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Check if the low-level search_executions function was called with the correct arguments
|
|
38
|
+
mock_low_level_search.assert_called_once_with(
|
|
39
|
+
filters=filters_dict, # Check if dict was passed
|
|
40
|
+
limit=5,
|
|
41
|
+
sort_by="duration_ms",
|
|
42
|
+
sort_ascending=True
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# --- Setup for test_find_recent_errors ---
|
|
47
|
+
mock_now = datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
|
|
48
|
+
expected_time_limit_iso = (mock_now - timedelta(minutes=10)).isoformat()
|
|
49
|
+
|
|
50
|
+
# Mock error logs returned by the DB
|
|
51
|
+
mock_db_errors = [
|
|
52
|
+
{"timestamp_utc": (mock_now - timedelta(minutes=1)).isoformat(), "error_code": "INVALID_INPUT"},
|
|
53
|
+
{"timestamp_utc": (mock_now - timedelta(minutes=2)).isoformat(), "error_code": "TIMEOUT"},
|
|
54
|
+
{"timestamp_utc": (mock_now - timedelta(minutes=15)).isoformat(), "error_code": "INVALID_INPUT"}, # Too old
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# [Core Fix] Create mock datetime class
|
|
59
|
+
# Replaces the datetime.datetime class itself.
|
|
60
|
+
MockDateTime = MagicMock()
|
|
61
|
+
# Set .now() to return a fixed time (mock_now) when called
|
|
62
|
+
MockDateTime.now = MagicMock(return_value=mock_now)
|
|
63
|
+
# Set .fromisoformat() to call the real datetime.fromisoformat function
|
|
64
|
+
# (If this is omitted, a TypeError occurs in the log_time <= time_limit comparison)
|
|
65
|
+
MockDateTime.fromisoformat = MagicMock(side_effect=datetime.fromisoformat)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@patch('vectorwave.search.execution_search.datetime', MockDateTime)
|
|
69
|
+
@patch('vectorwave.search.execution_search.find_executions', MagicMock(return_value=mock_db_errors))
|
|
70
|
+
def test_find_recent_errors():
|
|
71
|
+
"""
|
|
72
|
+
Tests if find_recent_errors (including manual filtering) works correctly.
|
|
73
|
+
"""
|
|
74
|
+
# 1. Run test
|
|
75
|
+
result = find_recent_errors(minutes_ago=10, error_codes=["INVALID_INPUT"])
|
|
76
|
+
|
|
77
|
+
# 2. Verify result (if manual filtering worked correctly)
|
|
78
|
+
assert len(result) == 1 # Logs from 15 minutes ago should be filtered out
|
|
79
|
+
assert result[0]["error_code"] == "INVALID_INPUT"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@patch('vectorwave.search.execution_search.find_executions')
|
|
83
|
+
def test_find_slowest_executions(mock_find_executions):
|
|
84
|
+
"""
|
|
85
|
+
Tests if find_slowest_executions sorts correctly by 'duration_ms'.
|
|
86
|
+
"""
|
|
87
|
+
find_slowest_executions(limit=3, min_duration_ms=100.5)
|
|
88
|
+
|
|
89
|
+
call_args = mock_find_executions.call_args
|
|
90
|
+
|
|
91
|
+
# [Fix] Check if filters_arg is a dict (or None)
|
|
92
|
+
filters_arg = call_args.kwargs['filters']
|
|
93
|
+
assert (filters_arg is None) or isinstance(filters_arg, dict)
|
|
94
|
+
|
|
95
|
+
# 2. Verify sort order
|
|
96
|
+
assert call_args.kwargs['sort_by'] == 'duration_ms'
|
|
97
|
+
assert call_args.kwargs['sort_ascending'] == False
|
|
98
|
+
assert call_args.kwargs['limit'] == 3
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@patch('vectorwave.search.execution_search.find_executions')
|
|
102
|
+
def test_find_by_trace_id(mock_find_executions):
|
|
103
|
+
"""
|
|
104
|
+
Tests if find_by_trace_id filters by 'trace_id' and sorts by time.
|
|
105
|
+
"""
|
|
106
|
+
find_by_trace_id("my-test-trace-123")
|
|
107
|
+
|
|
108
|
+
call_args = mock_find_executions.call_args
|
|
109
|
+
|
|
110
|
+
# [Fix] Check if filters_arg is a dict
|
|
111
|
+
filters_arg = call_args.kwargs['filters']
|
|
112
|
+
assert isinstance(filters_arg, dict)
|
|
113
|
+
|
|
114
|
+
# 1. Verify trace_id filter
|
|
115
|
+
assert filters_arg['trace_id'] == 'my-test-trace-123'
|
|
116
|
+
|
|
117
|
+
# 2. Verify sort order (time ascending)
|
|
118
|
+
assert call_args.kwargs['sort_by'] == 'timestamp_utc'
|
|
119
|
+
assert call_args.kwargs['sort_ascending'] == True
|
|
120
|
+
assert call_args.kwargs['limit'] == 100
|
|
@@ -41,6 +41,10 @@ class WeaviateSettings(BaseSettings):
|
|
|
41
41
|
global_custom_values: Optional[Dict[str, Any]] = None
|
|
42
42
|
failure_mapping: Optional[Dict[str, str]] = None
|
|
43
43
|
|
|
44
|
+
ALERTER_STRATEGY: str = "none"
|
|
45
|
+
ALERTER_WEBHOOK_URL: Optional[str] = None
|
|
46
|
+
ALERTER_MIN_LEVEL: str = "ERROR"
|
|
47
|
+
|
|
44
48
|
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8",extra='ignore')
|
|
45
49
|
|
|
46
50
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
from ...models.db_config import get_weaviate_settings
|
|
3
|
+
from .base import BaseAlerter
|
|
4
|
+
from .webhook_alerter import WebhookAlerter
|
|
5
|
+
from .null_alerter import NullAlerter
|
|
6
|
+
|
|
7
|
+
@lru_cache()
|
|
8
|
+
def get_alerter() -> BaseAlerter:
|
|
9
|
+
settings = get_weaviate_settings()
|
|
10
|
+
strategy = settings.ALERTER_STRATEGY.lower()
|
|
11
|
+
|
|
12
|
+
if strategy == "webhook":
|
|
13
|
+
if not settings.ALERTER_WEBHOOK_URL:
|
|
14
|
+
print("Warning: ALERTER_STRATEGY='webhook' but ALERTER_WEBHOOK_URL is not set. Using 'none'.")
|
|
15
|
+
return NullAlerter()
|
|
16
|
+
return WebhookAlerter(url=settings.ALERTER_WEBHOOK_URL)
|
|
17
|
+
|
|
18
|
+
# 기본값
|
|
19
|
+
return NullAlerter()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from .base import BaseAlerter
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WebhookAlerter(BaseAlerter):
|
|
10
|
+
def __init__(self, url: str):
|
|
11
|
+
self.url = url
|
|
12
|
+
|
|
13
|
+
self.STANDARD_KEYS = {
|
|
14
|
+
'trace_id', 'span_id', 'function_name', 'timestamp_utc',
|
|
15
|
+
'duration_ms', 'status', 'error_message', 'error_code'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
def notify(self, error_log: Dict[str, Any]):
|
|
19
|
+
try:
|
|
20
|
+
func_name = error_log.get('function_name', 'N/A')
|
|
21
|
+
error_code = error_log.get('error_code', 'UNKNOWN')
|
|
22
|
+
trace_id = error_log.get('trace_id', 'N/A')
|
|
23
|
+
duration = error_log.get('duration_ms', 0)
|
|
24
|
+
timestamp = error_log.get('timestamp_utc')
|
|
25
|
+
|
|
26
|
+
error_msg_full = error_log.get('error_message', 'No error message provided.')
|
|
27
|
+
error_msg_snippet = (error_msg_full[:1000] + '...') if len(error_msg_full) > 1000 else error_msg_full
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
fields = [
|
|
31
|
+
{"name": "Function Name", "value": f"`{func_name}`", "inline": True},
|
|
32
|
+
{"name": "Error Code", "value": f"`{error_code}`", "inline": True},
|
|
33
|
+
{"name": "Duration", "value": f"{duration:.2f} ms", "inline": True},
|
|
34
|
+
{"name": "Trace ID", "value": f"`{trace_id}`", "inline": False},
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
for key, value in error_log.items():
|
|
38
|
+
if key not in self.STANDARD_KEYS and value is not None:
|
|
39
|
+
value_str = str(value)
|
|
40
|
+
if value_str:
|
|
41
|
+
fields.append(
|
|
42
|
+
{"name": f"Attribute: {key}", "value": f"`{value_str}`", "inline": True}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
formatted_payload = {
|
|
46
|
+
"username": "VectorWave Alerter",
|
|
47
|
+
"embeds": [
|
|
48
|
+
{
|
|
49
|
+
"title": f"🚨 VectorWave ERROR: {func_name}",
|
|
50
|
+
"description": f"```python\n{error_msg_snippet}\n```",
|
|
51
|
+
"color": 15158332, # Discord Red
|
|
52
|
+
"fields": fields,
|
|
53
|
+
"timestamp": timestamp if timestamp else None
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
requests.post(self.url, json=formatted_payload, timeout=5)
|
|
59
|
+
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.error(f"WebhookAlerter failed to format or send notification: {e}")
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
fallback_payload = {
|
|
65
|
+
"content": f"🚨 ERROR (Fallback): {error_log.get('function_name')}\nCode: {error_log.get('error_code')}"
|
|
66
|
+
}
|
|
67
|
+
requests.post(self.url, json=fallback_payload, timeout=3)
|
|
68
|
+
except Exception as fe:
|
|
69
|
+
logger.error(f"WebhookAlerter fallback also failed: {fe}")
|