Skip to content

Commit 1b05f52

Browse files
committed
feat(ui): simplify live output and snapshot format
1 parent 05d3e84 commit 1b05f52

File tree

4 files changed

+65
-66
lines changed

4 files changed

+65
-66
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
110110

111111
### Changed
112112

113+
- Remove the kubectl command footer from Live views and markdown snapshots to reduce on-screen clutter.
113114
- Bump Kubernetes client to v34.1.0 and refresh supporting dependencies (requests 2.32.5, rich 14.1.0, PyYAML 6.0.3).
114115
- Raise the minimum supported Python version to 3.9 and align lint/type-check targets.
115116

README.md

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,19 @@ Kubernetes 클러스터에서 이벤트, Pod, Node 상태 등을 빠르게 확
1212

1313
1. **Event Monitoring**
1414
- 2초 간격으로 `kubectl get events`를 재실행해 최신 이벤트를 확인
15-
- 실제로 실행된 `kubectl` 명령을 화면 하단에 함께 표기
15+
- 실행 명령은 내부적으로 추적하며 UI에는 이벤트 데이터만 표시
1616

1717
2. **Container Monitoring (재시작된 컨테이너 및 로그)**
1818
- 최근에 재시작된 컨테이너를 시간 기준으로 정렬하여 확인하고, 특정 컨테이너의 이전 로그(-p 옵션)를 확인
1919

2020
3. **Pod Monitoring**
2121
- 생성된 순서, Running이 아닌 Pod, 전체/정상/비정상 Pod 개수를 조회
2222
- CPU/Memory 사용량 기준 상위 Pod를 실시간으로 확인하며 NodeGroup 필터링 가능
23+
- Ready 지표는 `[ready/total]` 형태로 고정돼 스프레드시트에서 날짜로 변환되지 않음
2324

2425
4. **Node Monitoring**
2526
- 생성된 순서(노드 정보), Unhealthy Node, CPU/Memory 사용량이 높은 노드를 확인
26-
- 모든 뷰에서 NodeGroup(라벨 기반) 필터링을 지원하고, 실행된 `kubectl` 명령을 하단에 노출
27+
- 모든 뷰에서 NodeGroup(라벨 기반) 필터링을 지원하고, UI는 노드 지표만 출력
2728

2829
## Requirements
2930

@@ -146,15 +147,25 @@ Kubernetes Monitoring Tool
146147
#### 스냅샷 저장 (코드블록 + CSV)
147148

148149
- Live 화면에서 `s`, `:s`, `save`, `:save`, `:export` 중 하나를 입력하고 Enter를 누르면 현재 프레임을 코드블록 형태로 `/var/tmp/kmp/YYYY-MM-DD-HH-MM-SS.md` 경로에 저장하고, 동일한 이름의 `.csv` 파일을 동시에 생성합니다.
149-
- `.md` 파일에는 화면의 표가 가독성 높은 텍스트 코드블록으로 기록되고, 실행 명령이 함께 포함됩니다.
150+
- `.md` 파일에는 화면의 표를 그대로 옮긴 텍스트 코드블록이 저장되며, 상태 메시지는 이탤릭 한 줄로 시작합니다.
151+
- 예시:
152+
153+
```
154+
*:white_check_mark: Event Monitoring*
155+
156+
Namespace LastSeen Type Reason Object Message
157+
--------- ------------------- ------ ------ ---------------------------------- ---------------
158+
default 2025-10-13 14:33:58 Normal Valid ClusterSecretStore/parameter-store store validated
159+
```
160+
- `.md` 파일에는 실행 명령이 포함되지 않으며, UI에서도 명령은 노출하지 않습니다.
150161
- `.csv` 파일은 동일한 데이터 집합을 구조화해 제공하며, 별도의 CSV 저장 명령(`csv`, `:csv`)도 동일 포맷으로 동작합니다.
151162
- 저장이 완료되면 CLI 하단에 두 파일 경로가 표시됩니다. `/var/tmp/kmp`에 쓰기 권한이 없으면 저장이 실패하며, 오류 메시지를 통해 원인을 안내합니다.
152163
- Live 모드 상단 `command input` 패널에서 `:` 프롬프트에 따라 입력 중인 문자열을 실시간으로 확인할 수 있어, `:save` 등 명령이 제대로 입력됐는지 즉시 파악할 수 있습니다.
153164

