calebdevans Cursor commited on
Commit
3f11ee6
ยท
unverified ยท
1 Parent(s): 8b07a54

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>

Files changed (2) hide show
  1. app.py +152 -86
  2. 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(f"Log content size: {len(log_content)} characters ({len(log_content.splitlines())} lines)")
92
-
93
- progress(0.05, desc="Preparing log file...")
94
-
95
- # Write content to temp file
96
- with tempfile.NamedTemporaryFile(mode="w", suffix=".log", delete=False) as f:
97
- f.write(log_content)
98
- temp_path = Path(f.name)
99
-
100
- logger.info(f"Temporary log file created: {temp_path}")
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.analyze_file_detailed(temp_path)
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 = ((input_tokens - output_tokens) / input_tokens * 100) if input_tokens > 0 else 0
152
-
153
- label = f"Anomalous Sections โ€” {input_tokens:,} โ†’ {output_tokens:,} tokens ({reduction:.0f}% reduction)"
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
- # Save to file
324
- cordon --output anomalies.xml system.log
 
 
 
 
 
 
 
 
 
 
 
 
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
- result = analyzer.analyze_file(Path("system.log"))
 
 
 
 
 
 
 
 
 
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=[log_input, window_size, k_neighbors, mode_select, anomaly_percentile, range_min, range_max],
 
 
 
 
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==0.3.1
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