gobby 0.2.5__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.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
gobby/utils/metrics.py
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Metrics collection service for Gobby Client Runtime.
|
|
3
|
+
|
|
4
|
+
Provides in-memory metrics collection for monitoring daemon and HTTP
|
|
5
|
+
server performance with Prometheus-compatible export format.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from threading import Lock
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Counter:
|
|
19
|
+
"""Simple counter metric."""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
help_text: str
|
|
23
|
+
value: int = 0
|
|
24
|
+
labels: dict[str, str] = field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
def inc(self, amount: int = 1) -> None:
|
|
27
|
+
"""Increment counter by amount."""
|
|
28
|
+
self.value += amount
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Gauge:
|
|
33
|
+
"""Gauge metric that can go up or down."""
|
|
34
|
+
|
|
35
|
+
name: str
|
|
36
|
+
help_text: str
|
|
37
|
+
value: float = 0.0
|
|
38
|
+
labels: dict[str, str] = field(default_factory=dict)
|
|
39
|
+
|
|
40
|
+
def set(self, value: float) -> None:
|
|
41
|
+
"""Set gauge to value."""
|
|
42
|
+
self.value = value
|
|
43
|
+
|
|
44
|
+
def inc(self, amount: float = 1.0) -> None:
|
|
45
|
+
"""Increment gauge by amount."""
|
|
46
|
+
self.value += amount
|
|
47
|
+
|
|
48
|
+
def dec(self, amount: float = 1.0) -> None:
|
|
49
|
+
"""Decrement gauge by amount."""
|
|
50
|
+
self.value -= amount
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class Histogram:
|
|
55
|
+
"""Histogram metric for tracking distributions."""
|
|
56
|
+
|
|
57
|
+
name: str
|
|
58
|
+
help_text: str
|
|
59
|
+
buckets: list[float] = field(
|
|
60
|
+
default_factory=lambda: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]
|
|
61
|
+
)
|
|
62
|
+
bucket_counts: dict[float, int] = field(default_factory=dict)
|
|
63
|
+
sum: float = 0.0
|
|
64
|
+
count: int = 0
|
|
65
|
+
labels: dict[str, str] = field(default_factory=dict)
|
|
66
|
+
|
|
67
|
+
def __post_init__(self) -> None:
|
|
68
|
+
"""Initialize bucket counts."""
|
|
69
|
+
for bucket in self.buckets:
|
|
70
|
+
self.bucket_counts[bucket] = 0
|
|
71
|
+
|
|
72
|
+
def observe(self, value: float) -> None:
|
|
73
|
+
"""Record an observation."""
|
|
74
|
+
self.sum += value
|
|
75
|
+
self.count += 1
|
|
76
|
+
|
|
77
|
+
# Update bucket counts
|
|
78
|
+
for bucket in self.buckets:
|
|
79
|
+
if value <= bucket:
|
|
80
|
+
self.bucket_counts[bucket] += 1
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class MetricsCollector:
|
|
84
|
+
"""
|
|
85
|
+
In-memory metrics collector with thread-safe operations.
|
|
86
|
+
|
|
87
|
+
Collects counters, gauges, and histograms for monitoring
|
|
88
|
+
daemon health, HTTP performance, and memory operations.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(self) -> None:
|
|
92
|
+
"""Initialize metrics collector."""
|
|
93
|
+
self._lock = Lock()
|
|
94
|
+
self._counters: dict[str, Counter] = {}
|
|
95
|
+
self._gauges: dict[str, Gauge] = {}
|
|
96
|
+
self._histograms: dict[str, Histogram] = {}
|
|
97
|
+
self._start_time = time.time()
|
|
98
|
+
|
|
99
|
+
# Initialize core metrics
|
|
100
|
+
self._initialize_metrics()
|
|
101
|
+
|
|
102
|
+
def _initialize_metrics(self) -> None:
|
|
103
|
+
"""Initialize standard metrics."""
|
|
104
|
+
# HTTP request metrics
|
|
105
|
+
self.register_counter(
|
|
106
|
+
"http_requests_total",
|
|
107
|
+
"Total number of HTTP requests received",
|
|
108
|
+
)
|
|
109
|
+
self.register_counter(
|
|
110
|
+
"http_requests_errors_total",
|
|
111
|
+
"Total number of HTTP requests that resulted in errors",
|
|
112
|
+
)
|
|
113
|
+
self.register_histogram(
|
|
114
|
+
"http_request_duration_seconds",
|
|
115
|
+
"HTTP request duration in seconds",
|
|
116
|
+
)
|
|
117
|
+
self.register_counter(
|
|
118
|
+
"session_registrations_total",
|
|
119
|
+
"Total number of session registration requests",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Memory operation metrics
|
|
123
|
+
self.register_counter(
|
|
124
|
+
"memory_saves_total",
|
|
125
|
+
"Total number of memory save requests",
|
|
126
|
+
)
|
|
127
|
+
self.register_counter(
|
|
128
|
+
"memory_saves_succeeded_total",
|
|
129
|
+
"Total number of successful memory saves",
|
|
130
|
+
)
|
|
131
|
+
self.register_counter(
|
|
132
|
+
"memory_saves_failed_total",
|
|
133
|
+
"Total number of failed memory saves",
|
|
134
|
+
)
|
|
135
|
+
self.register_histogram(
|
|
136
|
+
"memory_save_duration_seconds",
|
|
137
|
+
"Memory save operation duration in seconds",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Context restore metrics
|
|
141
|
+
self.register_counter(
|
|
142
|
+
"context_restores_total",
|
|
143
|
+
"Total number of context restore requests",
|
|
144
|
+
)
|
|
145
|
+
self.register_counter(
|
|
146
|
+
"context_restores_succeeded_total",
|
|
147
|
+
"Total number of successful context restores",
|
|
148
|
+
)
|
|
149
|
+
self.register_counter(
|
|
150
|
+
"context_restores_failed_total",
|
|
151
|
+
"Total number of failed context restores",
|
|
152
|
+
)
|
|
153
|
+
self.register_histogram(
|
|
154
|
+
"context_restore_duration_seconds",
|
|
155
|
+
"Context restore operation duration in seconds",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# MCP call metrics
|
|
159
|
+
self.register_counter(
|
|
160
|
+
"mcp_calls_total",
|
|
161
|
+
"Total number of MCP calls made",
|
|
162
|
+
)
|
|
163
|
+
self.register_counter(
|
|
164
|
+
"mcp_calls_succeeded_total",
|
|
165
|
+
"Total number of successful MCP calls",
|
|
166
|
+
)
|
|
167
|
+
self.register_counter(
|
|
168
|
+
"mcp_calls_failed_total",
|
|
169
|
+
"Total number of failed MCP calls",
|
|
170
|
+
)
|
|
171
|
+
self.register_histogram(
|
|
172
|
+
"mcp_call_duration_seconds",
|
|
173
|
+
"MCP call duration in seconds",
|
|
174
|
+
)
|
|
175
|
+
self.register_gauge(
|
|
176
|
+
"mcp_active_connections",
|
|
177
|
+
"Number of active MCP connections",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# MCP tool call metrics (specific to tool invocations)
|
|
181
|
+
self.register_counter(
|
|
182
|
+
"mcp_tool_calls_total",
|
|
183
|
+
"Total number of MCP tool calls made",
|
|
184
|
+
)
|
|
185
|
+
self.register_counter(
|
|
186
|
+
"mcp_tool_calls_succeeded_total",
|
|
187
|
+
"Total number of successful MCP tool calls",
|
|
188
|
+
)
|
|
189
|
+
self.register_counter(
|
|
190
|
+
"mcp_tool_calls_failed_total",
|
|
191
|
+
"Total number of failed MCP tool calls",
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Background task metrics
|
|
195
|
+
self.register_gauge(
|
|
196
|
+
"background_tasks_active",
|
|
197
|
+
"Number of currently active background tasks",
|
|
198
|
+
)
|
|
199
|
+
self.register_counter(
|
|
200
|
+
"background_tasks_total",
|
|
201
|
+
"Total number of background tasks created",
|
|
202
|
+
)
|
|
203
|
+
self.register_counter(
|
|
204
|
+
"background_tasks_completed_total",
|
|
205
|
+
"Total number of background tasks completed",
|
|
206
|
+
)
|
|
207
|
+
self.register_counter(
|
|
208
|
+
"background_tasks_failed_total",
|
|
209
|
+
"Total number of background tasks that failed",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Daemon health metrics
|
|
213
|
+
self.register_gauge(
|
|
214
|
+
"daemon_uptime_seconds",
|
|
215
|
+
"Daemon uptime in seconds",
|
|
216
|
+
)
|
|
217
|
+
self.register_gauge(
|
|
218
|
+
"daemon_memory_usage_bytes",
|
|
219
|
+
"Daemon memory usage in bytes",
|
|
220
|
+
)
|
|
221
|
+
self.register_gauge(
|
|
222
|
+
"daemon_cpu_percent",
|
|
223
|
+
"Daemon CPU usage percentage",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Hook execution metrics
|
|
227
|
+
self.register_counter(
|
|
228
|
+
"hooks_total",
|
|
229
|
+
"Total number of hook executions",
|
|
230
|
+
)
|
|
231
|
+
self.register_counter(
|
|
232
|
+
"hooks_succeeded_total",
|
|
233
|
+
"Total number of successful hook executions",
|
|
234
|
+
)
|
|
235
|
+
self.register_counter(
|
|
236
|
+
"hooks_failed_total",
|
|
237
|
+
"Total number of failed hook executions",
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def register_counter(
|
|
241
|
+
self, name: str, help_text: str, labels: dict[str, str] | None = None
|
|
242
|
+
) -> Counter:
|
|
243
|
+
"""
|
|
244
|
+
Register a new counter metric.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
name: Metric name
|
|
248
|
+
help_text: Description of what this metric measures
|
|
249
|
+
labels: Optional labels for this metric
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Counter instance
|
|
253
|
+
"""
|
|
254
|
+
with self._lock:
|
|
255
|
+
if name in self._counters:
|
|
256
|
+
return self._counters[name]
|
|
257
|
+
|
|
258
|
+
counter = Counter(name=name, help_text=help_text, labels=labels or {})
|
|
259
|
+
self._counters[name] = counter
|
|
260
|
+
return counter
|
|
261
|
+
|
|
262
|
+
def register_gauge(
|
|
263
|
+
self, name: str, help_text: str, labels: dict[str, str] | None = None
|
|
264
|
+
) -> Gauge:
|
|
265
|
+
"""
|
|
266
|
+
Register a new gauge metric.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
name: Metric name
|
|
270
|
+
help_text: Description of what this metric measures
|
|
271
|
+
labels: Optional labels for this metric
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Gauge instance
|
|
275
|
+
"""
|
|
276
|
+
with self._lock:
|
|
277
|
+
if name in self._gauges:
|
|
278
|
+
return self._gauges[name]
|
|
279
|
+
|
|
280
|
+
gauge = Gauge(name=name, help_text=help_text, labels=labels or {})
|
|
281
|
+
self._gauges[name] = gauge
|
|
282
|
+
return gauge
|
|
283
|
+
|
|
284
|
+
def register_histogram(
|
|
285
|
+
self,
|
|
286
|
+
name: str,
|
|
287
|
+
help_text: str,
|
|
288
|
+
buckets: list[float] | None = None,
|
|
289
|
+
labels: dict[str, str] | None = None,
|
|
290
|
+
) -> Histogram:
|
|
291
|
+
"""
|
|
292
|
+
Register a new histogram metric.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
name: Metric name
|
|
296
|
+
help_text: Description of what this metric measures
|
|
297
|
+
buckets: Histogram buckets (defaults to standard durations)
|
|
298
|
+
labels: Optional labels for this metric
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Histogram instance
|
|
302
|
+
"""
|
|
303
|
+
with self._lock:
|
|
304
|
+
if name in self._histograms:
|
|
305
|
+
return self._histograms[name]
|
|
306
|
+
|
|
307
|
+
histogram = Histogram(
|
|
308
|
+
name=name,
|
|
309
|
+
help_text=help_text,
|
|
310
|
+
buckets=buckets or [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0],
|
|
311
|
+
labels=labels or {},
|
|
312
|
+
)
|
|
313
|
+
self._histograms[name] = histogram
|
|
314
|
+
return histogram
|
|
315
|
+
|
|
316
|
+
def inc_counter(self, name: str, amount: int = 1) -> None:
|
|
317
|
+
"""
|
|
318
|
+
Increment a counter by amount.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
name: Counter name
|
|
322
|
+
amount: Amount to increment by (default: 1)
|
|
323
|
+
"""
|
|
324
|
+
with self._lock:
|
|
325
|
+
if name in self._counters:
|
|
326
|
+
self._counters[name].inc(amount)
|
|
327
|
+
else:
|
|
328
|
+
logger.warning(f"Counter {name} not registered")
|
|
329
|
+
|
|
330
|
+
def set_gauge(self, name: str, value: float) -> None:
|
|
331
|
+
"""
|
|
332
|
+
Set a gauge to value.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
name: Gauge name
|
|
336
|
+
value: Value to set
|
|
337
|
+
"""
|
|
338
|
+
with self._lock:
|
|
339
|
+
if name in self._gauges:
|
|
340
|
+
self._gauges[name].set(value)
|
|
341
|
+
else:
|
|
342
|
+
logger.warning(f"Gauge {name} not registered")
|
|
343
|
+
|
|
344
|
+
def inc_gauge(self, name: str, amount: float = 1.0) -> None:
|
|
345
|
+
"""
|
|
346
|
+
Increment a gauge by amount.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
name: Gauge name
|
|
350
|
+
amount: Amount to increment by
|
|
351
|
+
"""
|
|
352
|
+
with self._lock:
|
|
353
|
+
if name in self._gauges:
|
|
354
|
+
self._gauges[name].inc(amount)
|
|
355
|
+
else:
|
|
356
|
+
logger.warning(f"Gauge {name} not registered")
|
|
357
|
+
|
|
358
|
+
def dec_gauge(self, name: str, amount: float = 1.0) -> None:
|
|
359
|
+
"""
|
|
360
|
+
Decrement a gauge by amount.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
name: Gauge name
|
|
364
|
+
amount: Amount to decrement by
|
|
365
|
+
"""
|
|
366
|
+
with self._lock:
|
|
367
|
+
if name in self._gauges:
|
|
368
|
+
self._gauges[name].dec(amount)
|
|
369
|
+
else:
|
|
370
|
+
logger.warning(f"Gauge {name} not registered")
|
|
371
|
+
|
|
372
|
+
def observe_histogram(self, name: str, value: float) -> None:
|
|
373
|
+
"""
|
|
374
|
+
Record an observation in a histogram.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
name: Histogram name
|
|
378
|
+
value: Value to observe
|
|
379
|
+
"""
|
|
380
|
+
with self._lock:
|
|
381
|
+
if name in self._histograms:
|
|
382
|
+
self._histograms[name].observe(value)
|
|
383
|
+
else:
|
|
384
|
+
logger.warning(f"Histogram {name} not registered")
|
|
385
|
+
|
|
386
|
+
def get_uptime(self) -> float:
|
|
387
|
+
"""
|
|
388
|
+
Get collector uptime in seconds.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
Uptime in seconds
|
|
392
|
+
"""
|
|
393
|
+
return time.time() - self._start_time
|
|
394
|
+
|
|
395
|
+
def update_daemon_metrics(self, pid: int | None = None) -> None:
|
|
396
|
+
"""
|
|
397
|
+
Update daemon health metrics (uptime, memory, CPU).
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
pid: Process ID to monitor. If None, uses current process.
|
|
401
|
+
"""
|
|
402
|
+
import os
|
|
403
|
+
|
|
404
|
+
import psutil
|
|
405
|
+
|
|
406
|
+
try:
|
|
407
|
+
# Get process
|
|
408
|
+
process = psutil.Process(pid) if pid else psutil.Process(os.getpid())
|
|
409
|
+
|
|
410
|
+
# Update uptime
|
|
411
|
+
self.set_gauge("daemon_uptime_seconds", self.get_uptime())
|
|
412
|
+
|
|
413
|
+
# Update memory usage
|
|
414
|
+
mem_info = process.memory_info()
|
|
415
|
+
self.set_gauge("daemon_memory_usage_bytes", float(mem_info.rss))
|
|
416
|
+
|
|
417
|
+
# Update CPU usage
|
|
418
|
+
cpu_percent = process.cpu_percent(interval=0.1)
|
|
419
|
+
self.set_gauge("daemon_cpu_percent", cpu_percent)
|
|
420
|
+
|
|
421
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
|
|
422
|
+
logger.warning(f"Failed to update daemon metrics: {e}")
|
|
423
|
+
|
|
424
|
+
def record_mcp_call(self, duration: float, success: bool = True) -> None:
|
|
425
|
+
"""
|
|
426
|
+
Record an MCP call with duration and success status.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
duration: Call duration in seconds
|
|
430
|
+
success: Whether the call succeeded
|
|
431
|
+
"""
|
|
432
|
+
self.inc_counter("mcp_calls_total")
|
|
433
|
+
if success:
|
|
434
|
+
self.inc_counter("mcp_calls_succeeded_total")
|
|
435
|
+
else:
|
|
436
|
+
self.inc_counter("mcp_calls_failed_total")
|
|
437
|
+
self.observe_histogram("mcp_call_duration_seconds", duration)
|
|
438
|
+
|
|
439
|
+
def record_http_request(self, duration: float, error: bool = False) -> None:
|
|
440
|
+
"""
|
|
441
|
+
Record an HTTP request with duration and error status.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
duration: Request duration in seconds
|
|
445
|
+
error: Whether the request resulted in an error
|
|
446
|
+
"""
|
|
447
|
+
self.inc_counter("http_requests_total")
|
|
448
|
+
if error:
|
|
449
|
+
self.inc_counter("http_requests_errors_total")
|
|
450
|
+
self.observe_histogram("http_request_duration_seconds", duration)
|
|
451
|
+
|
|
452
|
+
def record_memory_save(self, duration: float, success: bool = True) -> None:
|
|
453
|
+
"""
|
|
454
|
+
Record a memory save operation.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
duration: Operation duration in seconds
|
|
458
|
+
success: Whether the save succeeded
|
|
459
|
+
"""
|
|
460
|
+
self.inc_counter("memory_saves_total")
|
|
461
|
+
if success:
|
|
462
|
+
self.inc_counter("memory_saves_succeeded_total")
|
|
463
|
+
else:
|
|
464
|
+
self.inc_counter("memory_saves_failed_total")
|
|
465
|
+
self.observe_histogram("memory_save_duration_seconds", duration)
|
|
466
|
+
|
|
467
|
+
def record_context_restore(self, duration: float, success: bool = True) -> None:
|
|
468
|
+
"""
|
|
469
|
+
Record a context restore operation.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
duration: Operation duration in seconds
|
|
473
|
+
success: Whether the restore succeeded
|
|
474
|
+
"""
|
|
475
|
+
self.inc_counter("context_restores_total")
|
|
476
|
+
if success:
|
|
477
|
+
self.inc_counter("context_restores_succeeded_total")
|
|
478
|
+
else:
|
|
479
|
+
self.inc_counter("context_restores_failed_total")
|
|
480
|
+
self.observe_histogram("context_restore_duration_seconds", duration)
|
|
481
|
+
|
|
482
|
+
def get_all_metrics(self) -> dict[str, Any]:
|
|
483
|
+
"""
|
|
484
|
+
Get all metrics as a dictionary.
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
Dictionary containing all metrics
|
|
488
|
+
"""
|
|
489
|
+
with self._lock:
|
|
490
|
+
return {
|
|
491
|
+
"counters": {
|
|
492
|
+
name: {"value": counter.value, "labels": counter.labels}
|
|
493
|
+
for name, counter in self._counters.items()
|
|
494
|
+
},
|
|
495
|
+
"gauges": {
|
|
496
|
+
name: {"value": gauge.value, "labels": gauge.labels}
|
|
497
|
+
for name, gauge in self._gauges.items()
|
|
498
|
+
},
|
|
499
|
+
"histograms": {
|
|
500
|
+
name: {
|
|
501
|
+
"count": hist.count,
|
|
502
|
+
"sum": hist.sum,
|
|
503
|
+
"buckets": hist.bucket_counts,
|
|
504
|
+
"labels": hist.labels,
|
|
505
|
+
}
|
|
506
|
+
for name, hist in self._histograms.items()
|
|
507
|
+
},
|
|
508
|
+
"uptime_seconds": self.get_uptime(),
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
def export_prometheus(self) -> str:
|
|
512
|
+
"""
|
|
513
|
+
Export metrics in Prometheus text format.
|
|
514
|
+
|
|
515
|
+
Returns:
|
|
516
|
+
Metrics in Prometheus exposition format
|
|
517
|
+
"""
|
|
518
|
+
lines = []
|
|
519
|
+
|
|
520
|
+
with self._lock:
|
|
521
|
+
# Export counters
|
|
522
|
+
for name, counter in self._counters.items():
|
|
523
|
+
lines.append(f"# HELP {name} {counter.help_text}")
|
|
524
|
+
lines.append(f"# TYPE {name} counter")
|
|
525
|
+
labels_str = self._format_labels(counter.labels)
|
|
526
|
+
lines.append(f"{name}{labels_str} {counter.value}")
|
|
527
|
+
|
|
528
|
+
# Export gauges
|
|
529
|
+
for name, gauge in self._gauges.items():
|
|
530
|
+
lines.append(f"# HELP {name} {gauge.help_text}")
|
|
531
|
+
lines.append(f"# TYPE {name} gauge")
|
|
532
|
+
labels_str = self._format_labels(gauge.labels)
|
|
533
|
+
lines.append(f"{name}{labels_str} {gauge.value}")
|
|
534
|
+
|
|
535
|
+
# Export histograms
|
|
536
|
+
for name, hist in self._histograms.items():
|
|
537
|
+
lines.append(f"# HELP {name} {hist.help_text}")
|
|
538
|
+
lines.append(f"# TYPE {name} histogram")
|
|
539
|
+
labels_str = self._format_labels(hist.labels)
|
|
540
|
+
|
|
541
|
+
# Export bucket counts
|
|
542
|
+
for bucket, count in sorted(hist.bucket_counts.items()):
|
|
543
|
+
bucket_labels = {**hist.labels, "le": str(bucket)}
|
|
544
|
+
bucket_labels_str = self._format_labels(bucket_labels)
|
|
545
|
+
lines.append(f"{name}_bucket{bucket_labels_str} {count}")
|
|
546
|
+
|
|
547
|
+
# Export +Inf bucket
|
|
548
|
+
inf_labels = {**hist.labels, "le": "+Inf"}
|
|
549
|
+
inf_labels_str = self._format_labels(inf_labels)
|
|
550
|
+
lines.append(f"{name}_bucket{inf_labels_str} {hist.count}")
|
|
551
|
+
|
|
552
|
+
# Export sum and count
|
|
553
|
+
lines.append(f"{name}_sum{labels_str} {hist.sum}")
|
|
554
|
+
lines.append(f"{name}_count{labels_str} {hist.count}")
|
|
555
|
+
|
|
556
|
+
return "\n".join(lines) + "\n"
|
|
557
|
+
|
|
558
|
+
def _format_labels(self, labels: dict[str, str]) -> str:
|
|
559
|
+
"""
|
|
560
|
+
Format labels for Prometheus exposition format.
|
|
561
|
+
|
|
562
|
+
Args:
|
|
563
|
+
labels: Label dictionary
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
Formatted labels string (e.g., '{method="GET",status="200"}')
|
|
567
|
+
"""
|
|
568
|
+
if not labels:
|
|
569
|
+
return ""
|
|
570
|
+
|
|
571
|
+
label_pairs = [f'{k}="{v}"' for k, v in sorted(labels.items())]
|
|
572
|
+
return "{" + ",".join(label_pairs) + "}"
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
# Global metrics collector instance
|
|
576
|
+
_metrics_collector: MetricsCollector | None = None
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def get_metrics_collector() -> MetricsCollector:
|
|
580
|
+
"""
|
|
581
|
+
Get global metrics collector instance.
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
MetricsCollector instance
|
|
585
|
+
"""
|
|
586
|
+
global _metrics_collector
|
|
587
|
+
if _metrics_collector is None:
|
|
588
|
+
_metrics_collector = MetricsCollector()
|
|
589
|
+
return _metrics_collector
|