154165
### 1. Event Monitoring
155166

156167
- 전체 이벤트 혹은 `type!=Normal` 이벤트를 2초 간격으로 재조회하여 최신 상태를 확인
157-
- tail -n [사용자 지정] 개수만큼 표시하며, 실행된 `kubectl get events ...` 명령을 화면 하단에서 확인 가능
168+
- tail -n [사용자 지정] 개수만큼 표시하며, 이벤트 본문만 갱신
158169

159170
### 2. Container Monitoring (재시작된 컨테이너 및 로그)
160171

@@ -164,12 +175,12 @@ Kubernetes Monitoring Tool
164175
### 3. Pod Monitoring (생성된 순서)
165176

166177
- `kubectl get po ... --sort-by=.metadata.creationTimestamp` 명령을 2초 간격으로 실행하여 최신 생성 순서를 확인
167-
- tail -n [사용자 지정] 개수만큼 표시하며, 화면 하단에서 실행 명령을 확인 가능
178+
- Ready 컬럼은 `[ready/total]` 형태로 노출되어 스프레드시트 자동 변환을 방지
168179

169180
### 4. Pod Monitoring (Running이 아닌 Pod 확인)
170181

171182
- `kubectl get pods ... | grep -ivE ' Running'` 명령을 2초 간격으로 실행해 Running이 아닌 Pod만 필터링
172-
- Pod IP 및 Node Name 표시 옵션 제공, 실행 명령은 항상 화면 하단에 노출
183+
- Pod IP 및 Node Name 표시 옵션 제공, Ready 컬럼은 `[ready/total]` 형태로 제공
173184

174185
### 5. Pod Monitoring (전체/정상/비정상 Pod 개수)
175186

@@ -179,22 +190,22 @@ Kubernetes Monitoring Tool
179190
### 6. Pod Monitoring (CPU/Memory 사용량 높은 순 정렬)
180191

181192
- `kubectl top pod` 결과를 2초마다 조회하고 CPU/Memory 기준으로 정렬하여 상위 N개 Pod를 표시
182-
- NodeGroup 라벨 기반 필터링을 지원하며, 실제 실행된 명령을 화면 하단에서 확인 가능
193+
- NodeGroup 라벨 기반 필터링을 지원하며, Ready 컬럼은 `[ready/total]` 형태로 출력
183194

184195
### 7. Node Monitoring (생성된 순서)
185196

186197
- 노드 생성 시간(`.metadata.creationTimestamp`) 기준으로 정렬된 목록을 2초마다 재조회
187-
- Zone(`topology.ebs.csi.aws.com/zone`)와 NodeGroup(`NODE_GROUP_LABEL`)을 함께 출력하며, 실행 명령을 하단에 표기
198+
- Zone(`topology.ebs.csi.aws.com/zone`)와 NodeGroup(`NODE_GROUP_LABEL`)을 함께 출력
188199

189200
### 8. Node Monitoring (Unhealthy Node 확인)
190201

191202
- `kubectl get nodes ... | grep -ivE ' Ready '` 명령을 2초마다 실행하여 Ready가 아닌 노드만 필터링
192-
- NodeGroup 필터링을 지원하고, 실행 명령을 하단에서 제공
203+
- NodeGroup 필터링을 지원하며, 건강 상태와 라벨 정보를 함께 제공
193204

194205
### 9. Node Monitoring (CPU/Memory 사용량 높은 순 정렬)
195206

