runspec-logops 0.1.0__tar.gz
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.
- runspec_logops-0.1.0/.gitignore +61 -0
- runspec_logops-0.1.0/CHANGELOG.md +24 -0
- runspec_logops-0.1.0/PKG-INFO +11 -0
- runspec_logops-0.1.0/pyproject.toml +38 -0
- runspec_logops-0.1.0/runspec_logops/__init__.py +21 -0
- runspec_logops-0.1.0/runspec_logops/bundle.py +30 -0
- runspec_logops-0.1.0/runspec_logops/codemap.py +23 -0
- runspec_logops-0.1.0/runspec_logops/digest.py +34 -0
- runspec_logops-0.1.0/runspec_logops/runspec.toml +111 -0
- runspec_logops-0.1.0/tests/__init__.py +0 -0
- runspec_logops-0.1.0/tests/test_runnables.py +56 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
.Python
|
|
7
|
+
*.egg
|
|
8
|
+
*.egg-info/
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
.eggs/
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
env/
|
|
15
|
+
.env
|
|
16
|
+
pip-wheel-metadata/
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.mypy_cache/
|
|
19
|
+
.ruff_cache/
|
|
20
|
+
htmlcov/
|
|
21
|
+
.coverage
|
|
22
|
+
coverage.xml
|
|
23
|
+
*.cover
|
|
24
|
+
|
|
25
|
+
# Node
|
|
26
|
+
node_modules/
|
|
27
|
+
dist/
|
|
28
|
+
*.js.map
|
|
29
|
+
.npm
|
|
30
|
+
|
|
31
|
+
# Go
|
|
32
|
+
*.exe
|
|
33
|
+
*.test
|
|
34
|
+
*.out
|
|
35
|
+
vendor/
|
|
36
|
+
|
|
37
|
+
# IDE
|
|
38
|
+
.idea/
|
|
39
|
+
.vscode/
|
|
40
|
+
*.iml
|
|
41
|
+
*.iws
|
|
42
|
+
*.ipr
|
|
43
|
+
.DS_Store
|
|
44
|
+
Thumbs.db
|
|
45
|
+
|
|
46
|
+
# Docs
|
|
47
|
+
site/
|
|
48
|
+
|
|
49
|
+
# Misc
|
|
50
|
+
*.log
|
|
51
|
+
*.tmp
|
|
52
|
+
|
|
53
|
+
# External reference repos (cloned locally, not committed)
|
|
54
|
+
chainlit-docs/
|
|
55
|
+
.chainlit/
|
|
56
|
+
|
|
57
|
+
# Claude Code local config (machine-specific)
|
|
58
|
+
.claude/launch.json
|
|
59
|
+
|
|
60
|
+
# Stray committed test venv (removed from tracking)
|
|
61
|
+
.venv-test/
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# runspec-logops Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] — 2026-06-18
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
Three runnables that let an agent triage application issues from a log file
|
|
8
|
+
cheaply — the runnables do the heavy lifting on the host where the log lives and
|
|
9
|
+
return small structured JSON, so the agent reads condensed conclusions, not raw
|
|
10
|
+
data:
|
|
11
|
+
|
|
12
|
+
- **`summarize-log`** (autonomous) — streams a log file and clusters repeated
|
|
13
|
+
errors by signature, returning the top-N distinct issues with counts,
|
|
14
|
+
first/last timestamp and one bounded sample each. Output size is bounded
|
|
15
|
+
regardless of input size. Supports a severity floor (`--level`) and a time
|
|
16
|
+
window (`--since`/`--until`).
|
|
17
|
+
- **`map-trace`** (autonomous) — resolves a stack trace, or a `summarize-log`
|
|
18
|
+
signature, to the few relevant source snippets in a checkout (`--repo`).
|
|
19
|
+
- **`bundle-digest`** (confirm) — summarises a log, optionally maps a trace to
|
|
20
|
+
code, and packs digest + snippets + manifest into one small zip for transfer.
|
|
21
|
+
|
|
22
|
+
Thin wrappers over `runspec-logops-core`. To wrap the logic privately (bake in
|
|
23
|
+
corporate paths/defaults, surface only your own runnables), depend on
|
|
24
|
+
`runspec-logops-core` directly rather than installing this package.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runspec-logops
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Log-condensing + code-mapping runnables for runspec — cheap application-support triage from a log file
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: runspec-logops-core>=0.1.0
|
|
7
|
+
Requires-Dist: runspec>=0.27.0
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
10
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
11
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "runspec-logops"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
description = "Log-condensing + code-mapping runnables for runspec — cheap application-support triage from a log file"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"runspec>=0.27.0",
|
|
12
|
+
"runspec-logops-core>=0.1.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
summarize-log = "runspec_logops.digest:main_summarize_log"
|
|
17
|
+
map-trace = "runspec_logops.codemap:main_map_trace"
|
|
18
|
+
bundle-digest = "runspec_logops.bundle:main_bundle_digest"
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
dev = [
|
|
22
|
+
"ruff",
|
|
23
|
+
"mypy",
|
|
24
|
+
"pytest>=8.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[tool.pytest.ini_options]
|
|
28
|
+
testpaths = ["tests"]
|
|
29
|
+
|
|
30
|
+
[tool.mypy]
|
|
31
|
+
python_version = "3.10"
|
|
32
|
+
|
|
33
|
+
[tool.ruff]
|
|
34
|
+
line-length = 200
|
|
35
|
+
target-version = "py310"
|
|
36
|
+
|
|
37
|
+
[tool.ruff.lint]
|
|
38
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""runspec-logops — log-condensing + code-mapping runnables.
|
|
2
|
+
|
|
3
|
+
The pure logic lives in the dependency-free ``runspec-logops-core`` package; the
|
|
4
|
+
modules here wrap each helper in a runspec runnable. The helper functions are
|
|
5
|
+
re-exported for convenience and back-compat (e.g. ``from runspec_logops import
|
|
6
|
+
summarize_log``). To import the helpers *without* surfacing any runnables in a
|
|
7
|
+
venv — the corporate-wrapping path — depend on ``runspec-logops-core`` directly
|
|
8
|
+
instead.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from runspec_logops_core import (
|
|
12
|
+
build_bundle,
|
|
13
|
+
map_trace_to_sources,
|
|
14
|
+
summarize_log,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"summarize_log",
|
|
19
|
+
"map_trace_to_sources",
|
|
20
|
+
"build_bundle",
|
|
21
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import runspec as rs
|
|
5
|
+
from runspec_logops_core import build_bundle, map_trace_to_sources, summarize_log
|
|
6
|
+
from runspec_logops_core.errors import SourceNotFoundError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main_bundle_digest() -> None:
|
|
10
|
+
spec = rs.parse("bundle-digest")
|
|
11
|
+
file_path = str(spec.file)
|
|
12
|
+
# Unset optionals resolve to an Arg wrapping value None; read .value so the
|
|
13
|
+
# absent case is a real None rather than the string "None".
|
|
14
|
+
repo = str(spec.repo) if spec.repo.value is not None else None
|
|
15
|
+
trace = str(spec.trace) if spec.trace.value is not None else None
|
|
16
|
+
destination = str(spec.destination)
|
|
17
|
+
top = int(spec.top)
|
|
18
|
+
try:
|
|
19
|
+
digest = summarize_log(file_path, top=top)
|
|
20
|
+
code_map = None
|
|
21
|
+
if trace and repo:
|
|
22
|
+
code_map = map_trace_to_sources(repo, trace)
|
|
23
|
+
result = build_bundle(digest, code_map, dest_dir=destination, metadata={"trace": trace})
|
|
24
|
+
print(json.dumps(result))
|
|
25
|
+
except SourceNotFoundError as e:
|
|
26
|
+
print(json.dumps({"error": str(e), "file": file_path}))
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
except OSError as e:
|
|
29
|
+
print(json.dumps({"error": str(e), "file": file_path}))
|
|
30
|
+
sys.exit(1)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import runspec as rs
|
|
5
|
+
from runspec_logops_core import map_trace_to_sources
|
|
6
|
+
from runspec_logops_core.errors import SourceNotFoundError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main_map_trace() -> None:
|
|
10
|
+
spec = rs.parse("map-trace")
|
|
11
|
+
repo = str(spec.repo)
|
|
12
|
+
trace = str(spec.trace)
|
|
13
|
+
context = int(spec.context)
|
|
14
|
+
max_files = int(spec.max_files)
|
|
15
|
+
try:
|
|
16
|
+
result = map_trace_to_sources(repo, trace, context=context, max_files=max_files)
|
|
17
|
+
print(json.dumps(result))
|
|
18
|
+
except SourceNotFoundError as e:
|
|
19
|
+
print(json.dumps({"error": str(e), "repo": repo}))
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
except OSError as e:
|
|
22
|
+
print(json.dumps({"error": str(e), "repo": repo}))
|
|
23
|
+
sys.exit(1)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import runspec as rs
|
|
5
|
+
from runspec_logops_core import summarize_log
|
|
6
|
+
from runspec_logops_core.errors import SourceNotFoundError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main_summarize_log() -> None:
|
|
10
|
+
spec = rs.parse("summarize-log")
|
|
11
|
+
file_path = str(spec.file)
|
|
12
|
+
level = str(spec.level)
|
|
13
|
+
# Unset optionals resolve to an Arg wrapping value None; read .value so the
|
|
14
|
+
# absent case is a real None rather than the string "None".
|
|
15
|
+
since = spec.since.value
|
|
16
|
+
until = spec.until.value
|
|
17
|
+
top = int(spec.top)
|
|
18
|
+
max_sample_lines = int(spec.max_sample_lines)
|
|
19
|
+
try:
|
|
20
|
+
digest = summarize_log(
|
|
21
|
+
file_path,
|
|
22
|
+
level=level,
|
|
23
|
+
since=since,
|
|
24
|
+
until=until,
|
|
25
|
+
top=top,
|
|
26
|
+
max_sample_lines=max_sample_lines,
|
|
27
|
+
)
|
|
28
|
+
print(json.dumps(digest))
|
|
29
|
+
except SourceNotFoundError as e:
|
|
30
|
+
print(json.dumps({"error": str(e), "file": file_path}))
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
except OSError as e:
|
|
33
|
+
print(json.dumps({"error": str(e), "file": file_path}))
|
|
34
|
+
sys.exit(1)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
[config]
|
|
2
|
+
autonomy-default = "autonomous"
|
|
3
|
+
|
|
4
|
+
[config.logging]
|
|
5
|
+
# Per-invocation log files: one {runnable}.{utc-ts}.{run_id}.log per run.
|
|
6
|
+
# Multi-writer safe for shared venvs (use `runspec logs` to view/compact/prune).
|
|
7
|
+
store = "per-run"
|
|
8
|
+
|
|
9
|
+
# ── Log condensing ─────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
[summarize-log]
|
|
12
|
+
serve = ["local"]
|
|
13
|
+
description = "Condense a log file into a small digest: cluster repeated errors by signature and return the top-N distinct issues with counts, first/last timestamp and one sample each. The digest size is bounded regardless of how big the log is, so read this instead of the raw log."
|
|
14
|
+
autonomy = "autonomous"
|
|
15
|
+
output = "json"
|
|
16
|
+
|
|
17
|
+
[summarize-log.args.file]
|
|
18
|
+
type = "path"
|
|
19
|
+
short = "-f"
|
|
20
|
+
description = "Path to the log file to summarise"
|
|
21
|
+
|
|
22
|
+
[summarize-log.args.level]
|
|
23
|
+
type = "choice"
|
|
24
|
+
options = ["all", "warning", "error", "critical"]
|
|
25
|
+
default = "all"
|
|
26
|
+
description = "Only include events at or above this severity"
|
|
27
|
+
|
|
28
|
+
[summarize-log.args.since]
|
|
29
|
+
type = "str"
|
|
30
|
+
required = false
|
|
31
|
+
description = "Window start: ISO timestamp, 'now', or '<n> <unit> ago' (e.g. '2 hours ago')"
|
|
32
|
+
|
|
33
|
+
[summarize-log.args.until]
|
|
34
|
+
type = "str"
|
|
35
|
+
required = false
|
|
36
|
+
description = "Window end: ISO timestamp, 'now', or '<n> <unit> ago'"
|
|
37
|
+
|
|
38
|
+
[summarize-log.args.top]
|
|
39
|
+
type = "int"
|
|
40
|
+
short = "-n"
|
|
41
|
+
default = 10
|
|
42
|
+
description = "Number of distinct signatures to return (most frequent first)"
|
|
43
|
+
|
|
44
|
+
[summarize-log.args.max-sample-lines]
|
|
45
|
+
type = "int"
|
|
46
|
+
default = 20
|
|
47
|
+
description = "Max lines of the sample kept per signature"
|
|
48
|
+
|
|
49
|
+
# ── Trace → code ────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
[map-trace]
|
|
52
|
+
serve = ["local"]
|
|
53
|
+
description = "Map a stack trace (or a summarize-log signature like 'NullPointerException@com.acme.OrderSvc.price') to the few relevant source snippets in a checkout. Returns only the lines around each frame, never the whole repo."
|
|
54
|
+
autonomy = "autonomous"
|
|
55
|
+
output = "json"
|
|
56
|
+
|
|
57
|
+
[map-trace.args.repo]
|
|
58
|
+
type = "path"
|
|
59
|
+
short = "-r"
|
|
60
|
+
description = "Path to the source checkout to search"
|
|
61
|
+
|
|
62
|
+
[map-trace.args.trace]
|
|
63
|
+
type = "str"
|
|
64
|
+
short = "-t"
|
|
65
|
+
description = "The stack trace text, or a signature emitted by summarize-log"
|
|
66
|
+
|
|
67
|
+
[map-trace.args.context]
|
|
68
|
+
type = "int"
|
|
69
|
+
default = 8
|
|
70
|
+
description = "Lines of context to include around each resolved frame"
|
|
71
|
+
|
|
72
|
+
[map-trace.args.max-files]
|
|
73
|
+
type = "int"
|
|
74
|
+
default = 5
|
|
75
|
+
description = "Maximum number of frames/files to resolve"
|
|
76
|
+
|
|
77
|
+
# ── Bundle for transfer ─────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
[bundle-digest]
|
|
80
|
+
serve = ["local"]
|
|
81
|
+
description = "Summarise a log and (optionally) map a trace to code, then pack the digest + snippets + manifest into one small zip ready for transfer to the local machine. Writes a file."
|
|
82
|
+
autonomy = "confirm"
|
|
83
|
+
output = "json"
|
|
84
|
+
|
|
85
|
+
[bundle-digest.args.file]
|
|
86
|
+
type = "path"
|
|
87
|
+
short = "-f"
|
|
88
|
+
description = "Path to the log file"
|
|
89
|
+
|
|
90
|
+
[bundle-digest.args.repo]
|
|
91
|
+
type = "path"
|
|
92
|
+
short = "-r"
|
|
93
|
+
required = false
|
|
94
|
+
description = "Path to the source checkout for the optional code map (omit for a digest-only bundle)"
|
|
95
|
+
|
|
96
|
+
[bundle-digest.args.trace]
|
|
97
|
+
type = "str"
|
|
98
|
+
required = false
|
|
99
|
+
description = "Optional trace/signature to map to code; omit for a digest-only bundle"
|
|
100
|
+
|
|
101
|
+
[bundle-digest.args.destination]
|
|
102
|
+
type = "path"
|
|
103
|
+
short = "-d"
|
|
104
|
+
default = "/tmp"
|
|
105
|
+
description = "Directory to write the zip into"
|
|
106
|
+
|
|
107
|
+
[bundle-digest.args.top]
|
|
108
|
+
type = "int"
|
|
109
|
+
short = "-n"
|
|
110
|
+
default = 10
|
|
111
|
+
description = "Number of distinct signatures to include in the digest"
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Smoke tests: each runnable parses args, calls its core helper, prints JSON.
|
|
2
|
+
|
|
3
|
+
The runnables are exercised end-to-end via their installed console scripts (the
|
|
4
|
+
same path `runspec serve` invokes), so this also proves the runspec.toml ships in
|
|
5
|
+
the package and resolves.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _run(script: str, *args: str) -> dict:
|
|
16
|
+
exe = shutil.which(script)
|
|
17
|
+
if not exe:
|
|
18
|
+
pytest.skip(f"{script} console script not installed")
|
|
19
|
+
proc = subprocess.run([exe, *args], capture_output=True, text=True)
|
|
20
|
+
assert proc.returncode == 0, proc.stderr
|
|
21
|
+
return json.loads(proc.stdout)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_helpers_reexported() -> None:
|
|
25
|
+
from runspec_logops_core import summarize_log as core_summarize
|
|
26
|
+
|
|
27
|
+
from runspec_logops import build_bundle, map_trace_to_sources, summarize_log
|
|
28
|
+
|
|
29
|
+
assert summarize_log is core_summarize
|
|
30
|
+
assert callable(map_trace_to_sources) and callable(build_bundle)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_summarize_log_runnable(tmp_path) -> None:
|
|
34
|
+
log = tmp_path / "app.log"
|
|
35
|
+
log.write_text("\n".join(["2026-06-18T08:00:00Z ERROR NullPointerException: boom\n\tat com.acme.Svc.go(Svc.java:9)"] * 5 + ["2026-06-18T08:01:00Z INFO ok"]) + "\n")
|
|
36
|
+
out = _run("summarize-log", "--file", str(log), "-n", "5")
|
|
37
|
+
assert out["file"] == str(log)
|
|
38
|
+
assert out["top"][0]["count"] == 5
|
|
39
|
+
assert out["top"][0]["signature"].startswith("NullPointerException@")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_map_trace_runnable(tmp_path) -> None:
|
|
43
|
+
(tmp_path / "app.py").write_text("\n".join(f"line {i}" for i in range(1, 30)) + "\n")
|
|
44
|
+
trace = 'File "app.py", line 10, in handler'
|
|
45
|
+
out = _run("map-trace", "--repo", str(tmp_path), "--trace", trace, "--context", "2")
|
|
46
|
+
assert out["files_matched"] == 1
|
|
47
|
+
assert out["frames"][0]["file"] == "app.py"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_bundle_digest_runnable(tmp_path) -> None:
|
|
51
|
+
log = tmp_path / "app.log"
|
|
52
|
+
log.write_text("2026-06-18T08:00:00Z ERROR ValueError: bad\n")
|
|
53
|
+
dest = tmp_path / "out"
|
|
54
|
+
out = _run("bundle-digest", "--file", str(log), "--destination", str(dest))
|
|
55
|
+
assert out["destination"].endswith(".zip")
|
|
56
|
+
assert "digest.json" in out["contents"]
|