@misterhuydo/sentinel 1.5.39 → 1.5.41
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.
- package/package.json +1 -1
- package/python/sentinel/__init__.py +1 -1
- package/python/sentinel/sentinel_boss.py +107 -107
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.5.
|
|
1
|
+
__version__ = "1.5.41"
|
|
@@ -152,15 +152,20 @@ COMPLETE TOOL REFERENCE
|
|
|
152
152
|
── Log Management ─────────────────────────────────────────────────────────────
|
|
153
153
|
|
|
154
154
|
6. fetch_logs Run fetch_log.sh on demand — pull fresh logs from servers now.
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
When grep_filter is set, results go to TEMP files (workspace/fetched_temp/)
|
|
156
|
+
and do NOT affect the main rolling logs. Temp files are cleared on every
|
|
157
|
+
custom fetch, and filter_logs searches them automatically.
|
|
158
|
+
Use grep_filter for INFO-level or feature-specific patterns that the default
|
|
159
|
+
WARN/ERROR filter would miss.
|
|
160
|
+
"fetch logs", "fetch SSOLWA with filter provision/phone", "fetch without filter"
|
|
157
161
|
|
|
158
162
|
7. search_logs Live SSH grep on production servers using GREP_FILTER.
|
|
159
163
|
Falls back to cached files if SSH unavailable.
|
|
160
164
|
"search logs for illegal PIN in 1881", "find NullPointerException in STS"
|
|
161
165
|
|
|
162
|
-
8. filter_logs Instant keyword/regex search on locally-synced logs
|
|
163
|
-
|
|
166
|
+
8. filter_logs Instant keyword/regex search on locally-synced logs + any temp fetch results.
|
|
167
|
+
No SSH, sub-second. Also searches workspace/fetched_temp/ if a custom
|
|
168
|
+
fetch was recently run.
|
|
164
169
|
"filter logs for TryDig", "errors last 6h", "find appid=X in STS logs"
|
|
165
170
|
|
|
166
171
|
9. tail_log Last N lines of a log source live, no filter.
|
|
@@ -545,9 +550,6 @@ When to act vs. when to ask:
|
|
|
545
550
|
tasks, fixes, releases). Always call get_status or list_recent_commits first to verify live
|
|
546
551
|
state. Session memory is a snapshot — tasks complete, commits land, queues drain between turns.
|
|
547
552
|
If you remember "task X was in-flight", check whether it finished before telling the user to wait.
|
|
548
|
-
- NEVER repeat a deployment assertion you made in a prior turn without re-verifying it.
|
|
549
|
-
If you said "version X is not deployed" in a previous message and the user asks again, do NOT
|
|
550
|
-
just echo the same claim — check list_recent_commits or startup logs fresh before answering.
|
|
551
553
|
- Prefer filter_logs over search_logs when synced logs are available — it's instant and never causes session timeout.
|
|
552
554
|
Use search_logs (live SSH fetch) when:
|
|
553
555
|
• The user explicitly wants live/real-time data
|
|
@@ -556,15 +558,27 @@ When to act vs. when to ask:
|
|
|
556
558
|
(synced logs may simply be stale — do NOT conclude the change isn't live yet; fetch live first)
|
|
557
559
|
When filter_logs returns no hits after a recent release, always retry with search_logs before
|
|
558
560
|
telling the user the log line isn't there.
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
561
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
562
|
+
LOG RESULTS DO NOT PROVE DEPLOYMENT STATUS
|
|
563
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
564
|
+
You are FORBIDDEN from asserting "release X is not deployed" or "servers are still on old version"
|
|
565
|
+
based solely on log search results — not from zero hits, not from finding old lines without new ones.
|
|
566
|
+
|
|
567
|
+
WHY: A log line only appears when that exact code path executes. Finding 0 hits or only old log
|
|
568
|
+
lines says nothing about what code version is running. The new line simply hasn't been triggered yet.
|
|
569
|
+
|
|
570
|
+
WRONG — NEVER SAY THESE:
|
|
571
|
+
"Zero hits for 'provision/phone called by appId'. Release 6.29.34 has not deployed."
|
|
572
|
+
"Found 3 old DEBUG entries but no new INFO line — servers are still on the previous version."
|
|
573
|
+
"The new logging code from 6.29.34 is still not deployed."
|
|
574
|
+
|
|
575
|
+
CORRECT:
|
|
576
|
+
"No 'provision/phone called by appId' line yet — the endpoint hasn't logged that path since
|
|
577
|
+
the last fetch. This says nothing about whether 6.29.34 is deployed."
|
|
578
|
+
|
|
579
|
+
To verify if a release is live, offer to search for startup log lines:
|
|
580
|
+
search_logs with query "Starting|started in|version|initialized"
|
|
581
|
+
NEVER repeat a deployment assertion from a prior turn without doing this check first.
|
|
568
582
|
- If a tool call will take a moment (search, fetch, pull), prefix your reply with a brief "working" line ending in "..." before the results, e.g. "Searching SSOLWA for TryDig activity..." then the actual output.
|
|
569
583
|
Never just say a working line and stop — always follow it with the results in the same message.
|
|
570
584
|
|
|
@@ -1156,7 +1170,14 @@ _TOOLS = [
|
|
|
1156
1170
|
"Run fetch_log.sh for one or all configured log sources to pull the latest logs "
|
|
1157
1171
|
"from remote servers right now. Use for: 'fetch logs', 'run fetch_log.sh', "
|
|
1158
1172
|
"'grab latest logs from SSOLWA', 'try fetch_log.sh for STS', "
|
|
1159
|
-
"'pull logs from server', 'get fresh logs'
|
|
1173
|
+
"'pull logs from server', 'get fresh logs'.\n\n"
|
|
1174
|
+
"IMPORTANT: When a grep_filter is provided, results go to a TEMPORARY location "
|
|
1175
|
+
"(workspace/fetched_temp/) and do NOT overwrite the main rolling logs. "
|
|
1176
|
+
"The temp files are cleared on every custom fetch. "
|
|
1177
|
+
"After a custom fetch, filter_logs will automatically search the temp results too. "
|
|
1178
|
+
"Use grep_filter whenever the user wants to find INFO-level or feature-specific log lines "
|
|
1179
|
+
"(e.g. 'provision/phone', 'appId', startup messages) — the default filter only captures "
|
|
1180
|
+
"WARN|ERROR|FATAL|Exception|Error lines."
|
|
1160
1181
|
),
|
|
1161
1182
|
"input_schema": {
|
|
1162
1183
|
"type": "object",
|
|
@@ -1176,7 +1197,11 @@ _TOOLS = [
|
|
|
1176
1197
|
},
|
|
1177
1198
|
"grep_filter": {
|
|
1178
1199
|
"type": "string",
|
|
1179
|
-
"description":
|
|
1200
|
+
"description": (
|
|
1201
|
+
"Custom grep filter (regex). Results saved to temp files, main logs untouched. "
|
|
1202
|
+
"Pass 'none' to fetch all lines unfiltered. "
|
|
1203
|
+
"Use when searching for INFO-level or feature-specific patterns."
|
|
1204
|
+
),
|
|
1180
1205
|
},
|
|
1181
1206
|
},
|
|
1182
1207
|
},
|
|
@@ -2814,104 +2839,44 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
|
|
|
2814
2839
|
return json.dumps({"error": f"Invalid regex: {e}"})
|
|
2815
2840
|
|
|
2816
2841
|
synced_base = Path("workspace/synced")
|
|
2817
|
-
|
|
2818
|
-
return json.dumps({
|
|
2819
|
-
"error": "No synced logs found.",
|
|
2820
|
-
"hint": "Log sync runs every SYNC_INTERVAL_SECONDS (default 300s). "
|
|
2821
|
-
"If just started, wait a minute then try again.",
|
|
2822
|
-
})
|
|
2842
|
+
temp_base = Path(cfg_loader.sentinel.workspace_dir) / "fetched_temp"
|
|
2823
2843
|
|
|
2824
2844
|
# Build cutoff timestamp for since_hours filter
|
|
2825
2845
|
cutoff = None
|
|
2826
2846
|
if since_hours:
|
|
2827
2847
|
cutoff = _datetime.now(_tz.utc) - timedelta(hours=int(since_hours))
|
|
2828
2848
|
|
|
2829
|
-
#
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
if not src_dirs:
|
|
2837
|
-
available = [d.name for d in synced_base.iterdir() if d.is_dir()]
|
|
2838
|
-
return json.dumps({
|
|
2839
|
-
"error": f"No synced source matching '{source_f}'",
|
|
2840
|
-
"available_sources": available,
|
|
2841
|
-
})
|
|
2842
|
-
|
|
2843
|
-
results = []
|
|
2844
|
-
total_matches = 0
|
|
2845
|
-
for src_dir in src_dirs:
|
|
2846
|
-
for log_file in sorted(src_dir.glob("*")):
|
|
2847
|
-
try:
|
|
2848
|
-
lines = log_file.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
2849
|
-
matches = []
|
|
2850
|
-
for line in lines:
|
|
2851
|
-
if not pat.search(line):
|
|
2852
|
-
continue
|
|
2853
|
-
if cutoff:
|
|
2854
|
-
# Try to parse timestamp from line
|
|
2855
|
-
from .log_fetcher import _parse_line_ts
|
|
2856
|
-
ts = _parse_line_ts(line)
|
|
2857
|
-
if ts and ts < cutoff:
|
|
2858
|
-
continue
|
|
2859
|
-
matches.append(line[:300])
|
|
2860
|
-
if len(matches) >= max_matches:
|
|
2861
|
-
break
|
|
2862
|
-
if matches:
|
|
2863
|
-
results.append({
|
|
2864
|
-
"source": src_dir.name,
|
|
2865
|
-
"file": log_file.name,
|
|
2866
|
-
"matches": matches,
|
|
2867
|
-
})
|
|
2868
|
-
total_matches += len(matches)
|
|
2869
|
-
except Exception:
|
|
2870
|
-
pass
|
|
2871
|
-
|
|
2872
|
-
if not results:
|
|
2873
|
-
return json.dumps({
|
|
2874
|
-
"query": query_f,
|
|
2875
|
-
"total_matches": 0,
|
|
2876
|
-
"sources_searched": [d.name for d in src_dirs],
|
|
2877
|
-
"note": "No matches found in synced logs.",
|
|
2878
|
-
})
|
|
2849
|
+
# Collect candidate directories from both synced/ and fetched_temp/
|
|
2850
|
+
def _collect_dirs(base):
|
|
2851
|
+
if not base.exists():
|
|
2852
|
+
return []
|
|
2853
|
+
if source_f:
|
|
2854
|
+
return [d for d in sorted(base.iterdir()) if d.is_dir() and source_f in d.name.lower()]
|
|
2855
|
+
return [d for d in sorted(base.iterdir()) if d.is_dir()]
|
|
2879
2856
|
|
|
2857
|
+
src_dirs = _collect_dirs(synced_base)
|
|
2858
|
+
temp_dirs = _collect_dirs(temp_base)
|
|
2859
|
+
all_search_dirs = src_dirs + [(d, True) for d in temp_dirs] # True = is_temp
|
|
2880
2860
|
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
except _re.error as e:
|
|
2884
|
-
return json.dumps({"error": f"Invalid regex: {e}"})
|
|
2861
|
+
# Flatten to (dir, is_temp) pairs
|
|
2862
|
+
search_pairs = [(d, False) for d in src_dirs] + [(d, True) for d in temp_dirs]
|
|
2885
2863
|
|
|
2886
|
-
|
|
2887
|
-
|
|
2864
|
+
if not search_pairs:
|
|
2865
|
+
available = ([d.name for d in synced_base.iterdir() if d.is_dir()] if synced_base.exists() else [])
|
|
2888
2866
|
return json.dumps({
|
|
2889
|
-
"error": "No synced logs found.",
|
|
2867
|
+
"error": f"No synced or temp source matching '{source_f}'" if source_f else "No logs found.",
|
|
2868
|
+
"available_sources": available,
|
|
2890
2869
|
"hint": "Log sync runs every SYNC_INTERVAL_SECONDS (default 300s). "
|
|
2891
2870
|
"If just started, wait a minute then try again.",
|
|
2892
2871
|
})
|
|
2893
2872
|
|
|
2894
|
-
|
|
2895
|
-
if since_hours:
|
|
2896
|
-
cutoff = _datetime.now(_tz.utc) - timedelta(hours=int(since_hours))
|
|
2897
|
-
|
|
2898
|
-
if source_f:
|
|
2899
|
-
src_dirs = [d for d in sorted(synced_base.iterdir())
|
|
2900
|
-
if d.is_dir() and source_f in d.name.lower()]
|
|
2901
|
-
else:
|
|
2902
|
-
src_dirs = [d for d in sorted(synced_base.iterdir()) if d.is_dir()]
|
|
2903
|
-
|
|
2904
|
-
if not src_dirs:
|
|
2905
|
-
available = [d.name for d in synced_base.iterdir() if d.is_dir()]
|
|
2906
|
-
return json.dumps({
|
|
2907
|
-
"error": f"No synced source matching '{source_f}'",
|
|
2908
|
-
"available_sources": available,
|
|
2909
|
-
})
|
|
2910
|
-
|
|
2911
|
-
all_matches = [] # list of (source_name, line)
|
|
2873
|
+
all_matches = [] # list of (source_label, line)
|
|
2912
2874
|
sources_hit = set()
|
|
2913
|
-
for src_dir in
|
|
2914
|
-
|
|
2875
|
+
for src_dir, is_temp in search_pairs:
|
|
2876
|
+
label = src_dir.name + (" [temp]" if is_temp else "")
|
|
2877
|
+
for log_file in sorted(src_dir.glob("**/*")):
|
|
2878
|
+
if not log_file.is_file():
|
|
2879
|
+
continue
|
|
2915
2880
|
try:
|
|
2916
2881
|
lines = log_file.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
2917
2882
|
for line in lines:
|
|
@@ -2922,8 +2887,8 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
|
|
|
2922
2887
|
ts = _parse_line_ts(line)
|
|
2923
2888
|
if ts and ts < cutoff:
|
|
2924
2889
|
continue
|
|
2925
|
-
all_matches.append((
|
|
2926
|
-
sources_hit.add(
|
|
2890
|
+
all_matches.append((label, line[:300]))
|
|
2891
|
+
sources_hit.add(label)
|
|
2927
2892
|
if len(all_matches) >= max_matches:
|
|
2928
2893
|
break
|
|
2929
2894
|
except Exception:
|
|
@@ -2932,12 +2897,19 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
|
|
|
2932
2897
|
break
|
|
2933
2898
|
|
|
2934
2899
|
total = len(all_matches)
|
|
2900
|
+
sources_searched = [d.name + (" [temp]" if is_temp else "") for d, is_temp in search_pairs]
|
|
2935
2901
|
if total == 0:
|
|
2902
|
+
has_temp = bool(temp_dirs)
|
|
2936
2903
|
return json.dumps({
|
|
2937
2904
|
"query": query_f,
|
|
2938
2905
|
"total_matches": 0,
|
|
2939
|
-
"sources_searched":
|
|
2940
|
-
"note":
|
|
2906
|
+
"sources_searched": sources_searched,
|
|
2907
|
+
"note": (
|
|
2908
|
+
"No matches found. "
|
|
2909
|
+
+ ("Temp fetch results were also checked. " if has_temp else "")
|
|
2910
|
+
+ "If searching for a specific log line from a new feature, use fetch_logs "
|
|
2911
|
+
"with a matching grep_filter first — the default filter only captures WARN/ERROR."
|
|
2912
|
+
),
|
|
2941
2913
|
})
|
|
2942
2914
|
|
|
2943
2915
|
# Pattern grouping: count occurrences of each error signature
|
|
@@ -2983,7 +2955,7 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
|
|
|
2983
2955
|
"query": query_f,
|
|
2984
2956
|
"total_matches": total,
|
|
2985
2957
|
"sources_hit": sorted(sources_hit),
|
|
2986
|
-
"sources_searched":
|
|
2958
|
+
"sources_searched": sources_searched,
|
|
2987
2959
|
"top_patterns": top_patterns,
|
|
2988
2960
|
"sample_lines": sample_lines,
|
|
2989
2961
|
"time_span": time_span,
|
|
@@ -3081,6 +3053,20 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
|
|
|
3081
3053
|
if not props_files:
|
|
3082
3054
|
return json.dumps({"error": f"No log-config found matching '{source_filter}'"})
|
|
3083
3055
|
|
|
3056
|
+
# When a custom grep_filter is set, route output to a temp directory
|
|
3057
|
+
# so the main rolling logs are never polluted by user-requested searches.
|
|
3058
|
+
# The temp dir is cleared before each custom fetch.
|
|
3059
|
+
workspace_dir = Path(cfg_loader.sentinel.workspace_dir)
|
|
3060
|
+
temp_base = workspace_dir / "fetched_temp"
|
|
3061
|
+
use_temp = bool(grep_override)
|
|
3062
|
+
|
|
3063
|
+
if use_temp:
|
|
3064
|
+
# Clear old temp results
|
|
3065
|
+
import shutil
|
|
3066
|
+
if temp_base.exists():
|
|
3067
|
+
shutil.rmtree(temp_base)
|
|
3068
|
+
temp_base.mkdir(parents=True, exist_ok=True)
|
|
3069
|
+
|
|
3084
3070
|
results = []
|
|
3085
3071
|
for props in props_files:
|
|
3086
3072
|
env = os.environ.copy()
|
|
@@ -3088,6 +3074,9 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
|
|
|
3088
3074
|
env["TAIL"] = str(tail_override)
|
|
3089
3075
|
if grep_override:
|
|
3090
3076
|
env["SENTINEL_GREP_FILTER_OVERRIDE"] = grep_override
|
|
3077
|
+
if use_temp:
|
|
3078
|
+
# Tell fetch_log.sh where to write output files
|
|
3079
|
+
env["OUTPUT_DIR"] = str(temp_base)
|
|
3091
3080
|
|
|
3092
3081
|
cmd = ["bash", str(script)]
|
|
3093
3082
|
if debug:
|
|
@@ -3100,13 +3089,24 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
|
|
|
3100
3089
|
)
|
|
3101
3090
|
output = (r.stdout or "").strip()
|
|
3102
3091
|
stderr = (r.stderr or "").strip()
|
|
3092
|
+
|
|
3093
|
+
# Collect lines from temp files for this source
|
|
3094
|
+
temp_lines = []
|
|
3095
|
+
if use_temp:
|
|
3096
|
+
for f in sorted((temp_base / props.stem).glob("*.log")) if (temp_base / props.stem).exists() else []:
|
|
3097
|
+
try:
|
|
3098
|
+
temp_lines.extend(f.read_text(encoding="utf-8", errors="replace").splitlines())
|
|
3099
|
+
except Exception:
|
|
3100
|
+
pass
|
|
3101
|
+
|
|
3103
3102
|
results.append({
|
|
3104
3103
|
"source": props.stem,
|
|
3105
3104
|
"returncode": r.returncode,
|
|
3106
3105
|
"output": output[-2000:] if output else "",
|
|
3107
3106
|
"stderr": stderr[-1000:] if stderr else "",
|
|
3107
|
+
**({"lines": temp_lines, "temp_file": str(temp_base / props.stem)} if use_temp else {}),
|
|
3108
3108
|
})
|
|
3109
|
-
logger.info("Boss fetch_logs %s rc=%d", props.stem, r.returncode)
|
|
3109
|
+
logger.info("Boss fetch_logs %s rc=%d (temp=%s)", props.stem, r.returncode, use_temp)
|
|
3110
3110
|
except subprocess.TimeoutExpired:
|
|
3111
3111
|
results.append({"source": props.stem, "error": "timed out after 120s"})
|
|
3112
3112
|
except Exception as e:
|