196207
- `kubectl top node` 결과를 2초마다 조회해 CPU 혹은 메모리 기준으로 정렬, 상위 N개 노드를 표시
197-
- NodeGroup 라벨 기반 필터링을 지원하며, 실행된 명령을 항상 하단에서 확인 가능 (`-l node.kubernetes.io/app=<값>`)
208+
- NodeGroup 라벨 기반 필터링을 지원하며, 리소스 사용량 지표만 출력
198209

199210
## Development
200211

kubernetes_monitoring.py

Lines changed: 14 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -317,9 +317,6 @@ def _format_plain_snapshot(payload: SnapshotPayload) -> str:
317317
else:
318318
lines.extend(["```", "No data available.", "```"])
319319

320-
if payload.command:
321-
lines.append("*Command*")
322-
lines.extend(["```", payload.command.strip(), "```"])
323320
return "\n".join(lines)
324321

325322

@@ -330,7 +327,10 @@ def _format_table_snapshot(
330327
command: str,
331328
status: str = "info",
332329
) -> str:
333-
"""테이블 데이터를 현재 화면과 유사한 텍스트 코드블록으로 변환."""
330+
"""테이블 데이터를 현재 화면과 유사한 텍스트 코드블록으로 변환.
331+
332+
`command` 인자는 호환성을 위해 유지하며 렌더링에는 사용하지 않는다.
333+
"""
334334
sanitized_headers = [str(header).strip() for header in headers]
335335
column_count = len(sanitized_headers)
336336

@@ -378,25 +378,13 @@ def _format_row(cells: Sequence[str]) -> str:
378378

379379
icon = _status_icon(status)
380380
title_text = title.strip() or "Snapshot"
381-
title_line = f"{icon} {title_text}" if icon else title_text
382-
status_text = status.strip().upper() or "INFO"
381+
if icon:
382+
title_line = f"*{icon} {title_text}*"
383+
else:
384+
title_line = f"*{title_text}*"
383385

384-
command_lines = [
385-
line.rstrip() for line in command.strip().splitlines() if line.strip()
386-
]
387-
if not command_lines:
388-
command_lines = ["(no command)"]
389-
390-
snapshot_lines: List[str] = [
391-
title_line,
392-
f"Status: {status_text}",
393-
"",
394-
*table_lines,
395-
"",
396-
"Command:",
397-
*(f"$ {line}" for line in command_lines),
398-
]
399-
return "\n".join(["```text", *snapshot_lines, "```"])
386+
snapshot_lines: List[str] = [title_line, "", "```", *table_lines, "```"]
387+
return "\n".join(snapshot_lines)
400388

401389

