kctl-sentry 0.6.3__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.
- kctl_sentry/__init__.py +3 -0
- kctl_sentry/__main__.py +5 -0
- kctl_sentry/cli.py +136 -0
- kctl_sentry/commands/__init__.py +0 -0
- kctl_sentry/commands/alerts.py +176 -0
- kctl_sentry/commands/config_cmd.py +400 -0
- kctl_sentry/commands/dashboard.py +86 -0
- kctl_sentry/commands/doctor_cmd.py +57 -0
- kctl_sentry/commands/environments.py +51 -0
- kctl_sentry/commands/health.py +72 -0
- kctl_sentry/commands/issues.py +285 -0
- kctl_sentry/commands/projects.py +173 -0
- kctl_sentry/commands/releases.py +208 -0
- kctl_sentry/commands/skill_cmd.py +76 -0
- kctl_sentry/commands/stats.py +138 -0
- kctl_sentry/commands/teams.py +120 -0
- kctl_sentry/core/__init__.py +0 -0
- kctl_sentry/core/callbacks.py +33 -0
- kctl_sentry/core/client.py +84 -0
- kctl_sentry/core/config.py +122 -0
- kctl_sentry/core/exceptions.py +10 -0
- kctl_sentry/core/plugins.py +13 -0
- kctl_sentry-0.6.3.dist-info/METADATA +17 -0
- kctl_sentry-0.6.3.dist-info/RECORD +26 -0
- kctl_sentry-0.6.3.dist-info/WHEEL +4 -0
- kctl_sentry-0.6.3.dist-info/entry_points.txt +2 -0
kctl_sentry/__init__.py
ADDED
kctl_sentry/__main__.py
ADDED
kctl_sentry/cli.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Main CLI entry point for kctl-sentry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from kctl_lib import KctlError, handle_cli_error
|
|
10
|
+
|
|
11
|
+
from kctl_sentry import __version__
|
|
12
|
+
from kctl_sentry.commands.alerts import app as alerts_app
|
|
13
|
+
from kctl_sentry.commands.config_cmd import app as config_app
|
|
14
|
+
from kctl_sentry.commands.dashboard import app as dashboard_app
|
|
15
|
+
from kctl_sentry.commands.environments import app as environments_app
|
|
16
|
+
from kctl_sentry.commands.health import app as health_app
|
|
17
|
+
from kctl_sentry.commands.issues import app as issues_app
|
|
18
|
+
from kctl_sentry.commands.projects import app as projects_app
|
|
19
|
+
from kctl_sentry.commands.releases import app as releases_app
|
|
20
|
+
from kctl_sentry.commands.stats import app as stats_app
|
|
21
|
+
from kctl_sentry.commands.teams import app as teams_app
|
|
22
|
+
from kctl_sentry.core.callbacks import AppContext
|
|
23
|
+
from kctl_sentry.core.plugins import discover_and_load_plugins
|
|
24
|
+
from kctl_sentry.commands.skill_cmd import app as skill_app
|
|
25
|
+
from kctl_sentry.commands.doctor_cmd import app as doctor_app
|
|
26
|
+
from kctl_lib.self_update import notify_if_outdated
|
|
27
|
+
from kctl_lib.tui import add_tui_command
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def version_callback(value: bool) -> None:
|
|
31
|
+
if value:
|
|
32
|
+
typer.echo(f"kctl-sentry {__version__}")
|
|
33
|
+
raise typer.Exit()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
app = typer.Typer(
|
|
37
|
+
name="kctl-sentry",
|
|
38
|
+
help="Kodemeio Sentry CLI — error triage, release tracking, and project management.",
|
|
39
|
+
no_args_is_help=True,
|
|
40
|
+
rich_markup_mode="rich",
|
|
41
|
+
pretty_exceptions_enable=False,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@app.callback()
|
|
46
|
+
def main(
|
|
47
|
+
ctx: typer.Context,
|
|
48
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
|
|
49
|
+
quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Suppress info messages")] = False,
|
|
50
|
+
profile: Annotated[str | None, typer.Option("--profile", "-p", help="Config profile name")] = None,
|
|
51
|
+
format: Annotated[str, typer.Option("--format", "-f", help="Output format: pretty, json, csv, yaml")] = "pretty",
|
|
52
|
+
no_header: Annotated[bool, typer.Option("--no-header", help="Omit header row in CSV output")] = False,
|
|
53
|
+
auth_token: Annotated[str | None, typer.Option("--auth-token", help="Auth token override")] = None,
|
|
54
|
+
version: Annotated[
|
|
55
|
+
bool, typer.Option("--version", "-V", callback=version_callback, is_eager=True, help="Show version")
|
|
56
|
+
] = False,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Kodemeio Sentry CLI."""
|
|
59
|
+
ctx.ensure_object(dict)
|
|
60
|
+
ctx.obj = AppContext(
|
|
61
|
+
json_mode=json_output,
|
|
62
|
+
quiet=quiet,
|
|
63
|
+
profile=profile,
|
|
64
|
+
format=format,
|
|
65
|
+
no_header=no_header,
|
|
66
|
+
auth_token_override=auth_token,
|
|
67
|
+
)
|
|
68
|
+
notify_if_outdated(ctx.obj.output, "kctl-sentry", __version__)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
app.add_typer(config_app, name="config")
|
|
72
|
+
app.add_typer(health_app, name="health")
|
|
73
|
+
app.add_typer(dashboard_app, name="dashboard")
|
|
74
|
+
app.add_typer(issues_app, name="issues")
|
|
75
|
+
app.add_typer(projects_app, name="projects")
|
|
76
|
+
app.add_typer(releases_app, name="releases")
|
|
77
|
+
app.add_typer(alerts_app, name="alerts")
|
|
78
|
+
app.add_typer(stats_app, name="stats")
|
|
79
|
+
app.add_typer(teams_app, name="teams")
|
|
80
|
+
app.add_typer(environments_app, name="environments")
|
|
81
|
+
app.add_typer(skill_app, name="skill", hidden=True)
|
|
82
|
+
app.add_typer(doctor_app, name="doctor")
|
|
83
|
+
|
|
84
|
+
# Load third-party plugins via entry points
|
|
85
|
+
discover_and_load_plugins(app)
|
|
86
|
+
add_tui_command(app, service_key="sentry", version=__version__)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.command("self-update")
|
|
90
|
+
def self_update_cmd(ctx: typer.Context) -> None:
|
|
91
|
+
"""Check for updates and upgrade kctl-sentry."""
|
|
92
|
+
actx = ctx.obj
|
|
93
|
+
out = actx.output
|
|
94
|
+
|
|
95
|
+
from kctl_lib.self_update import check_update
|
|
96
|
+
from kctl_lib.self_update import update as do_update
|
|
97
|
+
|
|
98
|
+
latest = check_update("kctl-sentry", __version__)
|
|
99
|
+
if latest:
|
|
100
|
+
out.info(f"Updating to {latest}...")
|
|
101
|
+
do_update("kctl-sentry")
|
|
102
|
+
out.success(f"Updated to {latest}")
|
|
103
|
+
else:
|
|
104
|
+
out.success("Already up to date")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@app.command()
|
|
108
|
+
def completions(
|
|
109
|
+
shell: Annotated[str, typer.Argument(help="Shell type: zsh, bash, fish")] = "zsh",
|
|
110
|
+
install: Annotated[bool, typer.Option("--install", help="Install completions")] = False,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Generate or install shell completions."""
|
|
113
|
+
from kctl_lib.completions import get_completion_script, install_completions
|
|
114
|
+
|
|
115
|
+
if install:
|
|
116
|
+
path = install_completions("kctl-sentry", shell)
|
|
117
|
+
if path:
|
|
118
|
+
typer.echo(f"Completions installed to {path}")
|
|
119
|
+
else:
|
|
120
|
+
typer.echo(f"Could not install completions for {shell}", err=True)
|
|
121
|
+
raise typer.Exit(code=1)
|
|
122
|
+
else:
|
|
123
|
+
script = get_completion_script("kctl-sentry", shell)
|
|
124
|
+
typer.echo(script)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _run() -> None:
|
|
128
|
+
"""Entry point with error handling."""
|
|
129
|
+
try:
|
|
130
|
+
app()
|
|
131
|
+
except KctlError as e:
|
|
132
|
+
handle_cli_error(e)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
_run()
|
|
File without changes
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Alert rule management commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from kctl_sentry.core.callbacks import AppContext
|
|
10
|
+
from kctl_sentry.core.exceptions import KctlError
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="Manage alert rules.")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command("list")
|
|
16
|
+
def list_(
|
|
17
|
+
ctx: typer.Context,
|
|
18
|
+
project: Annotated[str | None, typer.Option("--project", "-p", help="Filter by project slug")] = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
"""List alert rules."""
|
|
21
|
+
c: AppContext = ctx.obj
|
|
22
|
+
out = c.output
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
rules = c.client.project_get(project, "/rules/") if project else c.client.org_get("/alert-rules/")
|
|
26
|
+
if not isinstance(rules, list):
|
|
27
|
+
rules = []
|
|
28
|
+
|
|
29
|
+
rows: list[list[str]] = []
|
|
30
|
+
for rule in rules:
|
|
31
|
+
rule_id = str(rule.get("id", ""))
|
|
32
|
+
name = (rule.get("name", "") or "")[:50]
|
|
33
|
+
# Project-scoped rules have 'projects', org-scoped have 'project'
|
|
34
|
+
proj_info = ""
|
|
35
|
+
if isinstance(rule.get("projects"), list):
|
|
36
|
+
proj_info = ", ".join(rule["projects"])
|
|
37
|
+
elif isinstance(rule.get("project"), dict):
|
|
38
|
+
proj_info = rule["project"].get("slug", "")
|
|
39
|
+
|
|
40
|
+
status_val = rule.get("status", "active")
|
|
41
|
+
owner = ""
|
|
42
|
+
if isinstance(rule.get("owner"), dict):
|
|
43
|
+
owner = rule["owner"].get("name", "")
|
|
44
|
+
elif isinstance(rule.get("owner"), str):
|
|
45
|
+
owner = rule["owner"]
|
|
46
|
+
|
|
47
|
+
rows.append([rule_id, name, proj_info, status_val, owner])
|
|
48
|
+
|
|
49
|
+
out.table(
|
|
50
|
+
"Alert Rules",
|
|
51
|
+
[
|
|
52
|
+
("ID", "cyan"),
|
|
53
|
+
("Name", ""),
|
|
54
|
+
("Project", ""),
|
|
55
|
+
("Status", "green"),
|
|
56
|
+
("Owner", "dim"),
|
|
57
|
+
],
|
|
58
|
+
rows,
|
|
59
|
+
data_for_json=rules,
|
|
60
|
+
)
|
|
61
|
+
except KctlError as e:
|
|
62
|
+
out.error(f"Failed to list alerts: {e}")
|
|
63
|
+
raise typer.Exit(1) from e
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@app.command("show")
|
|
67
|
+
def show(
|
|
68
|
+
ctx: typer.Context,
|
|
69
|
+
rule_id: Annotated[str, typer.Argument(help="Alert rule ID")],
|
|
70
|
+
project: Annotated[str, typer.Option("--project", "-p", help="Project slug")],
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Show alert rule details and trigger history."""
|
|
73
|
+
c: AppContext = ctx.obj
|
|
74
|
+
out = c.output
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
rule = c.client.project_get(project, f"/rules/{rule_id}/")
|
|
78
|
+
if not isinstance(rule, dict):
|
|
79
|
+
rule = {}
|
|
80
|
+
|
|
81
|
+
# Parse conditions
|
|
82
|
+
conditions = rule.get("conditions", [])
|
|
83
|
+
condition_strs: list[tuple[str, str]] = []
|
|
84
|
+
for cond in conditions if isinstance(conditions, list) else []:
|
|
85
|
+
if isinstance(cond, dict):
|
|
86
|
+
condition_strs.append(("Condition", cond.get("name", str(cond.get("id", "")))))
|
|
87
|
+
|
|
88
|
+
# Parse actions
|
|
89
|
+
actions = rule.get("actions", [])
|
|
90
|
+
action_strs: list[tuple[str, str]] = []
|
|
91
|
+
for act in actions if isinstance(actions, list) else []:
|
|
92
|
+
if isinstance(act, dict):
|
|
93
|
+
action_strs.append(("Action", act.get("name", str(act.get("id", "")))))
|
|
94
|
+
|
|
95
|
+
sections = [
|
|
96
|
+
(
|
|
97
|
+
"Alert Rule",
|
|
98
|
+
[
|
|
99
|
+
("ID", str(rule.get("id", ""))),
|
|
100
|
+
("Name", rule.get("name", "")),
|
|
101
|
+
("Status", rule.get("status", "")),
|
|
102
|
+
("Frequency", f"{rule.get('frequency', '')} seconds"),
|
|
103
|
+
("Date created", (rule.get("dateCreated", "") or "")[:19]),
|
|
104
|
+
],
|
|
105
|
+
),
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
if condition_strs:
|
|
109
|
+
sections.append(("Conditions", condition_strs))
|
|
110
|
+
if action_strs:
|
|
111
|
+
sections.append(("Actions", action_strs))
|
|
112
|
+
|
|
113
|
+
out.detail(
|
|
114
|
+
f"Alert Rule: {rule.get('name', rule_id)}",
|
|
115
|
+
sections,
|
|
116
|
+
data_for_json=rule,
|
|
117
|
+
)
|
|
118
|
+
except KctlError as e:
|
|
119
|
+
out.error(f"Failed to show alert: {e}")
|
|
120
|
+
raise typer.Exit(1) from e
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@app.command("create")
|
|
124
|
+
def create(
|
|
125
|
+
ctx: typer.Context,
|
|
126
|
+
project: Annotated[str, typer.Option("--project", "-p", help="Project slug")],
|
|
127
|
+
name: Annotated[str, typer.Option("--name", "-n", help="Alert rule name")],
|
|
128
|
+
metric: Annotated[str, typer.Option("--metric", help="Metric: events, users")] = "events",
|
|
129
|
+
threshold: Annotated[int, typer.Option("--threshold", help="Threshold value")] = 100,
|
|
130
|
+
time_window: Annotated[int, typer.Option("--time-window", help="Time window in minutes")] = 60,
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Create a new metric alert rule."""
|
|
133
|
+
c: AppContext = ctx.obj
|
|
134
|
+
out = c.output
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
# Map metric to Sentry's aggregate format
|
|
138
|
+
aggregate_map = {
|
|
139
|
+
"events": "count()",
|
|
140
|
+
"users": "count_unique(user)",
|
|
141
|
+
}
|
|
142
|
+
aggregate = aggregate_map.get(metric, "count()")
|
|
143
|
+
|
|
144
|
+
payload = {
|
|
145
|
+
"name": name,
|
|
146
|
+
"aggregate": aggregate,
|
|
147
|
+
"timeWindow": time_window,
|
|
148
|
+
"dataset": "events",
|
|
149
|
+
"query": "",
|
|
150
|
+
"thresholdType": 0, # Above threshold
|
|
151
|
+
"resolveThreshold": None,
|
|
152
|
+
"triggers": [
|
|
153
|
+
{
|
|
154
|
+
"label": "critical",
|
|
155
|
+
"alertThreshold": threshold,
|
|
156
|
+
"actions": [],
|
|
157
|
+
}
|
|
158
|
+
],
|
|
159
|
+
"projects": [project],
|
|
160
|
+
"owner": None,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
result = c.client.org_post("/alert-rules/", json=payload)
|
|
164
|
+
if not isinstance(result, dict):
|
|
165
|
+
result = {}
|
|
166
|
+
|
|
167
|
+
if out.json_mode:
|
|
168
|
+
out.raw_json(result)
|
|
169
|
+
else:
|
|
170
|
+
out.success(f"Alert rule created: {result.get('name', name)}")
|
|
171
|
+
out.kv("ID", str(result.get("id", "")))
|
|
172
|
+
out.kv("Metric", aggregate)
|
|
173
|
+
out.kv("Threshold", str(threshold))
|
|
174
|
+
except KctlError as e:
|
|
175
|
+
out.error(f"Failed to create alert: {e}")
|
|
176
|
+
raise typer.Exit(1) from e
|