Spaces:
Running on Zero
Running on Zero
feat: update HuggingFace space with new API and features
Browse files- Use analyze_text_detailed() instead of temp file workaround
- Add output format toggle (XML/JSON)
- Add token budget control for LLM context fitting
- Add max blocks and min score filtering controls
- Set show_progress=False since Gradio handles progress
- Use result.blocks for richer output summary
- Update documentation with new CLI and API examples
Co-authored-by: Cursor <cursoragent@cursor.com>
- app.py +152 -86
- requirements.txt +1 -1
app.py
CHANGED
|
@@ -6,7 +6,6 @@ HuggingFace Space Demo (ZeroGPU)
|
|
| 6 |
import logging
|
| 7 |
import os
|
| 8 |
import sys
|
| 9 |
-
import tempfile
|
| 10 |
import time
|
| 11 |
from pathlib import Path
|
| 12 |
|
|
@@ -66,106 +65,118 @@ def analyze_logs(
|
|
| 66 |
anomaly_percentile: float,
|
| 67 |
range_min: float,
|
| 68 |
range_max: float,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
progress=gr.Progress(),
|
| 70 |
):
|
| 71 |
"""Analyze log content and return results.
|
| 72 |
-
|
| 73 |
This function is decorated with @spaces.GPU to run on ZeroGPU.
|
| 74 |
The GPU is allocated when this function is called.
|
| 75 |
-
|
| 76 |
IMPORTANT: torch and cordon are imported here (not at module level)
|
| 77 |
because ZeroGPU only allocates GPU resources inside @spaces.GPU functions.
|
| 78 |
"""
|
| 79 |
# Lazy imports - only load when GPU is allocated
|
| 80 |
import torch
|
| 81 |
from cordon import AnalysisConfig, SemanticLogAnalyzer
|
| 82 |
-
|
| 83 |
start_time = time.time()
|
| 84 |
logger.info("=" * 60)
|
| 85 |
logger.info("Analysis started - GPU allocated by ZeroGPU")
|
| 86 |
-
|
| 87 |
if not log_content.strip():
|
| 88 |
logger.warning("Empty log content provided")
|
| 89 |
return "", "Anomalous Sections"
|
| 90 |
-
|
| 91 |
-
logger.info(
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
try:
|
| 103 |
-
progress(0.1, desc="Configuring analyzer...")
|
| 104 |
-
|
| 105 |
-
# Check if CUDA is available (should be True on ZeroGPU)
|
| 106 |
-
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 107 |
-
# Use larger batch size on GPU for better throughput
|
| 108 |
-
batch_size = 128 if device == "cuda" else 32
|
| 109 |
-
|
| 110 |
-
logger.info(f"Device: {device}")
|
| 111 |
-
logger.info(f"Batch size: {batch_size}")
|
| 112 |
-
|
| 113 |
-
if mode == "Percentile":
|
| 114 |
-
config = AnalysisConfig(
|
| 115 |
-
window_size=window_size,
|
| 116 |
-
k_neighbors=k_neighbors,
|
| 117 |
-
anomaly_percentile=anomaly_percentile,
|
| 118 |
-
device=device,
|
| 119 |
-
batch_size=batch_size,
|
| 120 |
-
)
|
| 121 |
-
logger.info(f"Configuration: mode=Percentile, window_size={window_size}, k_neighbors={k_neighbors}, percentile={anomaly_percentile}")
|
| 122 |
-
else:
|
| 123 |
-
config = AnalysisConfig(
|
| 124 |
-
window_size=window_size,
|
| 125 |
-
k_neighbors=k_neighbors,
|
| 126 |
-
anomaly_range_min=range_min,
|
| 127 |
-
anomaly_range_max=range_max,
|
| 128 |
-
device=device,
|
| 129 |
-
batch_size=batch_size,
|
| 130 |
-
)
|
| 131 |
-
logger.info(f"Configuration: mode=Range, window_size={window_size}, k_neighbors={k_neighbors}, range=[{range_min}, {range_max}]")
|
| 132 |
-
|
| 133 |
progress(0.15, desc="Loading embedding model...")
|
| 134 |
logger.info("Loading embedding model...")
|
| 135 |
model_load_start = time.time()
|
| 136 |
analyzer = SemanticLogAnalyzer(config)
|
| 137 |
logger.info(f"Embedding model loaded in {time.time() - model_load_start:.2f}s")
|
| 138 |
-
|
| 139 |
progress(0.4, desc="Analyzing patterns...")
|
| 140 |
logger.info("Starting log analysis...")
|
| 141 |
analysis_start = time.time()
|
| 142 |
-
result = analyzer.
|
| 143 |
logger.info(f"Analysis completed in {time.time() - analysis_start:.2f}s")
|
| 144 |
-
|
| 145 |
progress(0.9, desc="Formatting output...")
|
| 146 |
progress(1.0, desc="Complete!")
|
| 147 |
-
|
| 148 |
# Calculate token counts
|
| 149 |
input_tokens = estimate_tokens(log_content)
|
| 150 |
output_tokens = estimate_tokens(result.output)
|
| 151 |
-
reduction = (
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
total_time = time.time() - start_time
|
| 156 |
logger.info(f"Total analysis time: {total_time:.2f}s")
|
| 157 |
logger.info(f"Token reduction: {input_tokens:,} โ {output_tokens:,} ({reduction:.0f}%)")
|
| 158 |
logger.info("=" * 60)
|
| 159 |
-
|
| 160 |
return gr.update(value=result.output, label=label)
|
| 161 |
-
|
| 162 |
except Exception as e:
|
| 163 |
logger.error(f"Error during analysis: {str(e)}", exc_info=True)
|
| 164 |
error_msg = f"Analysis failed: {str(e)}"
|
| 165 |
return gr.update(value=error_msg, label="Error")
|
| 166 |
-
finally:
|
| 167 |
-
temp_path.unlink(missing_ok=True)
|
| 168 |
-
logger.info("Temporary file cleaned up")
|
| 169 |
|
| 170 |
|
| 171 |
CUSTOM_CSS = """
|
|
@@ -204,9 +215,9 @@ THEME = gr.themes.Base(
|
|
| 204 |
|
| 205 |
def create_interface():
|
| 206 |
"""Create the Gradio interface."""
|
| 207 |
-
|
| 208 |
with gr.Blocks(title="Cordon", theme=THEME, css=CUSTOM_CSS) as demo:
|
| 209 |
-
|
| 210 |
# Header
|
| 211 |
gr.HTML("""
|
| 212 |
<div style="text-align: center; padding: 16px 0 24px 0;">
|
|
@@ -216,10 +227,10 @@ def create_interface():
|
|
| 216 |
<p style="color: #94a3b8; margin: 0;">Reduce logs to their semantically anomalous parts.</p>
|
| 217 |
</div>
|
| 218 |
""")
|
| 219 |
-
|
| 220 |
# Main content - two equal columns
|
| 221 |
with gr.Row():
|
| 222 |
-
|
| 223 |
# Left: Input
|
| 224 |
with gr.Column(scale=1):
|
| 225 |
log_input = gr.Textbox(
|
|
@@ -227,15 +238,15 @@ def create_interface():
|
|
| 227 |
placeholder="Paste your log content here...",
|
| 228 |
lines=22,
|
| 229 |
)
|
| 230 |
-
|
| 231 |
with gr.Row():
|
| 232 |
load_sample_btn = gr.Button("๐ Load Sample", size="sm")
|
| 233 |
clear_btn = gr.Button("Clear", size="sm")
|
| 234 |
-
|
| 235 |
with gr.Row():
|
| 236 |
window_size = gr.Slider(2, 20, value=4, step=1, label="Window Size")
|
| 237 |
k_neighbors = gr.Slider(2, 20, value=5, step=1, label="k Neighbors")
|
| 238 |
-
|
| 239 |
with gr.Row():
|
| 240 |
mode_select = gr.Dropdown(
|
| 241 |
choices=["Percentile", "Range"],
|
|
@@ -246,21 +257,53 @@ def create_interface():
|
|
| 246 |
anomaly_percentile = gr.Slider(0.01, 0.5, value=0.1, step=0.01, label="Top N%", scale=2)
|
| 247 |
range_min = gr.Slider(0.0, 0.49, value=0.05, step=0.01, label="Min %", visible=False, scale=1)
|
| 248 |
range_max = gr.Slider(0.01, 0.5, value=0.15, step=0.01, label="Max %", visible=False, scale=1)
|
| 249 |
-
|
| 250 |
def toggle_mode(mode):
|
| 251 |
if mode == "Percentile":
|
| 252 |
return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
|
| 253 |
else:
|
| 254 |
return gr.update(visible=False), gr.update(visible=True), gr.update(visible=True)
|
| 255 |
-
|
| 256 |
mode_select.change(
|
| 257 |
fn=toggle_mode,
|
| 258 |
inputs=[mode_select],
|
| 259 |
outputs=[anomaly_percentile, range_min, range_max]
|
| 260 |
)
|
| 261 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
analyze_btn = gr.Button("๐ Analyze", variant="primary", size="lg")
|
| 263 |
-
|
| 264 |
# Right: Output
|
| 265 |
with gr.Column(scale=1):
|
| 266 |
output_text = gr.Textbox(
|
|
@@ -268,8 +311,8 @@ def create_interface():
|
|
| 268 |
placeholder="Results will appear here...",
|
| 269 |
lines=37,
|
| 270 |
)
|
| 271 |
-
|
| 272 |
-
|
| 273 |
# Documentation section
|
| 274 |
gr.Markdown("""
|
| 275 |
---
|
|
@@ -314,14 +357,26 @@ pip install cordon
|
|
| 314 |
# Basic usage
|
| 315 |
cordon system.log
|
| 316 |
|
| 317 |
-
# Customize parameters
|
| 318 |
cordon --window-size 10 --k-neighbors 10 --anomaly-percentile 0.05 app.log
|
| 319 |
|
| 320 |
# Use range mode (exclude top 5%, keep next 10%)
|
| 321 |
cordon --anomaly-range 0.05 0.15 app.log
|
| 322 |
|
| 323 |
-
#
|
| 324 |
-
cordon --
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
```
|
| 326 |
|
| 327 |
### Python API
|
|
@@ -331,28 +386,39 @@ from cordon import SemanticLogAnalyzer, AnalysisConfig
|
|
| 331 |
|
| 332 |
config = AnalysisConfig(window_size=4, k_neighbors=5, anomaly_percentile=0.1)
|
| 333 |
analyzer = SemanticLogAnalyzer(config)
|
| 334 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
```
|
| 336 |
|
| 337 |
### Links
|
| 338 |
|
| 339 |
- [GitHub Repository](https://github.com/calebevans/cordon)
|
| 340 |
-
- [PyPI Package](https://pypi.org/project/cordon/)
|
| 341 |
- [Red Hat Developer Article](https://developers.redhat.com/articles/2025/12/09/semantic-anomaly-detection-log-files-cordon)
|
| 342 |
""")
|
| 343 |
-
|
| 344 |
# Events
|
| 345 |
load_sample_btn.click(fn=load_sample_log, outputs=[log_input])
|
| 346 |
clear_btn.click(fn=lambda: "", outputs=[log_input])
|
| 347 |
-
|
| 348 |
-
# Use analyze_logs directly - it's decorated with @spaces.GPU
|
| 349 |
-
# Don't wrap it in another function or ZeroGPU won't allocate GPU properly
|
| 350 |
analyze_btn.click(
|
| 351 |
fn=analyze_logs,
|
| 352 |
-
inputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
outputs=[output_text],
|
| 354 |
)
|
| 355 |
-
|
| 356 |
return demo
|
| 357 |
|
| 358 |
|
|
@@ -371,10 +437,10 @@ try:
|
|
| 371 |
logger.info("Creating Gradio interface...")
|
| 372 |
demo = create_interface()
|
| 373 |
logger.info("Gradio interface created successfully")
|
| 374 |
-
|
| 375 |
# Detect if running on HuggingFace Spaces
|
| 376 |
is_hf_space = os.getenv("SPACE_ID") is not None
|
| 377 |
-
|
| 378 |
if is_hf_space:
|
| 379 |
# On HuggingFace: use strict port binding for health checks
|
| 380 |
logger.info("Running on HuggingFace Spaces - using port 7860")
|
|
|
|
| 6 |
import logging
|
| 7 |
import os
|
| 8 |
import sys
|
|
|
|
| 9 |
import time
|
| 10 |
from pathlib import Path
|
| 11 |
|
|
|
|
| 65 |
anomaly_percentile: float,
|
| 66 |
range_min: float,
|
| 67 |
range_max: float,
|
| 68 |
+
output_format: str = "xml",
|
| 69 |
+
token_budget: float | None = None,
|
| 70 |
+
max_blocks: float | None = None,
|
| 71 |
+
min_score: float = 0.0,
|
| 72 |
progress=gr.Progress(),
|
| 73 |
):
|
| 74 |
"""Analyze log content and return results.
|
| 75 |
+
|
| 76 |
This function is decorated with @spaces.GPU to run on ZeroGPU.
|
| 77 |
The GPU is allocated when this function is called.
|
| 78 |
+
|
| 79 |
IMPORTANT: torch and cordon are imported here (not at module level)
|
| 80 |
because ZeroGPU only allocates GPU resources inside @spaces.GPU functions.
|
| 81 |
"""
|
| 82 |
# Lazy imports - only load when GPU is allocated
|
| 83 |
import torch
|
| 84 |
from cordon import AnalysisConfig, SemanticLogAnalyzer
|
| 85 |
+
|
| 86 |
start_time = time.time()
|
| 87 |
logger.info("=" * 60)
|
| 88 |
logger.info("Analysis started - GPU allocated by ZeroGPU")
|
| 89 |
+
|
| 90 |
if not log_content.strip():
|
| 91 |
logger.warning("Empty log content provided")
|
| 92 |
return "", "Anomalous Sections"
|
| 93 |
+
|
| 94 |
+
logger.info(
|
| 95 |
+
f"Log content size: {len(log_content)} characters "
|
| 96 |
+
f"({len(log_content.splitlines())} lines)"
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
progress(0.1, desc="Configuring analyzer...")
|
| 100 |
+
|
| 101 |
+
# Check if CUDA is available (should be True on ZeroGPU)
|
| 102 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 103 |
+
batch_size = 128 if device == "cuda" else 32
|
| 104 |
+
|
| 105 |
+
logger.info(f"Device: {device}")
|
| 106 |
+
logger.info(f"Batch size: {batch_size}")
|
| 107 |
+
|
| 108 |
+
config_kwargs: dict = {
|
| 109 |
+
"window_size": window_size,
|
| 110 |
+
"k_neighbors": k_neighbors,
|
| 111 |
+
"device": device,
|
| 112 |
+
"batch_size": batch_size,
|
| 113 |
+
"show_progress": False,
|
| 114 |
+
"output_format": output_format,
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
if token_budget is not None and token_budget > 0:
|
| 118 |
+
config_kwargs["token_budget"] = int(token_budget)
|
| 119 |
+
if max_blocks is not None and max_blocks > 0:
|
| 120 |
+
config_kwargs["max_blocks"] = int(max_blocks)
|
| 121 |
+
if min_score > 0:
|
| 122 |
+
config_kwargs["min_score"] = min_score
|
| 123 |
+
|
| 124 |
+
if mode == "Percentile":
|
| 125 |
+
config_kwargs["anomaly_percentile"] = anomaly_percentile
|
| 126 |
+
else:
|
| 127 |
+
config_kwargs["anomaly_range_min"] = range_min
|
| 128 |
+
config_kwargs["anomaly_range_max"] = range_max
|
| 129 |
+
|
| 130 |
+
config = AnalysisConfig(**config_kwargs)
|
| 131 |
+
logger.info(f"Configuration: mode={mode}, {config_kwargs}")
|
| 132 |
+
|
| 133 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
progress(0.15, desc="Loading embedding model...")
|
| 135 |
logger.info("Loading embedding model...")
|
| 136 |
model_load_start = time.time()
|
| 137 |
analyzer = SemanticLogAnalyzer(config)
|
| 138 |
logger.info(f"Embedding model loaded in {time.time() - model_load_start:.2f}s")
|
| 139 |
+
|
| 140 |
progress(0.4, desc="Analyzing patterns...")
|
| 141 |
logger.info("Starting log analysis...")
|
| 142 |
analysis_start = time.time()
|
| 143 |
+
result = analyzer.analyze_text_detailed(log_content)
|
| 144 |
logger.info(f"Analysis completed in {time.time() - analysis_start:.2f}s")
|
| 145 |
+
|
| 146 |
progress(0.9, desc="Formatting output...")
|
| 147 |
progress(1.0, desc="Complete!")
|
| 148 |
+
|
| 149 |
# Calculate token counts
|
| 150 |
input_tokens = estimate_tokens(log_content)
|
| 151 |
output_tokens = estimate_tokens(result.output)
|
| 152 |
+
reduction = (
|
| 153 |
+
(input_tokens - output_tokens) / input_tokens * 100
|
| 154 |
+
if input_tokens > 0 else 0
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
block_count = len(result.blocks)
|
| 158 |
+
if block_count > 0:
|
| 159 |
+
scores = [b.max_score for b in result.blocks]
|
| 160 |
+
score_range = f"scores: {min(scores):.3f}-{max(scores):.3f}"
|
| 161 |
+
else:
|
| 162 |
+
score_range = "no anomalies"
|
| 163 |
+
|
| 164 |
+
label = (
|
| 165 |
+
f"Anomalous Sections โ {input_tokens:,} โ {output_tokens:,} tokens "
|
| 166 |
+
f"({reduction:.0f}% reduction) | {block_count} blocks, {score_range}"
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
total_time = time.time() - start_time
|
| 170 |
logger.info(f"Total analysis time: {total_time:.2f}s")
|
| 171 |
logger.info(f"Token reduction: {input_tokens:,} โ {output_tokens:,} ({reduction:.0f}%)")
|
| 172 |
logger.info("=" * 60)
|
| 173 |
+
|
| 174 |
return gr.update(value=result.output, label=label)
|
| 175 |
+
|
| 176 |
except Exception as e:
|
| 177 |
logger.error(f"Error during analysis: {str(e)}", exc_info=True)
|
| 178 |
error_msg = f"Analysis failed: {str(e)}"
|
| 179 |
return gr.update(value=error_msg, label="Error")
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
|
| 182 |
CUSTOM_CSS = """
|
|
|
|
| 215 |
|
| 216 |
def create_interface():
|
| 217 |
"""Create the Gradio interface."""
|
| 218 |
+
|
| 219 |
with gr.Blocks(title="Cordon", theme=THEME, css=CUSTOM_CSS) as demo:
|
| 220 |
+
|
| 221 |
# Header
|
| 222 |
gr.HTML("""
|
| 223 |
<div style="text-align: center; padding: 16px 0 24px 0;">
|
|
|
|
| 227 |
<p style="color: #94a3b8; margin: 0;">Reduce logs to their semantically anomalous parts.</p>
|
| 228 |
</div>
|
| 229 |
""")
|
| 230 |
+
|
| 231 |
# Main content - two equal columns
|
| 232 |
with gr.Row():
|
| 233 |
+
|
| 234 |
# Left: Input
|
| 235 |
with gr.Column(scale=1):
|
| 236 |
log_input = gr.Textbox(
|
|
|
|
| 238 |
placeholder="Paste your log content here...",
|
| 239 |
lines=22,
|
| 240 |
)
|
| 241 |
+
|
| 242 |
with gr.Row():
|
| 243 |
load_sample_btn = gr.Button("๐ Load Sample", size="sm")
|
| 244 |
clear_btn = gr.Button("Clear", size="sm")
|
| 245 |
+
|
| 246 |
with gr.Row():
|
| 247 |
window_size = gr.Slider(2, 20, value=4, step=1, label="Window Size")
|
| 248 |
k_neighbors = gr.Slider(2, 20, value=5, step=1, label="k Neighbors")
|
| 249 |
+
|
| 250 |
with gr.Row():
|
| 251 |
mode_select = gr.Dropdown(
|
| 252 |
choices=["Percentile", "Range"],
|
|
|
|
| 257 |
anomaly_percentile = gr.Slider(0.01, 0.5, value=0.1, step=0.01, label="Top N%", scale=2)
|
| 258 |
range_min = gr.Slider(0.0, 0.49, value=0.05, step=0.01, label="Min %", visible=False, scale=1)
|
| 259 |
range_max = gr.Slider(0.01, 0.5, value=0.15, step=0.01, label="Max %", visible=False, scale=1)
|
| 260 |
+
|
| 261 |
def toggle_mode(mode):
|
| 262 |
if mode == "Percentile":
|
| 263 |
return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
|
| 264 |
else:
|
| 265 |
return gr.update(visible=False), gr.update(visible=True), gr.update(visible=True)
|
| 266 |
+
|
| 267 |
mode_select.change(
|
| 268 |
fn=toggle_mode,
|
| 269 |
inputs=[mode_select],
|
| 270 |
outputs=[anomaly_percentile, range_min, range_max]
|
| 271 |
)
|
| 272 |
+
|
| 273 |
+
with gr.Accordion("Advanced Options", open=False):
|
| 274 |
+
with gr.Row():
|
| 275 |
+
output_format = gr.Dropdown(
|
| 276 |
+
choices=["xml", "json"],
|
| 277 |
+
value="xml",
|
| 278 |
+
label="Output Format",
|
| 279 |
+
scale=1,
|
| 280 |
+
)
|
| 281 |
+
token_budget = gr.Number(
|
| 282 |
+
value=None,
|
| 283 |
+
label="Token Budget",
|
| 284 |
+
precision=0,
|
| 285 |
+
minimum=1,
|
| 286 |
+
info="Max tokens for output (auto-adjusts percentile)",
|
| 287 |
+
scale=1,
|
| 288 |
+
)
|
| 289 |
+
with gr.Row():
|
| 290 |
+
max_blocks = gr.Number(
|
| 291 |
+
value=None,
|
| 292 |
+
label="Max Blocks",
|
| 293 |
+
precision=0,
|
| 294 |
+
minimum=1,
|
| 295 |
+
info="Limit to top N blocks by score",
|
| 296 |
+
scale=1,
|
| 297 |
+
)
|
| 298 |
+
min_score = gr.Slider(
|
| 299 |
+
0.0, 1.0, value=0.0, step=0.01,
|
| 300 |
+
label="Min Score",
|
| 301 |
+
info="Filter blocks below this score",
|
| 302 |
+
scale=1,
|
| 303 |
+
)
|
| 304 |
+
|
| 305 |
analyze_btn = gr.Button("๐ Analyze", variant="primary", size="lg")
|
| 306 |
+
|
| 307 |
# Right: Output
|
| 308 |
with gr.Column(scale=1):
|
| 309 |
output_text = gr.Textbox(
|
|
|
|
| 311 |
placeholder="Results will appear here...",
|
| 312 |
lines=37,
|
| 313 |
)
|
| 314 |
+
|
| 315 |
+
|
| 316 |
# Documentation section
|
| 317 |
gr.Markdown("""
|
| 318 |
---
|
|
|
|
| 357 |
# Basic usage
|
| 358 |
cordon system.log
|
| 359 |
|
| 360 |
+
# Customize parameters
|
| 361 |
cordon --window-size 10 --k-neighbors 10 --anomaly-percentile 0.05 app.log
|
| 362 |
|
| 363 |
# Use range mode (exclude top 5%, keep next 10%)
|
| 364 |
cordon --anomaly-range 0.05 0.15 app.log
|
| 365 |
|
| 366 |
+
# JSON output
|
| 367 |
+
cordon --format json system.log
|
| 368 |
+
|
| 369 |
+
# Quiet mode for CI
|
| 370 |
+
cordon --quiet -o anomalies.json --format json app.log
|
| 371 |
+
|
| 372 |
+
# Token budget (fit output in 2000 tokens)
|
| 373 |
+
cordon --token-budget 2000 system.log
|
| 374 |
+
|
| 375 |
+
# Filter output
|
| 376 |
+
cordon --max-blocks 10 --min-score 0.1 system.log
|
| 377 |
+
|
| 378 |
+
# Read from stdin
|
| 379 |
+
kubectl logs pod-name | cordon -
|
| 380 |
```
|
| 381 |
|
| 382 |
### Python API
|
|
|
|
| 386 |
|
| 387 |
config = AnalysisConfig(window_size=4, k_neighbors=5, anomaly_percentile=0.1)
|
| 388 |
analyzer = SemanticLogAnalyzer(config)
|
| 389 |
+
|
| 390 |
+
# From file
|
| 391 |
+
result = analyzer.analyze_file_detailed(Path("system.log"))
|
| 392 |
+
|
| 393 |
+
# From text (no temp file needed)
|
| 394 |
+
result = analyzer.analyze_text_detailed(log_text)
|
| 395 |
+
|
| 396 |
+
# Access structured blocks
|
| 397 |
+
for block in result.blocks:
|
| 398 |
+
print(f"Lines {block.start_line}-{block.end_line}: score={block.max_score:.4f}")
|
| 399 |
```
|
| 400 |
|
| 401 |
### Links
|
| 402 |
|
| 403 |
- [GitHub Repository](https://github.com/calebevans/cordon)
|
| 404 |
+
- [PyPI Package](https://pypi.org/project/cordon/)
|
| 405 |
- [Red Hat Developer Article](https://developers.redhat.com/articles/2025/12/09/semantic-anomaly-detection-log-files-cordon)
|
| 406 |
""")
|
| 407 |
+
|
| 408 |
# Events
|
| 409 |
load_sample_btn.click(fn=load_sample_log, outputs=[log_input])
|
| 410 |
clear_btn.click(fn=lambda: "", outputs=[log_input])
|
| 411 |
+
|
|
|
|
|
|
|
| 412 |
analyze_btn.click(
|
| 413 |
fn=analyze_logs,
|
| 414 |
+
inputs=[
|
| 415 |
+
log_input, window_size, k_neighbors, mode_select,
|
| 416 |
+
anomaly_percentile, range_min, range_max,
|
| 417 |
+
output_format, token_budget, max_blocks, min_score,
|
| 418 |
+
],
|
| 419 |
outputs=[output_text],
|
| 420 |
)
|
| 421 |
+
|
| 422 |
return demo
|
| 423 |
|
| 424 |
|
|
|
|
| 437 |
logger.info("Creating Gradio interface...")
|
| 438 |
demo = create_interface()
|
| 439 |
logger.info("Gradio interface created successfully")
|
| 440 |
+
|
| 441 |
# Detect if running on HuggingFace Spaces
|
| 442 |
is_hf_space = os.getenv("SPACE_ID") is not None
|
| 443 |
+
|
| 444 |
if is_hf_space:
|
| 445 |
# On HuggingFace: use strict port binding for health checks
|
| 446 |
logger.info("Running on HuggingFace Spaces - using port 7860")
|
requirements.txt
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# Cordon package from PyPI
|
| 2 |
-
cordon=
|
| 3 |
|
| 4 |
# UI dependencies
|
| 5 |
gradio==6.2.0
|
|
|
|
| 1 |
# Cordon package from PyPI
|
| 2 |
+
cordon>=1.0.2
|
| 3 |
|
| 4 |
# UI dependencies
|
| 5 |
gradio==6.2.0
|