flock-core 0.5.0b53__py3-none-any.whl → 0.5.0b55__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.
Potentially problematic release.
This version of flock-core might be problematic. Click here for more details.
- flock/agent.py +6 -2
- flock/components.py +17 -1
- flock/dashboard/service.py +293 -0
- flock/frontend/README.md +86 -0
- flock/frontend/src/components/modules/JsonAttributeRenderer.tsx +140 -0
- flock/frontend/src/components/modules/ModuleWindow.tsx +97 -29
- flock/frontend/src/components/modules/TraceModuleJaeger.tsx +1971 -0
- flock/frontend/src/components/modules/TraceModuleJaegerWrapper.tsx +13 -0
- flock/frontend/src/components/modules/registerModules.ts +10 -0
- flock/frontend/src/components/settings/MultiSelect.tsx +235 -0
- flock/frontend/src/components/settings/SettingsPanel.css +1 -1
- flock/frontend/src/components/settings/TracingSettings.tsx +404 -0
- flock/frontend/src/types/modules.ts +3 -0
- flock/logging/auto_trace.py +159 -0
- flock/logging/telemetry.py +17 -0
- flock/logging/telemetry_exporter/duckdb_exporter.py +216 -0
- flock/logging/telemetry_exporter/file_exporter.py +7 -1
- flock/logging/trace_and_logged.py +263 -14
- flock/orchestrator.py +130 -1
- flock/store.py +34 -1
- flock_core-0.5.0b55.dist-info/METADATA +681 -0
- {flock_core-0.5.0b53.dist-info → flock_core-0.5.0b55.dist-info}/RECORD +25 -18
- flock_core-0.5.0b53.dist-info/METADATA +0 -747
- {flock_core-0.5.0b53.dist-info → flock_core-0.5.0b55.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b53.dist-info → flock_core-0.5.0b55.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b53.dist-info → flock_core-0.5.0b55.dist-info}/licenses/LICENSE +0 -0
flock/agent.py
CHANGED
|
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
10
10
|
from pydantic import BaseModel
|
|
11
11
|
|
|
12
12
|
from flock.artifacts import Artifact, ArtifactSpec
|
|
13
|
+
from flock.logging.auto_trace import AutoTracedMeta
|
|
13
14
|
from flock.logging.logging import get_logger
|
|
14
15
|
from flock.registry import function_registry, type_registry
|
|
15
16
|
from flock.runtime import Context, EvalInputs, EvalResult
|
|
@@ -50,8 +51,11 @@ class AgentOutput:
|
|
|
50
51
|
)
|
|
51
52
|
|
|
52
53
|
|
|
53
|
-
class Agent:
|
|
54
|
-
"""Executable agent constructed via `AgentBuilder`.
|
|
54
|
+
class Agent(metaclass=AutoTracedMeta):
|
|
55
|
+
"""Executable agent constructed via `AgentBuilder`.
|
|
56
|
+
|
|
57
|
+
All public methods are automatically traced via OpenTelemetry.
|
|
58
|
+
"""
|
|
55
59
|
|
|
56
60
|
def __init__(self, name: str, *, orchestrator: Flock) -> None:
|
|
57
61
|
self.name = name
|
flock/components.py
CHANGED
|
@@ -5,8 +5,11 @@ from __future__ import annotations
|
|
|
5
5
|
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, Field, create_model
|
|
8
|
+
from pydantic._internal._model_construction import ModelMetaclass
|
|
8
9
|
from typing_extensions import Self, TypeVar
|
|
9
10
|
|
|
11
|
+
from flock.logging.auto_trace import AutoTracedMeta
|
|
12
|
+
|
|
10
13
|
|
|
11
14
|
if TYPE_CHECKING: # pragma: no cover - type checking only
|
|
12
15
|
from uuid import UUID
|
|
@@ -18,6 +21,14 @@ if TYPE_CHECKING: # pragma: no cover - type checking only
|
|
|
18
21
|
T = TypeVar("T", bound="AgentComponentConfig")
|
|
19
22
|
|
|
20
23
|
|
|
24
|
+
class TracedModelMeta(ModelMetaclass, AutoTracedMeta):
|
|
25
|
+
"""Combined metaclass for Pydantic models with auto-tracing.
|
|
26
|
+
|
|
27
|
+
This metaclass combines Pydantic's ModelMetaclass with AutoTracedMeta
|
|
28
|
+
to enable both Pydantic functionality and automatic method tracing.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
21
32
|
class AgentComponentConfig(BaseModel):
|
|
22
33
|
enabled: bool = True
|
|
23
34
|
model: str | None = None
|
|
@@ -37,7 +48,12 @@ class AgentComponentConfig(BaseModel):
|
|
|
37
48
|
return create_model(f"Dynamic{cls.__name__}", __base__=cls, **field_definitions)
|
|
38
49
|
|
|
39
50
|
|
|
40
|
-
class AgentComponent(BaseModel):
|
|
51
|
+
class AgentComponent(BaseModel, metaclass=TracedModelMeta):
|
|
52
|
+
"""Base class for agent components with lifecycle hooks.
|
|
53
|
+
|
|
54
|
+
All public methods are automatically traced via OpenTelemetry.
|
|
55
|
+
"""
|
|
56
|
+
|
|
41
57
|
name: str | None = None
|
|
42
58
|
config: AgentComponentConfig = Field(default_factory=AgentComponentConfig)
|
|
43
59
|
|
flock/dashboard/service.py
CHANGED
|
@@ -407,6 +407,299 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
407
407
|
"""
|
|
408
408
|
raise HTTPException(status_code=501, detail="Resume functionality coming in Phase 12")
|
|
409
409
|
|
|
410
|
+
@app.get("/api/traces")
|
|
411
|
+
async def get_traces() -> list[dict[str, Any]]:
|
|
412
|
+
"""Get OpenTelemetry traces from DuckDB.
|
|
413
|
+
|
|
414
|
+
Returns list of trace spans in OTEL format.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
[
|
|
418
|
+
{
|
|
419
|
+
"name": "Agent.execute",
|
|
420
|
+
"context": {
|
|
421
|
+
"trace_id": "...",
|
|
422
|
+
"span_id": "...",
|
|
423
|
+
...
|
|
424
|
+
},
|
|
425
|
+
"start_time": 1234567890,
|
|
426
|
+
"end_time": 1234567891,
|
|
427
|
+
"attributes": {...},
|
|
428
|
+
"status": {...}
|
|
429
|
+
},
|
|
430
|
+
...
|
|
431
|
+
]
|
|
432
|
+
"""
|
|
433
|
+
import json
|
|
434
|
+
from pathlib import Path
|
|
435
|
+
|
|
436
|
+
import duckdb
|
|
437
|
+
|
|
438
|
+
db_path = Path(".flock/traces.duckdb")
|
|
439
|
+
|
|
440
|
+
if not db_path.exists():
|
|
441
|
+
logger.warning(
|
|
442
|
+
"Trace database not found. Make sure FLOCK_AUTO_TRACE=true FLOCK_TRACE_FILE=true"
|
|
443
|
+
)
|
|
444
|
+
return []
|
|
445
|
+
|
|
446
|
+
try:
|
|
447
|
+
with duckdb.connect(str(db_path), read_only=True) as conn:
|
|
448
|
+
# Query all spans from DuckDB
|
|
449
|
+
result = conn.execute("""
|
|
450
|
+
SELECT
|
|
451
|
+
trace_id, span_id, parent_id, name, service, operation,
|
|
452
|
+
kind, start_time, end_time, duration_ms,
|
|
453
|
+
status_code, status_description,
|
|
454
|
+
attributes, events, links, resource
|
|
455
|
+
FROM spans
|
|
456
|
+
ORDER BY start_time DESC
|
|
457
|
+
""").fetchall()
|
|
458
|
+
|
|
459
|
+
spans = []
|
|
460
|
+
for row in result:
|
|
461
|
+
# Reconstruct OTEL span format from DuckDB row
|
|
462
|
+
span = {
|
|
463
|
+
"name": row[3], # name
|
|
464
|
+
"context": {
|
|
465
|
+
"trace_id": row[0], # trace_id
|
|
466
|
+
"span_id": row[1], # span_id
|
|
467
|
+
"trace_flags": 0,
|
|
468
|
+
"trace_state": "",
|
|
469
|
+
},
|
|
470
|
+
"kind": row[6], # kind
|
|
471
|
+
"start_time": row[7], # start_time
|
|
472
|
+
"end_time": row[8], # end_time
|
|
473
|
+
"status": {
|
|
474
|
+
"status_code": row[10], # status_code
|
|
475
|
+
"description": row[11], # status_description
|
|
476
|
+
},
|
|
477
|
+
"attributes": json.loads(row[12]) if row[12] else {}, # attributes
|
|
478
|
+
"events": json.loads(row[13]) if row[13] else [], # events
|
|
479
|
+
"links": json.loads(row[14]) if row[14] else [], # links
|
|
480
|
+
"resource": json.loads(row[15]) if row[15] else {}, # resource
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
# Add parent_id if exists
|
|
484
|
+
if row[2]: # parent_id
|
|
485
|
+
span["parent_id"] = row[2]
|
|
486
|
+
|
|
487
|
+
spans.append(span)
|
|
488
|
+
|
|
489
|
+
logger.debug(f"Loaded {len(spans)} spans from DuckDB")
|
|
490
|
+
return spans
|
|
491
|
+
|
|
492
|
+
except Exception as e:
|
|
493
|
+
logger.exception(f"Error reading traces from DuckDB: {e}")
|
|
494
|
+
return []
|
|
495
|
+
|
|
496
|
+
@app.get("/api/traces/services")
|
|
497
|
+
async def get_trace_services() -> dict[str, Any]:
|
|
498
|
+
"""Get list of unique services that have been traced.
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
{
|
|
502
|
+
"services": ["Flock", "Agent", "DSPyEngine", ...],
|
|
503
|
+
"operations": ["Flock.publish", "Agent.execute", ...]
|
|
504
|
+
}
|
|
505
|
+
"""
|
|
506
|
+
import duckdb
|
|
507
|
+
from pathlib import Path
|
|
508
|
+
|
|
509
|
+
db_path = Path(".flock/traces.duckdb")
|
|
510
|
+
|
|
511
|
+
if not db_path.exists():
|
|
512
|
+
return {"services": [], "operations": []}
|
|
513
|
+
|
|
514
|
+
try:
|
|
515
|
+
with duckdb.connect(str(db_path), read_only=True) as conn:
|
|
516
|
+
# Get unique services
|
|
517
|
+
services_result = conn.execute("""
|
|
518
|
+
SELECT DISTINCT service
|
|
519
|
+
FROM spans
|
|
520
|
+
WHERE service IS NOT NULL
|
|
521
|
+
ORDER BY service
|
|
522
|
+
""").fetchall()
|
|
523
|
+
|
|
524
|
+
# Get unique operations
|
|
525
|
+
operations_result = conn.execute("""
|
|
526
|
+
SELECT DISTINCT name
|
|
527
|
+
FROM spans
|
|
528
|
+
WHERE name IS NOT NULL
|
|
529
|
+
ORDER BY name
|
|
530
|
+
""").fetchall()
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
"services": [row[0] for row in services_result],
|
|
534
|
+
"operations": [row[0] for row in operations_result],
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
except Exception as e:
|
|
538
|
+
logger.exception(f"Error reading trace services: {e}")
|
|
539
|
+
return {"services": [], "operations": []}
|
|
540
|
+
|
|
541
|
+
@app.post("/api/traces/clear")
|
|
542
|
+
async def clear_traces() -> dict[str, Any]:
|
|
543
|
+
"""Clear all traces from DuckDB database.
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
{
|
|
547
|
+
"success": true,
|
|
548
|
+
"deleted_count": 123,
|
|
549
|
+
"error": null
|
|
550
|
+
}
|
|
551
|
+
"""
|
|
552
|
+
result = Flock.clear_traces()
|
|
553
|
+
if result["success"]:
|
|
554
|
+
logger.info(f"Cleared {result['deleted_count']} trace spans via API")
|
|
555
|
+
else:
|
|
556
|
+
logger.error(f"Failed to clear traces: {result['error']}")
|
|
557
|
+
|
|
558
|
+
return result
|
|
559
|
+
|
|
560
|
+
@app.post("/api/traces/query")
|
|
561
|
+
async def execute_trace_query(request: dict[str, Any]) -> dict[str, Any]:
|
|
562
|
+
"""
|
|
563
|
+
Execute a DuckDB SQL query on the traces database.
|
|
564
|
+
|
|
565
|
+
Security: Only SELECT queries allowed, rate-limited.
|
|
566
|
+
"""
|
|
567
|
+
import duckdb
|
|
568
|
+
from pathlib import Path
|
|
569
|
+
|
|
570
|
+
query = request.get("query", "").strip()
|
|
571
|
+
|
|
572
|
+
if not query:
|
|
573
|
+
return {"error": "Query cannot be empty", "results": [], "columns": []}
|
|
574
|
+
|
|
575
|
+
# Security: Only allow SELECT queries
|
|
576
|
+
query_upper = query.upper().strip()
|
|
577
|
+
if not query_upper.startswith("SELECT"):
|
|
578
|
+
return {"error": "Only SELECT queries are allowed", "results": [], "columns": []}
|
|
579
|
+
|
|
580
|
+
# Check for dangerous keywords
|
|
581
|
+
dangerous = ["DROP", "DELETE", "INSERT", "UPDATE", "ALTER", "CREATE", "TRUNCATE"]
|
|
582
|
+
if any(keyword in query_upper for keyword in dangerous):
|
|
583
|
+
return {
|
|
584
|
+
"error": "Query contains forbidden operations",
|
|
585
|
+
"results": [],
|
|
586
|
+
"columns": [],
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
db_path = Path(".flock/traces.duckdb")
|
|
590
|
+
if not db_path.exists():
|
|
591
|
+
return {"error": "Trace database not found", "results": [], "columns": []}
|
|
592
|
+
|
|
593
|
+
try:
|
|
594
|
+
with duckdb.connect(str(db_path), read_only=True) as conn:
|
|
595
|
+
result = conn.execute(query).fetchall()
|
|
596
|
+
columns = [desc[0] for desc in conn.description] if conn.description else []
|
|
597
|
+
|
|
598
|
+
# Convert to JSON-serializable format
|
|
599
|
+
results = []
|
|
600
|
+
for row in result:
|
|
601
|
+
row_dict = {}
|
|
602
|
+
for i, col in enumerate(columns):
|
|
603
|
+
val = row[i]
|
|
604
|
+
# Convert bytes to string, handle other types
|
|
605
|
+
if isinstance(val, bytes):
|
|
606
|
+
row_dict[col] = val.decode("utf-8")
|
|
607
|
+
else:
|
|
608
|
+
row_dict[col] = val
|
|
609
|
+
results.append(row_dict)
|
|
610
|
+
|
|
611
|
+
return {"results": results, "columns": columns, "row_count": len(results)}
|
|
612
|
+
except Exception as e:
|
|
613
|
+
logger.error(f"DuckDB query error: {e}")
|
|
614
|
+
return {"error": str(e), "results": [], "columns": []}
|
|
615
|
+
|
|
616
|
+
@app.get("/api/traces/stats")
|
|
617
|
+
async def get_trace_stats() -> dict[str, Any]:
|
|
618
|
+
"""Get statistics about the trace database.
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
{
|
|
622
|
+
"total_spans": 123,
|
|
623
|
+
"total_traces": 45,
|
|
624
|
+
"services_count": 5,
|
|
625
|
+
"oldest_trace": "2025-10-07T12:00:00Z",
|
|
626
|
+
"newest_trace": "2025-10-07T14:30:00Z",
|
|
627
|
+
"database_size_mb": 12.5
|
|
628
|
+
}
|
|
629
|
+
"""
|
|
630
|
+
import duckdb
|
|
631
|
+
from pathlib import Path
|
|
632
|
+
from datetime import datetime
|
|
633
|
+
|
|
634
|
+
db_path = Path(".flock/traces.duckdb")
|
|
635
|
+
|
|
636
|
+
if not db_path.exists():
|
|
637
|
+
return {
|
|
638
|
+
"total_spans": 0,
|
|
639
|
+
"total_traces": 0,
|
|
640
|
+
"services_count": 0,
|
|
641
|
+
"oldest_trace": None,
|
|
642
|
+
"newest_trace": None,
|
|
643
|
+
"database_size_mb": 0,
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
try:
|
|
647
|
+
with duckdb.connect(str(db_path), read_only=True) as conn:
|
|
648
|
+
# Get total spans
|
|
649
|
+
total_spans = conn.execute("SELECT COUNT(*) FROM spans").fetchone()[0]
|
|
650
|
+
|
|
651
|
+
# Get total unique traces
|
|
652
|
+
total_traces = conn.execute(
|
|
653
|
+
"SELECT COUNT(DISTINCT trace_id) FROM spans"
|
|
654
|
+
).fetchone()[0]
|
|
655
|
+
|
|
656
|
+
# Get services count
|
|
657
|
+
services_count = conn.execute(
|
|
658
|
+
"SELECT COUNT(DISTINCT service) FROM spans WHERE service IS NOT NULL"
|
|
659
|
+
).fetchone()[0]
|
|
660
|
+
|
|
661
|
+
# Get time range
|
|
662
|
+
time_range = conn.execute("""
|
|
663
|
+
SELECT
|
|
664
|
+
MIN(start_time) as oldest,
|
|
665
|
+
MAX(start_time) as newest
|
|
666
|
+
FROM spans
|
|
667
|
+
""").fetchone()
|
|
668
|
+
|
|
669
|
+
oldest_trace = None
|
|
670
|
+
newest_trace = None
|
|
671
|
+
if time_range and time_range[0]:
|
|
672
|
+
# Convert nanoseconds to datetime
|
|
673
|
+
oldest_trace = datetime.fromtimestamp(
|
|
674
|
+
time_range[0] / 1_000_000_000
|
|
675
|
+
).isoformat()
|
|
676
|
+
newest_trace = datetime.fromtimestamp(
|
|
677
|
+
time_range[1] / 1_000_000_000
|
|
678
|
+
).isoformat()
|
|
679
|
+
|
|
680
|
+
# Get file size
|
|
681
|
+
size_mb = db_path.stat().st_size / (1024 * 1024)
|
|
682
|
+
|
|
683
|
+
return {
|
|
684
|
+
"total_spans": total_spans,
|
|
685
|
+
"total_traces": total_traces,
|
|
686
|
+
"services_count": services_count,
|
|
687
|
+
"oldest_trace": oldest_trace,
|
|
688
|
+
"newest_trace": newest_trace,
|
|
689
|
+
"database_size_mb": round(size_mb, 2),
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
except Exception as e:
|
|
693
|
+
logger.exception(f"Error reading trace stats: {e}")
|
|
694
|
+
return {
|
|
695
|
+
"total_spans": 0,
|
|
696
|
+
"total_traces": 0,
|
|
697
|
+
"services_count": 0,
|
|
698
|
+
"oldest_trace": None,
|
|
699
|
+
"newest_trace": None,
|
|
700
|
+
"database_size_mb": 0,
|
|
701
|
+
}
|
|
702
|
+
|
|
410
703
|
@app.get("/api/streaming-history/{agent_name}")
|
|
411
704
|
async def get_streaming_history(agent_name: str) -> dict[str, Any]:
|
|
412
705
|
"""Get historical streaming output for a specific agent.
|
flock/frontend/README.md
CHANGED
|
@@ -34,9 +34,95 @@ The dashboard offers two complementary visualization modes:
|
|
|
34
34
|
### Extensible Module System
|
|
35
35
|
- **Custom Visualizations**: Add specialized views via the module system
|
|
36
36
|
- **Event Log Module**: Built-in table view for detailed event inspection
|
|
37
|
+
- **Trace Viewer Module**: Jaeger-style distributed tracing with timeline and statistics
|
|
37
38
|
- **Context Menu Integration**: Right-click to add modules at any location
|
|
38
39
|
- **Persistent Layout**: Module positions and sizes are saved across sessions
|
|
39
40
|
|
|
41
|
+
### Trace Viewer Module 🔍
|
|
42
|
+
|
|
43
|
+
The **Trace Viewer** provides production-grade distributed tracing powered by OpenTelemetry and DuckDB, enabling deep debugging and performance analysis.
|
|
44
|
+
|
|
45
|
+
#### Features
|
|
46
|
+
|
|
47
|
+
- **Timeline View**: Waterfall visualization showing span hierarchies and execution order
|
|
48
|
+
- Parent-child relationships with visual indentation
|
|
49
|
+
- Service-specific color coding for easy identification
|
|
50
|
+
- Duration bars proportional to execution time
|
|
51
|
+
- Hover tooltips with detailed timing information
|
|
52
|
+
- Expand/collapse nested spans
|
|
53
|
+
|
|
54
|
+
- **Statistics View**: Tabular view with comprehensive metrics
|
|
55
|
+
- Sortable columns (name, duration, status)
|
|
56
|
+
- Quick filtering and search
|
|
57
|
+
- Status indicators (OK, ERROR)
|
|
58
|
+
- Export capabilities
|
|
59
|
+
|
|
60
|
+
- **Full I/O Capture**: Complete input/output data for every operation
|
|
61
|
+
- All function arguments captured as JSON
|
|
62
|
+
- Return values fully serialized
|
|
63
|
+
- Automatic deep object serialization
|
|
64
|
+
- No truncation (except strings >5000 chars)
|
|
65
|
+
|
|
66
|
+
- **JSON Viewer**: Beautiful, interactive JSON exploration
|
|
67
|
+
- Syntax highlighting with theme integration
|
|
68
|
+
- Collapsible tree structure
|
|
69
|
+
- **Expand All / Collapse All** buttons for quick navigation
|
|
70
|
+
- Supports nested objects and arrays
|
|
71
|
+
- Preserves data types (strings, numbers, booleans)
|
|
72
|
+
|
|
73
|
+
- **Multi-Trace Support**: Open multiple traces simultaneously
|
|
74
|
+
- Compare execution patterns side-by-side
|
|
75
|
+
- Toggle traces on/off
|
|
76
|
+
- Independent expansion states
|
|
77
|
+
|
|
78
|
+
- **Performance**: Built on DuckDB (10-100x faster than SQLite)
|
|
79
|
+
- Sub-millisecond query times
|
|
80
|
+
- Handles thousands of spans efficiently
|
|
81
|
+
- SQL-powered analytics backend
|
|
82
|
+
|
|
83
|
+
#### Usage
|
|
84
|
+
|
|
85
|
+
1. **Enable Tracing** in your Flock application:
|
|
86
|
+
```bash
|
|
87
|
+
export FLOCK_AUTO_TRACE=true
|
|
88
|
+
export FLOCK_TRACE_FILE=true
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
2. **Run Your Application** to generate traces
|
|
92
|
+
```bash
|
|
93
|
+
python your_flock_app.py
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
3. **Open Trace Viewer** in the dashboard:
|
|
97
|
+
- Click "Add Module" or right-click canvas
|
|
98
|
+
- Select "Trace Viewer"
|
|
99
|
+
- Browse and select traces to visualize
|
|
100
|
+
|
|
101
|
+
4. **Analyze Traces**:
|
|
102
|
+
- Click trace rows to expand timeline/statistics
|
|
103
|
+
- Use search to filter by operation name
|
|
104
|
+
- Expand JSON attributes to inspect I/O data
|
|
105
|
+
- Click "Expand All" to see complete JSON structures
|
|
106
|
+
|
|
107
|
+
#### What Gets Traced
|
|
108
|
+
|
|
109
|
+
Every traced operation captures:
|
|
110
|
+
- ✅ **Input Arguments**: All function parameters (excluding `self`/`cls`)
|
|
111
|
+
- ✅ **Output Values**: Complete return values
|
|
112
|
+
- ✅ **Timing Data**: Start time, end time, duration in milliseconds
|
|
113
|
+
- ✅ **Span Hierarchy**: Parent-child relationships for call stacks
|
|
114
|
+
- ✅ **Service Names**: Automatically extracted from class names
|
|
115
|
+
- ✅ **Status Codes**: OK, ERROR, UNSET with error messages
|
|
116
|
+
- ✅ **Metadata**: Correlation IDs, agent names, context info
|
|
117
|
+
|
|
118
|
+
#### Tips
|
|
119
|
+
|
|
120
|
+
- **Finding Bottlenecks**: Sort Statistics view by duration (descending)
|
|
121
|
+
- **Error Investigation**: Look for red ERROR status indicators
|
|
122
|
+
- **I/O Debugging**: Expand JSON viewers to see exact inputs that caused issues
|
|
123
|
+
- **Multi-Trace Comparison**: Open related traces to compare execution patterns
|
|
124
|
+
- **JSON Navigation**: Use "Expand All" for complex nested structures
|
|
125
|
+
|
|
40
126
|
### Modern UI/UX
|
|
41
127
|
- **Glassmorphism Design**: Modern dark theme with semi-transparent surfaces and blur effects
|
|
42
128
|
- **Keyboard Shortcuts**: Navigate efficiently with Ctrl+M, Ctrl+F, and Esc
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import JsonView from '@uiw/react-json-view';
|
|
3
|
+
|
|
4
|
+
interface JsonAttributeRendererProps {
|
|
5
|
+
value: string;
|
|
6
|
+
maxStringLength?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Renders an attribute value, parsing it as JSON if possible and displaying
|
|
11
|
+
* it with a beautiful collapsible JSON viewer. Falls back to plain text if not JSON.
|
|
12
|
+
*/
|
|
13
|
+
const JsonAttributeRenderer: React.FC<JsonAttributeRendererProps> = ({
|
|
14
|
+
value,
|
|
15
|
+
maxStringLength = 200
|
|
16
|
+
}) => {
|
|
17
|
+
const [collapsed, setCollapsed] = useState<boolean | number>(2);
|
|
18
|
+
|
|
19
|
+
// Try to parse as JSON
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(value);
|
|
22
|
+
|
|
23
|
+
// If it's a simple primitive after parsing, just show it as text
|
|
24
|
+
if (typeof parsed === 'string' || typeof parsed === 'number' || typeof parsed === 'boolean' || parsed === null) {
|
|
25
|
+
return (
|
|
26
|
+
<div style={{
|
|
27
|
+
fontFamily: 'var(--font-family-mono)',
|
|
28
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
29
|
+
color: 'var(--color-text-secondary)',
|
|
30
|
+
wordBreak: 'break-word',
|
|
31
|
+
}}>
|
|
32
|
+
{String(parsed)}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// It's a complex object or array - use JSON viewer
|
|
38
|
+
return (
|
|
39
|
+
<div>
|
|
40
|
+
<div style={{
|
|
41
|
+
display: 'flex',
|
|
42
|
+
gap: 'var(--gap-xs)',
|
|
43
|
+
marginBottom: 'var(--gap-xs)',
|
|
44
|
+
}}>
|
|
45
|
+
<button
|
|
46
|
+
onClick={() => setCollapsed(false)}
|
|
47
|
+
style={{
|
|
48
|
+
padding: '2px 8px',
|
|
49
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
50
|
+
color: 'var(--color-text-secondary)',
|
|
51
|
+
backgroundColor: 'var(--color-bg-elevated)',
|
|
52
|
+
border: '1px solid var(--color-border-subtle)',
|
|
53
|
+
borderRadius: 'var(--radius-xs)',
|
|
54
|
+
cursor: 'pointer',
|
|
55
|
+
fontFamily: 'var(--font-family-mono)',
|
|
56
|
+
}}
|
|
57
|
+
onMouseEnter={(e) => {
|
|
58
|
+
e.currentTarget.style.backgroundColor = 'var(--color-bg-hover)';
|
|
59
|
+
}}
|
|
60
|
+
onMouseLeave={(e) => {
|
|
61
|
+
e.currentTarget.style.backgroundColor = 'var(--color-bg-elevated)';
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
Expand All
|
|
65
|
+
</button>
|
|
66
|
+
<button
|
|
67
|
+
onClick={() => setCollapsed(true)}
|
|
68
|
+
style={{
|
|
69
|
+
padding: '2px 8px',
|
|
70
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
71
|
+
color: 'var(--color-text-secondary)',
|
|
72
|
+
backgroundColor: 'var(--color-bg-elevated)',
|
|
73
|
+
border: '1px solid var(--color-border-subtle)',
|
|
74
|
+
borderRadius: 'var(--radius-xs)',
|
|
75
|
+
cursor: 'pointer',
|
|
76
|
+
fontFamily: 'var(--font-family-mono)',
|
|
77
|
+
}}
|
|
78
|
+
onMouseEnter={(e) => {
|
|
79
|
+
e.currentTarget.style.backgroundColor = 'var(--color-bg-hover)';
|
|
80
|
+
}}
|
|
81
|
+
onMouseLeave={(e) => {
|
|
82
|
+
e.currentTarget.style.backgroundColor = 'var(--color-bg-elevated)';
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
Collapse All
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
<div style={{
|
|
89
|
+
maxHeight: '400px',
|
|
90
|
+
overflowY: 'auto',
|
|
91
|
+
overflowX: 'auto',
|
|
92
|
+
}}>
|
|
93
|
+
<JsonView
|
|
94
|
+
value={parsed}
|
|
95
|
+
collapsed={collapsed}
|
|
96
|
+
displayDataTypes={false}
|
|
97
|
+
shortenTextAfterLength={0}
|
|
98
|
+
style={{
|
|
99
|
+
backgroundColor: 'var(--color-bg-elevated)',
|
|
100
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
101
|
+
fontFamily: 'var(--font-family-mono)',
|
|
102
|
+
padding: 'var(--space-component-sm)',
|
|
103
|
+
borderRadius: 'var(--radius-sm)',
|
|
104
|
+
'--w-rjv-line-color': 'var(--color-text-tertiary)',
|
|
105
|
+
'--w-rjv-key-string': 'var(--color-primary-500)',
|
|
106
|
+
'--w-rjv-info-color': 'var(--color-text-secondary)',
|
|
107
|
+
'--w-rjv-curlybraces-color': 'var(--color-text-tertiary)',
|
|
108
|
+
'--w-rjv-brackets-color': 'var(--color-text-tertiary)',
|
|
109
|
+
'--w-rjv-arrow-color': 'var(--color-text-tertiary)',
|
|
110
|
+
'--w-rjv-edit-color': 'var(--color-primary-500)',
|
|
111
|
+
'--w-rjv-add-color': 'var(--color-success)',
|
|
112
|
+
'--w-rjv-del-color': 'var(--color-error)',
|
|
113
|
+
'--w-rjv-update-color': 'var(--color-warning)',
|
|
114
|
+
'--w-rjv-border-left-color': 'var(--color-border-subtle)',
|
|
115
|
+
} as React.CSSProperties}
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
// Not valid JSON - display as plain text with word wrap
|
|
122
|
+
const displayValue = value.length > maxStringLength
|
|
123
|
+
? value.substring(0, maxStringLength) + '...'
|
|
124
|
+
: value;
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div style={{
|
|
128
|
+
fontFamily: 'var(--font-family-mono)',
|
|
129
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
130
|
+
color: 'var(--color-text-secondary)',
|
|
131
|
+
wordBreak: 'break-word',
|
|
132
|
+
whiteSpace: 'pre-wrap',
|
|
133
|
+
}}>
|
|
134
|
+
{displayValue}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export default JsonAttributeRenderer;
|