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.
Files changed (65) hide show
  1. {vectorwave-0.1.4/src/vectorwave.egg-info → vectorwave-0.1.5}/PKG-INFO +30 -2
  2. {vectorwave-0.1.4 → vectorwave-0.1.5}/Readme.md +28 -1
  3. {vectorwave-0.1.4 → vectorwave-0.1.5}/pyproject.toml +3 -2
  4. vectorwave-0.1.5/src/tests/monitoring/alert/test_alerter.py +70 -0
  5. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/monitoring/test_async_trace.py +24 -1
  6. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/monitoring/test_tracer.py +22 -1
  7. vectorwave-0.1.5/src/tests/search/test_execution_search.py +120 -0
  8. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/core/decorator.py +2 -0
  9. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/models/db_config.py +4 -0
  10. vectorwave-0.1.5/src/vectorwave/monitoring/alert/base.py +8 -0
  11. vectorwave-0.1.5/src/vectorwave/monitoring/alert/factory.py +19 -0
  12. vectorwave-0.1.5/src/vectorwave/monitoring/alert/null_alerter.py +7 -0
  13. vectorwave-0.1.5/src/vectorwave/monitoring/alert/webhook_alerter.py +69 -0
  14. vectorwave-0.1.5/src/vectorwave/monitoring/tracer.py +273 -0
  15. vectorwave-0.1.5/src/vectorwave/prediction/__init__.py +0 -0
  16. vectorwave-0.1.5/src/vectorwave/prediction/predictor.py +0 -0
  17. vectorwave-0.1.5/src/vectorwave/search/__init__.py +0 -0
  18. vectorwave-0.1.5/src/vectorwave/search/execution_search.py +148 -0
  19. vectorwave-0.1.5/src/vectorwave/search/extended_search.py +0 -0
  20. vectorwave-0.1.5/src/vectorwave/search/rag_search.py +0 -0
  21. vectorwave-0.1.5/src/vectorwave/vectorizer/__init__.py +0 -0
  22. {vectorwave-0.1.4 → vectorwave-0.1.5/src/vectorwave.egg-info}/PKG-INFO +30 -2
  23. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave.egg-info/SOURCES.txt +13 -0
  24. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave.egg-info/requires.txt +1 -0
  25. vectorwave-0.1.4/src/vectorwave/monitoring/tracer.py +0 -246
  26. {vectorwave-0.1.4 → vectorwave-0.1.5}/LICENSE +0 -0
  27. {vectorwave-0.1.4 → vectorwave-0.1.5}/MANIFEST.in +0 -0
  28. {vectorwave-0.1.4 → vectorwave-0.1.5}/NOTICE +0 -0
  29. {vectorwave-0.1.4 → vectorwave-0.1.5}/setup.cfg +0 -0
  30. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/__init__.py +0 -0
  31. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/batch/__init__.py +0 -0
  32. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/batch/test_batch.py +0 -0
  33. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/core/__init__.py +0 -0
  34. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/core/test_decorator.py +0 -0
  35. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/database/__init__.py +0 -0
  36. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/database/test_db.py +0 -0
  37. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/database/test_db_search.py +0 -0
  38. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/exception/__init__.py +0 -0
  39. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/models/__init__.py +0 -0
  40. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/models/test_db_config.py +0 -0
  41. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/tests/monitoring/__init__.py +0 -0
  42. {vectorwave-0.1.4/src/tests/prediction → vectorwave-0.1.5/src/tests/monitoring/alert}/__init__.py +0 -0
  43. {vectorwave-0.1.4/src/tests/vectorizer → vectorwave-0.1.5/src/tests/prediction}/__init__.py +0 -0
  44. {vectorwave-0.1.4/src/vectorwave/batch → vectorwave-0.1.5/src/tests/search}/__init__.py +0 -0
  45. {vectorwave-0.1.4/src/vectorwave/core → vectorwave-0.1.5/src/tests/vectorizer}/__init__.py +0 -0
  46. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/__init__.py +0 -0
  47. {vectorwave-0.1.4/src/vectorwave/database → vectorwave-0.1.5/src/vectorwave/batch}/__init__.py +0 -0
  48. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/batch/batch.py +0 -0
  49. {vectorwave-0.1.4/src/vectorwave/exception → vectorwave-0.1.5/src/vectorwave/core}/__init__.py +0 -0
  50. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/core/core.py +0 -0
  51. {vectorwave-0.1.4/src/vectorwave/models → vectorwave-0.1.5/src/vectorwave/database}/__init__.py +0 -0
  52. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/database/db.py +0 -0
  53. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/database/db_search.py +0 -0
  54. {vectorwave-0.1.4/src/vectorwave/monitoring → vectorwave-0.1.5/src/vectorwave/exception}/__init__.py +0 -0
  55. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/exception/exceptions.py +0 -0
  56. {vectorwave-0.1.4/src/vectorwave/prediction → vectorwave-0.1.5/src/vectorwave/models}/__init__.py +0 -0
  57. {vectorwave-0.1.4/src/vectorwave/vectorizer → vectorwave-0.1.5/src/vectorwave/monitoring}/__init__.py +0 -0
  58. /vectorwave-0.1.4/src/vectorwave/monitoring/monitoring.py → /vectorwave-0.1.5/src/vectorwave/monitoring/alert/__init__.py +0 -0
  59. /vectorwave-0.1.4/src/vectorwave/prediction/predictor.py → /vectorwave-0.1.5/src/vectorwave/monitoring/monitoring.py +0 -0
  60. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/vectorizer/base.py +0 -0
  61. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/vectorizer/factory.py +0 -0
  62. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/vectorizer/huggingface_vectorizer.py +0 -0
  63. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave/vectorizer/openai_vectorizer.py +0 -0
  64. {vectorwave-0.1.4 → vectorwave-0.1.5}/src/vectorwave.egg-info/dependency_links.txt +0 -0
  65. {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.4
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.4"
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
- pass
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
@@ -76,6 +76,8 @@ def vectorize(search_description: str,
76
76
  key
77
77
  )
78
78
 
79
+ static_properties.update(valid_execution_tags)
80
+
79
81
  batch.add_object(
80
82
  collection=settings.COLLECTION_NAME,
81
83
  properties=static_properties,
@@ -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,8 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Dict, Any
3
+
4
+
5
+ class BaseAlerter(ABC):
6
+ @abstractmethod
7
+ def notify(self, error_log: Dict[str, Any]):
8
+ pass
@@ -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,7 @@
1
+ from .base import BaseAlerter
2
+ from typing import Dict, Any
3
+
4
+ class NullAlerter(BaseAlerter):
5
+
6
+ def notify(self, error_log: Dict[str, Any]):
7
+ pass
@@ -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}")