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/logging.py
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logging utilities for request tracking and structured logging.
|
|
3
|
+
|
|
4
|
+
Provides request ID tracking, context propagation, custom log adapters,
|
|
5
|
+
and file-based logging configuration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import contextvars
|
|
9
|
+
import logging
|
|
10
|
+
import logging.handlers
|
|
11
|
+
import uuid
|
|
12
|
+
from collections.abc import MutableMapping
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, ClassVar
|
|
15
|
+
|
|
16
|
+
# Context variable for tracking request IDs across async operations
|
|
17
|
+
request_id_var: contextvars.ContextVar[str | None] = contextvars.ContextVar(
|
|
18
|
+
"request_id", default=None
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RequestIDFilter(logging.Filter):
|
|
23
|
+
"""Add request ID to log records if available."""
|
|
24
|
+
|
|
25
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
26
|
+
"""Add request_id attribute to log record."""
|
|
27
|
+
request_id = request_id_var.get()
|
|
28
|
+
record.request_id = request_id if request_id else "-"
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ContextLogger(logging.LoggerAdapter[logging.Logger]):
|
|
33
|
+
"""
|
|
34
|
+
Logger adapter that adds contextual information to log records.
|
|
35
|
+
|
|
36
|
+
Supports adding request_id, operation, duration_ms, and other metadata.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def process(
|
|
40
|
+
self, msg: str, kwargs: MutableMapping[str, Any]
|
|
41
|
+
) -> tuple[str, MutableMapping[str, Any]]:
|
|
42
|
+
"""Add extra context to log record."""
|
|
43
|
+
extra = kwargs.get("extra", {})
|
|
44
|
+
|
|
45
|
+
# Add request ID if available
|
|
46
|
+
request_id = request_id_var.get()
|
|
47
|
+
if request_id:
|
|
48
|
+
extra["request_id"] = request_id
|
|
49
|
+
|
|
50
|
+
# Merge with any existing extra data
|
|
51
|
+
if self.extra:
|
|
52
|
+
extra.update(self.extra)
|
|
53
|
+
|
|
54
|
+
kwargs["extra"] = extra
|
|
55
|
+
return msg, kwargs
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def generate_request_id() -> str:
|
|
59
|
+
"""Generate a unique request ID."""
|
|
60
|
+
return str(uuid.uuid4())
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def set_request_id(request_id: str | None = None) -> str:
|
|
64
|
+
"""
|
|
65
|
+
Set the request ID for the current context.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
request_id: Request ID to set. If None, generates a new one.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
The request ID that was set.
|
|
72
|
+
"""
|
|
73
|
+
if request_id is None:
|
|
74
|
+
request_id = generate_request_id()
|
|
75
|
+
request_id_var.set(request_id)
|
|
76
|
+
return request_id
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_request_id() -> str | None:
|
|
80
|
+
"""Get the current request ID from context."""
|
|
81
|
+
return request_id_var.get()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def clear_request_id() -> None:
|
|
85
|
+
"""Clear the request ID from context."""
|
|
86
|
+
request_id_var.set(None)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_context_logger(name: str, extra: dict[str, Any] | None = None) -> ContextLogger:
|
|
90
|
+
"""
|
|
91
|
+
Get a logger with context support.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
name: Logger name (usually __name__)
|
|
95
|
+
extra: Additional context to include in all log messages
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
ContextLogger instance
|
|
99
|
+
"""
|
|
100
|
+
logger = logging.getLogger(name)
|
|
101
|
+
return ContextLogger(logger, extra or {})
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ExtraFieldsFormatter(logging.Formatter):
|
|
105
|
+
"""
|
|
106
|
+
Custom formatter that includes extra fields in log output.
|
|
107
|
+
|
|
108
|
+
Formats log records to include any extra fields passed via the 'extra'
|
|
109
|
+
parameter, making debugging easier by showing all context.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
# Standard logging record attributes to exclude from extra fields
|
|
113
|
+
STANDARD_ATTRS: ClassVar[set[str]] = {
|
|
114
|
+
"name",
|
|
115
|
+
"msg",
|
|
116
|
+
"args",
|
|
117
|
+
"created",
|
|
118
|
+
"filename",
|
|
119
|
+
"funcName",
|
|
120
|
+
"levelname",
|
|
121
|
+
"levelno",
|
|
122
|
+
"lineno",
|
|
123
|
+
"module",
|
|
124
|
+
"msecs",
|
|
125
|
+
"message",
|
|
126
|
+
"pathname",
|
|
127
|
+
"process",
|
|
128
|
+
"processName",
|
|
129
|
+
"relativeCreated",
|
|
130
|
+
"thread",
|
|
131
|
+
"threadName",
|
|
132
|
+
"exc_info",
|
|
133
|
+
"exc_text",
|
|
134
|
+
"stack_info",
|
|
135
|
+
"asctime",
|
|
136
|
+
"request_id",
|
|
137
|
+
"short_name",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
141
|
+
"""Format log record including extra fields."""
|
|
142
|
+
# Strip gobby. prefix from logger name for cleaner output
|
|
143
|
+
# e.g., "gobby.http_server" -> "http_server"
|
|
144
|
+
# Use short_name attribute to avoid mutating record.name (which leaks to other handlers)
|
|
145
|
+
if record.name.startswith("gobby."):
|
|
146
|
+
record.short_name = record.name[6:] # len("gobby.") = 6
|
|
147
|
+
else:
|
|
148
|
+
record.short_name = record.name
|
|
149
|
+
|
|
150
|
+
# Format the base message
|
|
151
|
+
base_msg = super().format(record)
|
|
152
|
+
|
|
153
|
+
# Collect extra fields
|
|
154
|
+
extra_fields = {}
|
|
155
|
+
for key, value in record.__dict__.items():
|
|
156
|
+
if key not in self.STANDARD_ATTRS and not key.startswith("_"):
|
|
157
|
+
extra_fields[key] = value
|
|
158
|
+
|
|
159
|
+
# Append extra fields if any exist
|
|
160
|
+
if extra_fields:
|
|
161
|
+
extra_str = " | ".join(f"{k}={v}" for k, v in extra_fields.items())
|
|
162
|
+
return f"{base_msg} | {extra_str}"
|
|
163
|
+
|
|
164
|
+
return base_msg
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def setup_file_logging(verbose: bool = False) -> None:
|
|
168
|
+
"""
|
|
169
|
+
Configure rotating file handlers for logging.
|
|
170
|
+
|
|
171
|
+
Sets up two log files:
|
|
172
|
+
1. Main log: All messages (level from config, or DEBUG if verbose flag set)
|
|
173
|
+
2. Error log: Only ERROR and CRITICAL messages
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
verbose: If True, override config level to DEBUG
|
|
177
|
+
|
|
178
|
+
Log file paths and rotation settings are loaded from ~/.gobby/config.yaml:
|
|
179
|
+
- logging.level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
180
|
+
- logging.format: Log format (text or json)
|
|
181
|
+
- logging.client: Main log file path
|
|
182
|
+
- logging.client_error: Error log file path
|
|
183
|
+
- logging.max_size_mb: Max file size before rotation
|
|
184
|
+
- logging.backup_count: Number of backup files to keep
|
|
185
|
+
"""
|
|
186
|
+
# Load config to get log file paths and rotation settings
|
|
187
|
+
from gobby.config.app import load_config
|
|
188
|
+
|
|
189
|
+
config = load_config()
|
|
190
|
+
|
|
191
|
+
# Expand paths and ensure log directory exists
|
|
192
|
+
log_file_path = Path(config.logging.client).expanduser()
|
|
193
|
+
error_log_file_path = Path(config.logging.client_error).expanduser()
|
|
194
|
+
|
|
195
|
+
log_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
196
|
+
error_log_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
197
|
+
|
|
198
|
+
# Get rotation settings from config
|
|
199
|
+
max_bytes = config.logging.max_size_mb * 1024 * 1024
|
|
200
|
+
backup_count = config.logging.backup_count
|
|
201
|
+
|
|
202
|
+
# Get the gobby logger (package-level)
|
|
203
|
+
pkg_logger = logging.getLogger("gobby")
|
|
204
|
+
|
|
205
|
+
# Get logging level from config (verbose flag overrides to DEBUG)
|
|
206
|
+
if verbose:
|
|
207
|
+
log_level = logging.DEBUG
|
|
208
|
+
else:
|
|
209
|
+
config_level = getattr(config.logging, "level", "INFO").upper()
|
|
210
|
+
log_level = getattr(logging, config_level, logging.INFO)
|
|
211
|
+
pkg_logger.setLevel(log_level)
|
|
212
|
+
|
|
213
|
+
# Remove any existing handlers to avoid duplicates
|
|
214
|
+
for handler in pkg_logger.handlers[:]:
|
|
215
|
+
handler.close()
|
|
216
|
+
pkg_logger.removeHandler(handler)
|
|
217
|
+
|
|
218
|
+
# Get log format from config (text or json)
|
|
219
|
+
log_format_type = getattr(config.logging, "format", "text").lower()
|
|
220
|
+
|
|
221
|
+
# Create formatter based on config format
|
|
222
|
+
if log_format_type == "json":
|
|
223
|
+
# JSON format for structured logging
|
|
224
|
+
log_format = '{"time": "%(asctime)s", "level": "%(levelname)s", "module": "%(short_name)s", "func": "%(funcName)s", "message": "%(message)s"}'
|
|
225
|
+
formatter = ExtraFieldsFormatter(log_format, datefmt="%Y-%m-%dT%H:%M:%S")
|
|
226
|
+
else:
|
|
227
|
+
# Text format (default) - human readable
|
|
228
|
+
log_format = "%(asctime)s - %(levelname)-8s - %(short_name)s.%(funcName)s - %(message)s"
|
|
229
|
+
formatter = ExtraFieldsFormatter(log_format, datefmt="%Y-%m-%d %H:%M:%S")
|
|
230
|
+
|
|
231
|
+
# Create request ID filter
|
|
232
|
+
request_id_filter = RequestIDFilter()
|
|
233
|
+
|
|
234
|
+
# Create main log handler with rotation
|
|
235
|
+
main_handler = logging.handlers.RotatingFileHandler(
|
|
236
|
+
filename=str(log_file_path),
|
|
237
|
+
maxBytes=max_bytes,
|
|
238
|
+
backupCount=backup_count,
|
|
239
|
+
encoding="utf-8",
|
|
240
|
+
)
|
|
241
|
+
main_handler.setLevel(log_level)
|
|
242
|
+
main_handler.setFormatter(formatter)
|
|
243
|
+
main_handler.addFilter(request_id_filter)
|
|
244
|
+
pkg_logger.addHandler(main_handler)
|
|
245
|
+
|
|
246
|
+
# Create error log handler (ERROR and above only)
|
|
247
|
+
error_handler = logging.handlers.RotatingFileHandler(
|
|
248
|
+
filename=str(error_log_file_path),
|
|
249
|
+
maxBytes=max_bytes,
|
|
250
|
+
backupCount=backup_count,
|
|
251
|
+
encoding="utf-8",
|
|
252
|
+
)
|
|
253
|
+
error_handler.setLevel(logging.ERROR)
|
|
254
|
+
error_handler.setFormatter(formatter)
|
|
255
|
+
error_handler.addFilter(request_id_filter)
|
|
256
|
+
pkg_logger.addHandler(error_handler)
|
|
257
|
+
|
|
258
|
+
# Prevent propagation to root logger to avoid duplicate logs
|
|
259
|
+
pkg_logger.propagate = False
|
|
260
|
+
|
|
261
|
+
# Log setup confirmation
|
|
262
|
+
logger = logging.getLogger(__name__)
|
|
263
|
+
logger.debug(
|
|
264
|
+
f"File logging configured (level={logging.getLevelName(log_level)}, "
|
|
265
|
+
f"main_log={log_file_path}, error_log={error_log_file_path})"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def setup_mcp_logging(verbose: bool = False) -> tuple[logging.Logger, logging.Logger]:
|
|
270
|
+
"""
|
|
271
|
+
Configure separate loggers for MCP server and client operations.
|
|
272
|
+
|
|
273
|
+
Sets up dedicated log files for:
|
|
274
|
+
1. MCP Server: Logs for starting/stopping the MCP server, tool registration
|
|
275
|
+
2. MCP Client: Logs for connecting to downstream servers, proxy operations
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
verbose: If True, override config level to DEBUG
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Tuple of (mcp_server_logger, mcp_client_logger)
|
|
282
|
+
|
|
283
|
+
Log file paths are loaded from ~/.gobby/config.yaml:
|
|
284
|
+
- logging.mcp_server: MCP server log file path
|
|
285
|
+
- logging.mcp_client: MCP client log file path
|
|
286
|
+
"""
|
|
287
|
+
from gobby.config.app import load_config
|
|
288
|
+
|
|
289
|
+
config = load_config()
|
|
290
|
+
|
|
291
|
+
# Get log file paths from config
|
|
292
|
+
mcp_server_log_path = Path(config.logging.mcp_server).expanduser()
|
|
293
|
+
mcp_client_log_path = Path(config.logging.mcp_client).expanduser()
|
|
294
|
+
|
|
295
|
+
# Ensure directories exist
|
|
296
|
+
mcp_server_log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
297
|
+
mcp_client_log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
298
|
+
|
|
299
|
+
# Get rotation settings from config
|
|
300
|
+
max_bytes = config.logging.max_size_mb * 1024 * 1024
|
|
301
|
+
backup_count = config.logging.backup_count
|
|
302
|
+
|
|
303
|
+
# Get logging level from config (verbose flag overrides to DEBUG)
|
|
304
|
+
if verbose:
|
|
305
|
+
log_level = logging.DEBUG
|
|
306
|
+
else:
|
|
307
|
+
config_level = getattr(config.logging, "level", "INFO").upper()
|
|
308
|
+
log_level = getattr(logging, config_level, logging.INFO)
|
|
309
|
+
|
|
310
|
+
# Get log format from config
|
|
311
|
+
log_format_type = getattr(config.logging, "format", "text").lower()
|
|
312
|
+
|
|
313
|
+
if log_format_type == "json":
|
|
314
|
+
log_format = '{"time": "%(asctime)s", "level": "%(levelname)s", "name": "%(name)s", "message": "%(message)s"}'
|
|
315
|
+
date_format = "%Y-%m-%dT%H:%M:%S"
|
|
316
|
+
else:
|
|
317
|
+
log_format = "%(asctime)s - %(levelname)-8s - %(name)s - %(message)s"
|
|
318
|
+
date_format = "%Y-%m-%d %H:%M:%S"
|
|
319
|
+
|
|
320
|
+
formatter = logging.Formatter(log_format, datefmt=date_format)
|
|
321
|
+
|
|
322
|
+
# Setup MCP Server logger
|
|
323
|
+
mcp_server_logger = logging.getLogger("gobby.mcp.server")
|
|
324
|
+
mcp_server_logger.setLevel(log_level)
|
|
325
|
+
|
|
326
|
+
# Clear existing handlers to avoid duplicates
|
|
327
|
+
for handler in mcp_server_logger.handlers[:]:
|
|
328
|
+
handler.close()
|
|
329
|
+
mcp_server_logger.removeHandler(handler)
|
|
330
|
+
|
|
331
|
+
mcp_server_handler = logging.handlers.RotatingFileHandler(
|
|
332
|
+
filename=str(mcp_server_log_path),
|
|
333
|
+
maxBytes=max_bytes,
|
|
334
|
+
backupCount=backup_count,
|
|
335
|
+
encoding="utf-8",
|
|
336
|
+
)
|
|
337
|
+
mcp_server_handler.setLevel(log_level)
|
|
338
|
+
mcp_server_handler.setFormatter(formatter)
|
|
339
|
+
mcp_server_logger.addHandler(mcp_server_handler)
|
|
340
|
+
mcp_server_logger.propagate = False # Don't propagate to avoid duplicate logs
|
|
341
|
+
|
|
342
|
+
# Setup MCP Client logger
|
|
343
|
+
mcp_client_logger = logging.getLogger("gobby.mcp.client")
|
|
344
|
+
mcp_client_logger.setLevel(log_level)
|
|
345
|
+
|
|
346
|
+
# Clear existing handlers to avoid duplicates
|
|
347
|
+
for handler in mcp_client_logger.handlers[:]:
|
|
348
|
+
handler.close()
|
|
349
|
+
mcp_client_logger.removeHandler(handler)
|
|
350
|
+
|
|
351
|
+
mcp_client_handler = logging.handlers.RotatingFileHandler(
|
|
352
|
+
filename=str(mcp_client_log_path),
|
|
353
|
+
maxBytes=max_bytes,
|
|
354
|
+
backupCount=backup_count,
|
|
355
|
+
encoding="utf-8",
|
|
356
|
+
)
|
|
357
|
+
mcp_client_handler.setLevel(log_level)
|
|
358
|
+
mcp_client_handler.setFormatter(formatter)
|
|
359
|
+
mcp_client_logger.addHandler(mcp_client_handler)
|
|
360
|
+
mcp_client_logger.propagate = False # Don't propagate to avoid duplicate logs
|
|
361
|
+
|
|
362
|
+
# Log setup confirmation
|
|
363
|
+
mcp_server_logger.debug(f"MCP server logging configured (path={mcp_server_log_path})")
|
|
364
|
+
mcp_client_logger.debug(f"MCP client logging configured (path={mcp_client_log_path})")
|
|
365
|
+
|
|
366
|
+
return mcp_server_logger, mcp_client_logger
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def get_mcp_server_logger() -> logging.Logger:
|
|
370
|
+
"""Get the MCP server logger (creates if not configured)."""
|
|
371
|
+
return logging.getLogger("gobby.mcp.server")
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def get_mcp_client_logger() -> logging.Logger:
|
|
375
|
+
"""Get the MCP client logger (creates if not configured)."""
|
|
376
|
+
return logging.getLogger("gobby.mcp.client")
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Machine ID utility.
|
|
2
|
+
|
|
3
|
+
Provides stable machine identification stored in ~/.gobby/machine_id.
|
|
4
|
+
Uses py-machineid for hardware-based IDs with UUID fallback.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import threading
|
|
10
|
+
import uuid
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Thread-safe cache
|
|
16
|
+
_cache_lock = threading.Lock()
|
|
17
|
+
_cached_machine_id: str | None = None
|
|
18
|
+
|
|
19
|
+
# Default location for machine ID file
|
|
20
|
+
MACHINE_ID_FILE = Path("~/.gobby/machine_id").expanduser()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_machine_id() -> str | None:
|
|
24
|
+
"""Get stable machine ID from ~/.gobby/machine_id.
|
|
25
|
+
|
|
26
|
+
Strategy:
|
|
27
|
+
1. Return cached ID if available
|
|
28
|
+
2. Check ~/.gobby/machine_id file
|
|
29
|
+
3. If not present, generate ID and save to file
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Machine ID as string, or None if operations fail
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
OSError: If file operations fail
|
|
36
|
+
"""
|
|
37
|
+
global _cached_machine_id
|
|
38
|
+
|
|
39
|
+
# Fast path: Return cached ID
|
|
40
|
+
with _cache_lock:
|
|
41
|
+
if _cached_machine_id is not None:
|
|
42
|
+
return _cached_machine_id
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
machine_id = _get_or_create_machine_id()
|
|
46
|
+
if machine_id:
|
|
47
|
+
with _cache_lock:
|
|
48
|
+
_cached_machine_id = machine_id
|
|
49
|
+
return machine_id
|
|
50
|
+
except OSError as e:
|
|
51
|
+
# Let OSError propagate for file system issues
|
|
52
|
+
raise OSError(f"Failed to retrieve or create machine ID: {e}") from e
|
|
53
|
+
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get_or_create_machine_id() -> str:
|
|
58
|
+
"""Get or create machine ID from ~/.gobby/machine_id.
|
|
59
|
+
|
|
60
|
+
Strategy:
|
|
61
|
+
1. Read from file if present
|
|
62
|
+
2. Migrate from config.yaml if present there (one-time migration)
|
|
63
|
+
3. Generate new ID and save to file
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Machine ID string
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
OSError: If file operations fail
|
|
70
|
+
"""
|
|
71
|
+
# Ensure directory exists
|
|
72
|
+
MACHINE_ID_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
|
|
74
|
+
# Check if file exists and has content
|
|
75
|
+
if MACHINE_ID_FILE.exists():
|
|
76
|
+
content = MACHINE_ID_FILE.read_text().strip()
|
|
77
|
+
if content:
|
|
78
|
+
return content
|
|
79
|
+
|
|
80
|
+
# Generate new ID and save with atomic permissions
|
|
81
|
+
new_id = _generate_machine_id()
|
|
82
|
+
_write_file_secure(MACHINE_ID_FILE, new_id)
|
|
83
|
+
|
|
84
|
+
return new_id
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _write_file_secure(path: Path, content: str) -> None:
|
|
88
|
+
"""Write content to file with restrictive permissions atomically.
|
|
89
|
+
|
|
90
|
+
Uses os.open with O_CREAT to set permissions at creation time,
|
|
91
|
+
avoiding TOCTOU race condition with write_text()/chmod() pattern.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
path: File path to write to
|
|
95
|
+
content: Content to write
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
OSError: If file operations fail
|
|
99
|
+
"""
|
|
100
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
101
|
+
try:
|
|
102
|
+
os.write(fd, content.encode())
|
|
103
|
+
finally:
|
|
104
|
+
os.close(fd)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _generate_machine_id() -> str:
|
|
108
|
+
"""Generate a new machine ID.
|
|
109
|
+
|
|
110
|
+
Uses py-machineid for hardware-based ID, falls back to UUID4.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Generated machine ID string
|
|
114
|
+
"""
|
|
115
|
+
try:
|
|
116
|
+
import machineid
|
|
117
|
+
|
|
118
|
+
return str(machineid.id())
|
|
119
|
+
except ImportError:
|
|
120
|
+
# Library not available, use UUID fallback
|
|
121
|
+
return str(uuid.uuid4())
|
|
122
|
+
except Exception as e:
|
|
123
|
+
# machineid library failed (hardware access issues, etc.)
|
|
124
|
+
logger.debug(f"machineid.id() failed, using UUID fallback: {e}")
|
|
125
|
+
return str(uuid.uuid4())
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def clear_cache() -> None:
|
|
129
|
+
"""Clear the cached machine ID.
|
|
130
|
+
|
|
131
|
+
Useful for testing or when machine ID needs to be refreshed.
|
|
132
|
+
"""
|
|
133
|
+
global _cached_machine_id
|
|
134
|
+
with _cache_lock:
|
|
135
|
+
_cached_machine_id = None
|