402390
def _generate_snapshot_timestamp() -> str:
@@ -709,9 +697,7 @@ def update(
709697
body_renderable = _merge_renderables(renderable.body_renderables)
710698
command_descriptor = renderable.command
711699
input_renderable = renderable.input_panel
712-
footer_renderable = renderable.footer_panel or _command_panel(
713-
command_descriptor
714-
)
700+
footer_renderable = renderable.footer_panel
715701

716702
changed = self._sync_input_panel(current_input_state, input_renderable)
717703

@@ -823,16 +809,6 @@ def _run_shell_command(command: str) -> Tuple[str, Optional[str]]:
823809
return completed.stdout, None
824810

825811

826-
def _command_panel(command: str) -> Panel:
827-
"""공통적으로 사용하는 kubectl 명령 패널 생성."""
828-
command_display = command.strip() or "(no command)"
829-
return Panel(
830-
Text(f"$ {command_display}", style="bold cyan"),
831-
title="kubectl command",
832-
border_style="cyan",
833-
)
834-
835-
836812
def _merge_renderables(renderables: Sequence[RenderableType]) -> RenderableType:
837813
"""렌더러 목록을 단일 RenderableType으로 축약."""
838814
if not renderables:
@@ -871,7 +847,8 @@ def __rich_console__(self, console: Console, options): # type: ignore[override]
871847
yield self._input_panel
872848
for item in self.body_renderables:
873849
yield item
874-
yield self._footer_panel
850+
if self._footer_panel is not None:
851+
yield self._footer_panel
875852

876853
@property
877854
def input_panel(self) -> RenderableType:
@@ -883,7 +860,7 @@ def footer_panel(self) -> Optional[RenderableType]:
883860

884861

885862
def _compose_group(command: str, *renderables: RenderableType) -> _FrameRenderable:
886-
"""메인 콘텐츠와 kubectl 명령을 묶어 FrameRenderable 생성."""
863+
"""메인 콘텐츠와 메타데이터(command)를 함께 추적할 렌더러 생성."""
887864
return _FrameRenderable(command, *renderables)
888865

889866

tests/test_kubernetes_monitoring.py

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
22
import sys
33
from pathlib import Path
4-
from typing import Dict, Optional, Sequence
4+
from typing import Dict, Optional, Sequence, cast
55
from unittest.mock import MagicMock, patch
66

77
import pytest
@@ -144,15 +144,19 @@ def test_choose_namespace_failure(mock_client):
144144
kubernetes_monitoring.choose_namespace()
145145

146146

147-
def test_save_markdown_snapshot_success(tmp_path, monkeypatch):
147+
def test_save_markdown_snapshot_success(
148+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
149+
) -> None:
148150
"""코드블록 스냅샷 저장 성공 시 tmp 경로에 파일 생성."""
149151
monkeypatch.setattr(kubernetes_monitoring, "SNAPSHOT_EXPORT_DIR", tmp_path)
150152
path = kubernetes_monitoring._save_markdown_snapshot("hello world")
151153
assert path.parent == tmp_path
152154
assert path.read_text(encoding="utf-8") == "hello world\n"
153155

154156

155-
def test_save_markdown_snapshot_permission_error(monkeypatch):
157+
def test_save_markdown_snapshot_permission_error(
158+
monkeypatch: pytest.MonkeyPatch,
159+
) -> None:
156160
"""디렉터리 생성 실패 시 SnapshotSaveError 발생."""
157161

158162
def fail_mkdir(*args, **kwargs):
@@ -164,7 +168,7 @@ def fail_mkdir(*args, **kwargs):
164168
kubernetes_monitoring._save_markdown_snapshot("data")
165169

166170

167-
def test_format_table_snapshot_uses_code_block():
171+
def test_format_table_snapshot_uses_code_block() -> None:
168172
"""테이블 스냅샷이 코드블록 형태로 출력되는지 확인."""
169173
snapshot = kubernetes_monitoring._format_table_snapshot(
170174
title="Sample",
@@ -173,20 +177,23 @@ def test_format_table_snapshot_uses_code_block():
173177
command="kubectl get pods",
174178
status="success",
175179
)
176-
assert snapshot.startswith("```text")
177-
assert "| ---" not in snapshot
178-
assert "| A |" not in snapshot
179-
assert "Status: SUCCESS" in snapshot
180-
assert "$ kubectl get pods" in snapshot
181-
assert snapshot.endswith("```")
180+
lines = snapshot.splitlines()
181+
assert lines[0] == "*:white_check_mark: Sample*"
182+
assert lines[1] == ""
183+
assert lines[2] == "```"
184+
assert "A B" in lines[3]
185+
assert "Command" not in snapshot
186+
assert lines[-1] == "```"
182187

183188

184189
class _DummyLive:
185190
def __init__(self, console: Console) -> None:
186191
self.console = console
187192

188193

189-
def test_handle_snapshot_command_messages(monkeypatch, tmp_path):
194+
def test_handle_snapshot_command_messages(
195+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
196+
) -> None:
190197
"""저장 명령 처리 시 사용자 입력을 메시지에 포함한다."""
191198
kubernetes_monitoring._clear_input_display()
192199
invalid_console = Console(record=True)
@@ -195,7 +202,7 @@ def test_handle_snapshot_command_messages(monkeypatch, tmp_path):
195202
mock_tracker.latest_snapshot = None
196203
mock_tracker.latest_structured_data = None
197204
kubernetes_monitoring._handle_snapshot_command(
198-
invalid_live, mock_tracker, "invalid"
205+
cast(Live, invalid_live), mock_tracker, "invalid"
199206
)
200207
text_output = invalid_console.export_text()
201208
assert "입력 'invalid' 은(는) 지원하지 않는 명령입니다." in text_output
@@ -212,11 +219,12 @@ def test_handle_snapshot_command_messages(monkeypatch, tmp_path):
212219
)
213220

