Skip to content

Commit 9cb8a22

Browse files
authored
feat: Implement structured logging system with TUI and log guidelines (#160)
1 parent a80bbf8 commit 9cb8a22

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2488
-152
lines changed

.claude/docs/logging.md

Lines changed: 606 additions & 0 deletions
Large diffs are not rendered by default.

.github/pull_request_template.md

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,26 @@
1010
-
1111

1212
## 🧪 Testing
13-
<!-- How has this been tested? -->
14-
- [ ] Unit tests added/updated
15-
- [ ] Integration tests added/updated
16-
- [ ] Manual testing performed
17-
- [ ] All tests passing
13+
<!-- How has this been tested? Check all that apply -->
14+
- [ ] Unit tests added/updated (`poetry run pytest`)
15+
- [ ] All tests passing (`make test`)
16+
- [ ] Manual testing with `titan-dev`
17+
18+
<!-- If unit tests were added, briefly describe what is covered -->
19+
20+
## 📊 Logs
21+
<!-- List new log events introduced by this PR, or check the box if none -->
22+
- [ ] No new log events
23+
24+
<!-- If logs were added, list them:
25+
- `event_name` (DEBUG/INFO/ERROR) — description of when it fires and what fields it includes
26+
Example:
27+
- `git_command_ok` (DEBUG) — subcommand, duration
28+
- `ai_call_failed` (DEBUG) — provider, operation, max_tokens, duration
29+
-->
1830

1931
## ✅ Checklist
20-
- [ ] My code follows the project's style guidelines
21-
- [ ] I have performed a self-review of my own code
22-
- [ ] I have commented my code, particularly in hard-to-understand areas
23-
- [ ] I have made corresponding changes to the documentation
24-
- [ ] My changes generate no new warnings
25-
- [ ] I have added tests that prove my fix is effective or that my feature works
26-
- [ ] New and existing unit tests pass locally with my changes
27-
- [ ] Any dependent changes have been merged and published
32+
- [ ] Self-review done
33+
- [ ] Follows the project's [logging rules](.claude/docs/logging.md) (no secrets, no content in logs)
34+
- [ ] New and existing tests pass
35+
- [ ] Documentation updated if needed

plugins/titan-plugin-git/titan_plugin_git/clients/network/git_network.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
"""
99
import subprocess
1010
import shutil
11+
import time
1112
from typing import List, Optional
1213

14+
from titan_cli.core.logging.config import get_logger
15+
1316
from ...exceptions import (
1417
GitError,
1518
GitClientError,
@@ -44,6 +47,7 @@ def __init__(self, repo_path: str = "."):
4447
GitNotRepositoryError: If not in a git repository
4548
"""
4649
self.repo_path = repo_path
50+
self._logger = get_logger(__name__)
4751
self._check_git_installed()
4852
self._check_repository()
4953

@@ -98,6 +102,10 @@ def run_command(
98102
>>> output = network.run_command(["git", "status", "--short"])
99103
>>> # Returns: "M file1.txt\\n?? file2.txt"
100104
"""
105+
# Log only the subcommand — never args[2:] (may contain remote URLs, commit messages)
106+
subcommand = args[1] if len(args) > 1 else "unknown"
107+
start = time.time()
108+
101109
try:
102110
result = subprocess.run(
103111
args,
@@ -106,8 +114,19 @@ def run_command(
106114
text=True,
107115
check=check
108116
)
117+
self._logger.debug(
118+
"git_command_ok",
119+
subcommand=subcommand,
120+
duration=round(time.time() - start, 3),
121+
)
109122
return result.stdout.strip()
110123
except subprocess.CalledProcessError as e:
124+
self._logger.debug(
125+
"git_command_failed",
126+
subcommand=subcommand,
127+
duration=round(time.time() - start, 3),
128+
exit_code=e.returncode,
129+
)
111130
error_msg = e.stderr.strip() if e.stderr else str(e)
112131
if "not a git repository" in error_msg:
113132
raise GitNotRepositoryError(

plugins/titan-plugin-git/titan_plugin_git/clients/services/branch_service.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import List
99

1010
from titan_cli.core.result import ClientResult, ClientSuccess, ClientError
11+
from titan_cli.core.logging import log_client_operation
1112

1213
from ..network import GitNetwork
1314
from ...models.network.branch import NetworkGitBranch
@@ -34,6 +35,7 @@ def __init__(self, git_network: GitNetwork):
3435
"""
3536
self.git = git_network
3637

38+
@log_client_operation()
3739
def get_current_branch(self) -> ClientResult[str]:
3840
"""
3941
Get current branch name.
@@ -47,6 +49,7 @@ def get_current_branch(self) -> ClientResult[str]:
4749
except GitCommandError as e:
4850
return ClientError(error_message=str(e), error_code="BRANCH_ERROR")
4951

52+
@log_client_operation()
5053
def get_branches(self, remote: bool = False) -> ClientResult[List[UIGitBranch]]:
5154
"""
5255
List branches.
@@ -107,6 +110,7 @@ def get_branches(self, remote: bool = False) -> ClientResult[List[UIGitBranch]]:
107110
except GitCommandError as e:
108111
return ClientError(error_message=str(e), error_code="BRANCH_LIST_ERROR")
109112

113+
@log_client_operation()
110114
def create_branch(
111115
self, branch_name: str, start_point: str = "HEAD"
112116
) -> ClientResult[None]:
@@ -129,6 +133,7 @@ def create_branch(
129133
except GitCommandError as e:
130134
return ClientError(error_message=str(e), error_code="BRANCH_CREATE_ERROR")
131135

136+
@log_client_operation()
132137
def delete_branch(
133138
self, branch: str, force: bool = False
134139
) -> ClientResult[None]:
@@ -152,6 +157,7 @@ def delete_branch(
152157
except GitCommandError as e:
153158
return ClientError(error_message=str(e), error_code="BRANCH_DELETE_ERROR")
154159

160+
@log_client_operation()
155161
def checkout(self, branch: str) -> ClientResult[None]:
156162
"""
157163
Checkout a branch.
@@ -176,7 +182,8 @@ def checkout(self, branch: str) -> ClientResult[None]:
176182
except GitCommandError:
177183
return ClientError(
178184
error_message=msg.Git.BRANCH_NOT_FOUND.format(branch=branch),
179-
error_code="BRANCH_NOT_FOUND"
185+
error_code="BRANCH_NOT_FOUND",
186+
log_level="warning"
180187
)
181188

182189
# Checkout
@@ -192,10 +199,12 @@ def checkout(self, branch: str) -> ClientResult[None]:
192199
if "would be overwritten" in error_str or "uncommitted changes" in error_str:
193200
return ClientError(
194201
error_message=msg.Git.CANNOT_CHECKOUT_UNCOMMITTED_CHANGES,
195-
error_code="DIRTY_WORKING_TREE"
202+
error_code="DIRTY_WORKING_TREE",
203+
log_level="warning"
196204
)
197205
return ClientError(error_message=str(e), error_code="CHECKOUT_ERROR")
198206

207+
@log_client_operation()
199208
def branch_exists_on_remote(
200209
self, branch: str, remote: str = "origin"
201210
) -> ClientResult[bool]:

plugins/titan-plugin-git/titan_plugin_git/clients/services/commit_service.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import List, Optional
99

1010
from titan_cli.core.result import ClientResult, ClientSuccess, ClientError
11+
from titan_cli.core.logging import log_client_operation
1112

1213
from ..network import GitNetwork
1314
from ...exceptions import GitCommandError
@@ -34,6 +35,7 @@ def __init__(self, git_network: GitNetwork, main_branch: str = "main", default_r
3435
self.main_branch = main_branch
3536
self.default_remote = default_remote
3637

38+
@log_client_operation()
3739
def commit(
3840
self, message: str, all: bool = False, no_verify: bool = True
3941
) -> ClientResult[str]:
@@ -68,6 +70,7 @@ def commit(
6870
except GitCommandError as e:
6971
return ClientError(error_message=str(e), error_code="COMMIT_ERROR")
7072

73+
@log_client_operation()
7174
def get_current_commit(self) -> ClientResult[str]:
7275
"""
7376
Get current commit SHA (HEAD).
@@ -84,6 +87,7 @@ def get_current_commit(self) -> ClientResult[str]:
8487
except GitCommandError as e:
8588
return ClientError(error_message=str(e), error_code="COMMIT_ERROR")
8689

90+
@log_client_operation()
8791
def get_commit_sha(self, ref: str) -> ClientResult[str]:
8892
"""
8993
Get commit SHA for any git ref.
@@ -103,6 +107,7 @@ def get_commit_sha(self, ref: str) -> ClientResult[str]:
103107
except GitCommandError as e:
104108
return ClientError(error_message=str(e), error_code="COMMIT_ERROR")
105109

110+
@log_client_operation()
106111
def get_commits_vs_base(self) -> ClientResult[List[str]]:
107112
"""
108113
Get commit messages from base branch to HEAD.
@@ -129,6 +134,7 @@ def get_commits_vs_base(self) -> ClientResult[List[str]]:
129134
except GitCommandError as e:
130135
return ClientError(error_message=str(e), error_code="COMMIT_ERROR")
131136

137+
@log_client_operation()
132138
def get_branch_commits(
133139
self, base_branch: str, head_branch: str
134140
) -> ClientResult[List[str]]:
@@ -161,6 +167,7 @@ def get_branch_commits(
161167
except GitCommandError as e:
162168
return ClientError(error_message=str(e), error_code="COMMIT_ERROR")
163169

170+
@log_client_operation()
164171
def count_commits_ahead(self, base_branch: str = "develop") -> ClientResult[int]:
165172
"""
166173
Count how many commits current branch is ahead of base branch.
@@ -183,6 +190,7 @@ def count_commits_ahead(self, base_branch: str = "develop") -> ClientResult[int]
183190
except (GitCommandError, ValueError) as e:
184191
return ClientError(error_message=str(e), error_code="COMMIT_COUNT_ERROR")
185192

193+
@log_client_operation()
186194
def count_unpushed_commits(
187195
self, branch: Optional[str] = None, remote: str = "origin"
188196
) -> ClientResult[int]:

plugins/titan-plugin-git/titan_plugin_git/clients/services/diff_service.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
Uses network layer to execute commands and returns diff outputs.
77
"""
88
from titan_cli.core.result import ClientResult, ClientSuccess, ClientError
9+
from titan_cli.core.logging import log_client_operation
910

1011
from ..network import GitNetwork
1112
from ...exceptions import GitCommandError
@@ -30,6 +31,7 @@ def __init__(self, git_network: GitNetwork, default_remote: str = "origin"):
3031
self.git = git_network
3132
self.default_remote = default_remote
3233

34+
@log_client_operation()
3335
def get_diff(self, base_ref: str, head_ref: str = "HEAD") -> ClientResult[str]:
3436
"""
3537
Get diff between two references.
@@ -50,6 +52,7 @@ def get_diff(self, base_ref: str, head_ref: str = "HEAD") -> ClientResult[str]:
5052
except GitCommandError as e:
5153
return ClientError(error_message=str(e), error_code="DIFF_ERROR")
5254

55+
@log_client_operation()
5356
def get_uncommitted_diff(self) -> ClientResult[str]:
5457
"""
5558
Get diff of all uncommitted changes (staged + unstaged + untracked).
@@ -69,6 +72,7 @@ def get_uncommitted_diff(self) -> ClientResult[str]:
6972
except GitCommandError as e:
7073
return ClientError(error_message=str(e), error_code="DIFF_ERROR")
7174

75+
@log_client_operation()
7276
def get_staged_diff(self) -> ClientResult[str]:
7377
"""
7478
Get diff of staged changes only (index vs HEAD).
@@ -82,6 +86,7 @@ def get_staged_diff(self) -> ClientResult[str]:
8286
except GitCommandError as e:
8387
return ClientError(error_message=str(e), error_code="DIFF_ERROR")
8488

89+
@log_client_operation()
8590
def get_unstaged_diff(self) -> ClientResult[str]:
8691
"""
8792
Get diff of unstaged changes only (working directory vs index).
@@ -95,6 +100,7 @@ def get_unstaged_diff(self) -> ClientResult[str]:
95100
except GitCommandError as e:
96101
return ClientError(error_message=str(e), error_code="DIFF_ERROR")
97102

103+
@log_client_operation()
98104
def get_uncommitted_diff_stat(self) -> ClientResult[str]:
99105
"""
100106
Get diff stat summary of uncommitted changes (working tree vs HEAD).
@@ -114,6 +120,7 @@ def get_uncommitted_diff_stat(self) -> ClientResult[str]:
114120
except GitCommandError as e:
115121
return ClientError(error_message=str(e), error_code="DIFF_ERROR")
116122

123+
@log_client_operation()
117124
def get_file_diff(self, file_path: str) -> ClientResult[str]:
118125
"""
119126
Get diff for a specific file.
@@ -133,6 +140,7 @@ def get_file_diff(self, file_path: str) -> ClientResult[str]:
133140
except GitCommandError as e:
134141
return ClientError(error_message=str(e), error_code="DIFF_ERROR")
135142

143+
@log_client_operation()
136144
def get_branch_diff(self, base_branch: str, head_branch: str) -> ClientResult[str]:
137145
"""
138146
Get diff between two branches.
@@ -156,6 +164,7 @@ def get_branch_diff(self, base_branch: str, head_branch: str) -> ClientResult[st
156164
except GitCommandError as e:
157165
return ClientError(error_message=str(e), error_code="DIFF_ERROR")
158166

167+
@log_client_operation()
159168
def get_diff_stat(self, base_ref: str, head_ref: str = "HEAD") -> ClientResult[str]:
160169
"""
161170
Get diff stat summary between two references.

plugins/titan-plugin-git/titan_plugin_git/clients/services/remote_service.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from typing import Optional, Tuple
1010

1111
from titan_cli.core.result import ClientResult, ClientSuccess, ClientError
12+
from titan_cli.core.logging import log_client_operation
1213

1314
from ..network import GitNetwork
1415
from ...exceptions import GitCommandError
@@ -30,6 +31,7 @@ def __init__(self, git_network: GitNetwork):
3031
"""
3132
self.git = git_network
3233

34+
@log_client_operation()
3335
def push(
3436
self,
3537
remote: str = "origin",
@@ -69,6 +71,7 @@ def push(
6971
except GitCommandError as e:
7072
return ClientError(error_message=str(e), error_code="PUSH_ERROR")
7173

74+
@log_client_operation()
7275
def pull(
7376
self, remote: str = "origin", branch: Optional[str] = None
7477
) -> ClientResult[None]:
@@ -94,6 +97,7 @@ def pull(
9497
except GitCommandError as e:
9598
return ClientError(error_message=str(e), error_code="PULL_ERROR")
9699

100+
@log_client_operation()
97101
def fetch(
98102
self,
99103
remote: str = "origin",
@@ -127,6 +131,7 @@ def fetch(
127131
except GitCommandError as e:
128132
return ClientError(error_message=str(e), error_code="FETCH_ERROR")
129133

134+
@log_client_operation()
130135
def get_github_repo_info(self) -> ClientResult[Tuple[Optional[str], Optional[str]]]:
131136
"""
132137
Extract GitHub repository owner and name from 'origin' remote URL.

plugins/titan-plugin-git/titan_plugin_git/clients/services/stash_service.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from typing import Optional
1010

1111
from titan_cli.core.result import ClientResult, ClientSuccess, ClientError
12+
from titan_cli.core.logging import log_client_operation
1213

1314
from ..network import GitNetwork
1415
from ...exceptions import GitCommandError
@@ -31,6 +32,7 @@ def __init__(self, git_network: GitNetwork):
3132
"""
3233
self.git = git_network
3334

35+
@log_client_operation()
3436
def stash_push(self, message: Optional[str] = None) -> ClientResult[bool]:
3537
"""
3638
Stash uncommitted changes.
@@ -52,6 +54,7 @@ def stash_push(self, message: Optional[str] = None) -> ClientResult[bool]:
5254
except GitCommandError as e:
5355
return ClientError(error_message=str(e), error_code="STASH_ERROR")
5456

57+
@log_client_operation()
5558
def stash_pop(self, stash_ref: Optional[str] = None) -> ClientResult[bool]:
5659
"""
5760
Pop stash (apply and remove).
@@ -72,6 +75,7 @@ def stash_pop(self, stash_ref: Optional[str] = None) -> ClientResult[bool]:
7275
except GitCommandError as e:
7376
return ClientError(error_message=str(e), error_code="STASH_POP_ERROR")
7477

78+
@log_client_operation()
7579
def find_stash_by_message(self, message: str) -> ClientResult[Optional[str]]:
7680
"""
7781
Find stash by message.
@@ -98,6 +102,7 @@ def find_stash_by_message(self, message: str) -> ClientResult[Optional[str]]:
98102
except GitCommandError as e:
99103
return ClientError(error_message=str(e), error_code="STASH_FIND_ERROR")
100104

105+
@log_client_operation()
101106
def restore_stash(self, message: str) -> ClientResult[bool]:
102107
"""
103108
Restore stash by finding it with a message and popping it.

0 commit comments

Comments
 (0)