digitalkin 0.3.1.dev2__py3-none-any.whl → 0.3.2a3__py3-none-any.whl
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.
- base_server/server_async_insecure.py +6 -5
- base_server/server_async_secure.py +6 -5
- base_server/server_sync_insecure.py +5 -4
- base_server/server_sync_secure.py +5 -4
- digitalkin/__version__.py +1 -1
- digitalkin/core/job_manager/base_job_manager.py +1 -1
- digitalkin/core/job_manager/single_job_manager.py +78 -36
- digitalkin/core/job_manager/taskiq_broker.py +7 -6
- digitalkin/core/job_manager/taskiq_job_manager.py +9 -5
- digitalkin/core/task_manager/base_task_manager.py +3 -1
- digitalkin/core/task_manager/surrealdb_repository.py +29 -7
- digitalkin/core/task_manager/task_executor.py +46 -12
- digitalkin/core/task_manager/task_session.py +132 -102
- digitalkin/grpc_servers/module_server.py +95 -171
- digitalkin/grpc_servers/module_servicer.py +121 -19
- digitalkin/grpc_servers/utils/grpc_client_wrapper.py +36 -10
- digitalkin/grpc_servers/utils/utility_schema_extender.py +106 -0
- digitalkin/models/__init__.py +1 -1
- digitalkin/models/core/job_manager_models.py +0 -8
- digitalkin/models/core/task_monitor.py +23 -1
- digitalkin/models/grpc_servers/models.py +95 -8
- digitalkin/models/module/__init__.py +26 -13
- digitalkin/models/module/base_types.py +61 -0
- digitalkin/models/module/module_context.py +279 -13
- digitalkin/models/module/module_types.py +28 -392
- digitalkin/models/module/setup_types.py +547 -0
- digitalkin/models/module/tool_cache.py +230 -0
- digitalkin/models/module/tool_reference.py +160 -0
- digitalkin/models/module/utility.py +167 -0
- digitalkin/models/services/cost.py +22 -1
- digitalkin/models/services/registry.py +77 -0
- digitalkin/modules/__init__.py +5 -1
- digitalkin/modules/_base_module.py +188 -63
- digitalkin/modules/archetype_module.py +6 -1
- digitalkin/modules/tool_module.py +6 -1
- digitalkin/modules/triggers/__init__.py +8 -0
- digitalkin/modules/triggers/healthcheck_ping_trigger.py +45 -0
- digitalkin/modules/triggers/healthcheck_services_trigger.py +63 -0
- digitalkin/modules/triggers/healthcheck_status_trigger.py +52 -0
- digitalkin/services/__init__.py +4 -0
- digitalkin/services/communication/__init__.py +7 -0
- digitalkin/services/communication/communication_strategy.py +87 -0
- digitalkin/services/communication/default_communication.py +104 -0
- digitalkin/services/communication/grpc_communication.py +264 -0
- digitalkin/services/cost/cost_strategy.py +36 -14
- digitalkin/services/cost/default_cost.py +61 -1
- digitalkin/services/cost/grpc_cost.py +98 -2
- digitalkin/services/filesystem/grpc_filesystem.py +9 -2
- digitalkin/services/registry/__init__.py +22 -1
- digitalkin/services/registry/default_registry.py +156 -4
- digitalkin/services/registry/exceptions.py +47 -0
- digitalkin/services/registry/grpc_registry.py +382 -0
- digitalkin/services/registry/registry_models.py +15 -0
- digitalkin/services/registry/registry_strategy.py +106 -4
- digitalkin/services/services_config.py +25 -3
- digitalkin/services/services_models.py +5 -1
- digitalkin/services/setup/default_setup.py +1 -1
- digitalkin/services/setup/grpc_setup.py +1 -1
- digitalkin/services/storage/grpc_storage.py +1 -1
- digitalkin/services/user_profile/__init__.py +11 -0
- digitalkin/services/user_profile/grpc_user_profile.py +2 -2
- digitalkin/services/user_profile/user_profile_strategy.py +0 -15
- digitalkin/utils/__init__.py +15 -3
- digitalkin/utils/conditional_schema.py +260 -0
- digitalkin/utils/dynamic_schema.py +4 -0
- digitalkin/utils/schema_splitter.py +290 -0
- {digitalkin-0.3.1.dev2.dist-info → digitalkin-0.3.2a3.dist-info}/METADATA +12 -12
- digitalkin-0.3.2a3.dist-info/RECORD +144 -0
- {digitalkin-0.3.1.dev2.dist-info → digitalkin-0.3.2a3.dist-info}/WHEEL +1 -1
- {digitalkin-0.3.1.dev2.dist-info → digitalkin-0.3.2a3.dist-info}/top_level.txt +1 -0
- modules/archetype_with_tools_module.py +232 -0
- modules/cpu_intensive_module.py +1 -1
- modules/dynamic_setup_module.py +5 -29
- modules/minimal_llm_module.py +1 -1
- modules/text_transform_module.py +1 -1
- monitoring/digitalkin_observability/__init__.py +46 -0
- monitoring/digitalkin_observability/http_server.py +150 -0
- monitoring/digitalkin_observability/interceptors.py +176 -0
- monitoring/digitalkin_observability/metrics.py +201 -0
- monitoring/digitalkin_observability/prometheus.py +137 -0
- monitoring/tests/test_metrics.py +172 -0
- services/filesystem_module.py +7 -5
- services/storage_module.py +4 -2
- digitalkin/grpc_servers/registry_server.py +0 -65
- digitalkin/grpc_servers/registry_servicer.py +0 -456
- digitalkin-0.3.1.dev2.dist-info/RECORD +0 -119
- {digitalkin-0.3.1.dev2.dist-info → digitalkin-0.3.2a3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Tests for metrics collection.
|
|
2
|
+
|
|
3
|
+
Run with: python -m pytest tests/test_metrics.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
# Add the parent directory to the path so we can import digitalkin_observability
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
13
|
+
|
|
14
|
+
from digitalkin_observability import MetricsCollector, PrometheusExporter, get_metrics
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestMetricsCollector:
|
|
18
|
+
"""Tests for MetricsCollector singleton."""
|
|
19
|
+
|
|
20
|
+
def setup_method(self) -> None:
|
|
21
|
+
"""Reset metrics before each test."""
|
|
22
|
+
get_metrics().reset()
|
|
23
|
+
|
|
24
|
+
def test_singleton_returns_same_instance(self) -> None:
|
|
25
|
+
"""Test that get_metrics returns the same instance."""
|
|
26
|
+
m1 = get_metrics()
|
|
27
|
+
m2 = get_metrics()
|
|
28
|
+
assert m1 is m2
|
|
29
|
+
|
|
30
|
+
def test_inc_jobs_started(self) -> None:
|
|
31
|
+
"""Test incrementing jobs started counter."""
|
|
32
|
+
metrics = get_metrics()
|
|
33
|
+
metrics.inc_jobs_started("TestModule")
|
|
34
|
+
|
|
35
|
+
assert metrics.jobs_started_total == 1
|
|
36
|
+
assert metrics.active_jobs == 1
|
|
37
|
+
|
|
38
|
+
def test_inc_jobs_completed(self) -> None:
|
|
39
|
+
"""Test incrementing jobs completed counter."""
|
|
40
|
+
metrics = get_metrics()
|
|
41
|
+
metrics.inc_jobs_started("TestModule")
|
|
42
|
+
metrics.inc_jobs_completed("TestModule", 1.5)
|
|
43
|
+
|
|
44
|
+
assert metrics.jobs_completed_total == 1
|
|
45
|
+
assert metrics.active_jobs == 0
|
|
46
|
+
assert metrics.job_duration_seconds.count == 1
|
|
47
|
+
assert metrics.job_duration_seconds.total_sum == 1.5
|
|
48
|
+
|
|
49
|
+
def test_inc_jobs_failed(self) -> None:
|
|
50
|
+
"""Test incrementing jobs failed counter."""
|
|
51
|
+
metrics = get_metrics()
|
|
52
|
+
metrics.inc_jobs_started("TestModule")
|
|
53
|
+
metrics.inc_jobs_failed("TestModule")
|
|
54
|
+
|
|
55
|
+
assert metrics.jobs_failed_total == 1
|
|
56
|
+
assert metrics.active_jobs == 0
|
|
57
|
+
|
|
58
|
+
def test_inc_jobs_cancelled(self) -> None:
|
|
59
|
+
"""Test incrementing jobs cancelled counter."""
|
|
60
|
+
metrics = get_metrics()
|
|
61
|
+
metrics.inc_jobs_started("TestModule")
|
|
62
|
+
metrics.inc_jobs_cancelled("TestModule")
|
|
63
|
+
|
|
64
|
+
assert metrics.jobs_cancelled_total == 1
|
|
65
|
+
assert metrics.active_jobs == 0
|
|
66
|
+
|
|
67
|
+
def test_inc_messages_sent(self) -> None:
|
|
68
|
+
"""Test incrementing messages sent counter."""
|
|
69
|
+
metrics = get_metrics()
|
|
70
|
+
metrics.inc_messages_sent("message")
|
|
71
|
+
metrics.inc_messages_sent("file")
|
|
72
|
+
metrics.inc_messages_sent()
|
|
73
|
+
|
|
74
|
+
assert metrics.messages_sent_total == 3
|
|
75
|
+
|
|
76
|
+
def test_queue_depth_tracking(self) -> None:
|
|
77
|
+
"""Test queue depth tracking."""
|
|
78
|
+
metrics = get_metrics()
|
|
79
|
+
metrics.set_queue_depth("job1", 5)
|
|
80
|
+
metrics.set_queue_depth("job2", 3)
|
|
81
|
+
|
|
82
|
+
assert metrics.queue_depth["job1"] == 5
|
|
83
|
+
assert metrics.queue_depth["job2"] == 3
|
|
84
|
+
|
|
85
|
+
metrics.clear_queue_depth("job1")
|
|
86
|
+
assert "job1" not in metrics.queue_depth
|
|
87
|
+
|
|
88
|
+
def test_snapshot(self) -> None:
|
|
89
|
+
"""Test snapshot returns all metrics."""
|
|
90
|
+
metrics = get_metrics()
|
|
91
|
+
metrics.inc_jobs_started("TestModule")
|
|
92
|
+
metrics.inc_jobs_completed("TestModule", 0.5)
|
|
93
|
+
metrics.inc_messages_sent("message")
|
|
94
|
+
|
|
95
|
+
snapshot = metrics.snapshot()
|
|
96
|
+
|
|
97
|
+
assert snapshot["jobs_started_total"] == 1
|
|
98
|
+
assert snapshot["jobs_completed_total"] == 1
|
|
99
|
+
assert snapshot["messages_sent_total"] == 1
|
|
100
|
+
assert "job_duration_seconds" in snapshot
|
|
101
|
+
assert "by_module" in snapshot
|
|
102
|
+
assert "TestModule" in snapshot["by_module"]
|
|
103
|
+
|
|
104
|
+
def test_histogram_observe(self) -> None:
|
|
105
|
+
"""Test histogram observations."""
|
|
106
|
+
metrics = get_metrics()
|
|
107
|
+
metrics.observe_grpc_duration(0.05)
|
|
108
|
+
metrics.observe_grpc_duration(0.15)
|
|
109
|
+
|
|
110
|
+
assert metrics.grpc_request_duration_seconds.count == 2
|
|
111
|
+
assert metrics.grpc_request_duration_seconds.total_sum == pytest.approx(0.2)
|
|
112
|
+
|
|
113
|
+
def test_reset_clears_all_metrics(self) -> None:
|
|
114
|
+
"""Test reset clears all metrics."""
|
|
115
|
+
metrics = get_metrics()
|
|
116
|
+
metrics.inc_jobs_started("TestModule")
|
|
117
|
+
metrics.inc_errors()
|
|
118
|
+
|
|
119
|
+
metrics.reset()
|
|
120
|
+
|
|
121
|
+
assert metrics.jobs_started_total == 0
|
|
122
|
+
assert metrics.errors_total == 0
|
|
123
|
+
assert metrics.active_jobs == 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TestPrometheusExporter:
|
|
127
|
+
"""Tests for Prometheus exporter."""
|
|
128
|
+
|
|
129
|
+
def setup_method(self) -> None:
|
|
130
|
+
"""Reset metrics before each test."""
|
|
131
|
+
get_metrics().reset()
|
|
132
|
+
|
|
133
|
+
def test_export_returns_string(self) -> None:
|
|
134
|
+
"""Test that export returns a string."""
|
|
135
|
+
output = PrometheusExporter.export()
|
|
136
|
+
assert isinstance(output, str)
|
|
137
|
+
|
|
138
|
+
def test_export_contains_job_counters(self) -> None:
|
|
139
|
+
"""Test export contains job counters."""
|
|
140
|
+
metrics = get_metrics()
|
|
141
|
+
metrics.inc_jobs_started("TestModule")
|
|
142
|
+
|
|
143
|
+
output = PrometheusExporter.export()
|
|
144
|
+
|
|
145
|
+
assert "digitalkin_jobs_started_total 1" in output
|
|
146
|
+
assert "digitalkin_active_jobs 1" in output
|
|
147
|
+
|
|
148
|
+
def test_export_contains_histogram(self) -> None:
|
|
149
|
+
"""Test export contains histogram data."""
|
|
150
|
+
metrics = get_metrics()
|
|
151
|
+
metrics.observe_grpc_duration(0.05)
|
|
152
|
+
|
|
153
|
+
output = PrometheusExporter.export()
|
|
154
|
+
|
|
155
|
+
assert "digitalkin_grpc_request_duration_seconds" in output
|
|
156
|
+
assert "# TYPE digitalkin_grpc_request_duration_seconds histogram" in output
|
|
157
|
+
|
|
158
|
+
def test_export_contains_module_breakdown(self) -> None:
|
|
159
|
+
"""Test export contains per-module breakdown."""
|
|
160
|
+
metrics = get_metrics()
|
|
161
|
+
metrics.inc_jobs_started("MyModule")
|
|
162
|
+
|
|
163
|
+
output = PrometheusExporter.export()
|
|
164
|
+
|
|
165
|
+
assert 'digitalkin_jobs_by_module{module="MyModule",status="started"} 1' in output
|
|
166
|
+
|
|
167
|
+
def test_export_contains_help_and_type(self) -> None:
|
|
168
|
+
"""Test export contains HELP and TYPE comments."""
|
|
169
|
+
output = PrometheusExporter.export()
|
|
170
|
+
|
|
171
|
+
assert "# HELP digitalkin_jobs_started_total" in output
|
|
172
|
+
assert "# TYPE digitalkin_jobs_started_total counter" in output
|
services/filesystem_module.py
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import datetime
|
|
5
5
|
from collections.abc import Callable
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import Any
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel, Field
|
|
9
9
|
|
|
10
10
|
from digitalkin.logger import logger
|
|
11
11
|
from digitalkin.models.module import ModuleStatus
|
|
12
12
|
from digitalkin.modules.archetype_module import ArchetypeModule
|
|
13
|
+
from digitalkin.services.filesystem.filesystem_strategy import FileFilter, UploadFileData
|
|
13
14
|
from digitalkin.services.services_config import ServicesConfig
|
|
14
15
|
from digitalkin.services.services_models import ServicesMode
|
|
15
|
-
from digitalkin.services.filesystem.filesystem_strategy import FilesystemRecord, FileFilter, UploadFileData
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class ExampleInput(BaseModel):
|
|
@@ -62,7 +62,10 @@ class ExampleModule(ArchetypeModule[ExampleInput, ExampleOutput, ExampleSetup, E
|
|
|
62
62
|
|
|
63
63
|
# Define services_config_params with default values
|
|
64
64
|
services_config_strategies = {}
|
|
65
|
-
services_config_params = {
|
|
65
|
+
services_config_params = {
|
|
66
|
+
"cost": {"config": {}},
|
|
67
|
+
"storage": {"config": {}},
|
|
68
|
+
} # Filesystem has no config but it's enabled
|
|
66
69
|
|
|
67
70
|
def __init__(self, job_id: str, mission_id: str, setup_version_id: str) -> None:
|
|
68
71
|
"""Initialize the example module.
|
|
@@ -128,7 +131,6 @@ class ExampleModule(ArchetypeModule[ExampleInput, ExampleOutput, ExampleSetup, E
|
|
|
128
131
|
callback(record.model_dump())
|
|
129
132
|
# Call the callback with the output data
|
|
130
133
|
|
|
131
|
-
|
|
132
134
|
# Wait a bit to simulate processing time
|
|
133
135
|
await asyncio.sleep(1)
|
|
134
136
|
|
|
@@ -173,7 +175,7 @@ async def test_module() -> None:
|
|
|
173
175
|
|
|
174
176
|
# Check the storage
|
|
175
177
|
if module.status == ModuleStatus.STOPPED:
|
|
176
|
-
files,
|
|
178
|
+
files, _nb_results = module.filesystem.get_files(
|
|
177
179
|
filters=FileFilter(name="example_output.txt", context="test-mission-123"),
|
|
178
180
|
)
|
|
179
181
|
for file in files:
|
services/storage_module.py
CHANGED
|
@@ -64,7 +64,7 @@ class ExampleModule(ArchetypeModule[ExampleInput, ExampleOutput, ExampleSetup, E
|
|
|
64
64
|
|
|
65
65
|
# Define services_config_params with default values
|
|
66
66
|
services_config_strategies = {}
|
|
67
|
-
services_config_params = {"storage": {"config": {"example": ExampleOutput}},"cost": {"config":{}}}
|
|
67
|
+
services_config_params = {"storage": {"config": {"example": ExampleOutput}}, "cost": {"config": {}}}
|
|
68
68
|
|
|
69
69
|
def __init__(self, job_id: str, mission_id: str, setup_version_id: str) -> None:
|
|
70
70
|
"""Initialize the example module.
|
|
@@ -184,7 +184,9 @@ async def test_module() -> None:
|
|
|
184
184
|
def test_storage_directly() -> None:
|
|
185
185
|
"""Test the storage service directly."""
|
|
186
186
|
# Initialize storage service
|
|
187
|
-
storage = ServicesConfig().storage(
|
|
187
|
+
storage = ServicesConfig().storage(
|
|
188
|
+
mission_id="test-mission", setup_version_id="test-setup-123", config={"example": ExampleStorage}
|
|
189
|
+
)
|
|
188
190
|
|
|
189
191
|
# Create a test record
|
|
190
192
|
storage.store("example", "test_table", {"test_key": "test_value"}, "OUTPUT")
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
"""Registry gRPC server implementation for DigitalKin."""
|
|
2
|
-
|
|
3
|
-
from digitalkin_proto.agentic_mesh_protocol.module_registry.v1 import (
|
|
4
|
-
module_registry_service_pb2,
|
|
5
|
-
module_registry_service_pb2_grpc,
|
|
6
|
-
)
|
|
7
|
-
|
|
8
|
-
from digitalkin.grpc_servers._base_server import BaseServer
|
|
9
|
-
from digitalkin.grpc_servers.registry_servicer import RegistryModule, RegistryServicer
|
|
10
|
-
from digitalkin.logger import logger
|
|
11
|
-
from digitalkin.models.grpc_servers.models import RegistryServerConfig
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class RegistryServer(BaseServer):
|
|
15
|
-
"""gRPC server for DigitalKin module registry.
|
|
16
|
-
|
|
17
|
-
This server implements the ModuleRegistryService which allows modules to register
|
|
18
|
-
themselves and be discovered by other components in the system.
|
|
19
|
-
|
|
20
|
-
Attributes:
|
|
21
|
-
config: Server configuration.
|
|
22
|
-
registry_servicer: The gRPC servicer handling registry requests.
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
def __init__(
|
|
26
|
-
self,
|
|
27
|
-
config: RegistryServerConfig,
|
|
28
|
-
) -> None:
|
|
29
|
-
"""Initialize the registry server.
|
|
30
|
-
|
|
31
|
-
Args:
|
|
32
|
-
config: Server configuration.
|
|
33
|
-
"""
|
|
34
|
-
super().__init__(config)
|
|
35
|
-
self.config = config
|
|
36
|
-
self.registry_servicer: RegistryServicer | None = None
|
|
37
|
-
|
|
38
|
-
def _register_servicers(self) -> None:
|
|
39
|
-
"""Register the registry servicer with the gRPC server.
|
|
40
|
-
|
|
41
|
-
Raises:
|
|
42
|
-
RuntimeError: If server is not registered during server creation
|
|
43
|
-
"""
|
|
44
|
-
if self.server is None:
|
|
45
|
-
msg = "Server must be created before registering servicers"
|
|
46
|
-
raise RuntimeError(msg)
|
|
47
|
-
|
|
48
|
-
logger.debug("Registering registry servicer")
|
|
49
|
-
self.registry_servicer = RegistryServicer()
|
|
50
|
-
self.register_servicer(
|
|
51
|
-
self.registry_servicer,
|
|
52
|
-
module_registry_service_pb2_grpc.add_ModuleRegistryServiceServicer_to_server,
|
|
53
|
-
service_descriptor=module_registry_service_pb2.DESCRIPTOR,
|
|
54
|
-
)
|
|
55
|
-
logger.debug("Registered registry servicer")
|
|
56
|
-
|
|
57
|
-
def get_registered_modules(self) -> list[RegistryModule]:
|
|
58
|
-
"""Get a list of all registered modules.
|
|
59
|
-
|
|
60
|
-
Returns:
|
|
61
|
-
A list of module information objects.
|
|
62
|
-
"""
|
|
63
|
-
if self.registry_servicer:
|
|
64
|
-
return list(self.registry_servicer.registered_modules.values())
|
|
65
|
-
return []
|