214221
captured_timestamps: Dict[str, Optional[str]] = {}
222+
base_dir: Path = tmp_path
215223

216224
def fake_save_markdown(markdown: str, timestamp: Optional[str] = None) -> Path:
217225
captured_timestamps["markdown"] = timestamp
218226
name = f"{timestamp or 'md-fallback'}.md"
219-
target = tmp_path / name
227+
target: Path = base_dir / name
220228
target.write_text(markdown, encoding="utf-8")
221229
return target
222230

@@ -227,11 +235,11 @@ def fake_save_csv(
227235
) -> Path:
228236
captured_timestamps["csv"] = timestamp
229237
name = f"{timestamp or 'csv-fallback'}.csv"
230-
target = tmp_path / name
238+
target: Path = base_dir / name
231239
with target.open("w", encoding="utf-8") as handle:
232240
handle.write(",".join(headers) + "\n")
233241
for row in rows:
234-
handle.write(",".join(row) + "\n")
242+
handle.write(",".join(str(value) for value in row) + "\n")
235243
return target
236244

237245
monkeypatch.setattr(
@@ -252,15 +260,17 @@ def fake_save_csv(
252260
"headers": ["foo"],
253261
"rows": [["bar"]],
254262
}
255-
kubernetes_monitoring._handle_snapshot_command(success_live, mock_tracker, ":save")
263+
kubernetes_monitoring._handle_snapshot_command(
264+
cast(Live, success_live), mock_tracker, ":save"
265+
)
256266
text_output = success_console.export_text()
257267
assert "입력 ':save' 처리 성공" in text_output
258268
assert "스냅샷 저장 완료" in text_output
259269
assert "(CSV:" in text_output
260270
assert captured_timestamps["markdown"] == captured_timestamps["csv"]
261271

262272

263-
def test_live_frame_tracker_updates_sections():
273+
def test_live_frame_tracker_updates_sections() -> None:
264274
"""LiveFrameTracker가 섹션별 프레임 키를 독립적으로 추적한다."""
265275
kubernetes_monitoring._clear_input_display()
266276
console = Console(record=True)
@@ -271,7 +281,7 @@ def test_live_frame_tracker_updates_sections():
271281
tracker.update(frame_key, renderable, snapshot_markdown=None, input_state="")
272282
assert tracker.section_frames["input"] == ("input", ("hidden", ""))
273283
assert tracker.section_frames["body"] == frame_key
274-
assert tracker.section_frames["footer"] == ("footer", ("cmd-1",))
284+
assert tracker.section_frames["footer"] is None
275285

276286
renderable_updated = kubernetes_monitoring._compose_group(
277287
"cmd-2", Text("body-1")
@@ -280,4 +290,4 @@ def test_live_frame_tracker_updates_sections():
280290
frame_key, renderable_updated, snapshot_markdown=None, input_state=""
281291
)
282292
assert tracker.section_frames["body"] == frame_key
283-
assert tracker.section_frames["footer"] == ("footer", ("cmd-2",))
293+
assert tracker.section_frames["footer"] is None

0 commit comments

Comments
 (0)